算法学习——前缀和与差分

一.前缀和

1.一维前缀和

一维前缀和可以类比数学中的数列求和。

a1 a2 a3 a4 ... an // 数组a 
s1 s2 s3 s4 ... sn // 前缀和数组 
s1 = s0 + a1;
s2 = s1 + a2;
s3 = s2 + a3;
s4 = s3 = a4;
......
sn = sn-1 + an;

假设有数组a,求其前缀和数组s.其推导如上图。

一维前缀和公式就为

sn = sn-1 + an;

要求任意a[l] 到 a[r] 之间(包括两端的数)所有数的和就为

s[l,r] = s[r+1] - s[l]; 

杂谈:

对于任意一组数,假如我们只求一次前缀和,我们完全可以不开数组,用数组的意义在于,多次求和,这会剩下运算时间。

令人兴奋的是,我在这里看出用数组来储存对应的前缀和有一定的哲学意味,就好比我们做一些物理题会有二级结论,我们会首先推一遍,得出二级结论,然后记住这个二级结论。这里计算机也好像有了“记忆”,经过“推导”,记住了一些“二级结论”;但有利也有弊,我们记住了一些物理二级结论后,做题的速度虽然快了,但我们记得多了,脑的负担也增加了,计算机也是如此,虽然算的快了,但储存空间增大了。

说到底前缀和数组,是用空间换时间.

2.二位前缀和

 对于二维前缀和,我们可以把数组抽象抽象成如上图所示。

如图,一个直角坐标系的正正区域,每隔一个“1”距离,画一条线,构成了边长为“1”的正方形小格子,每个小格子存放着该正方形右下角坐标对应的数。这样,我们再求前缀和,就是数学几何问题了,而且是小学那种。

现在假设有一组矩形数组,要求对应的前缀和数组

//一个矩形数组 
a[1,1] a[1,2] ... a[1,n]
a[2,1] a[2,2] ... a[2,n]
.
.
.
a[n,1] a[n,2] ... a[n,n]
//对应的前缀和数组
s[1,1] s[1,2] ... a[1,n]
s[2,1] s[2,1] ... s[2,n]
.
.
.
s[n,1] s[n,1] ... s[n,n]

要求任意的s[i][j],就是求从原点到(i,j)围成的方格中所有数的和,可以等价于求该图形的面积(注意面积的最小单位是1*1)这时,图中划分了四个区域,红区,蓝区,红白区,黑区。整个图形的面积就是 s = 红 + 蓝 - 红蓝 + 黑。

刚学的时候,我有过一个比较蠢的想法,为什么又加又减的,直接从原点加到(i,j)不行了吗?当然是不可以的。比如在一维中,要算s[n],你得先算s[n-1]。这就是为什么,又加又减的。

最后得出前缀和公式是

s[i,j] = s[i-1,j] + s[i,j-1] + s[i-1,j-1] + a[i,j];

假如我们要求任意的矩形[x1,y1]-[x2,y2]的前缀和,就是

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

二.差分

1.一维差分

差分可以说是前缀和的逆运算。

b1 b2 b3 ... bn //差分数组
a1 a2 a3 ... an //原数组

b1 = a1 - a0;
b2 = a2 - a1;
b3 = a3 - a2;
...
bn = an - an-1;

假设有一个差分数组b,原数组a就是对应的前缀和。

所以对于给定的数组a,其对应有差分数组b。

bn = an - an-1;

利用差分数组与对应的原数组的关系,我们如果要对原数组更改,可以改变对应的差分数组来改变原数组。

扯了一堆,主要应用这一个问题,即我们给出一个原数组a,又让原数组的区间[l,r]里每个数都加上或减去一个常数c,那么我们可以让差分数组里的数b[l]+c,这样的效果是,a[l]以后的数都加了c,但我们只要区间[l,r]里的原数组加上c,所以我们让b[r+1]-c;

//让[2,4]区间里原数组a都加c 
b1 b2 b3 b4 b5 b6
   +c +c +c +c +c
            -c -c
a1 a2 a3 a4 a5 a6
   +c +c +c

所以这里对差分数组b进行的操作就是

b[l] += c; b[r+1] += c; 

然后对该区间的差分数组求和,记得改变后的原数组。

2.二维差分

二维数组主要仍是针对一维差分中提到的问题。

即给出一个矩阵原数组,让[x1,y1]-[x2,y2]矩阵原数组都加上常数c(其实是加或减,不妨认为是加)

解决该问题,我们大体分为三步:(1)构造原数组对应的差分数组。(2)对差分数组进行操作。(3)对差分数组求和。

1.我们先来看对二维差分数组进行操作。

 如图,把上图分为四个区域:黄区(有黄颜色的区域),红黄区,蓝黄区,红黄蓝区。(图可能不好分辨,尽力了。。。)现在我们让b[x1][y1]+c,其效果是黄区所有a数组的数都加了c;为了限制区间,我们让 b[x2+1][y1]-c,b[x1][y2+1]-c,效果是红黄区和蓝黄区原数组的数都正常了,但红黄蓝区就多减了c,所以我们让b[x2+1][y2+1],这样就全部操作就完成了。

其代码为

