DP优化——高维前缀和 (SOSDP)
算法介绍——高维前缀和
引入
我们都知道二维前缀和有这么一个容斥的写法:
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];
}
}
那换成三维前缀和,就有如下容斥代码:
非常的繁琐,于是就诞生了如下二维前缀和的写法:
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
s[i][j]=s[i][j-1]+a[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
s[i][j]+=s[i-1][j];
}
}
其实就是先统计列的前缀和,再统计行的前缀和。
那三维前缀和只需要三遍 forforfor 就搞定了,明显简单很多。
这就启发我们对于更高维度的前缀和,同样只需要做 \(n\)遍\(n\)层 for 循环。
正文
对于上面的 \(n\)遍\(n\)层for循环 的高维前缀和的代码,其复杂度显然不是我们能接受的,但是当每一维很小时就可以进行状态压缩。
最常见的就是每一维的大小为 \(2\) ,此时对于\(L\) 维数组就可以用一个长度为 \(L\) 的二进制数表示其中一个位置。
for(int i=0;i<L;i++){
for(int j=0;j<(1<<L);j++){
if(j>>i&1){
f[j]+=f[j^(1<<i)];
}
}
}
时间复杂度\(O(L\times2^L)\)
子集求和
对于一个二进制数 \(j\) ,如果另一个二进制数 \(i\) 满足,\(i \operatorname{and} j=i\) , 就说 \(j\) 包含 \(i\) , 即 \(i\) 是 \(j\) 的子集。
那上面的代码相当于对每一个 \(j\) 加上它的所有子集,我们称它为 子集求和。
超集求和
对于一个二进制数 \(j\) ,如果另一个二进制数 \(i\) 满足,\(i \operatorname{or} j=i\) , 此时 \(i\) 包含 \(j\) , 即 \(j\) 是 \(i\) 的子集,此时我们称 \(i\) 是 \(j\) 的超集 。
与子集求和类似的,我们有如下 超集求和 代码:
for(int i=0;i<L;i++){
for(int j=0;j<(1<<L);j++){
if(!(j>>i&1)){
f[j]+=f[j^(1<<i)];
}
}
}
应用
高维前缀和是计数的常用技巧,因此也被称为SOSDP。
其他
高维前缀和有时会配合Lucas定理使用,参见Lucas定理入门
例题
Compatible Numbers
\(a \operatorname{and} b = 0\) 等价于 \(a\) 是 \(b\) 的补集的子集,因为题目要求输出任意一个答案,所以只要把上述子集求和的代码中得加操作改成赋值操作即可。
#include<bits/stdc++.h>
using namespace std;
const int N=4e6+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n;
int a[N];
int f[1<<22];
signed main(){
n=read();
for(int i=1;i<=n;i++){
a[i]=read();
f[a[i]]=a[i];
}
for(int i=0;i<=21;i++) {
for(int j=0;j<(1<<22);j++) {
if((j&(1<<i))&&f[j^(1<<i)]) f[j]=f[j^(1<<i)];
}
}
for(int i=1;i<=n;i++){
int b=((1<<22)-1)^a[i]; //计算补集
if(f[b]) printf("%d ",f[b]);
else printf("%d ",-1);
}
return 0;
}
[ARC137D] Prefix XORs
设 \(A_{i,j}\) 表示第 \(j\) 次操作后 \(A_i\) 的值,根据常识或手推可以知道:
因为偶数个 \(A_i\) 异或起来的的结果为\(0\),所以 \(A_{i,0}\) 对 \(A_{n,k}\) 有贡献当且仅当 \(C^{k}_{n-i+k}\) 为奇数,即 $ C^{k}_{n-i+k} \equiv 1 \pmod 2 $,根据 Lucas 定理可知,此时 \(k\) 是 \(n-i+k\) 的子集,即 $k \operatorname{and} (n-i) = 0 $,用高维前缀和预处理即可
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m;
int a[N];
int f[1<<20];
signed main(){
n=read(),m=read();
for(int i=1;i<=n;i++){
a[i]=read();
}
for(int j=0;j<n;j++) f[j]=a[n-j];
for(int i=0;i<=19;i++){
for(int j=0;j<(1<<20);j++){
if(j>>i&1){
f[j]^=f[j^(1<<i)];
}
}
}
for(int k=0;k<m;k++){ //k从0开始
printf("%d ",f[((1<<20)-1)^k]); //求补集的答案
}
return 0;
}