2021 ICPC沈阳 L.Perfect Matchings(树形dp+容斥原理)

题目描述

题目链接

题目大意

给你一个2n个点的完全图,从这个图里面删除2n−1条边,这些边形成一颗树,问剩下的图里面点进行完美匹配有多少种方案?

题目分析

本 题 我 们 要 对 一 个 完 全 图 选 匹 配 边 , 而 有 一 些 边 不 能 选 择 ( 2 n − 1 条 边 ) , 我 们 可 以 先 算 出 包 含 这 2 n − 1 条 边 本题我们要对一个完全图选匹配边,而有一些边不能选择(2n-1条边),我们可以先算出包含这2n-1条边 2n12n1 的 所 有 方 案 , 然 后 再 用 总 的 方 案 数 减 去 这 些 方 案 就 行 了 。 的所有方案,然后再用总的方案数减去这些方案就行了。

然 后 我 们 要 求 出 包 含 树 边 的 所 有 匹 配 方 案 , 这 个 可 以 用 树 形 d p 来 求 : 然后我们要求出包含树边的所有匹配方案,这个可以用树形dp来求: dp

状 态 表 示 : 设 f [ u ] [ i ] [ 0 / 1 ] 表 示 以 点 u 为 根 节 点 的 子 树 中 , 有 i 个 匹 配 , 并 且 u 节 点 是 否 参 与 匹 配 的 方 案 数 状态表示:设f[u][i][0/1]表示以点u为根节点的子树中,有i个匹配,并且u节点是否参与匹配的方案数 f[u][i][0/1]uiu

状 态 转 移 : 对 于 一 个 以 u 为 根 的 节 点 , 枚 举 其 每 个 子 节 点 v 状态转移:对于一个以u为根的节点,枚举其每个子节点v uv
此 时 有 三 种 情 况 : 此时有三种情况:

1 、 合 并 u 与 其 子 树 v , 并 且 不 选 u , f [ u ] [ i + j ] [ 0 ] + = f [ u ] [ i ] [ 0 ] ∗ ( f [ v ] [ j ] [ 0 ] + f [ v ] [ j ] [ 1 ] ) ( 选 不 选 v 对 于 该 状 态 1、合并u与其子树v,并且不选u,f[u][i+j][0]+=f[u][i][0]*(f[v][j][0]+f[v][j][1])(选不选v对于该状态 1uvuf[u][i+j][0]+=f[u][i][0](f[v][j][0]+f[v][j][1])v 没 有 影 响 ) 没有影响)

2 、 合 并 u 与 其 子 树 v , 选 u , f [ u ] [ i + j ] [ 1 ] + = f [ u ] [ i ] [ 1 ] ∗ ( f [ v ] [ j ] [ 0 ] + f [ v ] [ j ] [ 1 ] ) 2、合并u与其子树v,选u,f[u][i+j][1]+=f[u][i][1]*(f[v][j][0]+f[v][j][1]) 2uvuf[u][i+j][1]+=f[u][i][1](f[v][j][0]+f[v][j][1])

3 、 合 并 u 与 其 子 树 v , 并 且 加 上 一 个 u − v 的 匹 配 , f [ u ] [ i + j + 1 ] [ 1 ] + = f [ u ] [ i ] [ 0 ] ∗ f [ v ] [ j ] [ 0 ] ( 因 为 要 加 一 个 u − v 的 新 匹 配 , 3、合并u与其子树v,并且加上一个u-v的匹配,f[u][i+j+1][1]+=f[u][i][0]*f[v][j][0](因为要加一个u-v的新匹配, 3uvuvf[u][i+j+1][1]+=f[u][i][0]f[v][j][0]uv 所 以 转 移 前 的 状 态 一 定 都 是 不 带 根 节 点 的 ) 所以转移前的状态一定都是不带根节点的)

计 算 出 所 有 的 状 态 之 后 , 再 回 来 看 怎 么 求 出 最 终 结 果 : 计算出所有的状态之后,再回来看怎么求出最终结果:

对 于 整 个 图 来 说 , 假 设 我 们 一 定 会 选 择 x 条 树 边 , 那 么 方 案 数 就 为 f [ 1 ] [ x ] [ 0 ] + f [ 1 ] [ x ] [ 1 ] , 而 其 余 的 边 对于整个图来说,假设我们一定会选择x条树边,那么方案数就为f[1][x][0]+f[1][x][1],而其余的边 xf[1][x][0]+f[1][x][1] 随 便 选 , 那 么 就 有 可 能 选 到 x , x + 1 , x + 2 , … … 条 树 边 。 随便选,那么就有可能选到x,x+1,x+2,……条树边。 便xx+1x+2
因 为 会 出 现 这 种 选 多 了 的 情 况 , 因 此 我 们 在 求 包 含 树 边 的 情 况 时 , 就 需 要 用 到 容 斥 原 理 。 因为会出现这种选多了的情况,因此我们在求包含树边的情况时,就需要用到容斥原理。

我 们 先 将 求 解 过 程 进 行 拆 分 , 枚 举 0 − n , 表 示 当 前 选 择 至 少 存 在 i 条 树 边 , 方 案 数 为 x = f [ 1 ] [ i ] [ 0 ] + f [ 1 ] [ i ] [ 1 ] 。 我们先将求解过程进行拆分,枚举0-n,表示当前选择至少存在i条树边,方案数为x=f[1][i][0]+f[1][i][1]。 0nix=f[1][i][0]+f[1][i][1] 这 样 就 匹 配 了 2 ∗ i 个 点 , 同 时 剩 下 了 2 ∗ n − 2 ∗ i 个 点 任 意 选 择 , 则 有 y 钟 方 案 数 。 当 前 情 况 的 答 案 即 为 : x ∗ y 。 这样就匹配了2*i个点,同时剩下了2*n-2*i个点任意选择,则有y钟方案数。当前情况的答案即为:x*y。 2i2n2iyxy

