[ZJOI2016]小星星

考虑我们这种恰好的方案数。
可以考虑子集反演。
我们设每个点映射为\(to_i\),设全集映射为\(S\)
我们发现其实最后我们要求是,有多少个映射满足树的条件且是一个排列。
那么我们发现其实排列不太好求。
我们不妨设\(g(S)\)为每个点至多使用的都是\(S\)集合里的点。
\(f(S)\)为恰好使用了所有点一次以上。
那么有\(g(S) = \sum\limits_{T \in S} f(T)\)
\(f(S) = \sum\limits_{T \in S} (-1)^{|S| - |T|}g(T)\)

然后因为\(S_1 = {1,2,....n}\),那么恰好使用它的方案一定是一个排列。

然后就很好做了
类似于树上背包,树边保证转移关系。

#include<iostream>
#include<cstdio>
#define ll long long 
#define N 18

int n,m;
int S;
int A[N][N];
int v[N];
int Cnt[(1 << N)];
struct P{int to,next;}e[N << 1];
int head[N],cnt;
ll dp[N][N];

inline void add(int x,int y){
	e[++cnt].to = y;
	e[cnt].next = head[x];
	head[x] = cnt;
}

inline void dfs(int u,int fa,int s){
	for(int i = 1;i <= v[0];++i)dp[u][i] = 1;
	for(int i = head[u];i;i = e[i].next){
		int vi = e[i].to;
		if(vi == fa)continue;
		dfs(vi,u,s);
		for(int j = 1;j <= v[0];++j){
			ll tmp = 0;
			for(int k = 1;k <= v[0];++k)
			if(A[v[j]][v[k]])tmp += dp[vi][k];
			dp[u][j] *= tmp;
		}
	}
}

int main(){
	scanf("%d%d",&n,&m);
	S = (1 << n) - 1;
	if(n == 1){puts("1");return 0;}	
	for(int i = 1;i <= m;++i){
		int x,y;
		scanf("%d%d",&x,&y);
		A[x][y] = A[y][x] = 1;
	}
	for(int i = 1;i <= n - 1;++i){
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y),add(y,x);
	}
	ll ans = 0;
	for(int s = 0;s <= S;++s){
		Cnt[s] = Cnt[s >> 1] + (s & 1);
		v[0] = 0;
		for(int i = 1;i <= n;++i)if((s >> (i - 1)) & 1)v[++v[0]] = i;
		dfs(1,0,s);
		ll g = 0;
		for(int i = 1;i <= v[0];++i)
		g += dp[1][i];
		ans += (n - Cnt[s] & 1) ? -g : g;
	}
	std::cout<<ans<<std::endl;
}
posted @ 2021-10-26 21:39  fhq_treap  阅读(53)  评论(0)    收藏  举报