我称之为补题解,感觉这可以帮助我把之前做过的没写题解的一些题目(好吧坑了好多题目)都补起来
首先容易想到(O(n! imes n))的大暴力,然后套路地发现在树上可以化为子集问题
我们设(f_{i,j,k})表示(i)的子树内,(i)映射为(j)之后且所有点映射完后构成了图上的点集(k)(状压)的方案数,显然我们可以在枚举一个子节点的同时枚举父节点的所有状态,然后再对于它的状态补集枚举子集即可,复杂度应该是(O(3^n imes n))的
然而这样无法通过此题,我们细细分析以下会发现现在的复杂度瓶颈主要是在枚举子集上,考虑如果我们能去掉后面那一维该有多好
考虑我们假设映射可以重复,那么显然此时会多算出一部分,比如映射过去的集合比全集少了一些元素
很直观地,我们考虑再减去少了一个元素的方案数,然后此时少了两个元素的又被多减了需要再加回去……
想到了什么,容斥啊!我们可以对映射过去的集合进行容斥,每次只把点映射到这个集合里去,然后算出此时的方案数即可
那么此时的方程显然就是:
[f_{x,j}=prod_{yin son(x)} sum_{j=1}^n f_{y,j} imes g_{i,j}
]
最后(sum_{i=1}^n f_{1,i})就是方案数,根据集合的元素取正负号即可
复杂度(O(2^n imes n^3)),可能需要卡常(代码是一年前的了233)
#include<cstdio>
#include<vector>
#define RI register int
using namespace std;
const int N=20;
int n,m,x,y,status,id[N],cnt; bool p[N][N];
vector <int> v[N]; long long ans,f[N][N];
inline void DFS(int now,int fa)
{
RI i,j,k; int lim=v[now].size(),to;
for (i=0;i<lim;++i) if ((to=v[now][i])!=fa) DFS(to,now);
for (i=1;i<=cnt;++i)
{
for (f[now][id[i]]=1,j=0;j<lim;++j)
if ((to=v[now][j])!=fa)
{
long long ret=0; for (k=1;k<=cnt;++k)
if (p[id[i]][id[k]]) ret+=f[to][id[k]];
f[now][id[i]]*=ret;
}
}
}
int main()
{
RI i,j; for (scanf("%d%d",&n,&m),i=1;i<=m;++i) scanf("%d%d",&x,&y),p[x][y]=p[y][x]=1;
for (i=1;i<n;++i) scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for (status=(1<<n)-1,i=0;i<=status;++i)
{
for (cnt=j=0;j<n;++j) if ((i>>j)&1) id[++cnt]=j+1;
DFS(1,0); for (j=1;j<=cnt;++j) ans+=((n-cnt)&1?-1LL:1LL)*f[1][id[j]];
}
return printf("%lld",ans),0;
}