1. 程式人生 > 其它 >NOWCODER – 小G砍樹(組合數學 + 換根dp)

NOWCODER – 小G砍樹(組合數學 + 換根dp)

技術標籤:動態規劃組合數學

https://ac.nowcoder.com/acm/problem/22732
終於把這題補了。。
首先是組合數學的部分,設dp[i]表示以i為根的樹的答案(合法排列數),對於根節點為x的樹來說,這棵樹的所有子節點可以形成((size_x – 1)!)種排列,假設節點y與節點x直接相連,則節點y能形成(size_y!)種排列,但是其中只有dp[y]種是合法的,所以((size_x – 1)!)需要先除掉(size_y!),再乘上dp[y],才是以y為根的子樹對dp[x]的真正貢獻。dp[i]更新方向為自底向上。
其次是換根部分,對於任意要提到根節點的節點我們都認為它的父親就是原來的根節點。既然x一定為根,那麼(size_x)就一直等於n,dp’[x]就等於dp[x]先除掉((n – 1)!),再乘上((n – 1 – size_y)!),再乘上原來子樹y的貢獻的倒數就好了。dp’[y]就等於dp[y]先除掉((size_y – 1)!),再乘上((n – 1)!),再乘上新子樹(原來的父親成為它的子樹)的貢獻即(\frac {dp’[x]} {(n – size_y)!})就好了,然後再把子節點更新一下,繼續dfs,此時dp[i]的更新方向自頂向下。

#include<iostream>
#include<vector>
using namespace std;
typedef long long ll;

const int mod = 998244353, inf = 0x3f3f3f3f;

const int maxn = 1e5 + 5;
vector<int> g[maxn];
int sz[maxn], n;
ll fac[maxn], dp[maxn], sum;
ll quickpow(ll x, ll k)
{
    ll res = 1;
    while (k){
        if
(k & 1) res = (res * x) % mod; x = (x * x) % mod; k >>= 1; } return res; } void dfs(int u, int p) { sz[u] = dp[u] = 1; for (int v : g[u]){ if (v == p) continue; dfs(v, u); sz[u] += sz[v]; dp[u] *= dp[v] * quickpow(fac[sz[v]],
mod - 2) % mod; dp[u] %= mod; } dp[u] *= fac[sz[u] - 1]; dp[u] %= mod; return ; } void dfs2(int u, int p) { sum += dp[u]; sum %= mod; for (int v : g[u]){ if (v == p) continue; ll t = dp[u] * quickpow(fac[n - 1], mod - 2) % mod * fac[n - 1 - sz[v]] % mod * fac[sz[v]] % mod * quickpow(dp[v], mod - 2) % mod; dp[v] = fac[n - 1] * quickpow(fac[sz[v] - 1], mod - 2) % mod * t % mod * quickpow(fac[n - sz[v]], mod - 2) % mod * dp[v] % mod; dfs2(v, u); } } int main() { fac[0] = 1; for (int i = 1; i < maxn; ++i) fac[i] = fac[i - 1] * i % mod; int l, r; cin >> n; for (int i = 1; i < n; ++i){ cin >> l >> r; g[l].push_back(r); g[r].push_back(l); } dfs(1, 0); dfs2(1, 0); cout << sum << endl; return 0; }