补 : y = C 2 n − 2 i n − i ∗ ( n − i ) ! 2 n − k 补:y=\frac{C^{n-i}_{2n-2i}*(n-i)!}{2^{n-k}} y=2nkC2n2ini(ni)!
将 2 n − 2 i 个 点 分 为 两 组 : C 2 n − 2 i n − i 将2n-2i个点分为两组:C^{n-i}_{2n-2i} 2n2i:C2n2ini
第 一 个 点 有 n − i 个 选 择 , 第 二 个 点 有 n − i + 1 个 选 择 … … : ( n − k ) ! 第一个点有n-i个选择,第二个点有n-i+1个选择……:(n-k)! nini+1(nk)!
这 样 排 会 存 在 重 复 , 每 一 条 匹 配 边 间 两 点 都 可 以 两 两 交 换 , 因 此 会 被 多 算 2 n − i 次 这样排会存在重复,每一条匹配边间两点都可以两两交换,因此会被多算2^{n-i}次 2ni

最 后 用 容 斥 原 理 将 所 有 情 况 加 起 来 即 可 , 然 后 如 果 有 i 条 树 边 必 选 那 么 容 斥 系 数 就 是 ( − 1 ) k ( i = 0 时 最后用容斥原理将所有情况加起来即可,然后如果有i条树边必选那么容斥系数就是(-1)^k(i=0时 i(1)ki=0 即 为 总 的 所 有 情 况 , i = 1 − n 为 存 在 树 边 的 情 况 ) 。 即为总的所有情况,i=1-n为存在树边的情况)。 i=1n

写这篇题解太不容易了,点个赞吧

代码如下
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <vector>
#include <algorithm>
#include <iomanip>
#define LL long long
#define ULL unsigned long long
#define PII pair<int,int>
#define PLL pair<LL,LL>
#define PDD pair<double,double>
#define x first
#define y second
using namespace std;
const int N=5e3+5,mod=998244353;
vector<int> h[N];
int fact[N],infact[N],p[N],sz[N];
LL f[N][N/2][2],g[N/2][2],ans;
int kmi(int a,int k)			//快速幂(用于求逆元)
{
	int res=1;
	while(k)
	{
		if(k&1) res=(LL)res*a%mod;
		a=(LL)a*a%mod;
		k>>=1;
	}
	return res;
}
void init(int n)	//预处理出阶乘、阶乘逆元、2的i次幂的逆元
{
	p[1]=kmi(2,mod-2);
	fact[0]=infact[0]=p[0]=1;
	for(int i=1;i<=n;i++)
	{
		fact[i]=(LL)fact[i-1]*i%mod;
		infact[i]=kmi(fact[i],mod-2);
		p[i]=(LL)p[i-1]*p[1]%mod;
	}
}
int C(int a,int b)		//求组合数C(a,b)
{
	if(b>a) return 0; 
	return (LL)fact[a]*infact[b]%mod*infact[a-b]%mod; 
}
void dfs(int u,int fa)		//dfs求树形dp
{
	sz[u]=f[u][0][0]=1;
	for(int v:h[u])
	{
		if(v==fa) continue;
		dfs(v,u);
		memset(g,0,sizeof g);		//定义一个g数组作为中间过度,防止重复计算
		for(int i=0;i<=sz[u]/2;i++)			//树上的边数为点数/2
			for(int j=0;j<=sz[v]/2;j++)		//合并v子树到u
			{	//三种状态转移
				g[i+j][0]=(g[i+j][0]+(LL)f[u][i][0]*(f[v][j][0]+f[v][j][1])%mod)%mod;
				g[i+j][1]=(g[i+j][1]+(LL)f[u][i][1]*(f[v][j][0]+f[v][j][1])%mod)%mod;
				g[i+j+1][1]=(g[i+j+1][1]+(LL)f[u][i][0]*f[v][j][0]%mod)%mod;
			}
		
		for(int i=0;i<=sz[u]/2+sz[v]/2+1;i++)		//将过渡数组转移回dp数组
			f[u][i][0]=g[i][0],f[u][i][1]=g[i][1];
		sz[u]+=sz[v];
	}
}
int main()
{
	cin.tie(0);
	ios::sync_with_stdio(false);
	int n;
	cin>>n;
	init(N-1);
	for(int i=1;i<n*2;i++)			//建树
	{
		int u,v;
		cin>>u>>v;
		h[u].push_back(v);
		h[v].push_back(u);
	}
	dfs(1,-1);
	LL ans=0;
	for(int i=0;i<=n;i++)		//求出最少i条树边的方案数
	{
		LL x=(f[1][i][0]+f[1][i][1])%mod;		//树边的贡献
		int a=n-i;
		LL y=(LL)C(a*2,a)*fact[a]%mod*p[a]%mod;	//非树边的贡献
		if(i&1) ans=(ans-x*y%mod+mod)%mod;		//容斥原理合并
		else ans=(ans+x*y%mod)%mod;
	}
	cout<<ans<<endl;
	return 0;
}
  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lwz_159

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值