[CF1032F]Vasya and Maximum Matching

题目

传送门

题解

一道只看代码很水实则很难的树形 DP 题,要做出这道题,需要巧妙地转换模型

一棵树去掉一些边,一定会构成许多连通块,我们首先需要意识到每个连通块的最大匹配都是相互独立,那么我们可以分块进行思考.

我们首先对这道题中这些出现唯一最大匹配的块的特点做一些分析.

首先一定有:

  1. 如果这个图存在唯一最大匹配,那么其所有叶节点一定都存在于匹配中;

想象一下,如果有某一个叶节点不存在于匹配,那么首先其父亲一定存在于匹配,那么我们可以将其父亲的匹配换成其父亲与他自己组成匹配,这样整个块匹配个数不变,但是匹配方式不唯一了,不符题意.

其次,我们可以将这些已经组成的匹配去掉,然后又成了一个新块,再对其叶节点匹配,再去掉匹配...一直这样进行下去,由于我们每次去除的是两个点,那么最后一定会剩下 \(1\) 个或 \(0\) 个点,如果最后剩下一个点,则说明这个块中,根没有被匹配,那么我们可以随便找一条单链的匹配往上移动一次,这样匹配个数不变,匹配方式不唯一而不符题意,即

  1. 只有偶数点的块才存在唯一最大匹配;

但是这个结论真的那么简单?不,还有一个特殊情况——一个孤点,所以还有第三点

  1. 孤点构成唯一最大匹配;

接下来考虑如何解决这道题.

首先,因为每个块最大匹配都是独立,所以对于原树的每个子树都是相互独立的,我们考虑使用树形 \(DP\) 统计方案数

我们观察我们在分析特性 \(1\)\(2\) 时做了些什么:对于每个非叶节点,如果它的某个儿子没有被匹配,那么将其与其儿子匹配,并且其余的儿子要么就和儿子的儿子匹配,要么他们自己成为孤点,那么我们即可定义状态:

定义 \(f[u][0]\)\(u\) 已经在子树中完成匹配的合法方案数.

对应地,我们亦定义 \(f[u][1]\)\(u\) 还未在子树中完成匹配,但是它的某些儿子仍然连接了它的合法方案数(后面的条件是为了与孤点进行区分)

但是我们还有特殊情况——孤点,那么我们还要多定义一个:

定义 \(f[u][2]\) 表示点 \(u\) 在其子树中是孤点的合法方案数(它的儿子们都不连接它)

那么,对于 \(f[u][0]\),我们有转移

\[f[u][0]=\sum_{x\in \text{son}_u}\left ( f[x][1]+f[x][2]\right )\prod_{v\in \text{son}_u,v\neq x}(f[v][0]\times 2+f[v][2]) \]

解释一下,因为点 \(u\) 如果在子树中已经完成匹配,那么它一定只和某一个点组成匹配,其他点自行解决(在自己的子树中匹配或者是孤点),所以我们钦定一个太子 \(x\),让其与 \(u\) 进行匹配,那么这个太子必须满足自己在自己的子树中没有进行匹配,或者它在其子树中本身是孤点,只是和其父皇有联系且组成匹配.

而那些不是太子的点,只能自行解决,如果它在自己的子树中完成匹配,那么边 \((u,v)\) 可连可不连,所以是 \(f[v][0]\times 2\),或者它本身是孤点,但是又不能和父皇相认,所以 \(f[v][2]\)\(\times 2\).

对于 \(f[u][2]\) 的转移,因为它是孤点,所以它所有儿子都不认它这个父亲,而且还要保证合法,所以它的不肖子孙们只能自行解决,显然有

\[f[u][2]=\prod_{v\in \text{son}_u}(f[v][0]+f[v][2]) \]

对于 \(f[u][1]\),它自己没有被匹配但是有儿子连他,即这些儿子们自己组成了匹配,但是又有些要和 \(u\) 相认,那么有

\[f[u][1]=\prod_{v\in \text{son}_u}(f[v][0]\times 2+f[v][2]) \]

它的儿子们都可连可不连,所以是 \(f[v][0]\times 2\),但是这会出现一个问题——我们把它是孤点的情况似乎也算出去了,考虑怎么除掉这些情况,发现我们对于 \(f[u][2]\) 的定义就是它是孤点的情况,所以我们只需要将 \(f[u][1]\) 减去 \(f[u][2]\) 就可以了,整理一下,就是

\[f[u][1]=\prod_{v\in \text{son}_u}(f[v][0]\times 2+f[v][2])-f[u][2] \]

所以我才要先算 f[u][1] 的

考虑时间复杂度,由于我们要在对 \(f[u][0]\) 进行转移时,要枚举一个太子,还要枚举其他的儿子,所以总复杂度为 \(\mathcal O(n^2)\),但是我们实际只需要记 \(pi=\prod_{v\in \text{son}_u}(f[v][0]\times 2+f[v][2])\) 先处理出来,枚举太子时即为 \((f[x][1]+f[x][2])\times pi\times \text{Inv}(f[x][0]\times 2+f[x][2])\)

