【算法】【优选算法】前缀和(上)

目录

一、前缀和算法简介

前缀和算法:就是快速求取数组中一段连续区间的和的算法。

1.1 一维前缀和模版

  • 预处理一个前缀和数组dp :数组中的元素dp i 表示从原数组 1 , i 的和。
  • 所以 dp i = dp i - 1 + arr i
  • 要求区间 l , r 的和,就是dp r - dp l - 1

1.2 二位前缀和模版

  • 预处理一个前缀和矩阵:dp i j 的含义就是从原数组(1,1)到(i , j)的和。
  • 我们求dp i j : dp i j = dp i - 1 j + dp i j - 1 - dp i - 1 j - 1 + arr i j 。就相当于下图的:B区 + C区 - A区 + arr i j
  • 求(x1,y1)到(x2,y2)的和:dpx2y2 - dpx1-1y2 - dpx2y1-1 + dpx1-1y1-1。对应下图就是: B区 - C区 - D区 + A区

二、【模板】前缀和

题目链接:【模板】前缀和

题目描述:

题目解析:

  • 给我们一个数组从1下标开始给数组元素,让我们打印出要求的每一段子数组和。

2.1 前缀和

解题思路:

  • 我们使用一个一个dp数组来表示每一段数组的和。即dp i 就表示原数组中1到 i 下标元素的和。
  • 求l到r的元素和,直接就是dp r - dp l - 1
  • 由于可能求0到2这种 l 等于0的子数组,所以我们将dp数组的长度置为n+1,并且dp 0 = 0。
  • 细节问题:由于元素值是比较大的,所以我们求和是有可能超出int范围的,所以dp数组要使用long类型。

解题代码:

java 复制代码
//时间复杂度:O(n+q)
//空间复杂度:O(n)
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int q = in.nextInt();
        long[] dp = new long[n+1];
        for(int i = 1; i <= n; i++) {
            dp[i] = dp[i-1] + (long)in.nextInt();
        }
        while(q > 0) {
            int l = in.nextInt();
            int r = in.nextInt();
            System.out.println(dp[r] - dp[l-1]);
            q--;
        }
    }
}

2.2 暴力枚举

解题思路:

  • 直接先将数组存下来,在通过 l 和 r来遍历求和。
  • 会超时。

解题代码:

java 复制代码
//时间复杂度:O(n*q)
//空间复杂度:O(1)
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int q = in.nextInt();
        int[] arr = new int[n+1];
        for(int i = 1; i <= n; i++) {
            arr[i] = in.nextInt();
        }
        while(q > 0) {
            int l = in.nextInt();
            int r = in.nextInt();
            long ret = 0;
            for(int i = l; i <= r; i++) {
                ret += arr[i];
            }
            System.out.println(ret);
            q--;
        }
    }
}

三、【模板】⼆维前缀和

题目链接:【模板】⼆维前缀和

题目描述:

题目解析:

  • 给我们一个二维数组,以(1,1)开始作为数组头。
  • 让我们返回给的两个下标包围的元素的和,例如给(1,1)和(2,3)那么和就是arr1,1+arr1,2+arr1,3+arr2,1+arr2,2+arr2,3
  • 我们使用一个表示前缀和的数组dp,dp i j 的含义就是从原数组(1,1)到(i , j)的和。
  • 那么dp i j = dp i - 1 j + dp i j - 1 - dp i - 1 j - 1 + arr i j
  • 求(x1 , y1)到(x2 , y2)的和就是dpx2y2 - dpx1 - 1 y2 - dpx2 y1 - 1 + dpx1 - 1y1 - 1
  • 由于可能求0到2这种 l 等于0的子数组,所以我们将dp数组的长度置为n+1m+1,并且dp 0 = 0。
  • 细节问题:由于元素值是比较大的,所以我们求和是有可能超出int范围的,所以dp数组要使用long类型。

3.1 前缀和

java 复制代码
//时间复杂度:O(n*m + q)
//空间复杂度:O(n*m)
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int m = in.nextInt();
        int q = in.nextInt();
        long[][] dp = new long[n+1][m+1];
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= m; j++) {
                dp[i][j] = dp[i][j-1] + dp[i-1][j] - dp[i-1][j-1] + in.nextInt();
            }
        } 
        while(q > 0) {
            int x1 = in.nextInt();
            int y1 = in.nextInt();
            int x2 = in.nextInt();
            int y2 = in.nextInt();
            System.out.println(dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]);
            q--;
        }       
    }
}

3.2 暴力枚举

解题思路:

  • 将数组保存下来,按要求求和即可。
  • 会超时。
java 复制代码
//时间复杂度:O(n*n*q)
//空间复杂度:O(1)
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int m = in.nextInt();
        int q = in.nextInt();
        long[][] arr = new long[n+1][m+1];
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= m; j++) {
                arr[i][j] =  in.nextInt();
            }
        } 
        while(q > 0) {
            int x1 = in.nextInt();
            int y1 = in.nextInt();
            int x2 = in.nextInt();
            int y2 = in.nextInt();
            long ret = 0;
            for(int i = x1; i <= x2; i++) {
                for(int j = y1; j <= y2; j++) {
                    ret += arr[i][j];
                }
            }
            System.out.println(ret);
            q--;
        }       
    }
}

