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


    题目描述

    小 Y 是一个心灵手巧的女孩子,她喜欢手工制作一些小饰品。她有 (n) 颗小星星,用 (m) 条彩色的细线串了起来,每条细线连着两颗小星星。

    有一天她发现,她的饰品被破坏了,很多细线都被拆掉了。这个饰品只剩下了 (n-1) 条细线,但通过这些细线,这颗小星星还是被串在一起,也就是这些小星星通过这些细线形成了树。小 Y 找到了这个饰品的设计图纸,她想知道现在饰品中的小星星对应着原来图纸上的哪些小星星。如果现在饰品中两颗小星星有细线相连,那么要求对应的小星星原来的图纸上也有细线相连。小 Y 想知道有多少种可能的对应方式。

    只有你告诉了她正确的答案,她才会把小饰品做为礼物送给你呢。

    输入格式

    第一行包含 (2) 个正整数 (n,m),表示原来的饰品中小星星的个数和细线的条数。

    接下来 (m) 行,每行包含 (2) 个正整数 (u,v),表示原来的饰品中小星星 (u)(v) 通过细线连了起来。这里的小星星从 (1) 开始标号。保证 (u eq 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\%) 的数据,(nleqslant 17)(mleqslant 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
    ",ans);
    	return 0;
    }
    

    显然这个暴力不可用(因为数组开太大 MLE 了),开小点应该还能过一些点。

    Continue

    我们继续考虑对这个暴力状压进行优化。本题的关键点就在于要求映射集合不能有重复的,那么我们直接去除这个限制。钦定有且仅有集合 (S) 能够出现在映射中。所以我们可以设 (f(S)) 为所有点映射恰好是集合 (S) 的情况。(g(S)) 为所有点映射最多为 (S) 的情况,那么我们就可以得到如下式子:

    [g(S) = sum_{Tsubseteq S}f(T) ]

    证明:显然。(T)(S) 的子集,所以 (g(S))(S) 集合使用不一定全的情况,所以就等于所有子集使用完全的情况求和。

    然后利用子集反演,得到:

    [f(S)=sum_{Tsubseteq S}(-1)^{|S|-|T|} imes g(T) ]

    答案就是 (f(全集))

    然后我们对上边的状压进行修改,用于求出 (g(S)) ,重新定义 (f[i][j][S])(i) 映射为 (j) ,使用集合最大为 (S) 的方案,其转移就可以这样:

    [f[x][j][S]=prod _{vsubseteq {son{x}}} (sum_{Tsubseteq S ,(x,v)subseteq E}f[v][T][S]) ]

    最终得到

    [g(S)=sum _{jsubseteq 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
    ",ans);
    	return 0;
    }
    
    
  • 相关阅读:
    关于swift 单元测试的补充
    架构设计案例分析-高速公路收费运营管理平台
    可以落地的软件架构
    循序渐进地培养面向对象的思维方式
    动态规划初学
    求解惑
    github eclipse项目
    关于x86 i586之类
    Manifest intent filter翻译
    消息处理机制小结
  • 原文地址:https://www.cnblogs.com/Vocanda/p/13777472.html
Copyright © 2020-2023  润新知