题目描述
题目大意
给你一个2n个点的完全图,从这个图里面删除2n−1条边,这些边形成一颗树,问剩下的图里面点进行完美匹配有多少种方案?
题目分析
本 题 我 们 要 对 一 个 完 全 图 选 匹 配 边 , 而 有 一 些 边 不 能 选 择 ( 2 n − 1 条 边 ) , 我 们 可 以 先 算 出 包 含 这 2 n − 1 条 边 本题我们要对一个完全图选匹配边,而有一些边不能选择(2n-1条边),我们可以先算出包含这2n-1条边 本题我们要对一个完全图选匹配边,而有一些边不能选择(2n−1条边),我们可以先算出包含这2n−1条边 的 所 有 方 案 , 然 后 再 用 总 的 方 案 数 减 去 这 些 方 案 就 行 了 。 的所有方案,然后再用总的方案数减去这些方案就行了。 的所有方案,然后再用总的方案数减去这些方案就行了。
然 后 我 们 要 求 出 包 含 树 边 的 所 有 匹 配 方 案 , 这 个 可 以 用 树 形 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]表示以点u为根节点的子树中,有i个匹配,并且u节点是否参与匹配的方案数
状
态
转
移
:
对
于
一
个
以
u
为
根
的
节
点
,
枚
举
其
每
个
子
节
点
v
状态转移:对于一个以u为根的节点,枚举其每个子节点v
状态转移:对于一个以u为根的节点,枚举其每个子节点v
此
时
有
三
种
情
况
:
此时有三种情况:
此时有三种情况:
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对于该状态 1、合并u与其子树v,并且不选u,f[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]) 2、合并u与其子树v,选u,f[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的新匹配, 3、合并u与其子树v,并且加上一个u−v的匹配,f[u][i+j+1][1]+=f[u][i][0]∗f[v][j][0](因为要加一个u−v的新匹配, 所 以 转 移 前 的 状 态 一 定 都 是 不 带 根 节 点 的 ) 所以转移前的状态一定都是不带根节点的) 所以转移前的状态一定都是不带根节点的)
计 算 出 所 有 的 状 态 之 后 , 再 回 来 看 怎 么 求 出 最 终 结 果 : 计算出所有的状态之后,再回来看怎么求出最终结果: 计算出所有的状态之后,再回来看怎么求出最终结果:
对
于
整
个
图
来
说
,
假
设
我
们
一
定
会
选
择
x
条
树
边
,
那
么
方
案
数
就
为
f
[
1
]
[
x
]
[
0
]
+
f
[
1
]
[
x
]
[
1
]
,
而
其
余
的
边
对于整个图来说,假设我们一定会选择x条树边,那么方案数就为f[1][x][0]+f[1][x][1],而其余的边
对于整个图来说,假设我们一定会选择x条树边,那么方案数就为f[1][x][0]+f[1][x][1],而其余的边
随
便
选
,
那
么
就
有
可
能
选
到
x
,
x
+
1
,
x
+
2
,
…
…
条
树
边
。
随便选,那么就有可能选到x,x+1,x+2,……条树边。
随便选,那么就有可能选到x,x+1,x+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]。 我们先将求解过程进行拆分,枚举0−n,表示当前选择至少存在i条树边,方案数为x=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。 这样就匹配了2∗i个点,同时剩下了2∗n−2∗i个点任意选择,则有y钟方案数。当前情况的答案即为:x∗y。
补
:
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=2n−kC2n−2in−i∗(n−i)!
将
2
n
−
2
i
个
点
分
为
两
组
:
C
2
n
−
2
i
n
−
i
将2n-2i个点分为两组:C^{n-i}_{2n-2i}
将2n−2i个点分为两组:C2n−2in−i
第
一
个
点
有
n
−
i
个
选
择
,
第
二
个
点
有
n
−
i
+
1
个
选
择
…
…
:
(
n
−
k
)
!
第一个点有n-i个选择,第二个点有n-i+1个选择……:(n-k)!
第一个点有n−i个选择,第二个点有n−i+1个选择……:(n−k)!
这
样
排
会
存
在
重
复
,
每
一
条
匹
配
边
间
两
点
都
可
以
两
两
交
换
,
因
此
会
被
多
算
2
n
−
i
次
这样排会存在重复,每一条匹配边间两点都可以两两交换,因此会被多算2^{n-i}次
这样排会存在重复,每一条匹配边间两点都可以两两交换,因此会被多算2n−i次
最 后 用 容 斥 原 理 将 所 有 情 况 加 起 来 即 可 , 然 后 如 果 有 i 条 树 边 必 选 那 么 容 斥 系 数 就 是 ( − 1 ) k ( i = 0 时 最后用容斥原理将所有情况加起来即可,然后如果有i条树边必选那么容斥系数就是(-1)^k(i=0时 最后用容斥原理将所有情况加起来即可,然后如果有i条树边必选那么容斥系数就是(−1)k(i=0时 即 为 总 的 所 有 情 况 , i = 1 − n 为 存 在 树 边 的 情 况 ) 。 即为总的所有情况,i=1-n为存在树边的情况)。 即为总的所有情况,i=1−n为存在树边的情况)。
写这篇题解太不容易了,点个赞吧
代码如下
#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;
}