一、区间求和问题
给定一个长度为n的序列a,有m次查询,每次查询输出一个连续区间的和。
使用暴力做法求解是将每次查询都遍历该区间求和
java
//暴力做法
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
int[] a = new int[100010];
for(int i = 1;i<=n;i++){
a[i] = scan.nextInt();
}
for(int i = 0;i<=m;i++){
int l = scan.nextInt(),r = scan.nextInt(),sum=0;
for(int j = l;j<=r;j++){
sum+=a[j];
}
System.out.println(sum);
}
}
}
可以看到,最坏情况下,时间复杂度为O(nm) ,这种区间求和问题就可以使用前缀和来优化
前缀和实现原理:
核心思想:
在一个新的数组上的每个元素都存储原数组开始位置 到新数组元素位置 之 和 ,最后通过 减法 来实现快速计算
原理解释:
创建一个新的数组 s ,其中每个元素 s[i] 表示原数组从开始位置到位置 i 元素之和,即
当我们需要查询 [2,3] 时,就是计算 a[2] + a[3] , 对于数组 s 来说只需要将 s[3] - s[1]
这一步的时间复杂度为 O(1)
通过上述计算可以得到公式
在创建时新数组时还可以通过迭代计算每个 s[i]
可以得到公式:
这里注意 s[1] == a[1] 但是在循环里要写成 s[1] = s[0] + a[1] ,才不会下标越界,所以数组下标都由1开始。
代码演示:
java
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int[] a = new int[100010];
int[] s = new int[100010];
int n = scan.nextInt();
int m = scan.nextInt();
s[0] = 0;
for(int i = 1;i<=n;i++){
a[i] = scan.nextInt();
s[i] = s[i-1]+a[i];
}
for(int i = 1;i<=m;i++){
int l = scan.nextInt();
int r = scan.nextInt();
System.out.println(s[r] - s[l-1]);
}
}
}
这样时间复杂度就来到了 O(n+m) 。
理解了一维前缀和,我们将问题上升一个维度,来到
二维前缀和:
给定一个 n*m 大小的矩阵 A,给定 q 组查询,每次查询给定两个坐标,需要输出坐标1到坐标2的所有值
暴力做法:
java
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int[][] a = new int[100][100];
int n = scan.nextInt();
int m = scan.nextInt();
int q = scan.nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
a[i][j] = scan.nextInt();
}
}
for(int i=1;i<=q;i++){
int sum = 0;
int x1= scan.nextInt(),y1= scan.nextInt(),x2= scan.nextInt(),y2= scan.nextInt();
for(int j=x1;j<=x2;j++){
for(int k=y1;k<=y2;k++){
sum+=a[j][k];
}
}
System.out.println(sum);
}
}
}
中间的部分有一个三重循环,时间复杂度来到了 O(n * m * q)
依旧使用前缀和的思想来完成,在二维数组上的每个元素都要存储从开始位置的和
在二维前缀和数组中,求(x,y)就是求从(1,1)开始到(x,y)的所有和
对(x,y)做进一步拆分,会发现由四个部分组成
先是当前点本身a(x,y):
s[x][y-1]:
s[x-1][y]:
s[x-1][y-1]:
因为s[x-1][y-1]被加了两次,所以需要减去一次
综上可以得到前缀和计算公式:
s[x][y] = a[x][y] + s[x-1][y] + s[x][y-1] - s[x-1][y-1] + ;
现在起点不从(1,1)开始,计算两点表示的子矩阵的和
就可以让(x2,y2)减去两边的长方形(x2,y1-1)和 (x1-1,y2) ,因为 (x-1,y-1) 被减去两次,所以需要加上一次,得到公式:
代码演示:
java
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int N = 1010;
int[][] a = new int[N][N];
int[][] s = new int[N][N];
int n = scan.nextInt();
int m = scan.nextInt();
int q = scan.nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
a[i][j] = scan.nextInt();
//构建前缀和数组
s[i][j] = a[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1];
}
}
for(int i=1;i<=q;i++){
int x1 = scan.nextInt(),y1 = scan.nextInt(),x2 = scan.nextInt(),y2 = scan.nextInt();
int sum = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
System.out.println(sum);
}
}
}
时间复杂度变为O(n*m+q)
二、区间修改问题
给定一个长度为n的序列a,有m组操作,每次操作将某一个连续区间 [ l ~ r ] 的元素都加上,最后输出操作结束后的数组a。
暴力做法为写一个循环,每次修改 [ l ~ r ] 的值,重复m次
java
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int[] a = new int[100010];
int n = scan.nextInt();
int m = scan.nextInt();
for(int i = 1;i<=n;i++){
a[i] = scan.nextInt();
}
for(int i = 1;i<=m;i++){
int l = scan.nextInt();
int r = scan.nextInt();
int d = scan.nextInt();
for(int j = l;j<=r;j++){
a[j] += d;
}
}
for(int i=1;i<=n;i++){
System.out.print(a[i] + " ");
}
}
}
时间复杂度为 O(nm),这种区间求和问题就可以使用差分来优化
差分实现原理:
核心思想:通过计算原数组中每个元素之间的差让元素和元素之间产生联系,再利用差不断复原出原数组。期间对差进行加减就会影响这个差之后的所有的元素
定义一个差分数组 b ,求出每一个元素之间的差
对 数组b 求前缀和 得到 新数组c
这里就可以得到一个结论,差分数组求前缀和就可以得到原数组
当我们对 b[1] + 1 后
b[1] 之后的元素都会 +1
当我们对 b[3] - 1 后
b[3] -1 之后的元素也都 -1 和如果原来的+1相呼应就可以得到修改前的元素,最后可以得到结论:
对差分数组b[l]+d , b[r+1]-d ,求解前缀和后,就可以得到修改后的数组c
代码演示
java
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int[] a = new int[100010];
int[] b = new int[100010];
int[] c = new int[100010];
int n = scan.nextInt();
int m = scan.nextInt();
for(int i = 1;i<=n;i++){
a[i] = scan.nextInt();
b[i] = a[i] - a[i-1];
}
for(int i = 1;i<=m;i++){
int l = scan.nextInt(),r = scan.nextInt(),d = scan.nextInt();
b[l] += d;
b[r+1] -= d;
}
for(int i = 1;i<=n;i++){
c[i] = c[i-1] + b[i];
}
for(int i = 1;i<=n;i++){
System.out.print(c[i]+" ");
}
}
}
时间复杂度 O(n+m)
差分数组的常见性质:
- 如果差分数组 b 除了 b[1] 以外所有值均为 0 ,则说明数组 a 的值全部都一样
差分数组还可以解决
归1问题:
一个数组 a 中共包含 n 个数,问最少多少次操作可以让 a 数组所有数都变成 1 。
操作的内容可以任选一个区间,使得区间内所有值 -1 ,数据保证一定有解
假设有一个数组【1,3,5,2,7,1】
3 变成 1 需要进行 3-1 次操作
5 变成 1 需要 4 次操作,因为 5 > 3 所以可以跟着 3 一起进行 2 次,所以只要 5-3 次
2 < 5 可以跟着 5 进行 1 次后就不用再进行,可以忽略不计
7 > 2 需要进行 7-2 次操作
综上可以得出看出,每一个数需要的操作次数可以通过减去前一个数来求出,当减去前一个数<0时,就可以忽略不计,求出每一个元素之间的差,刚好是差分数组所实现的内容,因为是归1,而第一项(a[0])是 0 ,a[1] 的操作次数会多一次,所以最后结果还需要 -1 。
代码演示:
java
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int[] a = new int[100010];
int[] b = new int[100010];
int n = scan.nextInt();
int sum = 0;
for(int i = 1;i<=n;i++){
a[i] = scan.nextInt();
b[i] = a[i] - a[i-1];
if(b[i]>=0){
sum += b[i];
}
}
System.out.println(sum-1);
}
}
二维差分:
给定一个 n*m 大小的矩阵 A,给定 q 组修改,每次查询给定两个坐标,需要修改坐标1到坐标2的所有值,最后打印修改完成的数组
暴力做法:
java
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int N = 1010;
int[][] a = new int[N][N];
int[][] s = new int[N][N];
int n = scan.nextInt();
int m = scan.nextInt();
int q = scan.nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
a[i][j] = scan.nextInt();
}
}
for(int i=1;i<=q;i++){
int x1 = scan.nextInt(),y1 = scan.nextInt(),x2 = scan.nextInt(),y2 = scan.nextInt(),d= scan.nextInt();
for(int j=x1;j<=x2;j++){
for(int k=y1;k<=y2;k++){
a[j][k] += d;
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
System.out.print(a[i][j]+" ");
}
System.out.println();
}
}
}
时间复杂度为 O(n*m*q)
接下来用差分思想进行优化
首先是用差让各个元素之间产生联系,这里减去上方和左方的格子
如果只是减去左方格子就变回了一维差分
(x,y-1)和(x-1,y)也是通过减去上方和左方的格子得来
这里会发现(x-1,y-1)被多减了一次,所以计算(x,y)时还需要再加上(x-1,y-1)
通过上图描述可得公式:
差分数组 b[x][y] = a[x][y] - a[x][y-1] - a[x-1][y] + a[x-1][y-1];
有了差分数组,就可以对元素进行修改
当对差分数组修改后,所有后续有关联的元素都会发生改变
当希望只在 (x1,x2) 和 (x2,y2) 之间进行修改,就需要对其他点进行修改
对于边上的两点(x2+1,y1)、(x1,y2+1)减少 d ,对于重复减的点(x2+1,y2+1)加上 d
对点修改完后,再通过前缀和对数组复原
代码演示:
java
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int N = 1010;
int[][] a = new int[N][N];
int[][] b = new int[N][N];
int n = scan.nextInt();
int m = scan.nextInt();
int q = scan.nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
a[i][j] = scan.nextInt();
b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1] ;
}
}
for(int i=1;i<=q;i++){
int x1=scan.nextInt(),y1=scan.nextInt(),x2=scan.nextInt(),y2=scan.nextInt(),d=scan.nextInt();
b[x1][y1]+=d;
b[x2+1][y1]-=d;
b[x1][y2+1]-=d;
b[x2+1][y2+1]+=d;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
b[i][j]=b[i][j]+b[i-1][j]+b[i][j-1]-b[i-1][j-1];
System.out.print(b[i][j]+" ");
}
System.out.println();
}
}
}
时间复杂度为O(n*m+q)