【DP】解析 SOSdp(子集和 dp)


引入

\(f[st]=\sum_{i\subseteq st} w[i]\) \(~\) \(~\) \((1)\)

解釋: \(i\subseteq st\)\(st\&i=i\) ,熟悉位運算的同學很容易看出 \(i\) 就是二進制表示的集合 \(st\)\(st\)子集

其中 \(w\) 是子集 \(i\) 所對應的貢獻。

舉例來說:
\(1010_{2}\) 的所有子集為 \(1010_{2},1000_{2},0010_{2},0000_{2}\)

那么對於 \((1)\) 式,當 \(st=1010_{2}\) 時,\(f[1010_{2}]=w[1010_{2}]+w[1000_{2}]+w[0010_{2}]+w[0000_{2}]\)
子集和dp 就是用來高效求解上述的 \(f\) 的。

原理

我們用 \(dp(st,i)\) 表示二進制表示的集合 \(st\) 的最后 \(i\) 位變化的所有子集的貢獻的和。

聽起來不太好理解,舉例來說:

我們約定,一個二進制數從右到左的下標 \(index\) 分別為 \(0,1,2...\) ,如

index: 4 3 2 1 0
number:1 0 1 1 0

\(dp(10110_{2},2)=w[10100_{2}]+w[10010_{2}]+w[10000_{2}]+w[10110_{2}]\)
(這里就是 \(index \in [0,2]\) 部分的所有子集)

考慮狀態轉移:
對於一個狀態 \(st\)

  • \(i\) 位為 \(0\) 時,有 \(dp(st,i)=dp(st,i-1)\)
  • \(i\) 位為 \(1\) 時,有 \(dp(st,i)=dp(st,i-1)+dp(st \oplus 2^i,i-1)\)
    原理很好理解,當 \(i\) 位為 \(0\) 時,只能選擇不取。 當 \(i\) 位為 \(1\) 時可以選擇取和不取,那么貢獻就是取和不取的貢獻之和。

代碼:類似於 \(01\) 背包,我們可以去掉一維(由柿子特征恆等變形)

void sos(){
	for(int i=0;i<(1<<N);i++)
		f[i]=w[i];
	for(int i=0;i<N;i++)
		for(int st=0;st<(1<<N);st++)
			if(st&(1<<i)) f[st]+=f[st^(1<<i)];
}

例題:
https://codeforces.com/gym/102576/problem/B

分析

利用 \(Lucas\) 定理,轉化為求 \(a_{j}\& a_{i}=a{i}\) 的對數,用sos求解即可。

代碼
#pragma GCC optimize("O3")
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;

inline int read()
{
	char c=getchar();
	int x=0,f=1;
	while(c<'0'||c>'9') {if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}

const int N=20;
int w[1<<N],f[1<<N];
int n;

void sos(){
	for(int i=0;i<N;i++)
		for(int st=0;st<(1<<N);st++)
			if(st&(1<<i)) f[st]+=f[st^(1<<i)];
}

int main(){
	int T; cin>>T;
	while(T--){
		memset(f,0,sizeof f);
		n=read();
		for(int i=1;i<=n;i++){
			w[i]=read();
			f[w[i]]++;
		}
		
		sos();
		
		ll res=0;
		for(int i=1;i<=n;i++)
			res+=f[w[i]];
		cout<<res<<endl;
	}	
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM