1. 程式人生 > 實用技巧 >小星星 [子集反演、容斥]

小星星 [子集反演、容斥]

題目描述

小 Y 是一個心靈手巧的女孩子,她喜歡手工製作一些小飾品。她有 \(n\) 顆小星星,用 \(m\) 條彩色的細線串了起來,每條細線連著兩顆小星星。

有一天她發現,她的飾品被破壞了,很多細線都被拆掉了。這個飾品只剩下了 \(n-1\) 條細線,但通過這些細線,這顆小星星還是被串在一起,也就是這些小星星通過這些細線形成了樹。小 Y 找到了這個飾品的設計圖紙,她想知道現在飾品中的小星星對應著原來圖紙上的哪些小星星。如果現在飾品中兩顆小星星有細線相連,那麼要求對應的小星星原來的圖紙上也有細線相連。小 Y 想知道有多少種可能的對應方式。

只有你告訴了她正確的答案,她才會把小飾品做為禮物送給你呢。

輸入格式

第一行包含 \(2\) 個正整數 \(n,m\),表示原來的飾品中小星星的個數和細線的條數。

接下來 \(m\) 行,每行包含 \(2\) 個正整數 \(u,v\),表示原來的飾品中小星星 \(u\)\(v\) 通過細線連了起來。這裡的小星星從 \(1\) 開始標號。保證 \(u\neq v\) ,且每對小星星之間最多隻有一條細線相連。

接下來 \(n-1\) 行,每行包含 \(2\) 個正整數 \(u,v\) ,表示現在的飾品中小星星 \(u\)\(v\) 通過細線連了起來。保證這些小星星通過細線可以串在一起。

輸出格式

輸出共 \(1\) 行,包含一個整數表示可能的對應方式的數量。

如果不存在可行的對應方式則輸出 \(0\)

輸入輸出樣例

輸入

4 3
1 2
1 3
1 4
4 1
4 2
4 3

輸出

6

說明/提示

對於 \(100\%\) 的資料,\(n\leqslant 17\)\(m\leqslant \frac {n(n-1)}{2}\)

分析

首先考慮樸素狀壓。我們要求的答案是這棵樹有多少中在圖上的節點標號對映方案,所以我們設 \(f[i][j][S]\) 表示將 \(i\) 節點對映為 \(j\) 節點,其子樹內的點使用的對映集合為 \(S\) 的方案數,答案顯然就是 \(\sum^{n}_{i=1}f[1][i][U]\) ,表示 \(1\) 對映為 \(i\)

,且子樹對映為全集的方案數。轉移的時候注意一下包含與不包含關係的判斷就行了。

Code 20pts

#include<bits/stdc++.h>
using namespace std;
const int L = 1 << 20;
char buffer[L],*S,*T;
#define gc (S == T && (T = (S = buffer) + fread(buffer,1,L,stdin),S == T) ? EOF : *S++)
inline int read(){
	int s = 0,f = 1;char ch = gc;
	for(;!isdigit(ch);ch = gc)if(ch == '-')f = -1;
	for(;isdigit(ch);ch = gc)s = s * 10 + ch - '0';
	return s * f;
}
#define rint register int
#define rll register long long
#define ll long long
const int maxn = 18;
ll ans,f[maxn][maxn][1<<maxn];
struct Node{
	int v,next;
}e[maxn<<2];
vector<int>g[maxn],vec[maxn];
int head[maxn],tot;
int n,m;
int siz[maxn];
inline void Add(rint x,rint y){
	e[++tot].v = y;
	e[tot].next = head[x];
	head[x] = tot;
}
inline void dfs(rint x,rint fa){
	siz[x] = 1;
	for(rint i = 1;i <= n;++i)f[x][i][1<<(i-1)] = 1;
	for(rint i = head[x];i;i = e[i].next){
		rint v = e[i].v;
		if(v == fa)continue;
		dfs(v,x);
		for(rint id = 1;id <= n;++id){//列舉當前點對映為哪個標號
			rint size = g[siz[x]].size();
			for(rint j = 0;j < size;++j){//列舉當前大小的所有狀態
				rint S = g[siz[x]][j];
				if(!(S & (1 << (id - 1))))continue;//如果該狀態不包括對映的標號就直接不管
				rint siz2 = vec[id].size();
				for(rint l = 0;l < siz2;++l){//找當前對映的標號連的邊
					rint idx = vec[id][l];
					if(S & (1 << (idx - 1)))continue;//如果該狀態包含了子樹中的邊就不選。
					rint siz3 = g[siz[v]].size();
					for(rint k = 0;k < siz3;++k){//列舉大小為子樹大小的所有狀態
						rint T = g[siz[v]][k];
						if(S & T || !(T & (1 << (idx - 1))))continue;//當前集合和子樹集合的狀態不能有交,不然可能算重,且子樹集合要包含子樹所列舉的那個對映
						f[x][id][S | T] += f[x][id][S] * f[v][idx][T];//乘法原理計算
					}
				}
			}
		}
		siz[x] += siz[v];
	}
}
int main(){
	n = read(),m = read();
	for(rint i = 1;i <= m;++i){
		rint x = read(),y = read();
		vec[x].push_back(y);
		vec[y].push_back(x);
	}
	for(rint i = 1;i < n;++i){
		rint x = read(),y = read();
		Add(x,y);
		Add(y,x);
	}
	rint mx = (1 << n) - 1;
	for(rint i = 0;i <= mx;++i){
		rint cnt = 0;
		for(rint j = 0;j < n;++j){
			if(i & (1 << j))cnt++;
		}
		g[cnt].push_back(i);//計算每個個數下都有哪些狀態。
	}
	dfs(1,0);
	for(rint i = 1;i <= n;++i){
		ans += f[1][i][mx];
	}
	printf("%lld\n",ans);
	return 0;
}

