LeetCode1074 / 560 / 1248 / 363 / 剑指 Offer 68 - II

本文介绍了如何高效地计算矩阵中元素和为目标值的子矩阵数量。通过前缀和与哈希表的结合,将时间复杂度降低,解决了O(m^2n^2)的暴力解法问题。同时,对比了不同解决方案,包括1074题的思路和363题的优化技巧,以及类似问题如求子数组和、优美子数组等的处理方法。
摘要由CSDN通过智能技术生成

1074. 元素和为目标值的子矩阵数量

题目描述
给出矩阵 matrix 和目标值 target,返回元素总和等于目标值的非空子矩阵的数量。

子矩阵 x1, y1, x2, y2 是满足 x1 <= x <= x2 且 y1 <= y <= y2 的所有单元 matrix[x][y] 的集合。

如果 (x1, y1, x2, y2) 和 (x1', y1', x2', y2') 两个子矩阵中部分坐标不同(如:x1 != x1'),那么这两个子矩阵也不同。

示例 1:
在这里插入图片描述

输入:matrix = [[0,1,0],[1,1,1],[0,1,0]], target = 0
输出:4
解释:四个只含 0 的 1x1 子矩阵。
示例 2:

输入:matrix = [[1,-1],[-1,1]], target = 0
输出:5
解释:两个 1x2 子矩阵,加上两个 2x1 子矩阵,再加上一个 2x2 子矩阵。
示例 3:

输入:matrix = [[904]], target = 0
输出:0

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-submatrices-that-sum-to-target
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

前缀和加四层循环,我看着矩阵大小估摸着好像能过,就写了一下,没想到还真过了,百分之34,时间复杂度O(m2n2)

class Solution {
    public int numSubmatrixSumTarget(int[][] matrix, int target) {
        //暴力解,求解一个前缀和数组,然后遍历起点和终点位置,求和
        //我感觉到是动态规划,因为只需要求矩阵的数目,而不需要具体多少
        int m = matrix.length;
        int n = matrix[0].length;
        //以当前位置为起点,右下角的矩阵
        int[][] pre = new int[m + 1][n + 1];
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                pre[i][j] = pre[i - 1][j] + pre[i][j - 1] + matrix[i - 1][j - 1] - pre[i - 1][j - 1];
            }
        }
        int count = 0;
        //起点i,j
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                //终点t,k
                for(int t = i; t < m; t++){
                    for(int k = j; k < n; k++){
                        if(pre[t + 1][k + 1] - pre[t + 1][j] - pre[i][k + 1] + pre[i][j] == target)
                            count++;
                    }
                }
            }
        }
        return count;
    }
}

看了答案,感觉又学到了,上个月363基本用过相同的思路,但是奈何当时没感觉,看来这样处理矩阵和也是一种套路
什么套路呢,就是先遍历上下边界(或者左右,主要看哪个长度小),然后计算此边界内,每列元素的和,将它存储到一个数组中。
这时,问题就变成了给定一个数组,和一个目标值,计算该数组内和为目标值的子数组的个数
这个问题的处理就用前缀和加哈希表能快速处理,也就是遍历数组中的元素,将遍历到元素之和存储在哈希表中,如果当前前缀和减去目标值也在哈希表中存在,那么就说明找到了一个子矩阵,使得子矩阵之和为目标值
具体呢,看代码吧:

class Solution {
    public int numSubmatrixSumTarget(int[][] matrix, int target) {
        //暴力解,求解一个前缀和数组,然后遍历起点和终点位置,求和
        //我感觉到是动态规划,因为只需要求矩阵的数目,而不需要具体多少
        int m = matrix.length;
        int n = matrix[0].length;
        int res = 0;    //最终结果
        //遍历上边界
        for(int i = 0; i < m; i++){
            //存储当前处理的每一列的和
            int[] sum = new int[n];
            //遍历下边界
            for(int j = i; j < m; j++){
                for(int k = 0; k < n; k++){
                    sum[k] += matrix[j][k];
                }
                //此时sum中存储的是i到j行,每一列元素的和
                //问题就变成了求子数组的和
                int pre = 0;    //前缀和
                Map<Integer, Integer> map = new HashMap<>();
                //这里为什么要预先放个0进去,是因为如果当前pre就等于目标值了,如果不放这个进去会统计不到这个值
                map.put(0, 1);
                for(int num : sum){
                    pre += num;
                    //如果存在pre - target,那么结果++
                    if(map.containsKey(pre - target)){
                        res += map.get(pre - target);
                    }
                    map.put(pre, map.getOrDefault(pre, 0) + 1);
                }
            }
        }
        return res;
    }
}

或者说,用363中三叶姐的那种写法,先处理二维矩阵的前缀和,然后再用上面相同的思路,即确定三个边界,来加速结果的查找,这样更好理解,只不过空间复杂度大了点

