• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
百事可爱
一起努力鸭~~~
博客园    首页    新随笔    联系   管理    订阅  订阅
差分与前缀和

差分与前缀和

1. 差分法 (解决区间加减问题)

  1. 当某一个数组要在很多不确定的区间,加上相同的一个数。我们如果每个都进行加法操作的话,那么复杂度 O(nm) 是平方阶的,非常消耗时间。

  2. 如果我们采用差分法,将数组拆分,构造出一个新的拆分数组,通过对数组区间的端点进行加减操作,最后将数组和并就能完成原来的操作。时间复杂度降低为 O(N)

  3. 差分法的特点:

    1. 将对于区间的加减操作转化为对于端点的操作;

    2. 时间复杂度为 O(n);

    3. 用于维护区间的增减但不能维护乘除;

    4. 差分后的序列比原来的数组序列多一个数。

      一个序列1 2 5 4 7 3,差分后得到1 1 3 -1 3 -4 -3

      这里注意得到的差分序列第一个数和原来的第一个数一样(相当于第一个数减0)

      差分序列最后比原序列多一个数(相当于0减最后一个数)

    (1) 差分算法解题的基本思路:

    若原数组是a[ ] ,进行差分后得到的数组是b[ ]

    1. b[1]=a[1];
    2. 从第 2 项到 n 项,利用 b[i]=a[i]-a[i-1] 差分式;
    3. 对于区间端点操作加减;对于每个 [l,r] 区间的加减操作都转化为对端点 l,r+1 的操作
    4. 首先假设有一个数组:

    a[]={1 2 3 4 5 7 2}

    差分后:(利用 b[i]= a[i]-a[i-1] 差分式)

    b[]={1 1 1 1 1 2 -5 -2}

    一般应用场景:

    让你对区间 [l,r] 加减操作 N 次

    如:

    从第二个元素到第五个元素每个+3

    从第二个元素到第四个元素每个-2

    从第一个元素到第三个元素每个+1

    对于每个 [l,r] 区间的加减操作都转化为对端点 l,r+1 的操作

    从第二个元素到第五个元素每个+3:
    转化为:[l]+3 并且 [r+1]-3

    那么原序列变成了:

    1 1 1 1 1 2 -5 -2

    1 4 1 1 1 -1 -5 -2

    然后我们按照 b[i]=b[i]+b[i-1] 复原:

    1 5 6 7 8 7 2 0

    去掉最后一项,跟原序列对比:

    1 2 3 4 5 7 2

    1 5 6 7 8 7 2

    确实是都加上了 3。

    我们继续操作:

    从第二个元素到第四个元素每个-2

    转化为:[l]-2 并且 [r+1]+2

    那么序列变成了:

    1 4 1 1 1 -1 -5

    1 2 1 1 3 -1 -5

    然后我们按照b[i]=b[i]+b[i-1] 复原

    1 3 4 5 8 7 2

    与上次复原后对比:

    1 5 6 7 8 7 2

    1 3 4 5 8 7 2

    确实是按照操作执行了。

    不用每次都复原,只用最后一次复原即可,这里演示给大家看。
    直接做三次,最后还原:
    从第二个元素到第五个元素每个+3

    从第二个元素到第四个元素每个-2

    从第一个元素到第三个元素每个+1
    a[]={1 2 3 4 5 7 2}
    原序列差分后:

    b[]={1 1 1 1 1 2 -5}

    2 号元素 + 3

    6 号元素 - 3

    2 号元素 - 2

    5 号元素 + 2

    1 号元素 + 1

    4 号元素 - 1

    差分序列变成:

    2 2 1 0 3 -1 -5
    复原后:

    2 4 5 5 8 7 5

    与原序列对比:

    1 2 3 4 5 7 2

    2 4 5 5 8 7 5
    所以还是非常方便快捷的。

(2)例题:

题目描述:

教室外有 N 棵树,根据不同的位置和树种,学校要对其上不同的药。 因为树的排列成线性,且非常长,我们可以将它们看作一条直线给他们编号。 树的编号从 0--N-1 且 N<1e6。 对于树的药是成区间分布,比如 3 - 5 号的树靠近下水道,所以他们要用驱蚊虫的药, 20 - 26 号的树,他们排水不好,容易涝所以要给他们用点促进根系的药。 诸如此类,每种不同的药要花不同的钱。 现在已知共有 M 个这样的区间,并且给你每个区间花的钱,请问最后,这些树木花了多少药费。

输入:

输入描述: 每组输入的第一行有两个整数 N(1 <= N<= 1000000)和 M(1 <= M <= 100000)。 N 代表马路的共计多少棵树,M代表区间的数目,N 和 M 之间用一个空格隔开。 接下来的 M 行每行包含三个不同的整数,用一个空格隔开,表示一个区域的起始点 L 和终止点 R 的坐标,以及花费。

输入样例:

500 3

150 300 4

100 200 20

470 471 19

输出描述: 输出包括一行,这一行只包含一个整数,所有的花费。

输出样例:

2662

题目解析:

  1. 利用b[i]=a[i]-a[i-1] 差分式。

    这里由于开始时都是 0,可以用,但是用完都还是 0,所以没有意义,所以直接跳过即可。

  2. 依次读入区间的值,然后将对于区间的操作转化为对于区间端点操作加减。 由于我们从1开始,所以数目整体区间要右移1位。

    对于每个 [l,r] 区间的加减操作都转化为对端点 l,r+1 的操作。

  3. 差分还原(前缀和)。

    差分算法解决区间加减问题通用框架

    //读入原始数据 n,m,a
    
    输入n,m
    
    for(int i=1;i<=n;i++){
        
        输入a[i]
    }
    
    //差分
    for(int i=1;i<=n;i++)
        
        b[i]=a[i]-a[i-1] 
    
    //区间操作
    while(m--)
    {
        输入l,r,value
        b[l]+value
        b[r+1]-value
    }
    
    //前缀和还原
    for(int i=1;i<n;i++){
           b[i]=b[i]+b[i-1]
    } 
    

    代码实现:

    package 差分与前缀和;
    
    import java.util.Scanner;
    
    public class ChaFen {
    	
    	static int b[]=new int [100005];//存放每个区间花费的数组
    	
    	public static void main(String[] args) {
    
    		Scanner scanner = new Scanner(System.in);
    		int n;// 代表马路的共计多少棵树
    		int m;// 代表区间的数目
    		n = scanner.nextInt();
    		m = scanner.nextInt();
    		// 接下来的 M 行每行包含三个不同的整数,用一个空格隔开,表示一个区域的起始点 L 和终止点 R 的坐标,以及花费
    		while (m > 0) {
    			m--;
    			int l, r, value;
    			l = scanner.nextInt();
    			r = scanner.nextInt();
    			value = scanner.nextInt();
    			b[l+1]+=value;//注意每个区间的起始点 L 和终止点 R 的坐标
    	        b[r+1+1]-=value;
    		}
    		
    		//差分还原
    		int sum=0;//总钱数
    		for(int i=1; i<=n; i++) {
    			  b[i]=b[i]+b[i-1];
    			  sum+=b[i];
    		}
    
    		 System.out.println(sum);
    	}
    
    }
    

2. 前缀和 (解决区间求和问题)

  1. 前缀和是指某序列的前 n 项和,可以把它理解为数学上的数列的前 n 项和。当对于某一数组区间进行多次询问[L,r] 的和时,如果正常处理,那么我们每次都要 [l,r]。查询 N 次,那么时间复杂度也是 O(nm) 也是平方阶的。
  2. 如果我们采用前缀和,构造出一个前缀和数组,通过对于端点的值的减法操作就能 O(1) 的求出 [l,r] 的和。然后 N 次查询的,就将复杂度降低为 O(n)

前缀和的特点:

  1. 将对于区间的求和操作转化为对于端点值的减法的操作;
  2. 区间求和操作的时间复杂度为 O(1);
  3. 数组存放时要从 1 开始;
  4. 前缀和数组比原来的数组序列多一个数,第 0 个

前缀和算法解题的基本思路:

  1. 利用 sum[i]=a[i]+sum[i-1] 差分式;
  2. 从第 1 项到 n 项,且第 0 项无数据默认为 0;
  3. 对于区间求和的操作转化为端点值相减。

