考试题目的小数版本
可以先考虑一个暴力, 就是枚举一个点作为最后的点, 然后枚举一个删边的序列。这个暴力显然比朴素的暴力复杂度要优秀一点。
考虑基于这个思想来写题。可以枚举一个点作为最后的点, 然后出所有的删边序列。算出在这个序列下点保留下来的概率, 然后把所有的概率加起来,除以也就是左右序列的个数就是答案了。
实际上下面的值是记录了一些序列, 然后取前一个前缀删掉。
考虑设表示当的子树里面还有条边没有删掉的时候, 上述的概率和是多少。(模拟题意不难发现, 这题其实就是当做删边来理解并没有什么问题)。由于是概率的和, 实际上值里面包含的是很多个在当前子树内的边的删边排列的”合法概率和”。
为了方便, 我们把当前枚举的点设置为根。
由于这题和每一条边都有关, 考虑在合并子树之前先把当前点和当前点父亲节点的连边计算到答案里面。
我们考虑怎么计算这个东西。
我们先考虑一下当前的子树里面剩下的边有没有父亲这一条边。

  • 如果没有, 转移为。意思就是子树里面原来删掉了,有个间隙可以插入边。
  • 如果有, 由于我们为了方便, 考虑去掉当前点和父亲连边的影响, 在当前点处计算。枚举一下断掉父亲边的时候当前点子树内还剩下多少边。不难发现, 当前点和父亲点的连边断掉的时候是要强制选择的, 所以还要除以。然后转移就是
    现在考虑怎么合并两个子树。由于我们处理的父边, 那这就比较好写了。

意思就是把删边序列和留下来的边的序列分别合并, 用组合数就好了。
至于原因一个就是我们要保证两个子树内的相对删边的顺序不变才可以保证这是合法的。

#include <bits/stdc++.h>

using namespace std;

#define R register
#define LL long long

inline int read() {
    int x = 0, f = 1; char a = getchar();
    for(; a > '9' || a < '0'; a = getchar()) if(a == '-') f = -1;
    for(; a <= '9' && a >= '0'; a = getchar()) x = x * 10 + a - '0';
    return x * f;
}

const int N = 50 + 5;
const int P = 998244353;

inline int qpow(int x, int k) {
    int res = 1;
    while(k) { 
        if(k & 1) res = 1LL * x * res % P;
        k >>= 1; x = 1LL * x * x % P;
    } return res;
}

const int inv2 = qpow(2, P - 2);

inline int Add(int x, int y) { return x + y >= P ? x + y - P : x + y; }

int n;
int fac[N], ifac[N];
inline int C(int x, int y) { return 1LL * fac[x] * ifac[x - y] % P * ifac[y] % P; }
void init() {
    fac[0] = 1;
    for(R int i = 1; i <= 50; i ++) fac[i] = 1LL * fac[i - 1] * i % P;
    ifac[50] = qpow(fac[50], P - 2);
    for(R int i = 49; i >= 0; i --) ifac[i] = 1LL * ifac[i + 1] * (i + 1) % P;
}

int dp[N][N], tmp[N];

struct edge {
    int to, next;
}e[N << 1];
int cnt, head[N];
void add(int x, int y) { e[++ cnt] = {y, head[x]}; head[x] = cnt; }

int siz[N];

inline void dfs(int u, int f) {
    siz[u] = 1;
    dp[u][0] = 1;
    for(R int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if(v == f) continue;
        dfs(v, u);
        for(R int j = 0; j < siz[u] + siz[v]; j ++) tmp[j] = 0;
        int tot = siz[u] + siz[v] - 1;
        for(R int p = 0; p < siz[u]; p ++)
            for(R int q = 0; q <= siz[v]; q ++) 
                tmp[p + q] = Add(tmp[p + q], 1LL * dp[u][p] * dp[v][q] % P * C(p + q, p) % P * C(tot - p - q, siz[v] - q) % P);
        siz[u] += siz[v];
        for(R int j = 0; j < siz[u]; j ++) dp[u][j] = tmp[j];
    }
    if(f) {
        for(R int i = 0; i <= siz[u]; i ++) tmp[i] = 0;
        for(R int i = 0; i < siz[u]; i ++) {
            tmp[i] = Add(tmp[i], 1LL * (siz[u] - i) * dp[u][i] % P);
            for(R int j = i + 1; j <= siz[u]; j ++)
                tmp[j] = Add(tmp[j], 1LL * dp[u][i] * inv2 % P);
        }
        for(R int i = 0; i <= siz[u]; i ++) dp[u][i] = tmp[i];
    }
}

signed main() {
    //#ifdef IN
    freopen("tree.in", "r", stdin);
    freopen("tree.out", "w", stdout);
    //#endif
    init();
    n = read();
    for(R int i = 1; i < n; i ++) {
        int x = read(), y = read();
        add(x, y); add(y, x);
    }
    for(R int i = 1; i <= n; i ++) {
        dfs(i, 0);
        printf("%lld\n", 1LL * dp[i][n - 1] * ifac[n - 1] % P);
    }
    return 0;    
}

给一份带取模的代码