class Solution {
    public int numSubmatrixSumTarget(int[][] matrix, int target) {
        //暴力解,求解一个前缀和数组,然后遍历起点和终点位置,求和
        //我感觉到是动态规划,因为只需要求矩阵的数目,而不需要具体多少
        int m = matrix.length;
        int n = matrix[0].length;
        int res = 0;    //最终结果
        //前缀和
        int[][] pre = new int[m + 1][n + 1];
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                pre[i][j] = pre[i - 1][j] + pre[i][j - 1] + matrix[i - 1][j - 1] - pre[i - 1][j - 1];
            }
        }
        //遍历上边界
        for(int i = 1; i <= m; i++){
            //遍历下边界
            for(int j = i; j <= m; j++){
                //哈希表存储,前缀和->出现的次数
                Map<Integer, Integer> map = new HashMap<>();
                //对于当前列
                for(int k = 1; k <= n; k++){
                    //计算前缀和
                    int right = pre[j][k] - pre[i - 1][k];
                    //这句话可以和上面map.put(0,1)互换,一个道理
                    if(right == target) res++;
                    if(map.containsKey(right - target)){
                        res += map.get(right - target);
                    }
                    map.put(right, map.getOrDefault(right, 0) + 1);
                }
            }
        }
        return res;
    }
}

最后,总结一下,毕竟也算一个类型题了。
对于二维数组中子矩阵和的查找,最容易想到的方法就是遍历四个边,时间复杂度是四次方。
而优化的方法呢,是遍历三个边,即通过三个边的处理,将这个问题转化成已知一个数组,求数组中子数组和为目标值的问题,然后就能通过前缀和加哈希表的思想来加速处理,使得时间复杂度降低

再把两道相似的题看一下:

560. 和为K的子数组

题目描述
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

示例 1 :

输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/subarray-sum-equals-k
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

刚刚那道题的前置题,懂了思路秒A

class Solution {
    public int subarraySum(int[] nums, int k) {
        int pre = 0;
        int res = 0;
        Map<Integer, Integer> map = new HashMap<>();
        for(int num : nums){
            pre += num;
            if(pre == k)
                res++;
            if(map.containsKey(pre - k))
                res += map.get(pre - k);
            map.put(pre, map.getOrDefault(pre, 0) + 1);
        }
        return res;
    }
}

1248. 统计「优美子数组」

题目描述
给你一个整数数组 nums 和一个整数 k。

如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。

请返回这个数组中「优美子数组」的数目。


示例 1:

输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。
示例 2:

输入:nums = [2,4,6], k = 1
输出:0
解释:数列中不包含任何奇数,所以不存在优美子数组。
示例 3:

输入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2
输出:16

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/count-number-of-nice-subarrays
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

和上面一样,主要练习这种前缀和+哈希表优化的思想
当然,滑动窗口也可以

class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        int pre = 0;
        int res = 0;
        int[] sum = new int[nums.length + 1];
        sum[0] = 1;
        for(int num : nums){
            if((num & 1) == 1)
                pre += 1;
            if(pre - k >= 0)
                res += sum[pre - k];
            sum[pre]++;
        }
        return res;
    }
}

363. 矩形区域不超过 K 的最大数值和

题目描述
给你一个 m x n 的矩阵 matrix 和一个整数 k ,找出并返回矩阵内部矩形区域的不超过 k 的最大数值和。

题目数据保证总会存在一个数值和不超过 k 的矩形区域。

示例 1:
在这里插入图片描述

输入:matrix = [[1,0,1],[0,-2,3]], k = 2
输出:2
解释:蓝色边框圈出来的矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。
示例 2:

输入:matrix = [[2,2,-1]], k = 3
输出:3

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

忘了具体什么时候的每日一题了,那时候应该还没有开始写csdn哈哈
基本上和今天的每日一题一样,这个题找的是数值和不超过k的矩阵区域,那么在遍历右边界的时候,因为矩阵中的值存在负数,所以不能保证是单调递增的,这时需要将数据结构由哈希表变成一个可以排序的数据结构,那么就可以用二分查找找到对应的最小值

class Solution {
    public int maxSumSubmatrix(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;

        // 预处理前缀和,二维矩阵的前缀和,这里简单
        int[][] sum = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }

        //将二维问题转化为一维问题,就是固定三个边界,找另一个边界
        int ans = Integer.MIN_VALUE;
        // 遍历子矩阵的上边界
        for (int top = 1; top <= m; top++) {
            // 遍历子矩阵的下边界
            for (int bot = top; bot <= m; bot++) {
                // 使用「有序集合」维护所有遍历到的右边界
                TreeSet<Integer> ts = new TreeSet<>();
                ts.add(0);
                // 遍历子矩阵的右边界,这里为什么从1开始,是因为上面的前缀和矩阵就是从1开始的
                for (int r = 1; r <= n; r++) {
                    // 通过前缀和计算 right,即上下边界中到0到right部分的和
                    int right = sum[bot][r] - sum[top - 1][r];
                    // 通过二分找 left,找到大于right- k的最小值
                    Integer left = ts.ceiling(right - k);
                    //如果left存在,那么就更新ans
                    if (left != null) {
                        int cur = right - left;
                        ans = Math.max(ans, cur);
                    }
                    // 将遍历过的 right 加到有序集合
                    ts.add(right);
                }
            }
        }
        return ans;
    }
}

