前缀和与差分算法

一维前缀和

先看一个例子,假如我们现在有一个数组

arr[]={3,2,5,6,7,8,9,4,2}

现在假如我们要想的得到区间 [ 3 , 6 ],上的数据和,那我们就需要遍历 [ 3 , 6 ] 这个区间进行求和。
代码如下:

#include <stdio.h>
int main()
{
	int arr[] = { 3,2,5,6,7,8,9,4,2 };
	int sum = 0;
	int l, r;
	scanf("%d%d", &l, &r);//获取区间
	for (int i = l; i <= r; i++)
	{
		sum += arr[i];
	}
	printf("%d", sum);
	return 0;
}

假如我们需要多次询问区间和时

#include <stdio.h>
int main()
{
	int arr[] = { 3,2,5,6,7,8,9,4,2 };
	int n = 0;
	scanf("%d", &n); //询问次数
	for (int j = 0; j < n; j++)
	{
		int l, r;
		int sum = 0;
		scanf("%d%d", &l, &r);//获取区间
		for (int i = l; i <= r; i++)
		{
			sum += arr[i];
		}
		printf("%d ", sum);
	}
	return 0;
}

它的时间复杂度将会是O(m*n),当数组很长,询问次数很多时,这样的求和方法效率就非常低下。
为了改进这种算法,就引入了前缀和

前缀和是指某序列的前n项和,可以把它理解为数学上的数列的前n项和,而差分可以看成前缀和的逆运算。
合理的使用前缀和与差分,可以将某些复杂的问题简单化。

前缀和公式

sum[i]=arr[i]          (i==0)
sum[i]=sum[i-1]+arr[i]  (i>0)

这个和高中的数列前n项和是差不多的
对于数组

arr[]={3,2,5,6,7,8,9,4,2}

我们可以根据公式求出他的前缀和数组

sum[]={3,5,10,16,23,31,40,44,46}

当我们要询问区间 [ l , r ] 上的数字和时

sum1=sum[r]-sum[l-1];

其实原理很好理解,比如我们我要求 [3 , 6 ] 上的区间和
sum[6]=arr[0]+arr[1]+arr[2]+arr[3]+arr[4]+arr[5]+arr[6]
sum[2]=arr[0]+arr[1]+arr[2];
sum[6]-sum[2]=arr[3]+arr[4]+arr[5]+arr[6]
如下图所示·
在这里插入图片描述
代码如下:

#include <stdio.h>
#include<stdlib.h>
int main()
{
	int arr[] = { 3,2,5,6,7,8,9,4,2 };
	int m = sizeof(arr) / sizeof(arr[0]); //求元素个数
	int* sum = (int*)malloc(sizeof(int) * m);//开辟前缀和数组
	if (sum == NULL)  //判断数组是否开辟成功
		return 0;
	for (int i = 0; i < m; i++)
	{
		sum[i] = (i == 0) ? arr[i] : arr[i] + sum[i - 1];
	}
	int n = 0;
	scanf("%d", &n); //询问次数
	for (int j = 0; j < n; j++)
	{
		int l, r;
		scanf("%d%d", &l, &r);//获取区间
		printf("%d ", sum[r]-sum[l-1]);
	}
	return 0;
}

通过求前缀和,我们可以把原本O(m*n)的时间复杂度化为O(n)

一维差分

还是刚刚的数组

arr[]={3,2,5,6,7,8,9,4,2}

假如我们要对区间 [ 3 , 6 ] 上的数据都加上 m ,我们就需要遍历这个区间。当我们需要多次进行如上操作时,
时间复杂度也将会成为O(m*n)
代码如下:

#include <stdio.h>
int main()
{
	int arr[] = { 3,2,5,6,7,8,9,4,2 };
	int n = 0;
	scanf("%d", &n);//获取操作次数
	for (int i = 0; i < n; i++)
	{
		int l, r;
		scanf("%d%d", &l, &r);//获取操作区间
		int p = 0;
		scanf("%d", &p);//获取要加的数字大小
		for (int j = l; j <= r; j++)
		{
			arr[i] += p;
		}
	}
	return 0;
}

当数组元素多,操作次数多时,这样的代码也是十低效的。
这样我们就需要引出差分了
差分数组

sub[i]=arr[i]-arr[i-1]   (i!=0)
sub[i]=arr[i]            (i==0)

对上面的数组求差分可以得到

sub[]={3,-1,3,1,1,1,1,-5,-2}

差分数组的性质
1.对差分数组进行前缀和,就可以的到原数组
分析:

原数组:  a1       a2        a3       a4       a5
差分数组:a1      a2-a1     a3-a2    a4-a3    a5-a4
前缀和:  a1       a2        a3       a4       a5

