小星星 [子集反演、容斥]
題目描述
小 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;
}