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;
}