前缀和与差分算法

方法:第一步:输入所需要的数组或矩阵(用for循环)第二步:求s数组的前缀和(用for循环将所有数组均求出)第三步:求区间和 

一、一维数组前缀和计算 

1、输入一个长度为 n 的整数序列。接下来再输入 m 个询问,每个询问输入一对 l , r。对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式
共 m 行,每行输出一个询问的结果。
数据范围
1 ≤ l ≤ r ≤ n
1 ≤ n , m ≤ 100000
−1000 ≤ 数列中元素的值 ≤ 1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10

#include<iostream>
using namespace std;
const int N = 100100;
int a[N], s[N];
int main()
{
	int n,m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)scanf("%d", &a[i]);
	for (int i = 1; i <= n; i++)s[i] = s[i - 1] + a[i];
//前缀和初始化(或者步设s[i]数组直接a[i]+=a[i-1];)

	while (m--)
	{
		int l, r;
		scanf(" %d%d", &l, &r);
		printf("%d\n", s[r] - s[l - 1]);//区间和计算(a[r]-a[l-1])
	}
	
		return 0;
}

注:

定义全局数组就是初始化下标0~N-1的位置都是0。全局数组若不初始化,编译器将其初始化为零。局部数组若不初始化,内容为随机值;

 数组下标从1开始;

while 循环的地方不可分开输入。

二、二维数组前缀和计算 

输入一个 n 行 mm列的整数矩阵,再输入 qq 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。对于每个询问输出子矩阵中所有数的和。

输入格式

第一行包含三个整数 n,m,q。

接下来 n行,每行包含 m 个整数,表示整数矩阵。

接下来 q行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。

输出格式

共 q 行,每行输出一个询问的结果。

数据范围

1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤矩阵内元素的值≤1000

输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21
#include<iostream>
using namespace std;
const int N = 1010;
int a[N][N], s[N][N];
int main()
{
	int n, m, q;
	scanf("%d%d%d", &n, &m, &q);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			scanf("%d", &a[i][j]);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
//求前缀和(或者s[i][j]+=s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] )

	
	while (q--)
	{
		int x1, y1, x2, y2;
		scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
		printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);//求区间和
	}

	return 0;
}

说明:两个for循环可以放在一起,但尽量别放在一起,容易出错;

在输入数值的时候我发现while循环的地方输入第一次循环的数后不可直接按回车键,不然得不到题目所要求的格式,得连着输入 

三、一维数组差分 

定义:差分可以看成前缀和的逆运算。

差分数组:

首先给定一个原数组a:a[1], a[2], a[3]…… a[n];

然后我们构造一个数组b : b[1] ,b[2] , b[3]…… b[i];

使得 a[i] = b[1] + b[2 ]+ b[3] +,,,,,, + b[i]也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。换句话说,每一个a[i]都是b数组中从头开始的一段区间和。

考虑如何构造差分b数组?

最为直接的方法如下图示:

我们只要有b数组,通过前缀和运算,就可以在O(n) 的时间内得到a数组 。

写差分代码的步骤:

1)输入数组a[i];

2)利用差分函数得数组a的差分数组b

3)利用差分函数计算[l,r]的差分数组b

4)将差分数组进行前缀和并输出

知道了差分数组有什么用呢?

给定区间[l ,r ],让我们把a数组中的[ l, r]区间中的每一个数都加上c,即 a[l] + c , a[l+1] + c , a[l+2] + c……a[r] + c;

暴力做法是for循环l到r区间,时间复杂度O(n),如果我们需要对原数组执行m次这样的操作,时间复杂度就会变成O(n*m)。有没有更高效的做法吗? 

差分做法始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i]的修改,会影响到a数组中从a[i]及往后的每一个数。

首先让差分b数组中的 b[l] + c ,a数组变成 a[l] + c ,a[l+1] + c…… a[n] + c;

然后我们打个补丁,b[r+1] - c, a数组变成 a[r+1] - c,a[r+2] - c……a[n] - c;

为啥还要打个补丁?

我们画个图理解一下这个公式的由来:


b[l] + c,效果使得a数组中 a[l]及以后的数都加上了c(红色部分),但我们只要求l到r区间加上c, 因此还需要执行 b[r+1] - c,让a数组中a[r+1]及往后的区间再减去c(绿色部分),这样对于a[r] 以后区间的数相当于没有发生改变。

因此我们得出一维差分结论:给a数组中的[ l, r]区间中的每一个数都加上c,只需对差分数组b做 b[l] + = c, b[r+1] - = c。时间复杂度为O(1), 大大提高了效率。

题目:

输入一个长度为 n的整数序列。接下来输入 m 个操作,每个操作包含三个整数 l,r,c表示将序列中 [l,r]之间的每个数加上c。

