AGC058F Authentic Tree DP 解题报告
AGC058F Authentic Tree DP 解题报告:
题意
令一棵树的函数 \(f(T)\) 为:
若 \(T\) 为单点,\(f(T)=1\)。
否则,对于边 \(e\),设 \(A_e,B_e\) 为割掉 \(e\) 后产生的两个连通块:
\[f(T)=\frac{\sum_{e\in T}f(A_e)f(B_e)}{n}
\]
给定一棵树,计算其权值。
\(1\leqslant n\leqslant 5000\)。
分析
最近好多人都在改,就把这篇博客发出来了。
很厉害的一道题目啊!!!
这道题巧妙利用了取模下的性质:\(x\) 的逆元等于 \(x+P\) 的逆元。(\(P=998244353\))
看到这个转移方程,肯定要赋予其一个组合意义:
将每条边建出一个对应点(称其为“新点”)连接其原来连接的两个点,然后在新点下面加入 \(P-1\) 个叶子。
然后给每个点赋予一个随机实数权值,求所有新点权值大于其邻居的概率。
那么转移方程就对应着枚举最大值,除以的 \(n\) 其实对应 \((n-1)P+n\)。
这个问题可以通过容斥解决,我们任取一个结点作为根,保留新点权值大于其儿子的限制,对新点权值大于其父亲的限制容斥。可以发现树分成了若干个互不影响的子连通块。(其结构是一棵有向树)
根据经典拓扑序结论,概率是每个点子树大小的倒数之乘积。于是令 \(f_{i,j}\) 表示 \(i\) 下面挂的连通块大小为 \(j\) 的方案数做树上背包即可。
复杂度 \(O(n^2)\)。
代码
#include<stdio.h>
#include<vector>
using namespace std;
const int maxn=5005,mod=998244353;
int n,ans;
int sz[maxn],f[maxn][maxn],inv[maxn],g[maxn];
vector<int>v[maxn];
void dfs(int x,int last){
sz[x]=1,f[x][1]=1;
for(int t=0;t<v[x].size();t++){
int y=v[x][t];
if(y==last)
continue;
dfs(y,x);
int sum=0;
for(int i=1;i<=sz[y];i++)
sum=(sum+f[y][i])%mod;
for(int i=1;i<=sz[x];i++)
for(int j=1;j<=sz[y];j++)
g[i+j]=(g[i+j]+1ll*f[x][i]*f[y][j])%mod;
sz[x]+=sz[y];
for(int i=0;i<=sz[x];i++)
f[x][i]=(1ll*f[x][i]*sum-g[i]+mod)%mod,g[i]=0;
}
for(int i=1;i<=sz[x];i++){
f[x][i]=1ll*f[x][i]*inv[i]%mod;
if(last!=0)
f[x][i]=1ll*f[x][i]*inv[i]%mod;
}
}
int main(){
scanf("%d",&n),inv[1]=1;
for(int i=2;i<=n;i++)
inv[i]=mod-1ll*(mod/i)*inv[mod%i]%mod;
for(int i=1,x,y;i<n;i++)
scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs(1,0);
for(int i=1;i<=n;i++)
ans=(ans+f[1][i])%mod;
printf("%d\n",ans);
return 0;
}