顯然這個暴力不可用(因為陣列開太大 MLE 了),開小點應該還能過一些點。

Continue

我們繼續考慮對這個暴力狀壓進行優化。本題的關鍵點就在於要求對映集合不能有重複的,那麼我們直接去除這個限制。欽定有且僅有集合 \(S\) 能夠出現在對映中。所以我們可以設 \(f(S)\) 為所有點對映恰好是集合 \(S\) 的情況。\(g(S)\) 為所有點對映最多為 \(S\) 的情況,那麼我們就可以得到如下式子:

\[g(S) = \sum_{T\subseteq S}f(T) \]

證明:顯然。\(T\)\(S\) 的子集,所以 \(g(S)\)\(S\) 集合使用不一定全的情況,所以就等於所有子集使用完全的情況求和。

然後利用子集反演,得到:

\[f(S)=\sum_{T\subseteq S}(-1)^{|S|-|T|}\times g(T) \]

答案就是 \(f(全集)\)

然後我們對上邊的狀壓進行修改,用於求出 \(g(S)\) ,重新定義 \(f[i][j][S]\)\(i\) 對映為 \(j\) ,使用集合最大為 \(S\) 的方案,其轉移就可以這樣:

\[f[x][j][S]=\prod _{v\subseteq \{son\{x\}\}} (\sum_{T\subseteq S ,(x,v)\subseteq E}f[v][T][S]) \]

最終得到

\[g(S)=\sum _{j\subseteq S} f[1][j][S] \]

然後開始亂七八糟根據一堆式子求個和就行了。程式碼卡卡常,跑過毫無壓力。

Code

#include<bits/stdc++.h>
using namespace std;
const int L = 1 << 20;
char buffer[L],*S,*T;
#define gc (S == T && (T = (S = buffer) + fread(buffer,1,L,stdin),S == T) ? EOF : *S++)
#define rint register int
#define rll register long long
#define reg register
#define ll long long
#define read() ({\
	rint s = 0,f = 1;reg char ch = gc;\
	for(;!isdigit(ch);ch = gc)if(ch == '-')f = -1;\
	for(;isdigit(ch);ch = gc)s = s * 10 + ch - '0';\
	s * f;\
})
const int maxn = 18;
ll ans,f[maxn][maxn];//壓掉狀態那一維,因為列舉狀態即可。
struct Node{
	int v,next;
}e[maxn<<2];
int head[maxn],tot;
int n,m;
int vec[maxn][maxn];
int jl[maxn],cnt[1<<maxn];
inline void Add(rint x,rint y){
	e[++tot].v = y;
	e[tot].next = head[x];
	head[x] = tot;
}
inline void dfs(rint x,rint fa){
	for(rint i = 1;i <= jl[0];++i)f[x][jl[i]] = 1;//初始化
	for(rint i = head[x];i;i = e[i].next){
		rint v = e[i].v;
		if(v == fa)continue;
		dfs(v,x);//遞歸回溯
		for(rint j = 1;j <= jl[0];++j){//列舉集合元素
			rll tmp = 0;
			for(rint k = 1;k <= jl[0];++k){//同上
				if(vec[jl[j]][jl[k]])tmp += f[v][jl[k]];//兩點之間有邊就加上貢獻
			}
			f[x][jl[j]] *= tmp;//乘法原理計算總貢獻
		}
	}
}
int main(){
	n = read(),m = read();
	for(rint i = 1;i <= m;++i){//記錄原圖中相連的邊
		rint x = read(),y = read();
		vec[x][y] = vec[y][x] = 1;
	}
	for(rint i = 1;i < n;++i){
		rint x = read(),y = read();
		Add(x,y);
		Add(y,x);
	}
	rint mx = (1 << n) - 1;//全集
	for(rint i = 0;i <= mx;++i){//列舉狀態
		cnt[i] = cnt[i>>1] + (i & 1);//計算當前狀態的元素個數
		jl[0] = 0;rll tmp = 0;
		for(rint j = 1;j <= n;++j)if(i & (1 << (j - 1)))jl[++jl[0]] = j;//記錄集合元素個數以及元素
		dfs(1,0);
		for(rint j = 1;j <= jl[0];++j)tmp += f[1][jl[j]];//求和
		ans += ((n - cnt[i]) & 1) ? -tmp : tmp;//根據子集反演的式子求和
	}
	printf("%lld\n",ans);
	return 0;
}