11.6-11.14力扣前缀和刷题
前缀和相关知识
1. 基本概念
假设有一个原数组 arr,长度为 n,我们构建一个前缀和数组 pre,长度为 n+1,其中:
pre[0] = 0
pre[i] = arr[0] + arr[1] + ... + arr[i-1],对于 i 从 1 到 n
这样,原数组中区间 [l, r](下标从 0 开始)的和可以通过前缀和数组计算:
sum(l, r) = pre[r+1] - pre[l]
例子:
假设 arr = [1, 2, 3, 4],则前缀和数组为:
pre[0] = 0
pre[1] = 1
pre[2] = 1 + 2 = 3
pre[3] = 1 + 2 + 3 = 6
pre[4] = 1 + 2 + 3 + 4 = 10
要计算区间 [1, 3] 的和(即元素 2、3、4):
sum(1, 3) = pre[4] - pre[1] = 10 - 1 = 9,正确。
2. 二维前缀和
前缀和可以扩展到二维数组(矩阵),用于快速计算子矩阵的和。设原矩阵 mat 大小为 m x n,我们构建一个二维前缀和数组 pre,大小为 (m+1) x (n+1),其中:
pre[i][j] 表示从 mat[0][0] 到 mat[i-1][j-1] 的所有元素之和。
构建公式:
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + mat[i-1][j-1]
查询子矩阵从 (r1, c1) 到 (r2, c2) 的和(下标从 0 开始):
sum = pre[r2+1][c2+1] - pre[r1][c2+1] - pre[r2+1][c1] + pre[r1][c1]
例子:
假设 mat = [[1, 2, 3], [4, 5, 6], [7, 8, 9]],则二维前缀和数组为:
pre = [ [0, 0, 0, 0], [0, 1, 3, 6], [0, 5, 12, 21], [0, 12, 27, 45] ]
要计算子矩阵 [1,1] 到 [2,2](即元素 5, 6, 8, 9)的和:
-
sum = pre[3][3] - pre[1][3] - pre[3][1] + pre[1][1] = 45 - 6 - 12 + 1 = 28,正确。
【1】303. 区域和检索 - 数组不可变
日期:11.6
1.题目链接:303. 区域和检索 - 数组不可变 - 力扣(LeetCode)
https://leetcode.cn/problems/range-sum-query-immutable/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:设计,数组,前缀和
3.方法:前缀和(一次题解)
大小:n + 1(比原数组多一个元素)
含义:
sums[0] = 0
sums[1] = nums[0]
sums[2] = nums[0] + nums[1]
...
sums[i] = nums[0] + nums[1] + ... + nums[i-1]
关键代码:
NumArray(vector<int>& nums) {int n=nums.size();sums.resize(n+1);for(int i=0;i<n;i++){sums[i+1]=sums[i]+nums[i];}}int sumRange(int i,int j){return sums[j+1]-sums[i];}
【2】528. 按权重随机选择
日期:11.7
1.题目链接:528. 按权重随机选择 - 力扣(LeetCode)
https://leetcode.cn/problems/random-pick-with-weight/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,二分查找,数学
3.方法:前缀和+二分查找(官方题解)
计算权重的前缀和,将权重映射到数轴上的区间
生成一个在 [1, 总权重] 范围内的随机数
使用二分查找找到随机数落在哪个区间,对应的索引就是结果

