前缀和
DP34 【模板】一维前缀和(了解)
一维前缀和模板
核心思路总结
- 核心目标
将「多次查询数组任意连续区间和」的操作,从暴力枚举的 O(n) 时间复杂度优化为 O(1),核心是通过预处理前缀和数组,避免重复计算区间和。
- 核心定义与公式
- 前缀和数组
sum定义:sum[0] = 0,sum[i]表示原数组a[1~i]的累加和(原数组从 1 开始存储有效数据); - 核心公式:
sum[i] = sum[i-1] + a[i](构建前缀和); - 区间和查询公式:任意区间
[l, r](左闭右闭,1-based 下标)的和 =sum[r] - sum[l-1](用前缀和差值快速计算)。
java
import java.util.Scanner;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static long[] a;
public static long[] sum;
public static int n,m,l,r;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n=in.nextInt();
m=in.nextInt();
a=new long[n+1];
sum=new long[n+1];
for(int i=1;i<=n;i++) a[i]=in.nextInt();
f();
while(m-->0){
l=in.nextInt();
r=in.nextInt();
System.out.println(sum[r]-sum[l-1]);
}
}
//预处理数组,计算前缀和
public static void f(){
sum[0]=0;
for(int i=1;i<=n;i++){
sum[i]=sum[i-1]+a[i];
}
}
}
DP35 【模板】二维前缀和(了解)
java
public int[][] matrixBlockSum(int[][] mat, int k) {
int m = mat.length;
int n = mat[0].length;
// 步骤1:构建二维前缀和矩阵(1-based,方便公式计算)
int[][] preSum = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] - preSum[i - 1][j - 1] + mat[i - 1][j - 1];
}
}
return preSum;
}
- 1-based 下标 :矩阵和前缀和数组均从
[1,1]开始,避免处理i=0或j=0的边界负数下标,简化公式; - 数据类型 :用
long而非int,牛客测试用例常包含大数,避免整数溢出; - 输入输出:严格适配牛客 OJ 的输入格式(先读 n/m/q,再读矩阵,最后处理 q 次查询)。
java
import java.util.Scanner;
// 牛客DP35 二维前缀和模板(类名需为Main)
public class Main {
// 定义全局数组(适配牛客输入习惯,避免方法传参繁琐)
public static long[][] matrix; // 原始矩阵(1-based)
public static long[][] preSum; // 二维前缀和矩阵(1-based)
public static int n, m, q; // n行m列,q次查询
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 1. 读取矩阵尺寸和查询次数
n = in.nextInt();
m = in.nextInt();
q = in.nextInt();
// 2. 初始化矩阵(1-based,避免边界处理麻烦)
matrix = new long[n + 1][m + 1];
preSum = new long[n + 1][m + 1];
// 3. 读取原始矩阵数据
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
matrix[i][j] = in.nextLong();
}
}
// 4. 预处理构建二维前缀和矩阵
build2DPrefixSum();
// 5. 处理q次查询
while (q-- > 0) {
// 读取查询的子矩形左上角(x1,y1)、右下角(x2,y2)(1-based)
int x1 = in.nextInt();
int y1 = in.nextInt();
int x2 = in.nextInt();
int y2 = in.nextInt();
// 计算并输出子矩形和
long res = querySubMatrixSum(x1, y1, x2, y2);
System.out.println(res);
}
in.close();
}
/**
* 构建二维前缀和矩阵
* 核心公式:preSum[i][j] = 上方 + 左方 - 左上方 + 当前值(避免重复计算)
*/
public static void build2DPrefixSum() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
preSum[i][j] = preSum[i - 1][j] // 上方区域和
+ preSum[i][j - 1] // 左方区域和
- preSum[i - 1][j - 1] // 左上方区域和(重复加了,需减去)
+ matrix[i][j]; // 当前位置值
}
}
}
/**
* 查询子矩形和
* @param x1 左上角行号(1-based)
* @param y1 左上角列号(1-based)
* @param x2 右下角行号(1-based)
* @param y2 右下角列号(1-based)
* @return 子矩形内所有元素的和
*/
public static long querySubMatrixSum(int x1, int y1, int x2, int y2) {
// 核心公式:大矩形和 - 左方多余区域 - 上方多余区域 + 左上方重复减去的区域
return preSum[x2][y2]
- preSum[x2][y1 - 1]
- preSum[x1 - 1][y2]
+ preSum[x1 - 1][y1 - 1];
}
}
724. 寻找数组的中心下标
- 问题转化
数组的「中心下标」定义为:下标 i 左侧所有元素的和 = 右侧所有元素的和。将该问题转化为前缀和的区间和对比:
- 左侧和 = 前缀和数组中「0 到 i-1」的和 →
sum[i](等价于sum[i+1] - nums[i]); - 右侧和 = 前缀和数组中「i+1 到 n-1」的和 →
sum[n] - sum[i+1];核心判断条件:左侧和 == 右侧和。
- 核心实现步骤
(1)构建前缀和数组
- 定义长度为
n+1的前缀和数组sum,sum[0] = 0,sum[i] = sum[i-1] + nums[i-1]; - 作用:将任意区间和的计算从
O(n)优化为O(1),避免重复求和。
(2)遍历验证中心下标
- 遍历数组每个下标
i(0 ≤ i < n):
-
- 计算
i左侧和:sum[i+1] - nums[i](即sum[i],前缀和到i位置再减去当前元素); - 计算
i右侧和:sum[n] - sum[i+1](总前缀和减去到i+1位置的前缀和); - 若左右和相等,直接返回
i(题目要求找最左侧的中心下标,遍历顺序保证这一点);
- 计算
- 遍历结束未找到则返回
-1。
- 核心逻辑本质
利用前缀和数组预处理所有区间和 ,通过 O(1) 的数学计算替代暴力枚举左右和(暴力法时间复杂度 O(n²),前缀和法 O(n)),高效验证每个下标是否满足中心条件。
238. 除自身以外数组的乘积
核心思路(对应最优解法)
- 问题拆解
对于任意下标 i,answer[i] = 左侧元素乘积 × 右侧元素乘积:
- 左侧乘积:
nums[0] × nums[1] × ... × nums[i-1]; - 右侧乘积:
nums[i+1] × nums[i+2] × ... × nums[n-1]。
- 核心实现步骤
(1)构建左侧乘积数组
- 初始化
left数组,left[0] = 1(第一个元素左侧无元素,乘积为 1); - 遍历数组:
left[i] = left[i-1] × nums[i-1](left[i]存储nums[i]左侧所有元素的乘积)。
(2)构建右侧乘积并计算结果
- 初始化
right变量(无需额外数组,空间优化),right = 1(最后一个元素右侧无元素,乘积为 1); - 逆序遍历数组:
-
answer[i] = left[i] × right(当前元素的最终结果);- 更新
right = right × nums[i](right存储nums[i]左侧元素的乘积,供前一个下标使用)。
java
class Solution {
public int[] productExceptSelf(int[] nums) {
int n=nums.length;
int[] left=new int[n]; //左乘积
int[] right=new int[n]; //右乘积
int[] ret=new int[n];
//初始化,最左/右边
left[0]=1;
right[n-1]=1;
//左乘积
for(int i=1;i<n;i++) left[i]=left[i-1]*nums[i-1];
//右乘积
for(int i=n-2;i>=0;i--) right[i]=right[i+1]*nums[i+1];
for(int i=0;i<n;i++) ret[i]=left[i]*right[i];
return ret;
}
}
空间优化(最优解)
无需单独存储 **left** 数组,直接在 **ans** 数组上构建左侧乘积 ,再逆序计算右侧乘积(利用right记录右乘积) ,空间复杂度从 O(n) 优化为 O(1)(输出数组不计入空间复杂度)。
java
class Solution {
public int[] productExceptSelf(int[] nums) {
int n=nums.length;
//1 2 3 4
//1 1 2 6 ans先初始化左乘积
//24 12 8 6 再倒序,利用right记录右乘积
int[] ans=new int[n];
//初始化,最左/右边
//不用left数组
ans[0]=1;
for(int i=1;i<n;i++){
ans[i]=ans[i-1]*nums[i-1];
}
int right=1;//初始化右乘积是1
for(int i=n-1;i>=0;i--){
ans[i]=ans[i]*right; //左乘积*右乘积
right=right*nums[i]; //更新右乘积
}
return ans;
}
}
560. 和为 K 的子数组
枚举暴力
java
class Solution {
public int subarraySum(int[] nums, int k) {
//枚举暴力
int num=0;
for(int start=0;start<nums.length;start++){
int sum=0;
for(int i=start;i<nums.length;i++){
sum+=nums[i];
if(sum==k) num++;
}
}
return num;
}
}
前缀和+哈希表优化
问题转化(核心逻辑)
将「统计和为 k 的连续子数组个数」转化为「统计前缀和差值为 k 的次数」:
- 设
curSum为「遍历到当前元素时的累计前缀和」,则子数组和为 k 等价于curSum(当前前缀和) - 某历史前缀和 = k; - 变形得:
某历史前缀和 = curSum - k(代码中用num表示该值),只需统计该历史前缀和出现的次数,就是「以当前元素为结尾的、和为 k 的子数组个数」。
java
class Solution {
//前缀和和哈希表优化
public int subarraySum(int[] nums, int k) {
//计算前缀和,出现次数
Map<Integer,Integer> hash=new HashMap<>();
int curSum=0,cnt=0;
hash.put(0,1); //0出现得次数是1
for(int i=0;i<nums.length;i++){
curSum+=nums[i];
//统计次数
int num=curSum-k; //后的前缀和-前面的前缀和=key
if(hash.containsKey(num))
cnt+=hash.get(num);//添加次数
//插入
hash.put(curSum,hash.getOrDefault(curSum,0)+1);
}
return cnt;
}
}
974. 和可被 K 整除的子数组
- 数学转化(核心)
根据同余定理 :若 (preSum[i] - preSum[j]) % k == 0,则 preSum[i] % k == preSum[j] % k。
- 子数组
nums[j...i-1]的和 =preSum[i] - preSum[j]; - 要求子数组和可被 k 整除 → 等价于「当前前缀和的余数」=「某历史前缀和的余数」;
- 因此,问题转化为:统计「相同余数的前缀和出现的次数」,次数之和即为答案。
- 关键细节:处理负数余数
数组中可能有负数,导致 preSum % k 为负数(如 (-1) % 5 = -1),需将余数转化为非负数 :公式:(preSum % k + k) % k(保证余数在 [0, k-1] 范围内)
java
class Solution {
public int subarraysDivByK(int[] nums, int k) {
int n=nums.length;
Map<Integer,Integer> hash=new HashMap<>();
int curSum=0;
int cnt=0;
hash.put(0,1); //前缀和为0的一次
for(int i=0;i<n;i++){
curSum+=nums[i];
//同余原理(把余数为负的转化为正数)
int remainder=(curSum%k+k)%k;
//查询
if(hash.containsKey(remainder)) cnt+=hash.get(remainder);
//插入
hash.put(remainder,hash.getOrDefault(remainder,0)+1);
}
return cnt;
}
}
简单优化一下,复用一下,时间更快
java
class Solution {
public int subarraysDivByK(int[] nums, int k) {
int n=nums.length;
Map<Integer,Integer> hash=new HashMap<>();
int curSum=0;
int cnt=0;
hash.put(0,1); //前缀和为0的一次
for(int i=0;i<n;i++){
curSum+=nums[i];
//同余原理(把余数为负的转化为正数)
int remainder=(curSum%k+k)%k;
int same=hash.getOrDefault(remainder,0);
cnt+=same;
//插入
hash.put(remainder,same+1);
}
return cnt;
}
}
525. 连续数组
核心思路总结
- 问题核心转化(最关键一步)
将「找 0 和 1 数量相等的最长连续子数组」转化为「找和为 0 的最长连续子数组」:
- 规则:把数组中的 0 替换为 - 1,1 保持不变;
- 逻辑:子数组中 0 和 1 数量相等 ↔ 替换后的子数组和为 0(-1 和 1 的数量相等,累加和为 0)。
- 前缀和 + 哈希表的核心逻辑
基于前缀和性质:子数组和为 0 ↔ 当前前缀和 **curSum** = 某历史前缀和(两个前缀和的差值为 0,对应中间子数组和为 0)。
- 哈希表
hash作用:键 = 前缀和值,值 = 该前缀和第一次出现的下标(只存第一次是为了保证子数组长度最长,后续相同前缀和无需更新); - 初始化
hash.put(0, -1):处理「从数组开头到当前下标」的子数组和为 0 的情况(比如前缀和到下标 2 为 0,长度 = 2 - (-1)=3)。
java
class Solution {
public int findMaxLength(int[] nums) {
int n=nums.length;
//找 0 和 1 数量相等的最长连续子数组」转化为「找和为 0 的最长连续子数组」
//前缀和 出现这个值的下标
Map<Integer,Integer> hash=new HashMap<>();
hash.put(0,-1); //初始化
int curSum=0,maxLen=0;
for(int i=0;i<n;i++){
//添加规则
curSum+= nums[i]==0?-1:1;
//出现和一个->即两部分之间差值为0->数量相同的0,1
if(hash.containsKey(curSum)){
maxLen=Math.max(maxLen,i-hash.get(curSum));
//不用更新下标,因为要保证最长
}else{ //不存在,记录
hash.put(curSum,i);
}
}
return maxLen;
}
}
1314. 矩阵区域和
核心思路(最优解法)
- 问题拆解
-
步骤 1:预处理二维前缀和矩阵,为后续子矩形和查询做准备;
-
步骤 2:对每个位置
(i,j),确定其有效矩形的左上角(x1,y1)和 右下角(x2,y2)(限制在矩阵范围内): -
x1 = max(0, i - k)、y1 = max(0, j - k)(左上方不越界);x2 = min(m-1, i + k)、y2 = min(n-1, j + k)(右下方不越界);
-
步骤 3:用二维前缀和公式计算
(x1,y1)到(x2,y2)的子矩形和,赋值给answer[i][j]。
- 关键细节
- 题目中矩阵是0-based 下标,需适配二维前缀和的 0/1-based 转换;
- 边界处理:有效矩形的上下左右边界不能超出矩阵的行 / 列范围(用
max/min限制)。
java
class Solution {
public int[][] matrixBlockSum(int[][] mat, int k) {
int m = mat.length;
int n = mat[0].length;
// 步骤1:构建二维前缀和矩阵(1-based,方便公式计算)
int[][] preSum = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] - preSum[i - 1][j - 1] + mat[i - 1][j - 1];
}
}
// 步骤2:遍历每个位置,计算有效区域和
int[][] answer = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 确定有效矩形的边界(转换为1-based)
int x1 = Math.max(0, i - k) + 1; // 0-based→1-based
int y1 = Math.max(0, j - k) + 1;
int x2 = Math.min(m - 1, i + k) + 1;
int y2 = Math.min(n - 1, j + k) + 1;
// 步骤3:二维前缀和公式计算区域和
answer[i][j] = preSum[x2][y2] - preSum[x2][y1 - 1] - preSum[x1 - 1][y2] + preSum[x1 - 1][y1 - 1];
}
}
return answer;
}
}