2.要对数组区间 [ l, r ] 的数据都加上m时,我们只需要对差分数组的

sub[l]+=m;
sub[r+1]-=m;

即可,所有操作做完后,做前缀和求出原数组即可
注意事项:如果r+1可以超过原数组大小,所以我们可以在开辟sub数组时多开辟一块空间,也可以对超过原数组的情况不做任何处理(不求差分),也是不会影响结果的。
代码如下:

#include <stdio.h>
#include<stdlib.h>
int main()
{
	int arr[] = { 3,2,5,6,7,8,9,4,2 };
	int m = sizeof(arr) / sizeof(arr[0]);//求数组大小
	int* sub=(int*)malloc(sizeof(int) * (m+1));
	if (sub == 0)
		return 0;//判断是否开辟成功
	for (int i = 0; i <= m; i++)
	{
		sub[i] = (i == 0) ? arr[i] : arr[i] - arr[i - 1];
	}
	int n = 0;
	scanf("%d", &n);//获取操作次数
	for (int i = 0; i < n; i++)
	{
		int l, r;
		scanf("%d%d", &l, &r);
		int p = 0;     //
		scanf("%d", &p);//获取要加的数
		sub[l] += p;
		sub[r + 1] -= p;
	}
	//恢复数组
	for (int i = 0; i < m; i++)
	{
		arr[i] = (i == 0) ? sub[i] : sub[i] + arr[i - 1];
	}

	return 0;
}

二维前缀和

上面我们讨论了一维数组,但二维数组我们需要怎样处理呢?
如下数组:

	int arr[][4] = { {1 ,2, 3, 4},
					 {5, 6, 7, 8},
					 {9, 10, 11,12},
					 {13,14,15, 16} };

如下图:
在这里插入图片描述
如过我们要求(1,1)到 (2,2)的数组和
也就是
在这里插入图片描述

也就是上图中红色框中和
当我们要多次询问的话,按普通算法来算的化时间复杂度是O(m*n)
代码如下