如果遇到毒瘤出题人卡逆元,那么你可以把出题人吊起来打一顿再处理出前缀积和后缀积即可。

代码

#include<cstdio>

#define rep(i,__l,__r) for(signed i=(__l),i##_end_=(__r);i<=i##_end_;++i)
#define fep(i,__l,__r) for(signed i=(__l),i##_end_=(__r);i>=i##_end_;--i)
#define erep(i,u) for(signed i=tail[u],v=e[i].to;i;i=e[i].nxt,v=e[i].to)
#define writc(a,b) fwrit(a),putchar(b)
#define mp(a,b) make_pair(a,b)
#define ft first
#define sd second
typedef long long LL;
// typedef pair<int,int> pii;
typedef unsigned long long ull;
typedef unsigned uint;
#define Endl putchar('\n')
// #define int long long
// #define int unsigned
// #define int unsigned long long

#define cg (c=getchar())
template<class T>inline void read(T& x){
    char c;bool f=0;
    while(cg<'0'||'9'<c)f|=(c=='-');
    for(x=(c^48);'0'<=cg&&c<='9';x=(x<<1)+(x<<3)+(c^48));
    if(f)x=-x;
}
template<class T>inline T read(const T sample){
    T x=0;char c;bool f=0;
    while(cg<'0'||'9'<c)f|=(c=='-');
    for(x=(c^48);'0'<=cg&&c<='9';x=(x<<1)+(x<<3)+(c^48));
    return f?-x:x;
}
template<class T>void fwrit(const T x){//just short,int and long long
    if(x<0)return (void)(putchar('-'),fwrit(-x));
    if(x>9)fwrit(x/10);
    putchar(x%10^48);
}
template<class T>inline T Max(const T x,const T y){return x>y?x:y;}
template<class T>inline T Min(const T x,const T y){return x<y?x:y;}
template<class T>inline T fab(const T x){return x>0?x:-x;}
inline int gcd(const int a,const int b){return b?gcd(b,a%b):a;}
inline void getInv(int inv[],const int lim,const int MOD){
    inv[0]=inv[1]=1;for(int i=2;i<=lim;++i)inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
}
inline LL mulMod(const LL a,const LL b,const LL mod){//long long multiplie_mod
    return ((a*b-(LL)((long double)a/mod*b+1e-8)*mod)%mod+mod)%mod;
}

const int MAXN=3e5;
const int MOD=998244353;

inline int Inv(int a,int n=MOD-2){
    int ret=1;
    for(;n>0;n>>=1){
        if(n&1)ret=1ll*ret*a%MOD;
        a=1ll*a*a%MOD;
    }return ret;
}

struct edge{int to,nxt;}e[MAXN*2+5];
int tail[MAXN+5],ecnt;
inline void add_edge(const int u,const int v){
    e[++ecnt]=edge{v,tail[u]};tail[u]=ecnt;
    e[++ecnt]=edge{u,tail[v]};tail[v]=ecnt;
}
int n;

inline void Init(){
    n=read(1);
    rep(i,1,n-1)add_edge(read(1),read(1));
}

int f[MAXN+5][3];
/**f[i][0]:它在其子树中已经被匹配
 * f[i][1]:它在其子树中未被匹配, 但有儿子连他
 * f[i][2]:它无子
 * */

void Dfs(const int u,const int fa){
    // printf("Now u == %d, fa == %d\n",u,fa);
    erep(i,u)if(v^fa)Dfs(v,u);
    f[u][0]=0;
    f[u][1]=f[u][2]=1;

    // if(isleaf)return;

    int pi=1;
    
    erep(i,u)if(v^fa)pi=1ll*pi*(f[v][0]*2%MOD+f[v][2])%MOD;

    erep(i,u)if(v^fa){
        f[u][0]+=1ll*(f[v][1]+f[v][2])*pi%MOD*Inv(f[v][0]*2%MOD+f[v][2])%MOD;
        if(f[u][0]>=MOD)f[u][0]-=MOD;
    }

    erep(i,u)if(v^fa)f[u][2]=1ll*f[u][2]*(f[v][0]+f[v][2])%MOD;

    erep(i,u)if(v^fa)
        f[u][1]=1ll*f[u][1]*(f[v][0]*2%MOD+f[v][2])%MOD;
    f[u][1]=(f[u][1]-f[u][2]+MOD)%MOD;
}

signed main(){
    Init();
    Dfs(1,0);
    writc((f[1][0]+f[1][2])%MOD,'\n');
    // rep(i,1,n){
    //     printf("When i == %2d : ",i);
    //     rep(j,0,2)writc(f[i][j],' ');Endl;
    // }
    return 0;
}
/*
10
7 4
3 8
7 8
9 5
10 8
9 7
2 4
1 3
6 9

ans == 161
*/
posted @ 2020-07-31 09:48  Arextre  阅读(163)  评论(0编辑  收藏  举报