preparing

「升维打击」- 高维前缀和与 SOSDP

高维前缀和

众所周知,一维前缀和即 \(s_i = \sum\limits_{p=1}^i a_p\),二维前缀和则是通过容斥原理来求:

由图,显然可以得到 \(s_{i,j}=a_{i,j}+s_{i-1,j}+s_{i,j-1}+s_{i-1,j-1}\)。那么,同理推到三维,可以得到 \(s_{i,j,k}=a_{i,j,k}+s_{i-1,j,k}+s_{i,j-1,k}+s_{i,j,k-1}-s_{i-1,j-1,k}-s_{i-1,j,k-1}-s_{i,j-1,k-1}+s_{i-1,j-1,k-1}\)。但是这样在处理 \(k\) 维时转移的复杂度是 \(\mathcal{O}(2^k)\) 的,太高了,于是我们需要找到一个更优的方法。

事实证明,可以分别对第一、二、三维进行前缀和,这样得到的结果也是正确的。代码即:

for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) for(int k=1;k<=p;k++) s[i][j][k]+=s[i-1][j][k];
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) for(int k=1;k<=p;k++) s[i][j][k]+=s[i][j-1][k];
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) for(int k=1;k<=p;k++) s[i][j][k]+=s[i][j][k-1];

这样做为什么是对的呢?以二维为例,可以用下图证明:

三维同理。于是,我们就完成了优化。

SOSDP(Sum Over Subsets DP,子集和 DP)

考虑这样一个问题:有一个长度为 \(n\) 的数组 \(a\),现在想要求出另一个长度为 \(n\) 的数组 \(f\),满足:\(f_x = \sum\limits_{i\subseteq x} a_i\),即 \(f_x = \sum\limits_{i\&x = i} a_i\)

暴力枚举 \(0\sim 2^{\left\lceil\log_2 n\right\rceil}\) 并判断的复杂度是 \(\mathcal{O}(4^n)\) 的,而就算只枚举有用的 \(i\) 复杂度也只是 \(\mathcal{O}(3^n)\) 的。但是,高维前缀和可以帮助我们优化:

我们假设 \(n=2\),把 \(i\) 二进制拆分,得到 \(f_{0,0} = a_{0,0},f_{1,0} = a_{0,0}+a_{1,0},f_{0,1}=a_{0,0}+a_{0,1},f_{1,1}=a_{0,0}+a_{1,0}+a_{0,1}+a_{1,1}\),注意到,这就是二维前缀和。而在 \(n\) 更大时也显然是如此。利用刚才得出的按维计算答案不变的结论,我们可以状压解决这个问题:

for(int i=0;i<(1<<n);i++)	f[i]=a[i];
for(int i=0;i<n;i++) for(int x=0;x<(1<<n);x++)
	if(x&(1<<i)) f[x]+=f[x^(1<<i)];

例 1:CF165E Compatible Numbers

大意:给定长度为 \(n\) 的数组 \(a\),对其中每个元素 \(a_i\),找到另一个元素 \(a_j\),使得 \(a_i\& a_j = 0\),或报告无解。

我们发现,\(a_i\& a_j = 0 \Leftrightarrow a_j \subseteq \lnot a_i\)\(\lnot a_i\) 表示将 \(a_i\) 取反)。那么,就变成了一个裸的 SOSDP 题。

#include<iostream>
#include<cstdio>
#define maxn 1000005
using namespace std;
int n,a[maxn],f[1<<22];
int main(){
	scanf("%d",&n); for(int i=1;i<=n;i++){scanf("%d",&a[i]); f[a[i]]=a[i];}
	for(int i=0;i<22;i++) for(int j=0;j<(1<<22);j++) if((j&(1<<i))&&f[j^(1<<i)]!=0) f[j]=f[j^(1<<i)];
	for(int i=1;i<=n;i++) printf("%d ",f[((1<<22)-1)^a[i]]?f[((1<<22)-1)^a[i]]:-1);
	return 0;
}

例 2:CF383E Vowels

大意:给定 \(n\) 个长度为 \(3\) 的字符串,字符集为 \(D\)a \(\sim\) x。对于一个 \(D\) 的子集 \(I\),定义一个字符串是合法的当且仅当串中有字符在 \(I\) 中。求对于所有 \(I\),合法字符串个数的平方的异或和。

发现不合法的字符串中的所有字母都不在 \(I\) 中,即是 \(I\) 补集的子集,于是直接使用 SOSDP 求解。

#include<iostream>
#include<cstdio>
using namespace std;
int n,x,ans=0,f[(1<<24)+5]; char a[5];
int main(){
	scanf("%d",&n); for(int i=1;i<=n;i++){scanf("%s",a+1); f[(1<<(a[1]-'a'))|(1<<(a[2]-'a'))|(1<<(a[3]-'a'))]++;}
	for(int i=0;i<24;i++) for(int j=0;j<(1<<24);j++) if(j&(1<<i)) f[j]+=f[j^(1<<i)];
	for(int i=0;i<(1<<24);i++) ans^=((n-f[i])*(n-f[i])); printf("%d",ans);
	return 0;
}

例 3:CF449D Jzzhu and Numbers

大意:给定一个长为 \(n\) 的序列 \(a\),求序列 \(1\le i_1 < i_2 < \ldots < i_k\le n\) 的个数,满足:\(a_{i_1}\& a_{i_2}\&\ldots\& a_{i_k}=0\)

实际上,SOSDP 除了统计子集和,也能够统计超集和(若 \(A\subseteq B\),则称 \(A\)\(B\) 的子集,\(B\)\(A\) 的超集),即 \(f_i = \sum\limits_{j\& i = i}a_j\)。那么,此时 \(g_i = 2^{f_i} - 1\) 就能够表示 \(a_{p_1}\& a_{p_2}\&\ldots\& a_{p_k} = j\)\(j\& i = i\)\(p\) 序列的个数(显然此时没有重复计数)。再记 \(a_{p_1}\& a_{p_2}\&\ldots\& a_{p_k} = i\)\(p\) 序列个数为 \(ans_i\)(那么答案就是 \(ans_0\)),即有 \(g_i = \sum\limits_{j\& i = i} ans_j\),于是,我们把前缀和变成差分即可求出 \(ans_0\)

#include<iostream>
#include<cstdio>
#define maxn 1000005
#define ll long long
#define mod 1000000007
using namespace std;
int n,a[maxn]; ll ans=0,f[(1<<20)+5];
ll qp(ll di,ll mi){ll res=1; while(mi){if(mi&1) res=res*di%mod; di=di*di%mod; mi>>=1;} return res;}
int main(){
	scanf("%d",&n); for(int i=1;i<=n;i++){scanf("%d",&a[i]); f[a[i]]++;}
	for(int i=0;i<20;i++) for(int j=0;j<(1<<20);j++) if(!(j&(1<<i))) f[j]+=f[j^(1<<i)];
	for(int i=0;i<(1<<20);i++) f[i]=(qp(2,f[i])-1+mod)%mod;
	for(int i=0;i<20;i++) for(int j=0;j<(1<<20);j++) if(!(j&(1<<i))) f[j]=(f[j]-f[j^(1<<i)]+mod)%mod;
	printf("%lld",f[0]); return 0;
}

参考资料

posted @ 2023-07-08 16:31  qzhwlzy  阅读(28)  评论(0编辑  收藏  举报