bzoj4455&UOJ185 [Zjoi2016]小星星
原題地址:
http://www.lydsy.com/JudgeOnline/problem.php?id=4455
http://uoj.ac/problem/185
題意:
給你一個n 個點m 條邊的無向圖, 再給你一棵n 個點的樹, 問有多少種點編號的映射方式, 使得n 個點恰好匹配,且樹上的邊均存在於原圖中。
數據範圍
n<=17,m<=n*(n-1)/2
題解:
好題。
容易想到的樹形DP是:
n=17,狀壓。
DP[u][i][s]表示對於u節點及其子樹,u節點對應i點,整個子樹映射到s這個集合的方案數,
但是每次轉移都需要枚舉子集,複雜度 3^n*n^2無法承受。
有句話叫,計數問題考慮容斥。
那麼如果把嚴格的範圍放寬泛一點,考慮轉移複雜度更低的DP+容斥。
現在我們枚舉s,進行DP,
不要求嚴格是s內一一對應,可以有不同的點映射到同一個點,即對應的集合至多是s的方案數。
之後再做一次容斥即可。
代碼:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstring>
#define LL long long
using namespace std;
const int N=20;
int n,m,g[N][N],head[N],to[2*N],nxt[2*N],num=0,cnt=0,q[N];
LL f[N][N],ans=0LL;
void build(int u,int v)
{
num++;
to[num]=v;
nxt[num]=head[u];
head[u]=num;
}
inline int getnum(int s)
{
int ret=0;
for(int i=0;i<n;i++) if(s&(1<<i)) {ret++; q[ret]=i+1;}
return ret;
}
void dfs(int u,int fa)
{
for(int i=head[u];i;i=nxt[i])
{
int v=to[i]; if(v==fa) continue;
dfs(v,u);
}
for(int i=1;i<=cnt;i++)
{
f[u][q[i]]=1;
for(int l=head[u];l;l=nxt[l])
{
LL cur=0;
int v=to[l]; if(v==fa) continue;
for(int j=1;j<=cnt;j++) if(g[q[i]][q[j]]) cur+=f[v][q[j]];
f[u][q[i]]*=cur;
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1,u,v;i<=m;i++) {scanf("%d%d",&u,&v); g[u][v]=g[v][u]=1;}
for(int i=1,u,v;i<n;i++) {scanf("%d%d",&u,&v); build(u,v); build(v,u);}
int top=1<<n;
for(int s=1;s<top;s++)
{
cnt=getnum(s); LL w;
if((cnt%2)==(n%2)) w=1LL; else w=-1LL;
dfs(1,1);
for(int i=1;i<=cnt;i++) ans+=w*f[1][q[i]];
}
printf("%lld\n",ans);
return 0;
}