前言
其实这本来是一个很小的知识点,但是其中蕴含了一些后面常用的思想,所以我不得不讲一下。
前缀和
前缀和,说白了就是存储每一个前缀的和。至于这个前缀是什么,我们需要简单分类。
一维前缀和
一维前缀和,就是求对于一个数组 \(a_1,a_2,\dots,a_n\) 的前缀和,显然 \(s_i=a_1+a_2+\dots+a_i\)。
这个东西的用处也很简单:比如你要求区间 \([l,r]\) 中 \(a_i\) 的和,也就是 \(a_l+a_{l+1}+\dots+a_r\),那么我们可以把等式改写为 \(a_1+a_2+\dots+a_r-(a_1+a_2+\dots+a_{l-1})=s_r-s_{l-1}\),这样我们就从一次 \(O(n)\) 的扫描变成了 \(O(1)\) 的询问。
这个东西大部分时候的用处都在于优化时间复杂度(因为它可以把一个求和过程从 \(O(n)\) 变成 \(O(1)\))。
至于 \(s_i\) 怎么求,也很简单:我们知道 \(s_i=a_1+a_2+\dots+a_i=a_1+a_2+\dots+a_{i-1}+a_i=s_{i-1}+a_i\),因此如果我们知道了 \(s_{i-1}\),那么我们就可以推出 \(s_i\)。这显然是容易的。
代码还是写一份吧:
s[0]=0;
for(int i=1;i<=n;i++)
{s[i]=s[i-1]+a[i];
}
查询:
int ans=s[r]-s[l-1];
二维前缀和
二维前缀和,说白了还是一种前缀和,只不过它从求一维数组的前缀和,改成了求二维数组的前缀和。
我们设 \(s_{i,j}=a_{1,1}+a_{1,2}+\dots+a_{2,1}+a_{2,2}\dots+a_{i,j}\),那么我们考虑左上角为 \((x_1,y_1)\)、右下角为 \((x_2,y_2)\) 之间的这一块区域的和怎么求。
显然我们可以加上 \(s_{x_2,y_2}\),但是因为上面那一部分不能算,所以我们要减去 \(s_{x_1-1,y_2}\),左边也不能算,所以要减去 \(s_{x_2,y_1-1}\),但是因为左上角被减了两次,所以我们需要额外加回来一次,也就是还要加上 \(s_{x_1-1,y_1-1}\)。
所以我们可以得到最终公式就是 \(s_{x_2,y_2}-s_{x_1-1,y_2}-s_{x_2,y_1-1}+s_{x_1-1,y_1-1}\)。
那我们要求这个二维前缀和也是很简单,照着上面一维前缀和的方式手推一下就行了。
展示一下最终公式:\(s_{i,j}=s_{i-1,j}+s_{i,j-1}-s_{i-1,j-1}+a_{i,j}\)。
前缀和暂时讲到这,因为前缀和真的是一个很简单的知识点,没有特别多要讲的地方。
差分
差分这个东西说着就很有意思了,因为它的思想不止在数字上,也可以扩大到很多方面。
不过为了方便,我们还是从最基础的开始讲。
我们考虑这样一个数列 \(a_1,a_2,\dots,a_n\),如果我要让区间 \([l,r]\) 内的所有数都加上 \(1\),那怎么才能快速操作呢?很遗憾你并不会树状数组、线段树、分块这些高级东西,所以你只能靠别的东西了。
我们考虑构造一个与前缀和相逆的东西,也就是说我们要让 \(a_i\) 成为另一个数列的前缀和,怎么构造呢?我们考虑构造数列 \(a_1,a_2-a_1,a_3-a_2,\dots,a_n-a_{n-1}\),那么你对它做前缀和,你惊人的发现这个数列的前缀和就是原数组,我们把这个数列叫做差分数列。求的方法就是上面写的 \(c_i=a_i-a_{i-1}\)。
那么如果我们要让一段区间 \([l,r]\) 里的数全部加 \(1\),我们只需要给 \(c_l\) 加 \(1\),这样后面的数在做前缀和的时候就都会额外加一个 \(1\) 了。但是我们不希望 \(r+1\) 之后的数也加 \(1\),所以我们只需要再给 \(c_{r+1}\) 减 \(1\),这样从 \(r+1\) 开始的所有数都会先加 \(1\) 再减 \(1\),相当于没变。
这样,我们就将一个需要 \(O(n)\) 执行的操作变成了 \(O(1)\)(当然最后还原是 \(O(n)\) 的)。
你可能觉得这东西没什么特别的,但是我告诉你:这个东西的思想是非常有趣的。
它有趣的点在于:我们可以把一个区间操作分解成两个单调操作,从而大大降低操作时的时间复杂度,而在最后才将所有的操作统一处理,实现复杂度的平衡。我们考虑一个更大胆的问题:如果我需要对区间 \([l,r]\) 内的所有数进行一次操作,这个操作可能是各种各样的,那我完全可以想办法在 \(l\) 处存下这个操作,再在 \(r+1\) 处标记删除这个操作,这样我们就可以将多个操作揉在一起,实现一次性操作,从而大大优化时间复杂度。
虽然现在你可能还看不懂,但之后你就明白了。
还是放一下基础代码:
for(int i=1;i<=n;i++)
{c[i]=a[i]-a[i-1];
}
修改:
c[l]++,c[r+1]--;
复原:
for(int i=1;i<=n;i++)
{a[i]=a[i-1]+c[i];
}
差不多就这些吧,反正这也不是什么大的知识点。