再贴一个三叶姐空间时间都优化过的代码

class Solution {
    public int maxSumSubmatrix(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;
        boolean isRight = n > m;
        int[] sum = isRight ? new int[n + 1] : new int[m + 1];
        int ans = Integer.MIN_VALUE;
        for (int i = 1; i <= (isRight ? m : n); i++) {
            Arrays.fill(sum, 0);
            for (int j = i; j <= (isRight ? m : n); j++) {
                TreeSet<Integer> ts = new TreeSet<>();
                ts.add(0);
                int a = 0;
                for (int fixed = 1; fixed <= (isRight ? n : m); fixed++) {
                    sum[fixed] += isRight ? mat[j - 1][fixed - 1] : mat[fixed - 1][j - 1] ;
                    a += sum[fixed];
                    Integer b = ts.ceiling(a - k);
                    if (b != null) {
                        int cur = a - b;
                        ans = Math.max(ans, cur);
                    }
                    ts.add(a);
                }
            }
        }
        return ans;
    }
}

作者:AC_OIer
链接:https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k/solution/gong-shui-san-xie-you-hua-mei-ju-de-ji-b-dh8s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 68 - II. 二叉树的最近公共祖先

题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]

在这里插入图片描述

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

刚开始想到的思路是,后序遍历的同时,用两个指针表示是否已经找到了对应的值,然后如果在某一个结点同时两个指针都是true,就返回这个结点。当然,问题很大,问题出在这样找的话,如果找到了第二个值就会直接返回第二个值所在的结点
于是,就想怎么标记当前已经找到了对应的结点,即在一颗子树中有相应的结点,然后就想到了给递归加返回值,也就是如果在找的过程中,已经找到了一个节点,就将该结点的父节点都标记为true,如果在某个结点,左右标记都为true或者一个为true的同时,当前结点的值也为一个要找的值,就将这个结点输出。后面的所有结点就不用遍历了。直接返回

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    TreeNode res = null;

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //怎么找一个祖先呢,就是先找到这两个值,然后用两个标记分别标记这两个值是否找到了
        //后序遍历

        helper(root, p.val, q.val);
        return res;
    }

    public boolean helper(TreeNode root, int p, int q){
        boolean t = false;
        if(root == null)
            return false;       
        boolean left = helper(root.left, p, q);
        //如果已经找到,直接返回
        if(res != null)
            return true;
        boolean right = helper(root.right, p, q);
        //如果已经找到,直接返回
        if(res != null)
            return true;
        //如果左右子树都找到了相应的值,那么这个结点就是要找的祖先
        if(left && right){
            res = root;
            return true;
        }
        //处理当前结点
        if(root.val == p)
            t = true;
        if(root.val == q)
            t = true;
        //如果左右有一个为true,并且当前结点也是要找的值,说明当前结点就是要找的祖先
        if((left || right) && t){
            res = root;
            return true;
        }
        //如果左右有true,返回true
        if(left || right)
            return true;
        //以上都不成立,返回当前结点的结果
        return t;
    }
}

然后看了一下三个月前写的代码,是这样写的
这样写会有一个问题,就是说如果这棵树中只有一个要找的结点,那么这种写法就不行了,因为并没有对所有的结点都进行遍历。
但是这个题中,规定了“p、q 为不同节点且均存在于给定的二叉树中”。所以这种写法可以,如果有一个结点等于了p或者q,那么以这个结点为根的子树的所有节点就不用遍历。这样会更加简洁
但是我刚刚写的和官解给的方法是一样的,这样更通用一点

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
   
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //另外一个方法,就是仔细思考一下:
        //怎么说呢,就是前序遍历找对应的pq,对于一个节点,如果其左右节点都不是pq,那么这个节点返回空
        //如果找到了pq,那么返回值对应就是p或者q

        //返回值
        //当root是其中一个时,返回root
        if(root == null || root == p || root == q){
            return root;
        }

        //前序遍历
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);

        //第一种情况,如果左右节点都不为空,说明该节点是公共节点
        /*
        if(left != null && right != null)
            return root;
        //第二种情况,如果都为空,说明没有,返回空
        if(left == null && right == null)
            return null;
        //如果单侧为空,说明另一侧有
        if(left == null && right != null)
            return right;
        if(left != null && right == null)
            return left;
        return root;
        */
        //合并一下情况
        if(left == null)
            return right;
        if(right == null)
            return left;
        return root;
    }

    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值