【Leetcode刷题】前缀和

本篇文章为 LeetCode 前缀和模块的刷题笔记,仅供参考。

一. 基础前缀和

Leetcode238.除自身以外数组的乘积

Leetcode238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请不要使用除法,且在 O(n) 时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
提示:
2 <= nums.length <= 105
-30 <= nums[i] <= 30
保证数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内

采用前缀和问题思想,使用 left 和 right 数组分别表示 nums[0…i] 和 nums[i…n] 的连续积:

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n=nums.size();
        vector<int> left(n);
        vector<int> right(n);
        for(int i=0;i<n;i++){
            if(i==0)    left[i]=nums[i];
            else        left[i]=nums[i]*left[i-1];
        }
        for(int i=n-1;i>=0;i--){
            if(i==n-1)  right[i]=nums[i];
            else        right[i]=nums[i]*right[i+1];
        }
        vector<int> ans(n);
        for(int i=0;i<n;i++){
            if(i==0)        ans[i]=right[i+1];
            else if(i==n-1) ans[i]=left[i-1];
            else            ans[i]=left[i-1]*right[i+1];
        }
        return ans;
    }
};

Leetcode304.二维区域和检索 - 矩阵不可变

Leetcode304.二维区域和检索 - 矩阵不可变
给定一个二维矩阵 matrix,以下类型的多个请求:

  • 计算其子矩形范围内元素的总和,该子矩阵的 左上角 为 (row1, col1) ,右下角 为 (row2, col2) 。

实现 NumMatrix 类:

  • NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
  • int sumRegion(int row1, int col1, int row2, int col2) 返回 左上角 (row1, col1) 、右下角 (row2, col2) 所描述的子矩阵的元素 总和 。

示例 1:
在这里插入图片描述
输入:
[“NumMatrix”,“sumRegion”,“sumRegion”,“sumRegion”]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
输出:
[null, 8, 11, 12]
解释:
NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)
提示:
m = matrix.length
n = matrix[i].length
1 <= m, n <= 200
-105 <= matrix[i][j] <= 105
0 <= row1 <= row2 < m
0 <= col1 <= col2 < n
最多调用 104 次 sumRegion 方法

二维前缀和问题,为了方便计算,使用 (m+1)*(n+1) 大小的 sums 数组表示前缀和

class NumMatrix {
public:
    vector<vector<int>> sums;
    NumMatrix(vector<vector<int>>& matrix) {
        int m=matrix.size();
        int n=matrix[0].size();
        vector<int> tmp(n+1);
        sums.resize(m+1,tmp);
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
                sums[i][j]=matrix[i-1][j-1]+sums[i-1][j]+sums[i][j-1]-sums[i-1][j-1];
            }
        }
    }
    
    int sumRegion(int row1, int col1, int row2, int col2) {
        return sums[row2+1][col2+1]-sums[row2+1][col1]-sums[row1][col2+1]+sums[row1][col1];
    }
};

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix* obj = new NumMatrix(matrix);
 * int param_1 = obj->sumRegion(row1,col1,row2,col2);
 */

二. 前缀和常见处理

Leetcode525.连续数组

Leetcode525.连续数组
给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
示例 2:
输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
提示:
1 <= nums.length <= 105
nums[i] 不是 0 就是 1

nums 数组中只有 0 和 1,较为简单,但前缀和处理后不容易用哈希表记录,因为元素都为正,不存在 0 这样的特殊值进行哈希。因此在 计算前缀和时将 0 改为 -1 计算,于是可以遍历 sums 的时候使用哈希,判断之前是否出现过 sums[i]:

class Solution {
public:
    int findMaxLength(vector<int>& nums) {
        int n=nums.size();
        vector<int> sums(n+1);
        for(int i=0;i<n;i++){
            int tmp=(nums[i]==0)?-1:1;		// 将 0 改为 -1 计算
            sums[i+1]=sums[i]+tmp;
        }
        int ans=0;
        unordered_map<int,int> mp;
        for(int i=0;i<=n;i++){
            if(mp.find(sums[i])!=mp.end())  ans=max(ans,i-mp[sums[i]]);
            if(mp.find(sums[i])==mp.end())  mp[sums[i]]=i;
        }
        return ans;
    }
};

Leetcode1124.表现良好的最长时间段

Leetcode1124.表现良好的最长时间段
给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。
我们认为当员工一天中的工作小时数大于 8 小时的时候,那么这一天就是「劳累的一天」。
所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。
请你返回「表现良好时间段」的最大长度。
示例 1:
输入:hours = [9,9,6,0,6,6,9]
输出:3
解释:最长的表现良好时间段是 [9,9,6]。
示例 2:
输入:hours = [6,6,6]
输出:0
提示:
1 <= hours.length <= 104
0 <= hours[i] <= 16