前缀和的一般解题过程:

/*首先假设有一个数组:

1 2 3 4 5 7 2

前缀和后:

0 1 3 6 10 15 22 24

一般应用场景:

让你对区间 [l,r] 求和操作N次

如:

从第二个元素到第五个元素的和
从第二个元素到第四个元素的和
 
....

这里我们先演示前2个:

对于每个 [l,r] 区间的求和操作转化为区间端点的加减操作

sum[l,r] = sum[r]- sum[l-1]

从第二个元素到第五个元素的和:

转化为: sum[5]- sum[1]

那么Sum[2,5]= sum[5]- sum[1]=15-1=14
在原序列中:2+3+4+5=14

确实是相等的,就是这么神奇。

我们继续操作:

从第二个元素到第四个元素的和

转化为:[4]-[1]

那么Sum[2,4]=[4]-[1]=9

且 2+3+4=9
*/

例题:

题目描述:

教室外有 N 棵树,根据不同的位置和树种,学校已经对其进行了多年的维护。因为树的排列成线性,且非常长,我们可以将它们看作一条直线给他们编号。 树的编号从 1--N 且 N<1e6。由于已经维护了多年,每一个树都由学校的园艺人员进行了维护费用的统计。 每棵树的前期维护费用各不相同,但是由于未来需要要打药,所以有些树木的维护费用太高的话,就要重新种植。由于维护费用也称区间分布,所以常常需要统一个区间里的树木的维护开销。 现在园艺人员想知道,某个区间内的树木维护开销是多少。共计 M 个区间需要查询。

输入描述:

每组输入的第一行有两个整数 N(1 <= N<= 1000000)和 M(1 <= M <= 100000)。 N 代表马路的共计多少棵树,M 代表区间的数目,N 和 M 之间用一个空格隔开。接下来的一行,包含 N 个数,每个数之间用空格隔开。 接下来的M行每行包含两个不同的整数,用一个空格隔开,表示一个区域的起始点L和终止点R的坐标。 输入样例:

10 3

7 5 6 4 2 5 0 8 5 3

1 5

2 6

3 7

输出描述:   

输出包括M行,每一行只包含一个整数,所有的花费。

输出样例:

24

22

17

题目解析:

  1. 利用sum[i]=a[i]+sum[i-1] 前缀和式在输入时求出前缀和;
  2. 依次读入区间的值,然后将对于区间的求和操作转化为对于区间端点操作加减,对于每个 [l,r] 区间的求和操作都转化为对端点[r]-[l-1]的操作。
  3. 输出答案。

前缀和一般解题过程:

//输 入 N 和 M 

//输入 N 个值 并计算前缀和
for( int i=1;i<=N;i++)
    输入a[i]
    并计算sum[i]=sum[i-1]+a[i]

//输入 M 个区间,计算结果

while(M)
    M--
    输入 L , R
    计算 [r]-[l-1],并输出

代码实现:

package 差分与前缀和;

import java.util.Scanner;

//所以此题是按树的编号作为了每一个树的维护费用
public class QianZhui {
	
	static int a[]=new int [100005];
    static int sum[]=new int [100005];

	public static void main(String[] args) {
		 Scanner sanner = new Scanner(System.in);
	        int n;  //n棵树
	        int m; // m个区间
	        n = sanner.nextInt();
	        m = sanner.nextInt();

	        for(int i=1;i<=n;i++)
	        {
	            a[i]= sanner.nextInt();
	            sum[i]=a[i]+sum[i-1];

	        }

	        while(m>0)
	        {
	            m--;
	            int l,r;
	            l = sanner.nextInt();
	            r = sanner.nextInt();
	            System.out.println((sum[r]-sum[l-1]));
	        }
	}
//验证:
    //a[i] = 7 5 6 4 2 5 0 8 5 3
//前缀和:
  //sum[i]= 0 7 12 18 22 24 29 29 37 42 45
    //比如:1和5的起终点
    sum[5]-sum[0] = 24-0=24 
     7+5+6+4+2=24
}

posted on 2022-03-20 16:04  精致猪猪侠  阅读(146)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3