【算法篇】4.前缀和

前缀和

DP34 【模板】一维前缀和(了解)

一维前缀和模板

核心思路总结

  1. 核心目标

将「多次查询数组任意连续区间和」的操作,从暴力枚举的 O(n) 时间复杂度优化为 O(1),核心是通过预处理前缀和数组,避免重复计算区间和。

  1. 核心定义与公式
  • 前缀和数组 sum 定义:sum[0] = 0sum[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=0j=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. 寻找数组的中心下标

  1. 问题转化

数组的「中心下标」定义为:下标 i 左侧所有元素的和 = 右侧所有元素的和。将该问题转化为前缀和的区间和对比

  • 左侧和 = 前缀和数组中「0 到 i-1」的和 → sum[i](等价于 sum[i+1] - nums[i]);
  • 右侧和 = 前缀和数组中「i+1 到 n-1」的和 → sum[n] - sum[i+1];核心判断条件:左侧和 == 右侧和
  1. 核心实现步骤

(1)构建前缀和数组

  • 定义长度为 n+1 的前缀和数组 sumsum[0] = 0sum[i] = sum[i-1] + nums[i-1]
  • 作用:将任意区间和的计算从 O(n) 优化为 O(1),避免重复求和。

(2)遍历验证中心下标

  • 遍历数组每个下标 i(0 ≤ i < n):
    1. 计算 i 左侧和:sum[i+1] - nums[i](即 sum[i],前缀和到 i 位置再减去当前元素);
    2. 计算 i 右侧和:sum[n] - sum[i+1](总前缀和减去到 i+1 位置的前缀和);
    3. 若左右和相等,直接返回 i(题目要求找最左侧的中心下标,遍历顺序保证这一点);
  • 遍历结束未找到则返回 -1
  1. 核心逻辑本质

利用前缀和数组预处理所有区间和 ,通过 O(1) 的数学计算替代暴力枚举左右和(暴力法时间复杂度 O(n²),前缀和法 O(n)),高效验证每个下标是否满足中心条件。

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

核心思路(对应最优解法)

  1. 问题拆解

对于任意下标 ianswer[i] = 左侧元素乘积 × 右侧元素乘积

  • 左侧乘积:nums[0] × nums[1] × ... × nums[i-1]
  • 右侧乘积:nums[i+1] × nums[i+2] × ... × nums[n-1]
  1. 核心实现步骤

(1)构建左侧乘积数组

  • 初始化 left 数组,left[0] = 1(第一个元素左侧无元素,乘积为 1);
  • 遍历数组:left[i] = left[i-1] × nums[i-1]left[i] 存储 nums[i] 左侧所有元素的乘积)。

(2)构建右侧乘积并计算结果

  • 初始化 right 变量(无需额外数组,空间优化),right = 1(最后一个元素右侧无元素,乘积为 1);
  • 逆序遍历数组:
    1. answer[i] = left[i] × right(当前元素的最终结果);
    2. 更新 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 整除的子数组

  1. 数学转化(核心)

根据同余定理 :若 (preSum[i] - preSum[j]) % k == 0,则 preSum[i] % k == preSum[j] % k

  • 子数组 nums[j...i-1] 的和 = preSum[i] - preSum[j]
  • 要求子数组和可被 k 整除 → 等价于「当前前缀和的余数」=「某历史前缀和的余数」;
  • 因此,问题转化为:统计「相同余数的前缀和出现的次数」,次数之和即为答案。
  1. 关键细节:处理负数余数

数组中可能有负数,导致 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. 连续数组

核心思路总结

  1. 问题核心转化(最关键一步)

将「找 0 和 1 数量相等的最长连续子数组」转化为「找和为 0 的最长连续子数组」:

  • 规则:把数组中的 0 替换为 - 1,1 保持不变;
  • 逻辑:子数组中 0 和 1 数量相等 ↔ 替换后的子数组和为 0(-1 和 1 的数量相等,累加和为 0)。
  1. 前缀和 + 哈希表的核心逻辑

基于前缀和性质:子数组和为 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. 问题拆解
  • 步骤 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]

  1. 关键细节
  • 题目中矩阵是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;
    }
}
相关推荐
计算机安禾2 小时前
【数据结构与算法】第4篇:算法效率衡量:时间复杂度和空间复杂度
java·c语言·开发语言·数据结构·c++·算法·visual studio
蓝色心灵-海2 小时前
小律书 技术架构详解:前后端分离的自律管理系统设计
java·http·小程序·架构·uni-app
华科易迅2 小时前
Spring AOP(XML最终+环绕通知)
xml·java·spring
jay神2 小时前
基于YOLOv8的传送带异物检测系统
人工智能·python·深度学习·yolo·可视化·计算机毕业设计
IT观测2 小时前
深度分析俩款主流移动统计工具Appvue和openinstall
android·java·数据库
Oueii2 小时前
嵌入式LinuxC++开发
开发语言·c++·算法
华科易迅2 小时前
Spring AOP(注解前置+后置通知)
java·后端·spring
sw1213892 小时前
嵌入式C++驱动开发
开发语言·c++·算法
神奇小汤圆2 小时前
一个 Spring Boot 项目,为什么要拆成 bootstrap、web、business、foundation、components、iot?
后端