计算元素贡献 专题
本专题尝试用比较容易理解的方式,去解释一系列计算元素贡献的一类题的通用解法与思考方式
序言
2022.5.22 上午的周赛T4 6077. 巫师的总力量和 - 力扣(LeetCode)就是这一类典型的题型,对于这类 统计序列中所有子数组的某个性质/值的总和 的问法,暴力枚举是一个显然的做法,但是通常它的复杂度会达到$O(n^2)$或者$O(n^3)$的级别,如果题目数据量在$10^4$以上,绝对会超时。那么,对于这一类问题,我们关注如何用简单的思路去实现一个$O(n)$的解法。
这个思路,用一句话来说就是:统计序列中 每个数/每种长度 产生的贡献。我们遍历序列,对于当前元素,如果我们能在$O(1)$的时间内计算出当前 元素/长度 对于答案产生的贡献值,我们就能够将时间复杂度控制在$O(n)$。
对于这样一个方法论,实际上有很多种类型的题目都可以应用,接下来由浅入深,结合例题进行解释。
较直接的问题
问题1:求所有子数组的和
抛砖引玉,先从最直接的一道题出发:1814 · 所有子数组之和 - LintCode 题目的意思是:
给出一个长度为 $n$ 的序列$[a_1,a_2,…,a_n]$(如无特殊说明,此后所有序列定义都由下标1开始),求这个序列的所有子数组的和,例如,序列$[1,2,3]$的所有子数组为$[1],[2],[3],[1,2],[2,3],[1,2,3]$,它们的和为$1+2+3+1+2+2+3+1+2+3=20$。
乍一看这题,有一个显然的解法,就是我们可以枚举每一个子数组,即 一层循环枚举左端点,一层枚举右端点,然后暴力求这个子数组的和(这个可以通过前缀和优化成$O(1)$求),因此复杂度为$O(n^2)$。
如果用统计贡献的思路,那么对于这样一个问题,通常有两种思考方式:
- 考虑计算每个元素对答案的贡献
- 采用动态规划的思路,考虑在遍历这个数组的时候,加入一个新的数对答案的贡献
思路1. 考虑计算每个元素对答案的贡献
我们先思考第1种思路(也是本问题最简单的思路),举个栗子,观察这样一个长度为4的序列$[1,3,5,2]$,思考对于$3$这个元素,它会出现在哪些子数组中?答案是$6$个子数组:$[3],[3,5],[3,5,2],[1,3],[1,3,5],[1,3,5,2]$,因此,它会对答案产生$6$次贡献,则它的贡献为$3\times 6=18$。
进一步地,仔细观察这些子数组的左右端点,我们能发现这样一个规律:左端点一定是小于等于当前数的位置,右端点一定大于等于当前数的位置(因为必须包含这个数),我们发现,$3$这个数处在的位置为$2$,因此它出现在$2\times (4-2+1)=6$个子数组中。更一般地,利用乘法原理我们知道,对于一个下标为$i$的数$a[i]$,它一定会出现在$i\times(n-i+1)$个子数组中(即 左侧选$i$个,右侧选$n-i+1$个,乘法原理),因此它对答案的贡献为$a[i]\times i\times (n-i+1)$。
遍历整个数组,对每个元素直接$O(1)$求出这个值累加到答案中,就完成了$O(n)$的解法。
1 |
|
这里我们来模拟一下计算流程,以 $[1,3,5,2]$为例。
- 考虑$a_1 = 1$计算对答案的贡献为$1\times 1\times 4 = 4$,加入至答案 $ans = 4$
- 考虑$a_2 = 3$计算对答案的贡献为$3\times 2\times 3 = 18$,加入至答案 $ans=4+18=22$
- 考虑$a_3 = 5$计算对答案的贡献为$5\times 3\times 2 = 30$,加入至答案 $ans=22+30=52$
- 考虑$a_4 = 2$计算对答案的贡献为$2\times 4\times 1 = 8$,加入至答案 $ans=52+8=60$
思路2. 动态规划,考虑加入一个新数对答案的贡献
接下来考虑动态规划的思路,我们定义一个状态$f(i)$表示处理完前$i$个数后的答案值。那么,对于$f(i-1)$已经求出的情况下,考虑新加入元素$a[i]$对答案的贡献。
此时,我们还是需要考虑元素$a[i]$的加入相比于前$i-1$个元素产生的贡献,多出了哪些贡献,这个贡献实际上就是 多出来的子数组的和,进一步地,我们发现这些多出来的子数组实际上都是以$a[i]$为结尾的子数组,而这些子数组,显然可以把它拼到以$a[i-1]$为结尾的子数组后面,这样以来,就相当于多出了$(i-1)$个(包含了$a[i]$的)子数组,此外,$a[i]$本身也可以自己作为一个新的子数组,所以总共产生了$(i-1+1=i)$个新的子数组,因此这里产生了一个值为$(i\times a[i])$的贡献,这里要特别注意,这个贡献实际上代表的是:添加$a[i]$后,相较于以$a[i-1]$为结尾的所有子数组的和 多出来的部分,因此我们需要再维护一个状态$g(i)$代表以$a[i]$为结尾的所有子数组的和,那么就有状态转移方程:$g(i) = g(i-1) + i\times a[i]$。对于$f(i)$的状态转移,就是把$g(i)$这部分加上就行了:$f(i) = f(i-1) + g(i)$。
更进一步思考,实际上$g(i)$维护了一个类似前缀和的东西:$\sum_\limits{i=1}^n i\times a[i]$
代码实现方面,由于状态$i$仅与$i-1$有关,因此可以滚动数组将空间优化为$O(1)$。
1 |
|
这里我们来模拟一下计算流程,还是刚刚的例子 $[1,3,5,2]$。
考虑第1个数$1$,以$a_1 = 1$为结尾的所有子数组和为:$1\times 1 = 1$(即$[1]$这一个子数组),此后计算对答案的贡献,$f = 1$;
考虑第2个数$3$,以$a_2 = 3$为结尾的所有子数组和为:$1+2\times 3 = 7$(即$[1,3],[3]$这两个子数组),此后计算对答案的贡献,$f = 1+7 = 8$;
考虑第3个数$5$,以$a_3 = 5$为结尾的所有子数组和为:$7+3\times 5 = 22$(即$[1,3,5],[3,5],[5]$这三个子数组),此后计算对答案的贡献,$f = 8+22 =30$;
考虑第4个数$2$,以$a_4 = 2$为结尾的所有子数组和为:$22+4\times 2 = 30$(即$[1,3,5,2],[3,5,2],[5,2],[2]$这四个子数组),此后计算对答案的贡献,$f = 30+30 = 60$
因此最终答案为$f=60$。
这样一个直接的问题,我们从两种完全不同的思路出发,得到了相同的结果。因此,对于此类计算贡献的问题,我们通常从这两种思路出发去寻找问题的解法,当然,其他很多问题并没有此题这么直接,通常情况下,他们会结合 数学、单调栈/单调队列、动态规划 等知识点综合考察。
问题1变式:求序列所有子集和
如果将问题1种的 子数组 改为 子集 ,则如何求解呢?
这个问题并不难,可以从思路1的角度出发,还是乘法原理,但左侧端点和右侧端点的选择变多了,对于$a[i]$来说,左侧能够选$2^{i-1}$个子集,右侧能够选$2^{n-i}$个子集,因此对答案的贡献为$a[i] \times 2^{i-1}\times 2^{n-i}$.
1 |
|
问题1进阶:求所有子数组平均数的和
问题1的进阶版本:
给出一个长度为 $n$ 的序列$[a_1,a_2,…,a_n]$,求这个序列的所有子数组的平均数,例如,序列$[1,3,5]$的所有子数组为$[1],[3],[5],[1,3],[3,5],[1,3,5]$,它们的平均数的和为$1+3+5+\frac{1+3}{2}+\frac{3+5}{2}+\frac{1+3+5}{3}=18$。
由于最终答案可能出现浮点数,因此最终输出结果有两种方式:
- 输出浮点数
- 输出模$10^9+7$意义下的答案
我们先考虑输出浮点数的情况,对于这样一个问题,我们同样考虑如何计算每个元素产生的贡献。我们发现,对于不同的子数组的长度,每个元素产生的贡献的权重是不一样的,比如说对于$[1,3,5]$这个序列来说,元素$1,3,5$对答案产生的贡献如下:
似乎可以看出有一种规律,我们把元素数写多一些,例如$[a_1,a_2,a_3,a_4,a_5]$种每个元素对答案的贡献分别为:
可以看出,右侧的值实际上是序列的$1+\frac12 + \frac13+…+\frac1n$某种加权和,而这个权重的规律符合三角波的规律,即:
对于$a1$来说,就是$\sum \limits_{i=1}^{n} \frac{1}{i}$;
对于$a2$来说,就是加上了$\sum \limits_{i=2}^{n-1} \frac{1}{i}$;
对于$a3$来说,就是再加上$\sum\limits_{i=3}^{n-2} \frac{1}{i}$,以此类推。
而对于过半之后的元素,也是经过类似的操作,只不过变成了减法。
因此,我们可以通过$O(n)$预处理出$\sum\limits_{i=1}^n \frac{1}{i}$这样一个前缀和,然后在遍历序列的时候$O(1)$处理变化值就行了,总复杂度$O(n)$。
对于输出模$10^9+7$意义下的答案这样的要求,只需$O(n)$预处理出$[1,2,,…,n]$这些元素的乘法逆元即可,复杂度依然是$O(n)$。
解法来源于我在StackExchange上问的相同的问题:algorithms - Sum of average of all subarrays - Computer Science Stack Exchange
1 |
|
同类题:
结合数论分块
同类题:
结合单调栈
E - Second Sum (atcoder.jp)(单调栈求左右第二个比自己大的数)
同类题:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!