#include <stdio.h>
#include<stdlib.h>
int main()
{
	int arr[][4] = { {1 ,2, 3, 4},
					 {5, 6, 7, 8},
					 {9, 10, 11,12},
					 {13,14,15, 16} };
	int n = 0;
	scanf("%d", &n);//获取操作次数
	for (int i = 0; i < n; i++)
	{
		int x1, y1, x2, y2;
		scanf("%d%d",& x1, &y1);
		scanf("%d%d",& x2, &y2);
		int sum = 0;
		for (int p = x1; p <= x2; p++)
		{
			for (int q = y1; q <= y2; q++)
			{
				sum += arr[p][q];
			}
		}
		printf("%d ", sum);
	}


	return 0;

这种算法是很低效的,为了提高效率,我们可以先求出二位前缀和数组

sum[i][j]是从(0,0)到(i,j)的和

具体方法如下:
在这里插入图片描述
如有要求

sum[2][2]

就可以

sum[2][2]=sum[1][2]+sum[2][1]+arr[2][2]-sum[1][1];

结合图还是很好理解的
总结如下:
当 i!=0,j!=0;时

sum[i][j]=sum[i-1][j]+sum[i][j-1]+arr[i][j]-sum[i-1][j-1];

当i==0,j!=0;时

sum[0][j]=sum[0][j-1]+arr[0][j];

当j==0,i!=0时

sum[i][0]=sum[i-1][0]+arr[i][0];

当i== 0, j==0时

sum[0][0]=arr[0][0];

代码如下:

int arr[][4] = { {1 ,2, 3, 4},
					 {5, 6, 7, 8},
					 {9, 10, 11,12},
					 {13,14,15, 16} };
	int a = 4, b = 4;
	int** sum = (int**)malloc(sizeof(int*) * a);
	for (int i = 0; i < a; i++)
	{
		sum[i] = (int*)malloc(sizeof(int) * b);
		for (int j = 0; j < b; j++)
		{
			if (i == 0 && j == 0)
				sum[0][0] = arr[0][0];
			else if (i != 0 && j == 0)
				sum[i][0] = sum[i - 1][0] + arr[i][0];
			else if (i == 0 && j != 0)
				sum[0][j] = sum[0][j - 1] + arr[0][j];
			else
				sum[i][j] = sum[i - 1][j] + sum[i][j - 1] + arr[i][j] - sum[i - 1][j - 1];
		}
	}

当我们有了前缀和数组后,解决最开始的问题就简单多了
如果要求(1,1)到(2,2)的和
在这里插入图片描述

sum2=sum[2][2]-sum[2][0]-sum[0][2]+sum[0][0]

总结如下:
(x1,y1),(x2,y2), 前提是 (x1<=x2,y1<=y2)
x1== 0&&y1==0

sum2=sum[x2][y2]

x1!=0&&y1==0

sum2=sum[x2][y2]-sum[x1-1][y2];

x1==0&y1!=0

sum2=sum[x2][y2]-sum[x2][y1-1]

x1!=0&&y1!=0

sum2=sum[x2][y2]-sum[x1-1][y2]-sum[x2][y1-1]+sum[x1-1][y2-1];

完整代码:

#include <stdio.h>
#include<stdlib.h>
int main()
{
	int arr[][4] = { {1 ,2, 3, 4},
					 {5, 6, 7, 8},
					 {9, 10, 11,12},
					 {13,14,15, 16} };
	int a = 4, b = 4;
	int** sum = (int**)malloc(sizeof(int*) * a);
	for (int i = 0; i < a; i++)
	{
		sum[i] = (int*)malloc(sizeof(int) * b);
		for (int j = 0; j < b; j++)
		{
			if (i == 0 && j == 0)
				sum[0][0] = arr[0][0];
			else if (i != 0 && j == 0)
				sum[i][0] = sum[i - 1][0] + arr[i][0];
			else if (i == 0 && j != 0)
				sum[0][j] = sum[0][j - 1] + arr[0][j];
			else
				sum[i][j] = sum[i - 1][j] + sum[i][j - 1] + arr[i][j] - sum[i - 1][j - 1];
		}
	}
	int n = 0;
	scanf("%d", &n);//获取操作次数
	for (int i = 0; i < n; i++)
	{
		int x1, y1, x2, y2;
		scanf("%d%d",& x1, &y1);
		scanf("%d%d",& x2, &y2);
		int sum2 = 0;
		if (!x1 && !x2)
			sum2 = sum[x2][y2];
		else if (!x1)
			sum2 = sum[x2][y2] - sum[x2][y1 - 1];
		else if (!y1)
			sum2 = sum[x2][y2] - sum[x1 - 1][y2];
		else
			sum2 = sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
		printf("%d ", sum2);
	}
	return 0;
}

二维差分

同理还是上面数组

int arr[][4] = { {1 ,2, 3, 4},
					 {5, 6, 7, 8},
					 {9, 10, 11,12},
					 {13,14,15, 16} };

假如我们想要把(1,1)到(2,2)这个区间都加上数字m,用原始的算法也是很慢的,为了加快效率,也是可以用的,也是可以用到差分的,也就是二维差分。
在这里插入图片描述
我们只需要对差分数组

sub[1][1]+=m
sub[3][1]-=m
sub[1][3]-=m
sub[3][3]+=m

总结:要在区间(x1,y1)到(x2,y2)都加上m

sub[x1][y1]+=m;
sub[x1][y2+1]-=m;
sub[x2+1][y1]-=m;
sub[x2+1][y2+1]+=m;

代码:

#include <stdio.h>
#include<stdlib.h>
int main()
{
	int arr[][4] = { {1 ,2, 3, 4},
					 {5, 6, 7, 8},
					 {9, 10, 11,12},
					 {13,14,15, 16} };
	int a = 4, b = 4;
	int n = 0;
	scanf("%d", &n);//获取操作次数
	int** sub = (int**)calloc(a+1,sizeof(int*));
	for (int i = 0; i < a+1; i++)
	{
		sub[i] = (int*)calloc(b + 1, sizeof(int));   //先把数组开辟出来
	}
	for (int p = 0; p < n; p++)    //求差分数组
	{
		int x1, y1, x2, y2;
		scanf("%d%d", &x1, &y1);    
		scanf("%d%d", &x2, &y2);      //获取操作区间
		int m = 0;                  //要加的数字大小
		scanf("%d", &m);
		sub[x1][y1] += m;
		sub[x2 + 1][y2 + 1] += m;
		sub[x1][y2 + 1] -= m;
		sub[x2 + 1][y1] -= m;
	}
	//对差分数组求前缀和
	for (int i = 0; i < a; i++)
	{
		for (int j = 0; j < b; j++)
		{
			if (i == 0 && j == 0)
				sub[0][0] = sub[0][0];
			else if (i != 0 && j == 0)
				sub[i][0] = sub[i - 1][0] + sub[i][0];
			else if (i == 0 && j != 0)
				sub[0][j] = sub[0][j - 1] + sub[0][j];
			else
				sub[i][j] = sub[i - 1][j] + sub[i][j - 1] + sub[i][j] - sub[i - 1][j - 1];
		}
	}
	//映射回原数组
	for (int i = 0; i < a; i++)
	{
		for (int j = 0; j < b; j++)
		{
			arr[i][j] += sub[i][j];
		}
	}
	return 0;
}
  • 19
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

binary~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值