(笔记)Sum over Subsets SOS 子集 DP 高维前缀和

本质上就是在做轮廓线 DP。这玩意是静态的,它能解决的东西轮廓线 DP 都能解决,它不能解决的东西轮廓线 DP 还能解决,所以学它干嘛。

下文集合运算 \(\oplus\) 统一表示按位异或,\(\cup\) 表示按位或(并集),\(\cap\) 表示按位与(交集)。

CF 原文

例题

\(F[mask]=\sum_{i\subseteq mask}A[i]\)

我们要解决什么问题?把集合视为 \(n\) 位二进制数,则该问题实则是每维只有 \(0,1\) 两种取值的 \(n\) 维偏序,因此可以直接高维前缀和求解。为什么这么称呼?我们来看几个实例:

\(1\) 维前缀和:\(F[i]=\sum_{a=0}^iG[a]\)

\(2\) 维前缀和:\(F[i][j]=\sum_{a=0}^i\sum_{b=0}^jG[a][b]\)

\(3\) 维前缀和:\(F[i][j][k]=\sum_{a=0}^i\sum_{b=0}^j\sum_{c=0}^kG[a][b][c]\)

\(\dots\)

\(n\) 维前缀和:\(F[i_1][i_2][\dots][i_n]=\sum_{j_1=0}^{i_1}\sum_{j_2=0}^{i_2}\dots\sum_{j_n=0}^{i_n}G[j_1][j_2][\dots][j_n]\)

对于这样的问题,我们不妨从后往前对每一维做一个前缀和,然后就可以得到 \(n\) 维前缀和了。我们的代码中也是如此实现。

朴素处理

方法1:枚举 \(set\)\(subset\) (包含可能不是 \(subset\) 的集合),每个枚举为 \(O(2^n)\),两次枚举共 \(O(4^n)\)

for(int mask=0;mask<=(1<<n)-1;mask++)
	for(int i=0;i<=mask;i++)
		if((mask&i)==i)F[mask]+=A[i];

方法2:枚举 \(set\) 并且只遍历是 \(subset\) 的集合,根据二项式定理,总循环数为 \(\begin{aligned} \sum_{i=0}^n \begin{pmatrix} n\\i\end{pmatrix} 2^i= \sum_{i=0}^n \begin{pmatrix} n\\i\end{pmatrix} 2^i 1^{n-i}=(2+1)^n=3^n\end{aligned}\)

时间复杂度为 \(O(3^n)\)

for(int mask=0;mask<=(1<<n)-1;mask++)
	for(int i=mask;i>=0;i--)
		i&=mask,F[mask]+=A[i];

子集 DP

方法3

留档先前讲解

Attention:下文所有 \(\oplus\) 表示位运算异或

规定 \(F(mask,i)\) 为对 \(mask\) 所有满足以下条件的子集的个数

1.子集第 \(i+1\) 或以上位与 \(mask\) 完全一致

e.g. \(mask=1101011\)

\(F(mask,3)=\{1101011,1101001,1101000,1101010\}\)

推出转移方程 \(F(mask,i)\)\(mask\)\(i\) 位为 \(0\) 时,

就为 \(F(mask,i-1)\)

(因为第 \(i\) 位要保持是 \(mask\) 的子集,不能取 \(1\),只能取 \(0\),所以相当于对答案没有贡献)

\(F(mask,i)\)\(mask\)\(i\) 位为 \(1\) 时,

\(F(mask,i)=F(mask,i-1)+F(mask\oplus 2^i,i-1)\)

其中 \(F(mask,i-1)\) 是所有子集第 \(i\) 位都保持 \(1\) 的情况

\(F(mask\oplus 2^i,i-1)\) 是所有子集第 \(i\) 位保持 \(0\) 的情况

(其实就相当于把 \(mask\)\(i\) 位换成 \(0\),然后直接转移)

不难用 \(O(n2^n)\) 解决

(实例代码中代码优化相当于置换 \(i\)\(j\),通过调换求值优先级且仍然合法来优化空间复杂度)

则开始初始化\(F(i)=A(i)\)即可

实质上,我们可以采取逐位考虑的思想。我们不妨在二进制上从低到高考虑对一个 \(mask\) 的子集怎么求和。让当前考虑到第 \(i(i=0\dots n-1)\) 位,用一个类似前缀和的数组 \(F[i][mask]\) 表示对于 \(mask\)\(i+1\)\(n-1\) 位不变,而 \(0\)\(i\) 位可以上为 \(mask\) 这些位上子集的所有子集的权值和。这样对于顺序考虑的 \(i\),每次我们只需要考虑从小到大地把每个 \(mask\) 该位上为 \(1\)权值和加上该位为 \(0\)权值和,即有转移:

