小星星 [子集反演、容斥]
题目描述
小 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\) 的情况,那么我们就可以得到如下式子:
证明:显然。\(T\) 是 \(S\) 的子集,所以 \(g(S)\) 为 \(S\) 集合使用不一定全的情况,所以就等于所有子集使用完全的情况求和。
然后利用子集反演,得到:
答案就是 \(f(全集)\) 。
然后我们对上边的状压进行修改,用于求出 \(g(S)\) ,重新定义 \(f[i][j][S]\) 为 \(i\) 映射为 \(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;
}