对 hours 数组处理同上,将大于 8 的元素修改为 1小于等于 8 的修改为 -1,这样就将问题转化成在 hours 数组中寻找最长连续累计和为正的子数组

最直接的思路是用哈希表处理,但 非负不是一个确定的状态值,所有大于等于 0 的值都可以构成非负,因此好像没法直接像【Leetcode523.连续的子数组和】用哈希表直接索引确定的数值。但由于本题对 hours 数组进行了处理,所有元素非 1 即 -1,那么一段连续子数组中的元素一定会经过首尾元素之间的每一个元素,因此 如果 sums[i] 前面有比 sums[i] 小的元素,那么一定会经过 sums[i-1]。但此时并不能说明 sums[i-1] 是离 sums[i] 最远的值,如 [9, 9, 9]。因此需要记录以 i 为结尾的连续累计和为正的子数组最大长度 v[i],于是 v[i]=i-mp[cursums-1]+(v[mp[cursums-1]]>0?v[mp[cursums-1]]:0)。为了保证 sums[i-1] 是离 sums[i] 最远的值,用哈希表只记录每个元素第一次出现的位置即可。

需要注意的是,每次寻找比 sums[i] 小的元素都是在 i 前面寻找,因此 在遍历过程中计算边计算前缀和边求解 就可以解决这个问题。

class Solution {
public:
    int longestWPI(vector<int>& hours) {
        // 处理hours数组
        int n=hours.size();
        for(int i=0;i<n;i++){
            hours[i]=hours[i]>8?1:-1;
        }
        // 计算前缀和并求解
        int cursums=0;              // 节省空间
        unordered_map<int,int> mp;  // sums[i]->i
        mp[0]=0;
        vector<int> v(n+1);         // i->maxlen[i]
        int ans=0;
        for(int i=1;i<=n;i++){
            cursums+=hours[i-1];
            if(mp.find(cursums-1)!=mp.end()){   // 出现过cursums-1
                int tmp=i-mp[cursums-1];
                if(v[mp[cursums-1]]>0)  tmp+=v[mp[cursums-1]];
                v[i]=tmp;           // 以hours[i]为结尾的最长表现良好的时间段
                ans=max(ans,tmp);
            }
            else{
                v[i]=0;             // 此前没有比cursums-1更小的元素
            }
            if(mp.find(cursums)==mp.end()){     // 只记录第一次出现的位置
                mp[cursums]=i;
            }
        }
        return ans;
    }
};

Leetcode1094.拼车

Leetcode1094.拼车
车上最初有 capacity 个空座位。车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向)
给定整数 capacity 和一个数组 trips , trip[i] = [numPassengersi, fromi, toi] 表示第 i 次旅行有 numPassengersi 乘客,接他们和放他们的位置分别是 fromi 和 toi 。这些位置是从汽车的初始位置向东的公里数。
当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false。
示例 1:
输入:trips = [[2,1,5],[3,3,7]], capacity = 4
输出:false
示例 2:
输入:trips = [[2,1,5],[3,3,7]], capacity = 5
输出:true
提示:
1 <= trips.length <= 1000
trips[i].length == 3
1 <= numPassengersi <= 100
0 <= fromi < toi <= 1000
1 <= capacity <= 105

法一:这是一道连续离散化问题,由于本题数据规模较小,完全可以使用长度 1001 的数组暴力枚举所有位置的乘客数量:

class Solution {
public:
    bool carPooling(vector<vector<int>>& trips, int capacity) {
        vector<int> path(1001);
        for(int i=0;i<trips.size();i++){
            for(int j=trips[i][1];j<trips[i][2];j++){
                path[j]+=trips[i][0];
            }
        }
        for(int i=0;i<1001;i++){
            if(path[i]>capacity)    return false;
        }
        return true;
    }
};

法二:采用 差分 的思想,使用数组 diff 记录每个节点的人数增减情况。然后遍历 diff 数组,该过程就相当于车向前行驶,passenger[i+1] = passenger[i] + diff[i+1]

class Solution {
public:
    bool carPooling(vector<vector<int>>& trips, int capacity) {
        vector<int> diff(1001);
        for(int i=0;i<trips.size();i++){
            diff[trips[i][1]]+=trips[i][0];
            diff[trips[i][2]]-=trips[i][0];
        }
        int passenger=diff[0];
        for(int i=0;i<1001;i++){
            passenger+=diff[i];
            if(passenger>capacity)  return false;
        }
        return true;
    }
};

三. 前缀和与其他模块结合

前缀和常与双指针、哈希、动态规划、有序集合等相结合。

