【优选算法---前缀和】和为K的子数组、和可被K整除的子数组、连续数组、矩阵区域和

发布于:2024-12-20 ⋅ 阅读:(8) ⋅ 点赞:(0)

一、和为K的子数组

题目链接:

560. 和为 K 的子数组 - 力扣(LeetCode)

题目介绍:

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 

子数组是数组中元素的连续非空序列。

  • 1 <= nums.length <= 2 * 104
  • -1000 <= nums[i] <= 1000
  • -107 <= k <= 107
思路:

对于暴力解法来说,我们需要确定一个起始下标,然后向后遍历,如果碰到一个区间之和为K了,记录一下,但是此时还需要向后遍历,因为数组中的元素可能存在0和负数,这样暴力解法的时间复杂度为O(n^2).

如果我们可以创建出一个前缀和数组,dp[i]表示0~i位置的和。那我们就可以通过访问一遍数组下标,以i为数组终点,我们要找一个下标到i位置的和为k,那0到这个下标之前的一个元素的和就是的dp[i]-k。

也就是说,我们要找以i位置为终点,找i位置前有多少个前缀和等于dp[i]-k。但是仔细想一下,创建前缀和数组时间复杂度为O(n),遍历每一个下标时,还需要遍历它之前的前缀和数组,这样的时间复杂度为O(n^2),看起来比暴力解法还复杂。

其实我们只关心 i 位置之前,所以我们并不需要真正创建一个前缀和数组,而且我们也没必要遍历前缀和数组,只需要创建一个哈希表,维护i位置之前前缀和与个数的映射即可,这个过程可以一遍求前缀和,一遍建立映射。

hash[0]=1, 是因为 前缀和 为0的默认就有一个。假设0~i这段区间的和正好为k,那我们就要去(0~-1)这个区间找前缀和为0的个数了,但是这个区间是不存在的,为了加上这个情况,所以将hash[0]设置为1

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int,int> hash;
        hash[0]=1;
        int sum=0;
        int ret=0;
        for(auto e:nums)
        {
            sum+=e;
            if(hash.count(sum-k))
            ret+=hash[sum-k];
            hash[sum]++;
        }
        return ret;
    }
};

二、和可被K整除的子数组

题目链接:

974. 和可被 K 整除的子数组 - 力扣(LeetCode)

题目介绍:

给定一个整数数组 nums 和一个整数 k ,返回其中元素之和可被 k 整除的非空 子数组 的数目。

子数组 是数组中 连续 的部分。

  • 1 <= nums.length <= 3 * 10^4
  • -104 <= nums[i] <= 10^4
  • 2 <= k <= 10^4
思路:

在写这个题之前我们需要知道两个知识:

  • 同余定理:  (a-b)/p=k .....0   --->      a%p  ==  b%p。用文字叙述就是,如果两个数相减的差能被 n 整除,那么这两个数对 n 取模的结果相同。
  • c++修正负数%正数为负数: (a%p+P)%p   【加P是为了让他变为正数,在取模是为了本来是正数的受影响】

同余定理验证:

了解了同余定理,再看这个题目,我们要找一个区间(x~i)的和可以被k整除,假设0~x-1区间的和为a, x~i区间的和为b,题目的要求也就是 (b-a)%k=0.

由同余定理可得, [0, x - 1] 区间与 [0, i] 区间内的前缀和同余。于是问题就变成:

找到在 [0, i - 1] 区间内,有多少前缀和的余数等于 sum[i] % k 的即可

class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) {
        //同余定理: (a-b)/p=k .....0   ->      a%p  ==  b%p
        //c++修正负数%正数为负数: (a%p+P)%p
        unordered_map<int,int> hash;
        hash[0]=1;
        int sum=0,ret=0;
        for(auto e:nums)
        {
            sum+=e;
            int r=(sum%k+k)%k;
            if(hash.count(r))
            ret+=hash[r];
            hash[r]++;
        }
        return ret;
    }
};

 三、连续数组

题目链接:

525. 连续数组 - 力扣(LeetCode)

题目介绍:

给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。

思路:

如果将所有的0都换为-1,那这个题找的就是和为0的最长子数组,这个题就与和为K的子数组相同了,不一样的是这个题要的是最长子数组的长度。

思路依旧是以i为终点,找i之前前缀和为sum[i]的下标计算长度,这里有个问题,如果之前存在前缀合为sum的下标,后面就不能直接更新了,因为肯定是以前那个下标到i的距离长,只有不存在sum时才会更新sum映射的下标

class Solution {
public:
    int findMaxLength(vector<int>& nums) {
        unordered_map<int,int> hash;
        hash[0]=-1;
        int sum=0,ret=0;
        for(int i=0;i<nums.size();i++)
        {
            sum+=nums[i]==0?-1:1;
            if(hash.count(sum))
            ret=max(ret,i-hash[sum]);
            else
            hash[sum]=i;
        }
        return ret;
    }
};

四、矩阵区域和

题目链接:

1314. 矩阵区域和 - 力扣(LeetCode) 

题目介绍:

给你一个 m x n 的矩阵 mat 和一个整数 k ,请你返回一个矩阵 answer ,其中每个 answer[i][j] 是所有满足下述条件的元素 mat[r][c] 的和: 

  • i - k <= r <= i + k,
  • j - k <= c <= j + k 且
  • (r, c) 在矩阵内。

  • m == mat.length
  • n == mat[i].length
  • 1 <= m, n, k <= 100
  • 1 <= mat[i][j] <= 100
思路:

这个题光看示例很难理解,我们以一个例子来讲这个题是要干什么

假设我们要求answer[0][0]的值,k为1.

其实就是求从(0,0)向四周扩展1格,然后求这个扩展后的矩形所有元素的和

与二维前缀和那个题目思路相同,先创建出一个二维前缀和数组,dp[i][j]表示以(0,0)为左上角,(i,j)为右下角的矩形的和。想要计算某个区域的面积,只需要提供这个区域两个角的坐标就可以了。

但是构建二维前缀和数组和使用是与之前那个题不一样的,因为那个题的二维前缀和数组的下标适合矩形的下标正好对应的,但是这个题为了方便解决边界情况,我们还需要将二维前缀和数据多加一行一列,但是这个题给的矩阵是从(0,0)开始的,所以我们定义的这个数组要映射到矩阵的话就要减1,同理,从矩阵映射到数组的话就要加1。

另外,因为我们要求左上角和右下角的坐标,但我们向外扩展时,坐标可能会到边界外,需要控制一下,因为要使用dp表时,可以在计算下表时就把1都加上

class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
        int m=mat.size(),n=mat[0].size();
        vector<vector<int>> answer(mat.size(),vector<int>(mat[0].size()));
        vector<vector<int>> dp(mat.size()+1,vector<int>(mat[0].size()+1));
        //初始化前缀和
        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]+mat[i-1][j-1];
            }
        }

        for(int i=0;i<m;i++)
        {
            for(int j=0;j<n;j++)
            {
                int x1=max(0,i-k)+1;
                int y1=max(0,j-k)+1;
                int x2=min(m-1,i+k)+1;
                int y2=min(n-1,j+k)+1;
                answer[i][j]=dp[x2][y2]-dp[x2][y1-1]-dp[x1-1][y2]+dp[x1-1][y1-1];
            }
        }
        return answer;
    }
};