[ZJOI2016]小星星&[SHOI2016]黑暗前的幻想乡(容斥)

时间:2022-01-12 02:29:38

这两道题思路比较像,所以把他们放到一块。

[ZJOI2016]小星星

题目描述

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

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

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

题解

做容斥题有一个基本模型,就是有一个限制,我们直接在转移或者统计复杂度过高,但如果把它放宽一点的话复杂度会降低许多。

然后总的条件数也支持2^n枚举,就可以去考虑容斥。

这个题是说,有一个n个点的无向图和n个点的一棵树,问有多少种一一对应的映射使得在树中有的边,图中也有。

看到树可以联想树形dp,因为我们要求一一对应,所以我们可以考虑设dp[i][j][s]表示以i为根的子树,i对应了图中的j,i子树对应了图中的集合s的方案数。

转移还是比较简单的。

我们观察到这个算法复杂度瓶颈在于枚举s,所以我们考虑能不能去掉。

去掉之后会出现树中的多个点对应了图中的一个点,方案数会算多。

怎么办?这个形式其实已经很明显了,直接套用容斥公式算就好了。

2^n枚举图中选那些点,然后做二维的树形dp就好了。

代码

#include<iostream>
#include<cstdio>
#define N 18
#define R register
using namespace std;
typedef long long ll;
int n,m,tot,head[N],cou[<<N];
ll dp[N][N],ans;
bool a[N][N],jin[N];
inline int rd(){
int x=;char c=getchar();bool f=;
while(!isdigit(c)){if(c=='-')f=;c=getchar();}
while(isdigit(c)){x=(x<<)+(x<<)+(c^);c=getchar();}
return f?-x:x;
}
struct edge{int n,to;}e[N*N*];
inline void add(int u,int v){
e[++tot].n=head[u];e[tot].to=v;head[u]=tot;
}
void dfs(int u,int fa){
for(R int i=;i<=n;++i)if(!jin[i])dp[u][i]=;else dp[u][i]=;
for(R int i=head[u];i;i=e[i].n)if(e[i].to!=fa){
int v=e[i].to;dfs(v,u);
for(R int j=;j<=n;++j)if(!jin[j]){
ll num=;
for(R int k=;k<=n;++k)if(a[j][k]&&!jin[k])num+=dp[v][k];
dp[u][j]*=num;
}
}
}
int main(){
n=rd();m=rd();int x,y;
for(R int i=;i<=m;++i){
x=rd();y=rd();
a[x][y]=a[y][x]=;
}
for(R int i=;i<n;++i){x=rd();y=rd();add(x,y);add(y,x);}
for(R int i=;i<(<<n);++i){
cou[i]=cou[i>>]+(i&);
for(R int j=;j<=n;++j)jin[j]=(i&(<<j-))!=;
dfs(,);
ll num=;
for(R int j=;j<=n;++j)num+=dp[][j];
if(cou[i]&)ans-=num;else ans+=num;
}
cout<<ans;
return ;
}

[SHOI2016]黑暗前的幻想乡

题目描述

四年一度的幻想乡大选开始了,最近幻想乡最大的问题是很多来历不明的妖怪涌入了幻想乡,扰乱了幻想乡昔日的秩序。但是幻想乡的建制派妖怪(人类)博丽灵梦和八云紫等人整日高谈所有妖怪平等,幻想乡多元化等等,对于幻想乡目前面临的种种大问题却给不出合理的解决方案。

风见幽香是幻想乡里少有的意识到了问题严重性的大妖怪。她这次勇敢地站了出来参加幻想乡大选,提出包括在幻想乡边境建墙(并让人类出钱),大力开展基础设施建设挽回失业率等一系列方案,成为了大选年出人意料的黑马并顺利地当上了幻想乡的大统领。

幽香上台以后,第一项措施就是要修建幻想乡的公路。幻想乡一共有 nn 个城市,之前原来没有任何路。幽香向选民承诺要减税,所以她打算只修 n-1n−1条公路将这些城市连接起来。但是幻想乡有正好 n-1n−1 个建筑公司,每个建筑公司都想在修路地过程中获得一些好处。虽然这些建筑公司在选举前没有给幽香钱,幽香还是打算和他们搞好关系,因为她还指望他们帮她建墙。所以她打算让每个建筑公司都负责一条路来修。

每个建筑公司都告诉了幽香自己有能力负责修建的路是哪些城市之间的。所以幽香打算 n - 1n−1条能够连接幻想乡所有城市的边,然后每条边都交给一个能够负责该边的建筑公司修建,并且每个建筑公司都恰好修建一条边。

幽香现在想要知道一共有多少种可能的方案呢?两个方案不同当且仅当它们要么修的边的集合不同,要么边的分配方式不同。

题解

这题和上一题相似,有两个限制。

上一题是要满足树中和图中都要有某条边。

这题是要满足我们的生成树中,既要有n个不同的点,还要满足每种颜色的边个出现一次。

如果我们去掉第二个限制,那就变成了生成树计数问题,套用矩阵树定理即可。

然后我们发现面前的这个问题还是可以套用容斥公式直接计算的,于是这道题被愉快的解决了。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define N 18
using namespace std;
typedef long long ll;
const int mod=1e9+;
ll a[N][N];
int n,m,cou[<<N];
bool tag[N];
inline int rd(){
int x=;char c=getchar();bool f=;
while(!isdigit(c)){if(c=='-')f=;c=getchar();}
while(isdigit(c)){x=(x<<)+(x<<)+(c^);c=getchar();}
return f?-x:x;
}
inline ll power(ll x,ll y){
ll ans=;
while(y){if(y&)ans=ans*x%mod;x=x*x%mod;y>>=;}
return ans;
}
struct node{int x,y;};
vector<node>vec[N];
inline ll ni(ll x){return power(x,mod-);}
inline ll gauss(ll tot){
ll ans=;
for(int i=;i<=tot;++i)
for(int j=i+;j<=tot;++j){
ll t=a[j][i]*ni(a[i][i])%mod;
for(int k=i;k<=tot;++k)a[j][k]=((a[j][k]-t*a[i][k])%mod+mod)%mod;
}
for(int i=;i<=tot;++i)ans=(ans*a[i][i]%mod+mod)%mod;
return ans;
}
inline ll work(){
memset(a,,sizeof(a));
for(int i=;i<n;++i)if(tag[i]){
for(int j=;j<vec[i].size();++j){
int x=vec[i][j].x,y=vec[i][j].y;
a[x][x]++;a[y][y]++;a[x][y]--;a[y][x]--;
}
}
return gauss(n-);
}
int main(){
n=rd();int x,y;
for(int i=;i<n;++i){
m=rd();
for(int j=;j<=m;++j){
x=rd();y=rd();vec[i].push_back(node{x,y});
}
}
ll ans=;
for(int i=;i<(<<n-);++i){
cou[i]=cou[i>>]+(i&);
for(int j=;j<=n-;++j)tag[j]=(i&(<<j-))==;
if(cou[i]&)ans-=work();else ans+=work();
ans=(ans%mod+mod)%mod;
}
cout<<ans;
return ;
}