void insert(int x1,int y1,int x2,int y2,int c)
{
	b[x1][y1] += c;
	b[x2+1][y1] -= c;
	b[x1][y2+1] -= c;
	b[x2+1][y2+1] += c;
}

2.然后我们看一下二维数组的构造这一步。

这里有两种方法,一是利用前缀和公式逆运算得到差分数组

for (int i=1;i<=n;i++) 
	for (int j=1;j<=m;j++)
		b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1];

这种方法较为容易理解。

二是利用差分的逆运算来得到差分数组

经过差分数组的操作那一步,我们可以知道,差分的效果就是得到这样一个数组:对其指定区间[x1,y1]-[x2,y2]的数组求前缀和,那么原数组a,都加了一个c,也就是说,对应区间的每一个数,加到其对应的坐标,该数多了一个c。

我们要构造的差分数组应满足这样的性质,即从b[1][1]开始加一直加到b[i][j],应有b[i][j] == a[i][j]。

所以我们现在建立一个数组b[N][N],这里边的数都为零,对其进行操作,操作的范围是每一个原数组的数的下标[i,j]-[i,j],常数c是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]);

3.最后就是第三步,对处理好的差分数组求和。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
前缀和差分是一类常用的算法,它们常常被用来优化一些区间操作的问题,如求区间和、区间最大值/最小值等等。下面我们将分别介绍前缀和差分的定义、用法和常见问题。 ## 前缀和 前缀和,顾名思义,就是把前面所有数的和都求出来,用一个数组存起来,以便之后的查询。 ### 定义 给定一个长度为 $n$ 的序列 $a$,令 $s_i = \sum_{j=1}^{i}a_j$,则 $s$ 称为序列 $a$ 的前缀和数组。 ### 用法 前缀和的主要作用是用 $O(1)$ 的时间复杂度求出一个区间 $[l,r]$ 的和,即 $s_r - s_{l-1}$。这是因为 $s_r$ 存储了序列从 $1$ 到 $r$ 的和,而 $s_{l-1}$ 存储了序列从 $1$ 到 $l-1$ 的和,因此区间 $[l,r]$ 的和可以通过两个前缀和相减计算得出。 前缀和的时间复杂度为 $O(n)$,因为需要遍历一遍序列求出前缀和数组。但是,如果有多个查询需要求区间和,那么使用前缀和可以将每次查询的时间复杂度降低到 $O(1)$。 ### 代码实现 下面是使用前缀和求区间和的代码实现: ```cpp vector<int> a; // 原序列 vector<int> s(a.size() + 1); // 前缀和数组 // 计算前缀和 for (int i = 1; i <= a.size(); i++) { s[i] = s[i - 1] + a[i - 1]; } // 查询区间 [l, r] 的和 int sum = s[r] - s[l - 1]; ``` ## 差分 差分前缀和相反,它主要用来对区间进行修改。我们可以利用差分数组进行区间修改,并最终得到修改后的序列。 ### 定义 给定一个长度为 $n$ 的序列 $a$,令 $d_i = a_i - a_{i-1}$($d_1 = a_1$),则 $d$ 称为序列 $a$ 的差分数组。 ### 用法 差分的主要作用是对区间进行修改。假设我们需要将区间 $[l,r]$ 的数加上 $k$,我们可以将差分数组的 $d_l$ 加上 $k$,将 $d_{r+1}$ 减去 $k$。这样,对差分数组求前缀和,就可以得到修改后的序列。 具体来说,我们可以按照以下步骤进行区间修改: 1. 对差分数组的 $d_l$ 加上 $k$; 2. 对差分数组的 $d_{r+1}$ 减去 $k$; 3. 对差分数组求前缀和,得到修改后的序列。 差分的时间复杂度为 $O(n)$,因为需要遍历一遍序列求出差分数组。但是,如果有多次区间修改需要进行,那么使用差分可以将每次修改的时间复杂度降低到 $O(1)$。 ### 代码实现 下面是使用差分进行区间修改的代码实现: ```cpp vector<int> a; // 原序列 vector<int> d(a.size() + 1); // 差分数组 // 计算差分数组 for (int i = 1; i < a.size(); i++) { d[i] = a[i] - a[i - 1]; } // 修改区间 [l, r],将数加上 k d[l] += k; d[r + 1] -= k; // 对差分数组求前缀和,得到修改后的序列 for (int i = 1; i < d.size(); i++) { a[i] = a[i - 1] + d[i]; } ``` ## 常见问题 ### 1. 差分数组的长度是多少? 差分数组的长度应该比原序列长度多 1,因为 $d_1 = a_1$。 ### 2. 什么情况下使用前缀和?什么情况下使用差分? 如果需要进行多次区间查询,那么使用前缀和可以将每次查询的时间复杂度降低到 $O(1)$;如果需要进行多次区间修改,那么使用差分可以将每次修改的时间复杂度降低到 $O(1)$。 ### 3. 前缀和差分的本质区别是什么? 前缀和差分都是用来优化区间操作的算法,它们的本质区别在于: - 前缀和是通过预处理前缀和数组来优化区间查询; - 差分是通过预处理差分数组来优化区间修改。 ### 4. 前缀和差分能否同时使用? 当然可以。如果需要同时进行区间查询和修改,我们可以先使用差分数组对区间进行修改,然后再对差分数组求前缀和,得到修改后的序列。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值