四、724.寻找数组的中⼼下标

题目链接:724.寻找数组的中⼼下标

题目描述:

题目解析:

  • 就是返回当前下标元素,第一个满足左边元素和与右边元素和相等的下标。
  • 数组头左边和为0,数组尾右边和为0。
  • 如果没有返回-1。

4.1 前缀和

解题思路:

  • 直接套用一维前缀和模版。
  • 注意现在的原数组是从0下标开始的,为了避免边界考虑,所以前缀和数组还是从1开始,只不过dp i = dp i - 1 + nums i - 1
  • 因为不包含i下标的元素,所以判断条件dpi-1 == dpn - dpi
  • dp i 表示的是原数组中 0 , i - 1的和,所以最后返回的也是i - 1
java 复制代码
//时间复杂度:O(n)
//空间复杂度:O(1)
class Solution {
    public int pivotIndex(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n+1];
        for(int i = 0; i < n; i++) {
            dp[i+1] = dp[i] + nums[i];
        }
        for(int i = 1; i < n+1; i++) {
            if(dp[i-1] == dp[n] - dp[i]) return i-1;
        }
        return -1;
    }
}

4.2 暴力枚举

解题思路:

  • 直接先将数组中所有元素和求出来,
  • 再遍历数组,用变量表示当前下标前的元素的和,
  • 判断是否符合条件即可。

解题代码:

java 复制代码
//时间复杂度:O(n)
//空间复杂度:O(1)
class Solution {
    public int pivotIndex(int[] nums) {
        int n = nums.length;
        int sum = 0;
        for(int i = 0; i < n; i++) {
            sum += nums[i];
        }
        int preSum = 0;
        for(int i = 0; i < n; i++) {
            if(i != 0) preSum += nums[i-1];
            if(preSum == sum - preSum - nums[i]) return i;
        }
        return -1;
    }
}

五、238.除⾃⾝以外数组的乘积

题目链接:238.除⾃⾝以外数组的乘积

题目描述:

题目解析:

  • 求数组中,除去该元素的剩下元素的乘积,存入结果数组中。
  • 不用担心超出int范围。

5.1 前缀和

解题思路:

  • 使用一个dp1数组,表示前缀积,从0, i - 1的乘积。
  • 所以dp i = dp i - 1 * nums i - 1
  • 使用一个dp2数组,表示后缀积,从 i + 1 , n - 1的乘积。
  • 所以dp i = dp i + 1 * nums i + 1
  • 最后放入结果数组中时: ret i = dp1 i * dp2 i
  • 细节处理:
    • 初始化dp1的时候dp1 0 会越界,先直接赋值为nums 0
    • 初始化dp2的时候dp2 n - 1 会越界,先直接赋值为nums n - 1

解题代码:

java 复制代码
//时间复杂度:O(n)
//空间复杂度:O(n)
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        //前缀积
        int[] dp1 = new int[n];
        for(int i = 0; i < n; i++) {
            if(i == 0) {
                dp1[i] = 1;
                continue;
            }
            dp1[i] = dp1[i-1] * nums[i - 1];
        }
        //后缀积
        int[] dp2 = new int[n];
        for(int i = n-1; i >=0; i--) {
            if(i == n-1) {
                dp2[i] = 1;
                continue;
            }
            dp2[i] = dp2[i+1] * nums[i+1];
        }
        //结果数组
        int[] ret = new int[n];
        for(int i = 0; i < n; i++) {
            ret[i] = dp1[i] * dp2[i];
        }
        return ret;
    }
}

5.2 暴力枚举

解题思路:

  • 直接遍历数组,在求积即可。
  • 会超时。

解题代码:

java 复制代码
//时间复杂度:O(n^2)
//空间复杂度:O(1)
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] ret = new int[n];
        for(int i = 0; i < n; i++) {
            ret[i] = 1;
            for(int j = 0; j < n; j++) {
                if(i != j) ret[i] *= nums[j];
            }
        }   
        return ret;
    }
}
相关推荐
徐小夕33 分钟前
万字长文!千万级文档 RAG 知识库系统落地实践
前端·算法·github
不知名的老吴1 小时前
线程的生命周期之线程“插队“
java·开发语言·python
akunkuntaimei1 小时前
2026年高考数学各省真题及答案(完整版)
算法·高考
ANnianStriver1 小时前
PetLumina-02-后端开发与前后端联调
java·ai·sa-token
Hello:CodeWorld1 小时前
C 风格变参 vs C++ 变参模板:核心区别与选型指南
c语言·c++·算法
杨了个杨89822 小时前
Keepalived + Nginx + HAProxy 高可用架构部署实战案例
java·nginx·架构
8Qi83 小时前
LeetCode 516:最长回文子序列
算法·leetcode·职场和发展·动态规划
马士兵教育4 小时前
Java还有前景吗?Java+AI大模型学习路线及项目?
java·人工智能·python·学习·机器学习
youngerwang4 小时前
【从搬运工到协处理器:网卡芯片架构、算法、验证与边缘演进深度剖析】
网络·算法·架构·芯片
snow@li4 小时前
Java:理解 Gradle / 后端项目的管家 / 打包SpringBoot 应用 / 完成编译、下载依赖、运行测试、打包 JAR/WAR / 速查表
java