美文网首页java学习之路算法提高之LeetCode刷题
leetCode进阶算法题+解析(十八)

leetCode进阶算法题+解析(十八)

作者: 唯有努力不欺人丶 | 来源:发表于2020-03-09 21:40 被阅读0次

    三角形最小路径和

    题目:给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

    例如,给定三角形:
    [
    [2],
    [3,4],
    [6,5,7],
    [4,1,8,3]
    ]
    自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
    说明:
    如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

    思路:额,这个我目前的思路就是遍历整个三角。总能找出路径最小的,至于进阶条件只使用O(n)额外空间先实现再优化吧。其实我仔细看了下,这个三角形好像是可以理解成从下到上的动归、每次往上到上一层节点,都会有上一层节点数最小的可能。比如第四层到第三层,则只会有三个可能。以6为根的6+1=7(因为6+4>7,所以直接pass),以5为根的5+1=6.以7为根的7+3=10.同理再往上一层,则变成了两种选择:以3 为根的3+6=9(因为3+7大于9,所以pass),以4为根的4+6=10.最后到了第一层变成2+9=11是最小值。我不知道我叙述明白了没有,但是这个一定是要从后往前dp。我去代码实现下。
    我写完了才发现我特么这个算法就是只使用O(n)的额外空间啊?!!!!有点小骄傲呢~~我直接贴代码:

    class Solution {
        public int minimumTotal(List<List<Integer>> triangle) {
            int len = triangle.size()-1;
            if(len<0) return 0;
            if(len<1) return triangle.get(0).get(0); 
            int[] p = new int[triangle.get(len).size()];
            for(int i = 0; i<p.length; i++ ){
                p[i] = triangle.get(len).get(i);
            }
            for(int i = len-1; i>0; i--){
                int size = triangle.get(i).size();
                for(int j = 0; j<size; j++){
                    //到下面的数字的最小值
                    int down = triangle.get(i).get(j)+p[j];
                    //到斜下面的数字的最小值
                    int recline = triangle.get(i).get(j)+p[j+1];
                    p[j] = Math.min(down,recline);
                }
            }
            return Math.min(p[0]+triangle.get(0).get(0),p[1]+triangle.get(0).get(0));
        }
    }
    

    讲真,我觉得这个题目如果给定的不是list<list>而是二维数组性能会好很多,而且代码也会更简洁。。。反正我这个思路是没问题的,而且额外空间也是符合要求的,至于为什么性能不咋地,我觉得也就是一些小细节没处理好。我去小修一下。

    class Solution {
        public int minimumTotal(List<List<Integer>> triangle) {
            int len = triangle.size()-1;
            if(len<0) return 0;
            int[] p = new int[triangle.get(len).size()+1];
            for(int i = len; i>=0; i--){
                int size = triangle.get(i).size();
                for(int j = 0; j<size; j++){
                    //到下面的数字的最小值
                    int down = triangle.get(i).get(j)+p[j];
                    //到斜下面的数字的最小值
                    int recline = triangle.get(i).get(j)+p[j+1];
                    p[j] = Math.min(down,recline);
                }
            }
            return p[0];
        }
    }
    

    修改完了 的代码性能好点了,但是依然不及格,我直接看看性能排行第一的代码吧。

    class Solution {
        int rows = 0;
        int[][] dp;    
        public int solve(List<List<Integer>> triangle, int row, int col) {
            if (row==rows-1){
            return triangle.get(row).get(col);
            } else {
                if(dp[row][col] != 0) {
                    return dp[row][col];
                }
                int tmp = triangle.get(row).get(col);
                int left = solve(triangle, row + 1, col);
                int right = solve(triangle, row + 1, col + 1);
                dp[row][col] = Math.min(left, right) + tmp;
                return dp[row][col];
            }
        }
        public int minimumTotal(List<List<Integer>> triangle) {
            rows = triangle.size();
            dp = new int[rows][rows];
            return solve(triangle, 0, 0);
        }
    }
    

    说真的, 这个不能算是O(n)额外空间吧?应该是n方吧?
    不管了,反正思路也就这样。下一题吧。

    单词接龙

    题目:给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:

    每次转换只能改变一个字母。
    转换过程中的中间单词必须是字典中的单词。
    说明:
    如果不存在这样的转换序列,返回 0。
    所有单词具有相同的长度。
    所有单词只由小写字母组成。
    字典中不存在重复的单词。
    你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
    示例 1:
    输入:
    beginWord = "hit",
    endWord = "cog",
    wordList = ["hot","dot","dog","lot","log","cog"]
    输出: 5
    解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
    返回它的长度 5。
    示例 2:
    输入:
    beginWord = "hit"
    endWord = "cog"
    wordList = ["hot","dot","dog","lot","log"]
    输出: 0
    解释: endWord "cog" 不在字典中,所以无法进行转换。

    思路:这个题题目很复杂啊。语言表述不少,但是做起来应该不难。首先有一点注意的:不应该出现来回来去重复选取的情况。比如hot-dot,再找dot的只差一个字母的单词时就不能选择hot了。所以这里应该是用标记的。相比于下标代替下标,数值代表状态(初始0是没用过,用过改成-1)我更倾向于直接用个boolean数组来表示状态。然后我去尝试写代码了。
    好了,实现是实现了,但是性能有点问题,我先贴出代码再优化:

    class Solution {
        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            //字典中没有endword直接返回0
            if(wordList.indexOf(endWord)==-1) return 0;
            Queue<String> queue = new LinkedList<String>();
            queue.offer(beginWord);
            int count = 0;
            while(!queue.isEmpty()){//当queue为空说明到不了endword了  
                count++;    
                int size = queue.size();
                for(int j = 0;j<size;j++){
                    String s = queue.poll();
                    for(int i = wordList.size()-1;i>=0;i--){    
                        String temp = wordList.get(i);         
                        if(canConvert(s,temp)){
                            if(endWord.equals(temp)){
                                return count+1;
                            }else{
                                queue.offer(temp);
                                wordList.remove(i);
                            }
                        }
                    }
                                
                }                 
            }
            return 0;
        }
        //只有一个字母不同才算是演变单词
        public boolean canConvert(String s1, String s2) {
            int count = 0;
            for (int i = 0; i < s1.length(); ++i) {
                if (s1.charAt(i) != s2.charAt(i)) {
                    ++count;
                    if (count > 1) {
                        return false;
                    }
                }
            }
            return count == 1;
        }
    }
    

    首先在做的过程中有些和思路不一样的修改,本来我是打算用标记来去除重复选项的。但是后来觉得挺没意义的,既然选过一次就不能再选了,还不如直接删除呢,然后就是以上代码的逻辑(因为有删除,所以从后往前遍历了。)
    首先代码580ms,其实可以想象,因为这个确实是一个乘积的时间复杂度。。其次先入先出习惯用队列了,其实list也完全可以实现的。并且我怀疑换成list性能会比现在好,但是这个应该不是问题的主要吧,其实下面这个我觉得也这个charAt也可以换换,换成数组比较字符会不会好点?我改一下试试。
    我的难受无以言表,兴致冲冲改了点细节,然后从几百到上千ms。。。但是我也要把改版的贴出来,毕竟也是劳动成果:

    class Solution {
        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            //字典中没有endword直接返回0
            if(wordList.indexOf(endWord)==-1) return 0;
            LinkedList<String> list = new LinkedList<String>();
            list.add(beginWord);
            int count = 0;
            while(list.size()!=0){//当queue为空说明到不了endword了  
                count++;    
                int size = list.size();
                for(int j = 0;j<size;j++){
                    String s = list.removeFirst();
                    for(int i = wordList.size()-1;i>=0;i--){    
                        String temp = wordList.get(i);         
                        if(canConvert(s.toCharArray(),temp.toCharArray())){
                            if(endWord.equals(temp)){
                                return count+1;
                            }else{
                                list.add(temp);
                                wordList.remove(i);
                            }
                        }
                    }                            
                }                 
            }
            return 0;
        }
        //只有一个字母不同才算是演变单词
        public boolean canConvert(char[] c1,char[] c2) {
            int count = 0;
            for (int i = 0; i < c1.length; i++) {
                if (c1[i]!=c2[i]) {
                    ++count;
                    if (count > 1) {
                        return false;
                    }
                }
            }
            return true;
        }
    }
    

    然后我直接去看性能排行第一的代码了,我已经放弃优化了。
    额,看完了,人家的注解很详细,并且逻辑很简单,属于一看就懂,一写就懵的那种,我直接贴出来膜拜下:

    /**
     * 在提交里看到的最优解,看懂了,解读一下分享出来:
     * 需要掌握的知识递进:
     * 1.BFS。
     * 2.双端BFS。
     * 3.临近点查找方式:
     * 首先将所有的字符存到结构为HashSet的dic字典里去,然后将字符串的每一位挨个从a变到z,
     * 在变化的时候实时去字典里查,因为是hashset,所以复杂度是O(1),非常快。
     * 如果查到了,则就是找到了临近点。
     */
    class Solution {
        //递归
        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            if (wordList == null || wordList.size() == 0) return 0;
            //hashset的好处:去重也完成了
            //开始端
            HashSet<String> start = new HashSet<>();
            //结束端
            HashSet<String> end = new HashSet<>();
            //所有字符串的字典
            HashSet<String> dic = new HashSet<>(wordList);
            start.add(beginWord);
            end.add(endWord);
            if (!dic.contains(endWord)) return 0;
            //经历过上面的一系列判定,到这里的时候,若是有路径,则最小是2,所以以2开始
            return bfs(start, end, dic, 2);
    
        }
    
        public int bfs(HashSet<String> st, HashSet<String> ed, HashSet<String> dic, int l) {
            //双端查找的时候,若是有任意一段出现了“断裂”,也就是说明不存在能够连上的路径,则直接返回0
            if (st.size() == 0) return 0;
            if (st.size() > ed.size()) {//双端查找,为了优化时间,永远用少的去找多的,比如开始的时候塞进了1000个,而结尾只有3个,则肯定是从少的那一端开始走比较好
                return bfs(ed, st, dic, l);
            }
            //BFS的标记行为,即使用过的不重复使用
            dic.removeAll(st);
            //收集下一层临近点
            HashSet<String> next = new HashSet<>();
            for (String s : st) {
                char[] arr = s.toCharArray();
                for (int i = 0; i < arr.length; i++) {
                    char tmp = arr[i];
                    //变化
                    for (char c = 'a'; c <= 'z'; c++) {
                        if (tmp == c) continue;
                        arr[i] = c;
                        String nstr = new String(arr);
                        if (dic.contains(nstr)) {
                            if (ed.contains(nstr)) return l;
                            else next.add(nstr);
                        }
                    }
                    //复原
                    arr[i] = tmp;
                }
            }
            return bfs(next, ed, dic, l + 1);
        }
    
    }
    

    讲真,他这个性能,我不懂为啥这么比较会比两个单词的比较性能高。。。我总怀疑是测试案例的问题。有大佬明白的也请指点下。。然后下一题了。

    求根到叶子节点数字之和

    题目:给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。例如,从根到叶子节点路径 1->2->3 代表数字 123。计算从根到叶子节点生成的所有数字之和。说明: 叶子节点是指没有子节点的节点。

    示例 1:
    输入: [1,2,3]
    1
    /
    2 3
    输出: 25
    解释:
    从根到叶子节点路径 1->2 代表数字 12.
    从根到叶子节点路径 1->3 代表数字 13.
    因此,数字总和 = 12 + 13 = 25.
    示例 2:
    输入: [4,9,0,5,1]
    4
    /
    9 0
    /
    5 1
    输出: 1026
    解释:
    从根到叶子节点路径 4->9->5 代表数字 495.
    从根到叶子节点路径 4->9->1 代表数字 491.
    从根到叶子节点路径 4->0 代表数字 40.
    因此,数字总和 = 495 + 491 + 40 = 1026.

    思路:额,我觉得这道题好像挺简单啊,我个人是打算用list存路径,然后遍历树,最后得出list<list>,再以数字的形式相加。。我不知道有没有坑,我先去实现看看。
    哎,我这一言难尽的小性能啊,写完我就觉得有改善的空间,不过先提交了下测试思路对不对,所以第一版本代码毕竟是劳动成果,先贴出来了:

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    class Solution {
        List<List<Integer>> list;
        public int sumNumbers(TreeNode root) {
            list = new ArrayList<List<Integer>>();
            dfs(new ArrayList(),root);
            int sum = 0;
            for(List<Integer> l : list){
                int count = 0;
                for(int i : l){
                    count = count*10+i;
                }
                sum += count;
            }
            return sum;
        }
        public void dfs(List<Integer> path, TreeNode root){
            if(root==null) return;
            path.add(root.val);
            //遍历到叶子节点了
            if(root.left==null && root.right==null){
                list.add(path);
                return;
            }
            dfs(new ArrayList(path),root.left);
            dfs(new ArrayList(path),root.right);
        }
    }
    

    然后我去优化了。好了,优化版本来了:

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    class Solution {
        List<Integer> list;
        public int sumNumbers(TreeNode root) {
            list = new ArrayList<Integer>();
            dfs(0,root);
            int sum = 0;
            for(int i : list){
                sum += i;
            }
            return sum;
        }
        public void dfs(int sum, TreeNode root){
            if(root==null) return;
            sum = sum*10 + root.val;
            //遍历到叶子节点了
            if(root.left==null && root.right==null){
                list.add(sum);
                return;
            }
            dfs(sum,root.left);
            dfs(sum,root.right);
        }
    }
    

    从 3ms 到1ms,进步挺大的,就是排名上不去。。我直接去看性能排行第一的代码吧:
    哎,这个我应该可以会的,怪我没静下心来,其实第一的代码比我多处理了一下,上面我的优化版还是用的list记录每一条路径的和,最后相加,其实这里可以直接存总路径。我直接贴代码了:

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    class Solution {
        int res;
        public int sumNumbers(TreeNode root) {
            if(root==null) return 0;
            dfs(0,root);
            return res;
        }
        public void dfs(int sum, TreeNode root){
            if(root==null) return;
            sum = sum*10 + root.val;
            //遍历到叶子节点了
            if(root.left==null && root.right==null){
                res += sum;
                return;
            }
            dfs(sum,root.left);
            dfs(sum,root.right);
        }
    }
    

    这道题是真的怪我,有点着急了,算是个教训吧。
    今天的笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利!生活健健康康!!

    相关文章

      网友评论

        本文标题:leetCode进阶算法题+解析(十八)

        本文链接:https://www.haomeiwen.com/subject/xxzudhtx.html