\[F[i][mask]=\begin{cases} F[i-1][mask] & mask\cap 2^i =0\\ F[i-1][mask]+F[i-1][mask\oplus 2^i] & mask\cap 2^i \neq 0 \end{cases} \]

时间复杂度 \(O(n2^n)\),此处使用了滚动数组优化 \(i\) 维。

for(int i=0;i<(1<<N);i++)F[i]=A[i];
for(int i=0;i<N;i++)
	for(int mask=0;mask<(1<<n);mask++)
		if(mask&(1<<i))
			F[mask]+=F[mask^(1<<i)];

我们有更好的方法:轮廓线 DP

理论上如果涉及子集和父集关系的判断,高维前缀和一般很难维护,这时候就可以使用记忆化搜索,钦定 \(f(i,j,...,state)\) 表示在第 \(i\) 行第 \(j\) 列 时 \([i][1\rightarrow j]\)\([i-1][(j+1)\rightarrow m]\) 状态拼起来为 \(state\) 的结果(称为轮廓线)。这样做更好处理子集关系,而且时间复杂度和直接高维前缀和是一样的。

高维前缀和的实质实现过程中,我们状态定义中 \(F[i][mask]\) 实质上就是做了这一过程。我们在 \(i+1\dots n-1\) 这些位上保留了原 \(mask\) 的强制条件,在 \(0\dots i\) 的位置上条件变成了只要是原 \(mask\) 这些位上的子集即可。

衍生品

实时求子集 sum/max/min 的数据结构

P10408 「SMOI-R1」Apple

发现有两种暴力方式。第一种是 \(O(1)\) 插入,\(O(2^n)\) 查询。第二种是 \(O(2^n)\) 插入,\(O(1)\) 查询。平衡一下,我们不妨挑出 \(S\) 的二进制高的那 \(10\) 位做第一种暴力,对低的那一位做第二种暴力。显然如果两个集合存在包含关系,那么它们拆开的这两部分也应都存在包含关系。

首次读入信息直接做一个 SOS DP,然后对于前 \(10\) 位开 \(2^{10}\) 个结构,每个结构里面再开 \(2^{10}\) 个结构表示固定高位对应的低 \(10\) 位的子集和。每次修改只需要改一个结构,在里面对所有超集进行加法,\(O(2^{n/2})\)。每次查询需要查 \(O(2^{n/2})\) 个结构,每个结构里面只需要调用一个信息。

时间复杂度 \(O((n+q)2^{n/2})\),单次操作/查询 \(O(2^{n/2})\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=20,lim=10;
int n,q,v[1<<N];
struct DS{
	LL sum[1<<lim];
	void init(){
		for(int i=0;i<lim;i++)
			for(int j=0;j<(1<<lim);j++)
				if(j&(1<<i))sum[j]+=sum[j^(1<<i)];
	}
	void modify(int p,LL x){
		for(int i=p;i<(1<<lim);i=(i+1)|p)
			sum[i]+=x;
	}
}T[1<<lim];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>q;
	for(int i=0;i<(1<<n);i++){
		cin>>v[i];
		int P=(i>>lim),Q=i^(P<<lim);
		T[P].sum[Q]=v[i];
	}
	for(int i=0;i<(1<<lim);i++)
		T[i].init();
	while(q--){
		int op,S;
		cin>>op>>S;
		int P=(S>>lim),Q=S^(P<<lim);
		if(op==1){
			LL ans=0;
			for(int i=P;i;i=(i-1)&P)
				ans+=T[i].sum[Q];
			ans+=T[0].sum[Q];
			cout<<ans<<'\n';
		}
		else {
			LL a;cin>>a;
			T[P].modify(Q,a-v[S]);
			v[S]=a;
		}
	}
	return 0;
}

封装一下这个结构大概长这样:

点击查看代码
struct SUB{
	const int lim=9;
	struct DS{
		int sum[1<<lim];
		void modify(int p,LL x){
			for(int i=p;i<(1<<lim);i=(i+1)|p)
				sum[i]+=x;
		}
	}T[1<<lim];
	void modify(int S,int x){
		int P=(S>>lim),Q=S^(P<<lim);
		T[P].modify(Q,x);
	}
	int query(int S){
		int P=(S>>lim),Q=S^(P<<lim);
		int res=T[0].sum[Q];
		for(int i=P;i;i=(i-1)&P)
			res+=T[i].sum[Q];
		return res;
	}
}T;

