【算法】—— 前缀和

一、区间求和问题

给定一个长度为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)

相关推荐
西岭千秋雪_5 分钟前
设计模式の装饰者&组合&外观模式
java·python·设计模式·组合模式·装饰器模式·外观模式
AI人H哥会Java17 分钟前
【JAVA】Java项目实战—分布式微服务项目:分布式文件存储系统
java
程序媛徐师姐20 分钟前
Java基于SpringBoot的飘香水果购物网站,附源码
java·spring boot·飘香水果购物网站·java飘香水果购物网站·飘香水果·水果购物网站
paterWang37 分钟前
小程序-基于java+SSM+Vue的模拟考试管理系统设计与实现
java·vue.js·小程序
梦.清..1 小时前
Java——多线程(中)
java·开发语言
良木林1 小时前
2024西游新生赛部分题解
c语言·数据结构·算法
Dawnㅤ1 小时前
MyBatis-Plus 实用工具:SqlHelper
java
乐茵安全2 小时前
基于python绘制数据表(上)
java·前端·python
宸码2 小时前
【机器学习】【无监督学习——聚类】从零开始掌握聚类分析:探索数据背后的隐藏模式与应用实例
人工智能·python·学习·算法·机器学习·数据挖掘·聚类
总是学不会.2 小时前
【Mysql】索引相关基础知识(二)
java·数据库·mysql·intellij-idea·开发