Leetcode930.和相同的二元子数组

Leetcode930.和相同的二元子数组
给你一个二元数组 nums ,和一个整数 goal ,请你统计并返回有多少个和为 goal 的 非空 子数组。
子数组 是数组的一段连续部分。
示例 1:
输入:nums = [1,0,1,0,1], goal = 2
输出:4
解释:
有 4 个满足题目要求的子数组:[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1]
示例 2:
输入:nums = [0,0,0,0,0], goal = 0
输出:15
提示:
1 <= nums.length <= 3 * 104
nums[i] 不是 0 就是 1
0 <= goal <= nums.length

法一:一开始的思路是前缀和+ 双指针,但双指针的麻烦在于对于 left 和 right 都指向 0 元素的时候无法移动指针,因为移动任何一个指针都会导致答案的缺失:

class Solution {
public:
    int numSubarraysWithSum(vector<int>& nums, int goal) {
        // 计算前缀和
        int n=nums.size();
        vector<int> sums(n+1);
        for(int i=0;i<n;i++)    sums[i+1]=sums[i]+nums[i];
        // 双指针搜索
        int left=1,right=1;
        int ans=0;
        while(left<=n && right<=n){
            while(left<=right && sums[right]-sums[left-1]<=goal){
                if(sums[right]-sums[left-1]==goal){
                    ans++;
                }
                if(right<n) right++;
                else        break;
            }
            left++;
        }
        return ans;
    }
};

在测试样例 nums = [0,0,0,0,0], goal = 0 时解答错误,因此该思路不可取。

法二:考虑前缀和+ 哈希,用哈希表记录 sums[i] 的每个元素的个数,于是遍历 sums[i] 即可:

class Solution {
public:
    int numSubarraysWithSum(vector<int>& nums, int goal) {
        // 计算前缀和
        int n=nums.size();
        vector<int> sums(n+1);
        for(int i=0;i<n;i++)    sums[i+1]=sums[i]+nums[i];
        // 哈希搜索
        int ans=0;
        unordered_map<int,int> mp;
        for(int i=0;i<=n;i++){
            if(mp.find(sums[i]-goal)!=mp.end()) ans+=mp[sums[i]-goal];
            mp[sums[i]]++;
        }
        return ans;
    }
};

Leetcode523.连续的子数组和

Leetcode523.连续的子数组和
给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:
子数组大小 至少为 2 ,且
子数组元素总和为 k 的倍数。
如果存在,返回 true ;否则,返回 false 。
如果存在一个整数 n ,令整数 x 符合 x = n * k ,则称 x 是 k 的一个倍数。0 始终视为 k 的一个倍数。
示例 1:
输入:nums = [23,2,4,6,7], k = 6
输出:true
解释:[2,4] 是一个大小为 2 的子数组,并且和为 6 。
示例 2:
输入:nums = [23,2,6,4,7], k = 6
输出:true
解释:[23, 2, 6, 4, 7] 是大小为 5 的子数组,并且和为 42 。
42 是 6 的倍数,因为 42 = 7 * 6 且 7 是一个整数。
示例 3:
输入:nums = [23,2,6,4,7], k = 13
输出:false
提示:
1 <= nums.length <= 105
0 <= nums[i] <= 109
0 <= sum(nums[i]) <= 231 - 1
1 <= k <= 231 - 1

先计算数组的前缀和 sums,随后问题就变成了寻找满足条件的 i 和 j 使得 sums[j] - sums[i] = n * k,即 j 之前存在元素和 sums[j] 同余。用 哈希表 记录即可:

class Solution {
public:
    bool checkSubarraySum(vector<int>& nums, int k) {
        // 计算前缀和
        int n=nums.size();
        vector<int> sums(n+1);
        for(int i=0;i<n;i++)    sums[i+1]=sums[i]+nums[i];
        // 计算连续数组和
        unordered_map<int,int> mp;
        mp[0]=0;
        for(int i=1;i<=n;i++){
            if(mp.find(sums[i]%k)!=mp.end() && mp[sums[i]%k]<i-1){
                return true;
            }   
            if(mp.find(sums[i]%k)==mp.end()){	// 保留下标最小的i
                mp[sums[i]%k]=i;
            }
        }
        return false;
    }
};

Leetcode813.最大平均值和的分组