and 和为 0 计数问题

解决思路是按照给出的 \(\text{bit}\) 分类为起来至少有 \(i\) 位是 \(1\),做一个反的子集 DP
超集 DP,就是在转移的时候把 \(1\) 视为 \(0\)\(0\) 视为 \(1\)。然后容斥原理就可以得到最终答案。

如:CF165E Compatible Numbers

CF449D Jzzhu and Numbers

思路需要稍微转换:P6442 [COCI2011-2012#6] KOŠARE

集合推导问题

CF1208F Bits And Pieces

我们先要对所求信息进行几步归约。

  1. 按位运算具有交换律所以 \(a_j\cap a_k\) 不用考虑 \(j,k\) 顺序。

  2. 考虑固定其中一个数如 \(a_i\)(实际想题可能要都试一遍),然后进行集合推导。具体来说,我们有以下集合运算法则:

首先根据高中必修一:

\[\overline A\cap \overline B=\overline{A\cup B}\\ \overline A\cup \overline B=\overline{A\cap B} \]

这两个东西很方便我们做补集转化。

我们发现,对于两个集合运算,\(A\cap B\) 就是对于 \(A\) 去掉其在 \(A\) 中出现了但是在 \(B\) 中没有出现的元素(对 \(B\) 同理),然后 \(A\cup B\) 就是对于 \(A\) 加上了其在 \(A\) 中未出现但是在 \(B\) 中出现了的元素。

然后我们对应地扭曲一下按位与(交集)和按位或(并集):

\[A\cup B=(\overline A\cap B)\oplus A\\ A\cap B=(A\cap \overline B)\oplus A \]

如果集合满足子集关系,我们不妨给他们定义一下加减法则,那么就可以直观表示成:

\[A\cup B=A+(\overline A\cap B)\\ A\cap B=A-(A\cap \overline B) \]

其中我们在固定 \(A\) 的情况下,求出 \(\cap,\cup\) 运算的对应值我们都可以利用按位考虑的技巧,从高到低位判断如果 \(\overline A\cap B,A\cup\overline B\) 是一个值 \(S\),那么它是否可以被达到就可以用一个超集 DP 完成。这样的判定性对于单个 \(A\)\(\log\) 级别的。

运用以上 trick,我们不难得到答案就是 \(a_i+(\overline{a_i}\cap a_j\cap a_k)\),只需要解决一个判定性问题,找到任两个 \(i<j,i<k\) 即可,记一个 \(i\) 最大值和次大值。不妨令值域 \(V=O(2^m)\),时间复杂度为 \(O((n+V)\log V)\)

反思:在部分需要存储最大和次大问题中,需要注意转移不要遗漏:

\[1\to 2\\ \text{最大}_1\to\text{最大}_2\\ \text{最大}_2\to\text{次大}_2\\ \text{次大}_1\to\text{次大}_2\\ \]

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2.1e6+5;
int cnt,n,m,a[N],F[N],G[N],mx;
inline void chkmx(int pre,int mask){
	if(F[pre]>F[mask]){
		if(F[mask]>G[mask])G[mask]=F[mask];
		F[mask]=F[pre];
	}
	else if(F[pre]>G[mask])G[mask]=F[pre];
	if(G[pre]>G[mask])G[mask]=G[pre];
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>cnt;
	for(int i=1;i<=cnt;i++){
		cin>>a[i],mx=max(mx,a[i]);
		G[a[i]]=F[a[i]];
		F[a[i]]=i;
	}
	n=int(floor(log2(mx)))+1;
	m=1<<n;
	for(int i=0;i<n;i++)
		for(int mask=0;mask<m;mask++){
			if((mask>>i)&1)continue;
			int pre=mask^(1<<i);
			chkmx(pre,mask);
		}
	int ans=0,all=m-1;
	for(int i=1;i<=cnt;i++){
		int na=all^a[i],res=0;
		for(int j=n-1;j>=0;j--){
			if(!((na>>j)&1))continue;
			res|=(1<<j);
			if(G[res]<=i)res^=(1<<j);
		}
		if(G[res]>i)ans=max(ans,a[i]+res);
	}
	cout<<ans;
	return 0;
}
posted @ 2025-04-24 14:51  TBSF_0207  阅读(42)  评论(0)    收藏  举报