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

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

作者: 唯有努力不欺人丶 | 来源:发表于2021-05-21 17:31 被阅读0次

    在这里要嘚瑟一下,5/25号的力扣夜猫赛,4道题都过了,第一次ak,太兴奋了,感觉这一年多的付出有了收获。从当年的完全小白到现在哪怕是运气好但是能够ak,也付出了很多时间精力吧。当然了一次ak只不过是加深了我的荣誉感和幸福度。我知道代表不了什么,可能下次依然只能1,2,3题。我只是更加确信:付出终有回报,努力不会欺人。


    ak镇楼图

    好了,闲话少叙,继续今天的刷题。

    形成两个异或相等数组的三元组数目

    题目:给你一个整数数组 arr 。现需要从数组中取三个下标 i、j 和 k ,其中 (0 <= i < j <= k < arr.length) 。a 和 b 定义如下:
    a = arr[i] ^ arr[i + 1] ^ ... ^ arr[j - 1]
    b = arr[j] ^ arr[j + 1] ^ ... ^ arr[k]
    注意:^ 表示 按位异或 操作。请返回能够令 a == b 成立的三元组 (i, j , k) 的数目。

    示例 1:
    输入:arr = [2,3,1,6,7]
    输出:4
    解释:满足题意的三元组分别是 (0,1,2), (0,2,2), (2,3,4) 以及 (2,4,4)
    示例 2:
    输入:arr = [1,1,1,1,1]
    输出:10
    示例 3:
    输入:arr = [2,3]
    输出:0
    示例 4:
    输入:arr = [1,3,5,7,9]
    输出:3
    示例 5:
    输入:arr = [7,11,12,9,5,2,7,17,22]
    输出:8
    提示:
    1 <= arr.length <= 300
    1 <= arr[i] <= 10^8

    思路:这个题怎么说呢,首先a==b。也就是a ^ b 会等于0.然后这个三元组形成的大前提就是从i到k下标异或的结果是0。而且这个数据范围才300.我觉得暴力法就能破解。思路很清晰,我去代码实现。
    讲真,这个题有毒吧?说好了的三元数组,结果两个数居然也可以。我简直了。。。然後用的双层循环。我要先附上第一版代码,虽然是错误的,但是原因是第一版代码一定要三个数才算解。其实正确答案去掉这个限制就行了:

    class Solution {
        public int countTriplets(int[] arr) {
            int ans = 0;
            for(int i = 0;i<arr.length-2;i++) {
                int temp = arr[i]^arr[i+1];
                for(int j = i+2;j<arr.length;j++) {
                    temp ^= arr[j];
                    if(temp == 0) {
                        //起始是i,结束是k,中间任何元素都可以当j。所以中间元素数就是可能数
                        ans += (j-i);
                    }
                }
            }
            return ans;
        }
    }
    

    去掉限制的正确代码:

    class Solution {
        public int countTriplets(int[] arr) {
            int ans = 0;
            for(int i = 0;i<arr.length-1;i++) {
                int temp = arr[i];
                for(int j = i+1;j<arr.length;j++) {
                    temp ^= arr[j];
                    if(temp == 0) {
                        //起始是i,结束是k,中间任何元素都可以当j。所以中间元素数就是可能数
                        ans += (j-i);
                    }
                }
            }
            return ans;
        }
    }
    

    这个题就这样了,比较简单,主要是知道如何计算可能数(x-y总结果是0,那么任何节点断开,两段异或都是0.所以可能数是x-y中间的元素数。所以是j-i)。这个理解了这个题就没啥难的,下一题。

    找出第K大

    题目:给你一个二维矩阵 matrix 和一个整数 k ,矩阵大小为 m x n 由非负整数组成。矩阵中坐标 (a, b) 的 值 可由对所有满足 0 <= i <= a < m 且 0 <= j <= b < n 的元素 matrix[i][j](下标从 0 开始计数)执行异或运算得到。请你找出 matrix 的所有坐标中第 k 大的值(k 的值从 1 开始计数)。

    示例 1:
    输入:matrix = [[5,2],[1,6]], k = 1
    输出:7
    解释:坐标 (0,1) 的值是 5 XOR 2 = 7 ,为最大的值。
    示例 2:
    输入:matrix = [[5,2],[1,6]], k = 2
    输出:5
    解释:坐标 (0,0) 的值是 5 = 5 ,为第 2 大的值。
    示例 3:
    输入:matrix = [[5,2],[1,6]], k = 3
    输出:4
    解释:坐标 (1,0) 的值是 5 XOR 1 = 4 ,为第 3 大的值。
    示例 4:
    输入:matrix = [[5,2],[1,6]], k = 4
    输出:0
    解释:坐标 (1,1) 的值是 5 XOR 2 XOR 1 XOR 6 = 0 ,为第 4 大的值。
    提示:
    m == matrix.length
    n == matrix[i].length
    1 <= m, n <= 1000
    0 <= matrix[i][j] <= 106

    1 <= k <= m * n

    思路:这个题咋说呢,读了好几遍题目才懂了题意。我的想法是用一个二维数组压缩。第一次记录每一列/行的异或结果。然后第二遍遍历记录每一行的异或结果。这样就获取所有可能的a,b的结果了。有点类似dp。然后排序,获取第k个值。思路差不多是这样,我去实现下试试。
    第一版代码:

    class Solution {
        public int kthLargestValue(int[][] matrix, int k) {
            int m = matrix.length;
            int n = matrix[0].length;
            int[][] dp = new int[m + 1][n + 1];
            List<Integer> ans = new ArrayList<Integer>();
            for (int i = 1; i <= m; ++i) {
                for (int j = 1; j <= n; ++j) {
                    dp[i][j] = dp[i - 1][j] ^ dp[i][j - 1] ^ dp[i - 1][j - 1] ^ matrix[i - 1][j - 1];
                    ans.add(dp[i][j]);
                }
            }
            ans.sort((i1,i2)->{return i2-i1;});
            return ans.get(k - 1);
        }
        
    }
    

    做的时候我还觉得我用到了dp,以为能不错。结果发现ac是ac了。但是性能低空略过,然后我觉得是m*n的时间复杂度是不可能再少了的。能优化的点也就是排序这块了。。然而我没啥思路。所以直接去看性能第一的代码吧:

    class Solution {
        public int kthLargestValue(int[][] matrix, int k) {
            final int N = (matrix == null) ? 0 : matrix.length;
            final int M = (N == 0) ? 0 : matrix[0].length;
            int[][] xor = new int[N + 1][M + 1];
            for (int i = 0; i < N; ++i) {
                for (int j = 0; j < M; ++j) {
                    xor[i + 1][j + 1] = matrix[i][j] ^ xor[i][j + 1] ^ xor[i + 1][j] ^ xor[i][j];
                }
            }
            int idx = 0;
            int[] nums = new int[N * M];
            for (int i = 1; i <= N; ++i) {
                for (int j = 1; j <= M; ++j) {
                    nums[idx++] = xor[i][j];
                }
            }
            return quickSelect(nums, 0, nums.length - 1, nums.length - k + 1);
        }
    
        private int quickSelect(int[] nums, int start, int end, int k) {
            if (start == end) {
                return nums[start];
            }
            int left = start;
            int right = end;
            int pivot = nums[start + (end - start) / 2];
            while (left <= right) {
                if (nums[left] < pivot) {
                    left++;
                } else if (nums[right] > pivot) {
                    right--;
                } else {
                    int temp = nums[left];
                    nums[left++] = nums[right];
                    nums[right--] = temp;
                }
            }
            if (start + k - 1 <= right) {
                return quickSelect(nums, start, right, k);
            }
            if (start + k - 1 >= left) {
                return quickSelect(nums, left, end, start + k - left);
            }
            return nums[right + 1];
        }
    }
    

    思路没啥问题,优化点也确实在排序上。我用的list。这里用了数组来存储。然后用了一个方法来获取第k个元素的值。重点应该是在这个排序上吧。反正这个题就这样了,下一题。

    根据前序和后序遍历构造二叉树

    题目:返回与给定的前序和后序遍历匹配的任何二叉树。pre 和 post 遍历中的值是不同的正整数。

    示例:
    输入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1]
    输出:[1,2,3,4,5,6,7]
    提示:
    1 <= pre.length == post.length <= 30
    pre[] 和 post[] 都是 1, 2, ..., pre.length 的排列
    每个输入保证至少有一个答案。如果有多个答案,可以返回其中一个。

    思路:这个题咋说呢,但凡带个中序就能确定一棵树了。问题是只有前序后序排列。所以其实应该是无法确定一棵树了。这里简单说下前序中序后序的顺序:前序:根,左,右。 中序:左,根,右。后序:左,右,根。然后现在给定是是前序和后序。所以知道了根左右和左右根。但是之所以说前后确定不了唯一的一棵树的原因也是这:左右子树的界限是不清楚的。继续说这个题目:因为左右节点的顺序是不会变的。不管是根左右还是左右根,起码都保证了左右节点的顺序不变。而且根的位置是从前到后了。所以我们可以判断当前pre中当前节点和post中节点的位置。如果说这个元素位置相对往后移动了。说明这个节点是一个根节点。然后如果是子节点上一层根节点,则只会往后移动两步。注意我说的不是相对位置。是指原本在他之后,结果在他之前的元素。比如上面demo中的本来是2,4,5.后序变成4.5,2.也就是两个元素提前面去了。同理1也是这样的。本来后面2-7.结果因为上述第二次,所以2乘3个元素到前面了。当然我这个思路是要都是完整的二叉树。我慢慢去代码试试吧。
    第一版代码:

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    class Solution {
        public TreeNode constructFromPrePost(int[] pre, int[] post) {
            return dfs(pre,post);
        }
        public TreeNode dfs(int[] pre, int[] post) {
            int len = pre.length;
            if(len == 0) return null;
            // 根节点比较好确定。前序第一个和后序最后一个。
            TreeNode ans = new TreeNode(pre[0]);
            if (len == 1) return ans;
            // 前序第二个元素是左子树根节点。后序倒数第二个元素是右子树根节点。如果不一样则递归分树
            if (pre[1] != post[post.length-2]) {//Arrays.copyOfRange(data,  2 ,  7 );
                int v = pre[1];
                int idx = -1;//记录左子树根节点在后序中的位置。
                for(int i = 0;i<post.length;i++) if(post[i] == v) idx = i;
                //左右子树都不包含当前根节点,也就是pre[0]和post[len-1]
                int[] post1 = Arrays.copyOfRange(post, 0, idx+1);
                int[] post2 = Arrays.copyOfRange(post, idx+1, len-1);
                int[] pre1 = Arrays.copyOfRange(pre,1,1+post1.length);
                int[] pre2 = Arrays.copyOfRange(pre,post1.length+1,len);
                ans.left = dfs(pre1, post1);
                ans.right = dfs(pre2, post2);
            }else {//说明当前树只有一个根。左右无所谓了
                ans.left = dfs(Arrays.copyOfRange(pre,1,len), Arrays.copyOfRange(post, 0, len-1));
            }
            return ans;
        }
    }
    

    咳咳,和之前的思路不能说一模一样,只能说几乎不同。。。因为我在分解左右子树的时候就发现,其实这个题可以转化成递归问题。然后就和之前的思路完全不同了。。。反正最终变成了上述的代码。过是过了,毕竟节点30个以内。不过我觉得优化空间还是蛮大的。比如说复制数组可以换成传起始下标?不过这个太复杂了懒得自己去想了,我直接去看看性能第一的代码吧:
    开过光的嘴预测到了答案:

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    class Solution {
        public TreeNode constructFromPrePost(int[] pre, int[] post) {
            return helper(pre,post,0,pre.length-1,0,post.length-1);
        }
        public TreeNode helper(int[] pre,int[] post,int prestart,int preend,int poststart,int postend){
            if(prestart>preend||poststart>postend)return null;
            TreeNode root=new TreeNode(pre[prestart]);
            if (prestart == preend)
                return root;
            int index=0;
            while(post[index]!=pre[prestart+1]){
                index++;
            }
            root.left=helper(pre,post,prestart+1,prestart+1+index-poststart,poststart,index);
            root.right=helper(pre,post,prestart+2+index-poststart,preend,index+1,preend-1);
            return root;
            
        }
    }
    

    果然是不复制数组,而是直接用下标表示数值的方法,性能超过了百分百。其实不过是2ms进化到1ms而已。总而言之二叉树的题目,第一反应递归一般不会出错。这个题就这样了,下一题。

    不相交的线

    题目:在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足: nums1[i] == nums2[j]
    且绘制的直线不与任何其他连线(非水平线)相交。
    请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。以这种方法绘制线条,并返回可以绘制的最大连线数。
    题目截图

    示例 1:
    输入:nums1 = [1,4,2], nums2 = [1,2,4]
    输出:2
    解释:可以画出两条不交叉的线,如上图所示。
    但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。
    示例 2:
    输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
    输出:3
    示例 3:
    输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
    输出:2
    提示:
    1 <= nums1.length <= 500
    1 <= nums2.length <= 500
    1 <= nums1[i], nums2[i] <= 2000

    思路:不知道为啥审完题我第一反应又是dp,我这怕不是dp综合征吧。题目标签是数组。但是我还是觉得可以用dp来做。每一对元素可以分为连和不连。如果连则取下面的下标往下遍历。如果不连则从当前遍历。两个游标记录上下的线当前指向。思路还算清晰,不太好用言语表达出来,我去实现下试试。
    真的要代码实现之前,我发现了个问题:其实这个问题可以转换思路,变成nums1和nums2的最长子序列。因为可以按照子序列的顺序连线。肯定不存在交叉。然后这个题就做过好几遍了,我觉得我思路没问题,我去写代码。
    第一版代码:

    class Solution {
        public int maxUncrossedLines(int[] nums1, int[] nums2) {
            int[][] dp = new int[nums1.length+1][nums2.length+1];
            for(int i = 0;i<nums1.length;i++) {
                for(int j = 0;j<nums2.length;j++) {
                    if(nums1[i] == nums2[j]) {
                        dp[i+1][j+1] = dp[i][j]+1;
                    }else {
                        dp[i+1][j+1] = Math.max(dp[i][j+1], dp[i+1][j]);
                    }
                }
            }
            return dp[nums1.length][nums2.length];
        }
    }
    

    ac是ac了,性能没眼看了。。但是别的不说思路起码是对的。然后这个子数组的优化没啥好说的,毕竟我的技术最多也就能压缩下空间。时间复杂度少不了。。我直接去看看性能第一的代码吧:
    !!!!!我踏马简直了,一样的思路压缩了下空间,性能就上来了。。一点脾气没有了,附上代码:

    class Solution {
        public int maxUncrossedLines(int[] A, int[] B) {
            int lena = A.length, lenb = B.length;
            int[] dp = new int[lenb+1];
            for(int i = 1;i<=lena;i++) {
                int[] temp = new int[lenb+1];
                for(int j = 1;j<=lenb;j++) {
                    if(A[i-1]==B[j-1]) {
                        temp[j] = dp[j-1]+1;
                    }
                    else {
                        temp[j] = Math.max(dp[j], temp[j-1]);
                    }
                }
                dp = temp;
            }
            return dp[lenb];
        }
    }
    

    因为当前数值只和上一层有关,所以压缩成两个数组很容易想到,我是没想到性能差别这么大,哎,这个题就这样吧,下一题。

    查找和替换模式

    题目:你有一个单词列表 words 和一个模式 pattern,你想知道 words 中的哪些单词与模式匹配。如果存在字母的排列 p ,使得将模式中的每个字母 x 替换为 p(x) 之后,我们就得到了所需的单词,那么单词与模式是匹配的。(回想一下,字母的排列是从字母到字母的双射:每个字母映射到另一个字母,没有两个字母映射到同一个字母。)返回 words 中与给定模式匹配的单词列表。你可以按任何顺序返回答案。

    示例:
    输入:words = ["abc","deq","mee","aqq","dkd","ccc"], pattern = "abb"
    输出:["mee","aqq"]
    解释:
    "mee" 与模式匹配,因为存在排列 {a -> m, b -> e, ...}。
    "ccc" 与模式不匹配,因为 {a -> c, b -> c, ...} 不是排列。
    因为 a 和 b 映射到同一个字母。
    提示:
    1 <= words.length <= 50
    1 <= pattern.length = words[i].length <= 20

    思路:这个题一开始看题目没太明白,但是看了示例秒懂。有点类似中文的按照格式写成语,ABAB,AABB,ABBA之类的模板。只不过这个题的模板是pattern,然后我们照着这个模板去套单词就行了。感觉挺好实现的。因为单词数不超过50.而且长度不超过20。所以我觉得暴力应该可能可以。这个题咋说呢,在我看来是怎么都能解。但是怎么能解的好又没啥思路。
    第一版本代码:

    class Solution {
        String[] c;
        public List<String> findAndReplacePattern(String[] words, String pattern) {
            c = new String[] {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T"};
            pattern = getStr(pattern);
            List<String> ans = new ArrayList<String>();
            for(String s:words) if(getStr(s).equals(pattern)) ans.add(s);
            return ans;
        }
        public String getStr(String str) {
            int temp = 0;
            for(int i = 0;i<str.length();i++) {
                char cur = str.charAt(i);
                if(cur<97) continue;//这个元素已经替换过了,直接下一个
                str = str.replaceAll(cur+"", c[temp++]);
            }
            return str;
        }
    }
    

    仗着数据少就硬生生的暴力,我觉得20,50的数据范围绝对不会超时,果然ac了,但是性能只超过了百分之7。。emmmmm....其实别的实现方法也有好多,如说记录出现的下标,然后下标比对什么的。再来一种实现方式吧:

    class Solution {
        public List<String> findAndReplacePattern(String[] words, String pattern) {
            int[] temp = getStr(pattern);
            List<String> ans = new ArrayList<String>();
            for(String s:words) {
                int[] cur = getStr(s);
                boolean flag = true;
                for(int i = 0;i<cur.length;i++) {
                    if(cur[i] != temp[i]) {
                        flag = false;
                        break;
                    }
                }
                if(flag) ans.add(s);
            }
            return ans;
        }
        public int[] getStr(String str) {
            int[] c = new int[str.length()];
            char[] s = str.toCharArray();
            int temp = 1;
            for(int i = 0;i<s.length;i++) {
                if(c[i] != 0) continue;
                c[i] = temp++;
                for(int j = i+1;j<s.length;j++) {
                    if(c[j] != 0) continue;
                    if(s[j] == s[i]) c[j] = c[i];
                }
            }
            return c;
        }
    }
    

    改了下思路,直接用染色的思路实现了,去掉了来回来去字符串操作,性能一下子就上来了。虽然实际上代码看着多了。但是这种实现比较好理解。思路很简单,一个元素代表一个数字。从前往后出现的依次递加。这样哪怕不是一样的字符但是格式一样数组也应该一样。
    本篇笔记就记到这里了,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利,生活健健康康,周末愉快哟~!

    相关文章

      网友评论

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

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