Leetcode813.最大平均值和的分组
给定数组 nums 和一个整数 k 。我们将给定的数组 nums 分成 最多 k 个非空子数组,且数组内部是连续的 。 分数 由每个子数组内的平均值的总和构成。
注意我们必须使用 nums 数组中的每一个数进行分组,并且分数不一定需要是整数。
返回我们所能得到的最大 分数 是多少。答案误差在 10-6 内被视为是正确的。
示例 1:
输入: nums = [9,1,2,3,9], k = 3
输出: 20.00000
解释:
nums 的最优分组是[9], [1, 2, 3], [9]. 得到的分数是 9 + (1 + 2 + 3) / 3 + 9 = 20.
我们也可以把 nums 分成[9, 1], [2], [3, 9].
这样的分组得到的分数为 5 + 2 + 6 = 13, 但不是最大值.
示例 2:
输入: nums = [1,2,3,4,5,6,7], k = 4
输出: 20.50000
提示:
1 <= nums.length <= 100
1 <= nums[i] <= 104
1 <= k <= nums.length

本题显然具有 0 - 1 背包的影子,即分组不多于 k 的情况下求出最大均值和,因此使用 动态规划 求解。为了简化动态规划中遍历元素进行叠加的过程,先计算其前缀和 sums 再进行 dp。

使用 dp[i][j] 表示前 i 个元素在分为 j 组的情况下能够得到的最大平均值和,因此有状态转移方程:
d p [ i ] [ j ] = { − ∞ i < j 或 i = 0 或 j = 0 s u m s [ i ] / i j = 1 m a x { d p [ t ] [ j − 1 ] + ( s u m s [ i ] − s u m s [ t ] ) / ( i − t ) } ( 1 ≤ t < i ) i ≥ j dp[i][j] = \begin{cases} -\infty & i < j 或 i = 0 或 j = 0 \\ sums[i]/i & j=1 \\ max\{dp[t][j-1] + (sums[i]-sums[t])/(i-t)\}(1\leq t < i) & i \geq j \\ \end{cases} dp[i][j]= sums[i]/imax{dp[t][j1]+(sums[i]sums[t])/(it)}(1t<i)i<ji=0j=0j=1ij

class Solution {
public:
    double largestSumOfAverages(vector<int>& nums, int k) {
        // 计算前缀和
        int n=nums.size();
        vector<int> sums(n+1);
        sums[0]=0;
        for(int i=0;i<n;i++)    sums[i+1]=sums[i]+nums[i];
        // 初始化
        vector<vector<double>> dp;
        vector<double> tmp(k+1);
        dp.resize(n+1,tmp);
        // dp
        for(int i=0;i<=n;i++){
            for(int j=0;j<=k;j++){
                if(i==0 || j==0 || i<j){
                    dp[i][j]=DBL_MIN;
                }else if(j==1){
                    dp[i][1]=(double)sums[i]/i;
                }else{
                    double maxval=DBL_MIN;
                    for(int t=1;t<i;t++){
                        double tmp=dp[t][j-1]+(double)(sums[i]-sums[t])/(i-t);
                        maxval=max(tmp,maxval);
                    }
                    dp[i][j]=maxval;
                }
                
            }
        }
        return dp[n][k];
    }
};

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

Leetcode363.矩形区域不超过 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
提示:
m = matrix.length
n = matrix[i].length
1 <= m, n <= 100
-100 <= matrix[i][j] <= 100
-105 <= k <= 105

显然需要计算前缀和,最大数值和 = max{sums[i][j] - sums[p][q]}。但是遍历搜索 (i, j, p, q) 的时间复杂度是 O(m2*n2),需要降低。

这是一道经典的前缀和矩阵的模板题:先在 O(m2) 的时间内 上下定维,然后在小于 O(n2) 的时间内解决问题。对于 [i, j] 行范围内的元素,遍历 sums 的列,想要找到一个最小的大于等于 sums[j][k]-k 的元素。因此使用 set,set 可以自动排序,使用 lower_bound 函数即可找出最小的大于等于 sums[j][k]-k 的元素:
在这里插入图片描述

class Solution {
public:
    int maxSumSubmatrix(vector<vector<int>>& matrix, int k) {
        // 计算前缀和
        int m=matrix.size();
        int n=matrix[0].size();
        vector<vector<int>> sums;
        vector<int> tmp(n+1);
        sums.resize(m+1,tmp);
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
                sums[i][j]=matrix[i-1][j-1]+sums[i-1][j]+sums[i][j-1]-sums[i-1][j-1];
            }
        }
        // 计算最大和
        int ans=INT_MIN;
        for(int i=1;i<=m;i++){      // 确定上维
            for(int j=i;j<=m;j++){  // 确定下维
                set<int> st;
                st.insert(0);
                for(int t=1;t<=n;t++){
                    int cur=sums[j][t]-sums[i-1][t];    // matrix[i][1]~matrix[j][t]之和
                    auto it=st.lower_bound(cur-k);
                    if(it!=st.end()){
                        ans=max(ans,cur-*it);
                    }
                    st.insert(cur);
                }
            }
        }
        return ans;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值