高维前缀和略解
Ameiyo
·
2019-05-12 13:24:04
·
个人记录
高维前缀和略解
made\ by\ Ameiyo
前言
似乎我们教练也说是叫 高维前缀和 。。。
那么高位前缀和又是什么呢。。。
算了这些是题外话。
关于高维前缀和:
目前看来可以用于 DP 的优化,但是有一定的局限性。
若对于所有的状态 S ,其需要向 S' 贡献答案,在这里我们称 S' 为 S 的父亲, S 为 S' 的子孙。
高维前缀和的使用条件:
(这个条件其实无视也无所谓,因为是我自己根据证明方法胡诌的。换句话说,如果证明方法,或者说更新方法不一样,约束也就不一样了)
(1) S' 的父亲必是其子孙 S 的父亲之一。
(2) S 到其某个父亲的 S' 的转移路径或是直接转移,或是能通过其他直接转移一个个转移上去,并且中间的状态必是 S 的父亲。
(3) 父子关系不能有环。
从上述第三点也可以看出状态的转移满足无后效性,所以高维前缀和往往是对 DP 的优化。
loop d : // 遍历所有直接转移
loop S : // 遍历所有状态
if (check(S + d)) dp(S + d) += dp(S) // 如果合法,更新
而 DP 的做法往往是这样:
loop S : // 遍历所有的状态
loop D : // 遍历所有转移,包括间接转移
if (check(S + d)) dp(S + d) += val(S) // 如果合法,更新
这个代码看起来没有 DP 的样子,实际上他可能是某个 DP 的内层,这个时候优化可能就很有用了。
对比两份代码,我们会发现高位前缀和对时间的优化主要是针对转移。相当于是把大的转移拆分成小的互不干扰的转移,这种优化实际上有一个很经典的实例—— 多重背包的二进制优化 。
在理解时,可以借助二进制优化来思考 。
更详细的讲解
为了更好的理解,这里先使用较常用的二进制表示集合(状态)的方法来做例子。(没错就是刷状压时遇到的QAQ)
先看一道题。
CodeChef MAXOR: Good Pairs
(这里给出的是 vjudge 链接)
You are given N integers: A_1 , A_2 , ..., A_N . You need to count the number of pairs of indices (i, j) such that 1 \leq i < j \leq N and A_i | A_j \leq max(A_i, A_j) .
给你 n 个数,每个数是 A_i ,求有多少对 (i, j) ,满足 A_i | A_j \leq max(A_i, A_j) 。
1 \leq N \leq 10 ^ 6
0 \leq A_i \leq 10 ^ 6
1 \leq Sum\ of\ N\ over\ all\ test\ cases \leq 10 ^ 6
因为 x\ |\ y\ \geq x ,所以满足 A_i\ |\ A_j \leq max( A_i,\ A_j ) 就相当于 A_i\ |\ A_j\ =\ max(A_i,\ A_j ) 。在 A_i \leq A_j 的时候就相当于 A[i] 的二进制表示法是 A_j 的子集(被包含)。
我们用 f[i] 表示二进制表示法下是 i 的子集的数的个数,其中有 g[i] 个于 i 相同。
那么 i 对 Ans 的贡献就是
g[i] \cdot (g[i] - 1) / 2 + (f[i] - g[i]) \cdot g[i]
重点就是 f[i] 的统计。
如果是普通的枚举,那么复杂度将会达到 O( \Sigma C_n ^ k \cdot 2 ^ k ) , n 可以达到 20 ,这会是 TLE 级别的复杂度(不想算。。但是在 k = 10 的时候就已经有亿万级别了)。
(复杂度的计算是这样的,每个状态被自己的所有子集更新一次,子集个数是 2 ^ k , k 是状态中 1 的个数。那么 1 的个数是 k 的状态个数是 C_n ^ k ,也就是上述的那样。在 k = 10,\ n = 20 时,大小为 189190144 )
我应该没算错QAQ。
所以我们需要用到高维前缀和。
初始时 f[i] = g[i] ,然后
loop i from 0 to 21 : // 枚举直接转移(通过某一位上的1)
loop S from Maxn to 0 : // 枚举状态
if (S & (1 << i)) dp(S) += dp(S ^ (1 << i)) // 如果可以更新
注意枚举转移放在了外层循环,而不是内层,否则就会重复许多。
下面证明这样子对于所有的 S 而言,更新到了其全部的父亲并且没有重复更新。
这里我们建一个图,当且仅当 S 能通过改变一个 0 为 1 变为 S' 时,从 S 为起点,向 S' 连边,于是一条边就对应着代码里的一次转移。
比如说 000 可以向 010, 001, 100 连边,但是不能向 011 连边,因为它改变了两个 0 为 1 。
那么, 3 位的图就是这个样子
这张图也可以很好的解释为什么在内层循环枚举转移会重复,因为每次把每个状态的所有边同时走了一遍。
可以发现在每一层的节点中, 1 的个数都是相同的,并且上下两层呈递增关系。
每走一条边,就对应着代码里一次更新,也就是相当于把 到达过 当前状态 S 的点累积到 S' 中。
(用“到达过”是因为加了之后不会去掉)
对于某个 S 及其任意一个父亲 S' ,设 t = S' - S ,也就是二者的差。
假设 t 中 1 的个数为 cnt ,那么 S 需要走 cnt 条边才能到达 S' 。
比如说按照代码,三位的转移如下:
| 转移的1 \ 结点 | 000 | 001 | 010 | 100 |
| :------------: | :--: | :-----: | :-----: | :-----: |
| 初始 | 000 | 001 | 010 | 100 |
| 经过001的更新 | 000 | 000,001 | 010 | 100 |
| 经过010的更新 | 000 | 000,001 | 000,010 | 100 |
| 经过100的更新 | 000 | 000,001 | 000,010 | 000,100 |
| --- | --- | --- | --- | --- |
| __转移的1 \ 结点__ | __011__ | __101__ | __110__ | __111__ |
| 初始 | 011 | 101 | 110 | 111 |
| 经过001的更新 | 010,011 | 100,101 | 110 | 110,111 |
| 经过010的更新 | $ \ $ 000,001,010,011 | 100,101 | 100,110 | $ \ $ 100,101,110,111 |
| 经过100的更新 | $ \ $ 000,001,010,011 | $ \ $ 000,001,100,101 | $ \ $ 000,010,100,110 | $ \ $ 000,001,010,100,011,101,110,111 |
而走的边的种类是按顺序枚举的,也就是说 $ S $ 到 $ S' $ 的路径用边来描述是一个有序数列,既然顺序固定,边的种类又没有变化,那么这个数列是唯一的。也就是说 $ S $ 按照代码只会在一种 __转移的集合__ 中更新到 $ S' $ 。也就是不会重复更新。
而既然每条路径都是存在的,也就是 $ t $ 是必然能通过一种序列满足的,也就是说这个数列是必然存在的,那么对于 $ S $ 所有的父亲而言,对应的 $ t $ 都是有对应的 __符合代码的转移路径__ 的,即所有的父亲都会更新到。
个人认为可能有一点抽象,还不懂得同学可以手动模拟一下,记录当前状态下的 $ dp $ 数组包含了那些点,然后顺着每次挑一种边累加下去。。。(可以对着上面的表格)
这种模拟的方法可以帮助你很大程度上理解算法,然后看证明可能会更容易看懂。如果还没有看懂可以 $ QQ $ (1134406557)找我。
然后回到这道题。
解析前面也说的很详细了,裸的高维前缀和就行,直接上代码吧。
```cpp
n = read<int>();
for (reg int i = 1, x; i <= n; ++i) {
x = read<int>();
++f[x], ++g[x];
}
for (reg int i = 0; i < 21; ++i)
for (reg int j = 0; j <= N - 5; ++j)
if (j & (1 << i)) f[j] += f[j ^ (1 << i)];
ll Ans = 0;
for (reg int i = 0; i <= N - 5; ++i) {
if (g[i]) {
Ans += g[i] * (g[i] - 1) / 2;
Ans += g[i] * (f[i] - g[i]);
}
}
cout << Ans << endl;
```
(注意多组数据别忘记清零)
---
## 条件的进一步提炼
这里再来看一看高维前缀和使用的条件,借助证明把他提取出来。
我们会注意到证明中很重要的一点,那就是 __转移的路径__ 。事实上,只要保证了按照代码的运行,转移的路径 __只有一条并且一定存在__ 就满足了必定更新与不重复更新,也就保证了答案的正确性。
(事实上就是对上述三条的总结QAQ)
无环就是显然的只有一条路径的必要条件,也即无后效性。
也就是说,高维前缀和的使用与否可能是取决于代码,甚至是对数组意义的定义。
---
## 更多的例题
目前找到的题目比较少。。。不是和上一道题类似就是涉及到没学过的算法,或许等老师之后给我们开专题后会加上来。
(说白了就是我太菜了嘤嘤嘤)