2021上海icpc G

本文介绍了一种基于树形结构的动态规划算法,用于解决特定类型的组合计数问题。该算法适用于连通图上的边分组问题,即如何将图中的边两两分组,使得每组内的两条边至少有一个公共顶点,特别地,当顶点数量为奇数时。通过递归分解问题,利用子问题的解来构建更复杂问题的解。
题意:

给出一副连通图,有nnn个节点,他们有n−1n-1n1条边,将这n−1n-1n1条边两两分组,使得每一组的两条边需要有一个公共点,保证nnn是奇数,求有多少种分组方法

Solution:

一副nnn个节点的连通图,并且有n−1n-1n1条边,这显然是一颗树

我们不妨分点考虑,如果当前点有关的边是kkk条,当kkk为偶数时,我们可以直接分组,当kkk为奇数时,偶数部分显然可以分一组,但剩余的边呢?剩余的边我们不妨留给父亲节点来分组,这样就有一个方法,设dp[u]dp[u]dp[u]uuu为根的子树内能有多少种分法,并且分配完可能有剩余的边。当我们在uuu的可用边为kkk时,当kkk为偶数,就有

dp[u]=f(k)×Πv∈sonudp[v] dp[u]=f(k)\times\Pi_{v\in son_{u}}dp[v] dp[u]=f(k)×Πvsonudp[v]

此时uuu不留边给父亲,其中f(x)f(x)f(x)xxx个元素两两分组的分法有多少种

kkk为奇数时

dp[u]=f(k−1)×Πv∈sonudp[v] dp[u]=f(k-1)\times\Pi_{v\in son_{u}}dp[v] dp[u]=f(k1)×Πvsonudp[v]

此时留一条边给父亲使用,那么对kkk

k=1+∑v∈sonukv k=1+\sum_{v\in son_{u}}k_{v} k=1+vsonukv

kvk_vkv是儿子给vvv节点留给父亲的边数

对于f(x)f(x)f(x),我们不妨这样考虑,先对xxx个元素全排列,然后按顺序放入111x2\frac{x}{2}2x组,组内没有先后顺序,所以每个组需要除2!2!2!,组和组之间也没有先后顺序,所以总体还要除以(x2)!(\frac{x}{2})!(2x)!,于是

f(x)=x!(2!)x2×(x2)! f(x)=\frac{x!}{(2!)^{\frac{x}{2}}\times (\frac{x}{2})!} f(x)=(2!)2x×(2x)!x!
预处理阶乘,阶乘逆元,2的幂,2的幂的逆元即可

#include<bits/stdc++.h>
using namespace std;

using ll=long long;
const int N=100005,inf=0x3fffffff;
const long long INF=0x3f3f3f3f3f3f,mod=998244353;

ll qpow(ll a,ll b)
{
    ll ret=1,base=a;
    while(b)
    {
        if(b&1) ret=ret*base%mod;
        base=base*base%mod;
        b>>=1;
    }
    return ret;
}

ll inv(ll x){return qpow(x,mod-2);}
const long long inv2=inv(2);

struct way
{
    int to,next;
}edge[N<<1];
int cnt,head[N],in[N];

void add(int u,int v)
{
    edge[++cnt].to=v;
    edge[cnt].next=head[u];
    head[u]=cnt;
    in[u]++;
}

int n,sum[N];
ll ans=1,fac[N],pw[N],invfac[N],invpw[N],dp[N];

//x个元素两两分组有多少种分法
ll calc(int x){return fac[x]*invpw[x/2]%mod*invfac[x/2]%mod;}

int dfs(int u,int fa)
{
    //dp[u]->分配这棵子树有多少种方法?
    sum[u]=dp[u]=1;
    int tot=1;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].to;
        if(v==fa) continue;
        tot+=dfs(v,u);
        sum[u]+=sum[v];
        dp[u]=dp[u]*dp[v]%mod;
        // if(sum[v]%2==0) dp[u]=dp[u]*(in[v]-1)%mod;
    }
    dp[u]=dp[u]*calc(tot%2?tot-1:tot)%mod;
    return tot%2;
}

//n个点,n-1条边,n是奇数
//把n-1条边分为(n-1)个集合,每个集合有两条边,这两条边有公共点
//有多少种分法?
int main()
{
    cin>>n;
    for(int i=fac[0]=pw[0]=invfac[0]=invpw[0]=1;i<N;i++)
    {
        fac[i]=fac[i-1]*i%mod;
        pw[i]=pw[i-1]*2%mod;
    }
    invfac[N-1]=inv(fac[N-1]);
    invpw[N-1]=inv(pw[N-1]);
    for(int i=N-2;i>=1;i--)
    {
        invfac[i]=invfac[i+1]*(i+1)%mod;
        invpw[i]=invpw[i+1]*2%mod;
    }
    for(int i=1;i<n;i++)
    {
        int u,v; scanf("%d%d",&u,&v);
        add(u,v); add(v,u);
    }
    dfs(1,0);
    cout<<dp[1];
    return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值