请你输出进行完所有操作后的序列。

输入格式

第一行包含两个整数 n 和 m。

第二行包含 n个整数,表示整数序列。

接下来 m 行,每行包含三个整数 l,r,c表示一个操作。

输出格式

共一行,包含 n 个整数,表示最终序列。

数据范围

1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000

输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2

代码:

#include<iostream>
using namespace std;
const int N = 100100;
int a[N], b[N];
void insert(int l, int r, int c)//构建差分函数
{
	b[l] += c;
	b[r + 1] -= c;
}
int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)scanf("%d", &a[i]);
	for (int i = 1; i <= n; i++)insert(i, i, a[i]);//得数组a[i]
	while (m--)
	{
		int l, r, c;
		scanf("%d%d%d", &l, &r, &c);
		insert(l, r, c);
	}
	for (int i = 1; i <= n; i++)b[i] += b[i - 1];//一维前缀和
	for (int i = 1; i <= n; i++)printf("%d ", b[i]);
	return 0;

}

注:for (int i = 1; i <= n; i ) insert(i, i, a[i]);
对于这一行,其实是假定a数组最开始都是0,那么b数组初始时就是a数组的差分数组了,对于每一个a[i],相当于插入了一个数,可以直接调用insert函数即可。当然也可以从差分数组的定义出发,for(int i=1;i<=n;i) b[i]=a[i]-a[i-1]; 用这一行替换上一行,效果一样,只是上边的把a数组当成全为0,读入的a[i]再插入,这一个把读入后的当做a数组。

四、二维数组差分

 思路:已知原数组a中被选中的子矩阵为 以(x1,y1)为左上角,以(x2,y2)为右下角所围成的矩形区域;始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i][j]的修改,会影响到a数组中从a[i][j]及往后的每一个数。

假定我们已经构造好了b数组,类比一维差分,我们执行以下操作来使被选中的子矩阵中的每个元素的值加上c,

b[x1][y1] += c;

b[x1,][y2+1] -= c;

b[x2+1][y1] -= c;

b[x2+1][y2+1] += c;

每次对b数组执行以上操作,等价于:

for(int i=x1;i<=x2;i++)
  for(int j=y1;j<=y2;j++)
    a[i][j]+=c;
图解过程:

b[x1][ y1 ] +=c ; 对应图1 ,让整个a数组中蓝色矩形面积的元素都加上了c。
b[x1,][y2+1]-=c ; 对应图2 ,让整个a数组中绿色矩形面积的元素再减去c,使其内元素不发生改变。
b[x2+1][y1]- =c ; 对应图3 ,让整个a数组中紫色矩形面积的元素再减去c,使其内元素不发生改变。
b[x2+1][y2+1]+=c; 对应图4,,让整个a数组中红色矩形面积的元素再加上c,红色内的相当于被减了两次,再加上一次c,才能使其恢复。

将上述操作封装成一个插入函数: 

void insert(int x1,int y1,int x2,int y2,int c)
{     //对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
    b[x1][y1]+=c;
    b[x2+1][y1]-=c;
    b[x1][y2+1]-=c;
    b[x2+1][y2+1]+=c;
}

我们可以先假想a数组为空,那么b数组一开始也为空,但是实际上a数组并不为空,因此我们每次让b数组以(i,j)为左上角到以(i,j)为右下角面积内元素(其实就是一个小方格的面积)去插入 c=a[i][j],等价于原数组a中(i,j) 到(i,j)范围内 加上了 a[i][j] ,因此执行n*m次插入操作,就成功构建了差分b数组.

题目:

输入一个长度为 n 的整数序列。接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r]之间的每个数加上c。请你输出进行完所有操作后的序列。

输入格式

第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数序列。

接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。

输出格式

共一行,包含 n 个整数,表示最终序列。

数据范围

1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000

输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2

代码:

#include<iostream>
using namespace std;
const int N = 1010;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2,int y2,int c)
{
	b[x1][y1] += c;
	b[x1][y2 + 1] -= c;
	b[x2 + 1][y1] -= c;
	b[x2 + 1][y2 + 1] += c;
}
int main()
{
	int n, m, q;
	scanf("%d%d%d", &n, &m, &q);
	for (int i = 1; i <=n; i++)
		for (int j = 1; j <=m; j++)
			scanf("%d", &a[i][j]);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			insert(i, j, i, j, a[i][j]);
	while (q--)
	{
		int x1, y1, x2, y2, c;
		scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
		insert(x1, y1, x2, y2, c);
	}
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++)
			printf("%d ", b[i][j]);
		cout <<endl;
	}
	return 0;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值