小星星 [子集反演、容斥]

题目描述

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

posted @ 2020-10-07 15:14  Vocanda  阅读(234)  评论(0编辑  收藏  举报