关键代码:
private:mt19937 gen; // 随机数生成器uniform_int_distribution<int> dis; // 均匀分布vector<int> pre; // 前缀和数组
public:// 初始化随机数生成器和前缀和数组Solution(vector<int>& w): gen(random_device{}()),dis(1, accumulate(w.begin(), w.end(), 0)) {partial_sum(w.begin(),w.end(),back_inserter(pre));}// 根据权重随机选择索引int pickIndex(){int x=dis(gen); // 生成随机数return lower_bound(pre.begin(),pre.end(), x)-pre.begin();}
【3】713. 乘积小于 K 的子数组
日期:11.8
1.题目链接:713. 乘积小于 K 的子数组 - 力扣(LeetCode)
https://leetcode.cn/problems/subarray-product-less-than-k/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,二分查找,滑动窗口
3.方法一:二分查找(官方题解)
核心洞察:利用对数的性质将乘积问题转换为求和问题
乘积:nums[i] × nums[i+1] × ... × nums[j] < k
取对数:log(nums[i]) + log(nums[i+1]) + ... + log(nums[j]) < log(k)
这样就将乘积小于k的问题转换成了对数和小于log(k)的问题
关键代码:
vector<double> logPrefix(n+1);for(int i=0;i<n;i++){logPrefix[i+1]=logPrefix[i]+log(nums[i]);}double logk=log(k); // k的对数值int ret=0; // 遍历每个可能的子数组结束位置jfor(int j=0;j<n;j++){// 使用二分查找找到满足条件的最小起始位置lint l=upper_bound(logPrefix.begin(), logPrefix.begin()+j+1, logPrefix[j+1]-logk+1e-10)-logPrefix.begin();ret +=j+1-l; // 统计以j结尾的满足条件的子数组数量}
4.方法二:滑动窗口(一次题解)
维护一个窗口 [i, j],保证窗口内所有元素的乘积 < k
当加入新元素导致乘积 >= k 时,从左侧移除元素直到乘积 < k
对于每个位置 j,以 j 结尾的满足条件的子数组数量为 j - i + 1
关键代码:
int n=nums.size(),ret=0; int prod=1,i=0; for(int j=0;j<n;j++){prod *=nums[j]; // 将当前元素加入窗口,更新乘积 // 当乘积超过等于k时,移动左边界缩小窗口while(i<=j&&prod>=k){prod/=nums[i]; // 移除左边界元素i++; // 左边界右移}ret += j-i+1;}return ret;}
【4】724. 寻找数组的中心下标
日期:11.9
1.题目链接:724. 寻找数组的中心下标 - 力扣(LeetCode)
https://leetcode.cn/problems/find-pivot-index/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,数组
3.方法一:前缀和(一次题解)
记数组的全部元素之和为 total,当遍历到第 i 个元素时,设其左侧元素之和为 sum,则其右侧元素之和为 total−nums i −sum。左右侧元素相等即为 sum=total−nums i−sum,2×sum+numsi=total。
当中心索引左侧或右侧没有元素时,即为零个项相加,这在数学上称作「空和」(empty sum)。在程序设计中约定「空和是零」
关键代码:
int total=accumulate(nums.begin(),nums.end() 0);int sum=0;for (int i=0;i<nums.size();++i){if(2*sum+nums[i]==total){return i;}sum+=nums[i];}return -1;
【5】813. 最大平均值和的分组
日期:11.10
1.题目链接:813. 最大平均值和的分组 - 力扣(LeetCode)
https://leetcode.cn/problems/largest-sum-of-averages/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,数组,动态规划
3.方法一:前缀和(半解)
为了方便计算子数组的平均值,使用一个数组 prefix 来保存数组 nums 的前缀和。使用 dp[i][j] 表示 nums 在区间 [0,i−1] 被切分成 j 个子数组的最大平均值和,显然 i≥j,计算分两种情况讨论:
当 j=1 时,dp[i][j] 是对应区间 [0,i−1] 的平均值;
当 j>1 时,我们将可以将区间 [0,i−1] 分成 [0,x−1] 和 [x,i−1] 两个部分,其中 x≥j−1,那么 dp[i][j] 等于所有这些合法的切分方式的平均值和的最大值。
因此转移方程为:

关键代码:
vector<double> prefix(n+1);for(int i=0;i<n;i++){prefix[i+1]=prefix[i]+nums[i];} // 初始化DP数组vector<double> dp(n+1);for(int i=1;i<=n;i++){dp[i]=prefix[i]/i; // 前i个元素的平均值(只分1组)} // 动态规划,考虑分组数从2到kfor(int j=2;j<=k;j++){// 从后往前更新,避免覆盖当前轮次需要使用的值for(int i=n;i>=j;i--){for(int x=j-1;x<i;x++){// 状态转移:前x个元素分j-1组,剩下的元素作为第j组dp[i]=max(dp[i], dp[x]+(prefix[i]-prefix[x])/(i-x));}}}
【6】930. 和相同的二元子数组
日期:11.11
1.题目链接:930. 和相同的二元子数组 - 力扣(LeetCode)
https://leetcode.cn/problems/binary-subarrays-with-sum/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,数组,哈希表,滑动窗口
3.方法一:哈希表(一次题解)
使用前缀和记录从开始到当前位置的累加和
使用哈希表记录每个前缀和出现的次数
对于每个位置,检查是否存在前缀和等于 当前前缀和 - goal
关键代码:
for(auto& num : nums){cnt[sum]++; // 记录当前前缀和出现的次数sum+=num; // 更新前缀和ret+=cnt[sum-goal]; // 统计满足条件的子数组数量}
4.方法二:滑动窗口(半解)
维护两个滑动窗口 [left1, right] 和 [left2, right]
第一个窗口保证和 <= goal,第二个窗口保证和 < goal
两个窗口左指针的差值就是和为 goal 的子数组数量
left1:保证 [left1, right] 区间和 <= goal 的最小左边界
left2:保证 [left2, right] 区间和 < goal 的最小左边界
right:当前右边界
所有以 right 结尾且和为 goal 的子数组,其左边界必须在 [left1, left2-1] 范围内
如果左边界 < left1,那么和 > goal
如果左边界 >= left2,那么和 < goal
所以左边界在 [left1, left2-1] 时,和正好等于 goal
关键代码:
while(right<n){// 更新两个窗口的和(加入当前右指针元素)sum1+=nums[right];sum2+=nums[right]; // 调整第一个左指针:确保窗口[left1, right]的和 <= goalwhile(left1<=right&&sum1>goal){sum1-=nums[left1];left1++;} // 调整第二个左指针:确保窗口[left2, right]的和 < goalwhile(left2<=right&&sum2>=goal){sum2-=nums[left2];left2++;} // 关键:统计满足条件的子数组数量ret+=left2-left1;right++;}
【7】1109. 航班预订统计
日期:11.12
1.题目链接:1109. 航班预订统计 - 力扣(LeetCode)
https://leetcode.cn/problems/corporate-flight-bookings/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,数组
3.方法一:哈希表(半解)
差分数组的核心思想是:
要给区间 [l, r] 的所有元素加上值 val 时
只需要在 l 位置加上 val,在 r+1 位置减去 val
最后通过前缀和计算,这个区间的所有元素都会自动加上 val
差分数组技巧:
通过标记区间开始和结束的位置,避免对每个区间内的每个元素单独更新
最后通过一次前缀和计算得到最终结果
// 第一步:处理每个预订,构建差分数组for(auto& booking : bookings){int first=booking[0]-1; // 起始航班索引(转为0-based)int last=booking[1]-1; // 结束航班索引(转为0-based)int seats=booking[2]; nums[first]+=seats;// 在结束位置的下一个位置减去座位数if(last+1<n){nums[last+1]-=seats;}} // 第二步:通过前缀和计算每个航班的实际座位数for(int i=1;i<n;i++){nums[i]+=nums[i-1];}
【8】1413. 逐步求和得到正数的最小值
日期:11.12
1.题目链接:1413. 逐步求和得到正数的最小值 - 力扣(LeetCode)
https://leetcode.cn/problems/minimum-value-to-get-positive-step-by-step-sum/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,数组,贪心,二分查找
3.方法一:贪心(一次题解)
要保证所有的累加和 accSum 满足 accSum+startValue≥1,只需保证累加和的最小值 accSumMin 满足 accSumMin+startValue≥1,那么 startValue 的最小值即可取 −accSumMin+1。
int accSum=0,accSumMin=0;for(int num : nums){accSum+=num;accSumMin=min(accSumMin,accSum);}return -accSumMin+1;
【9】1140. 石子游戏 II
日期:11.13
1.题目链接:1140. 石子游戏 II - 力扣(LeetCode)
https://leetcode.cn/problems/stone-game-ii/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,数组,记忆化搜索,动态规划
3.方法一:记忆化搜索(官方题解)
dp[i][m]:从位置 i 开始,当前 M = m 时,当前玩家能比对手多得的石头数量。
状态转移方程:对于状态 (i, m),当前玩家可以取 x 堆石头(1 <= x <= 2m):
dp[i][m] = max( sum(i, i+x-1) - dp[i+x][max(m, x)] )
sum(i, i+x-1):当前玩家本轮得到的石头数量
dp[i+x][max(m, x)]:对手在剩余游戏中的净胜分数
当前玩家的净胜分 = 本轮得分 - 对手的净胜分
关键代码:
// 从后往前填充DP表for(int i=n;i>=0;i--){for(int m=1;m<=n;m++){if(i==n){// 基础情况:没有石头可拿dp[i][m]=0;}else{int sum=0;// 尝试所有可能的取法:取1到2*m堆石头for(int x=1;x<=2*m;x++){if(i+x>n){break; // 不能取超过剩余的石头}// 累加当前取的石头价值sum+=piles[i+x-1];// 状态转移:当前得分减去对手在剩余游戏中的最优得分dp[i][m]=max(dp[i][m],sum-dp[i+x][min(n, max(m, x))]);}}}} // 计算最终得分:总分数加上净胜分数的一半return (dp[0][1]+accumulate(piles.begin(), piles.end(), 0))/2;
【10】1480. 一维数组的动态和
日期:11.14
1.题目链接:1480. 一维数组的动态和 - 力扣(LeetCode)
https://leetcode.cn/problems/running-sum-of-1d-array/description/?envType=problem-list-v2&envId=prefix-sum
2.类型:前缀和,数组
3.方法一:原地修改(一次题解)

这样我们可以从下标 1 开始遍历 nums 数组,每次让 nums[i] 变为 nums[i−1]+nums[i] 即可(因为此时的 nums[i−1] 即为 runningSum[i−1])。
关键代码:
for(int i=1;i<n;i++){nums[i]+=nums[i-1];}
