[SHOI2012]随机树

可以看出本题是由单独的两问组成的,我们先来思考第一问。

可以发现如果想直接使用期望性来计算每个深度对答案的贡献,你会发现由于同一形态的树是可以由很多种操作序列操作而成的,不好去计算这样的操作序列个数,因此需要换一种思路。当直接使用期望线性性不好计算时,另一个方法一般是考虑使用 \(dp\)

观察一下这个扩展叶子节点的过程,不难发现我们只需要知道当前扩展的叶子节点的深度就可以知道扩展后叶子节点的深度之和,于是很简单地可以令 \(dp_i\) 表示扩展到 \(i\) 个叶子节点时叶子节点的期望平均深度。那么有转移:

\[dp_i = \frac{2 \times (dp_{i - 1} + 1) + (i - 2) \times dp_{i - 1}}{i} \]

整理可得:

\[dp_i = dp_{i - 1} + \frac{2}{i} \]

再来考虑第二问。如果直接正向考虑扩展叶子节点,你会发现我们所需要的状态很难描述。因此反过来想,除了往下扩展做为转移外,能否反过来往上将左右子树合并做为扩展呢?恰好是可以的,你会发现当左右子树高度确定时这颗树的高度也会随之确定,那么操作方案的不同实质上是影响了左右子树的大小。因此我们可以令 \(dp_{i, j}\) 表示有 \(i\) 个叶子节点深度为 \(j\) 的树的概率,\(p_{i, j}\) 表示有 \(i\) 个叶子节点的树中有 \(j\) 个叶子在左子树的概率,则会有转移:

\[dp_{i, j} = \sum\limits_{k = 1} ^ {i - 1} p_{i, k} \times dp_{k, j - 1} \sum\limits_{l = 0} ^ {j - 1} dp_{i - k, l} + \sum\limits_{k = 1} ^ {i - 1} p_{i, i - k} \times dp_{k, j - 1} \sum\limits_{l = 0} ^ {j - 2} dp_{i - k, l} \]

后面只转移到 \(dp_{i - k, j - 2}\) 是为了避免重复计算两颗子树同时高度为 \(j - 1\) 的情况。

于是现在的问题在于 \(p_{i, j}\) 怎么求。不难发现实际上 \(p_{i, j}\) 是能够用类似第一问的方法转移的,即考虑扩展叶子节点:

\[p_{i, j} = \frac{j - 1}{i - 1} \times p_{i - 1, j - 1} + \frac{i - j - 1}{i - 1} \times p_{i - 1, j} \]

那么我们预先 \(dp\)\(p_{i, j}\) 后在计算 \(dp\) 时使用前缀和优化即可做到 \(O(n ^ 3)\)

#include<bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for(int i = l; i <= r; ++i)
const int N = 100 + 5;
double ans, p[N][N], S[N][N], dp[N][N]; 
int q, n;
int read(){
    char c; int x = 0, f = 1;
    c = getchar();
    while(c > '9' || c < '0'){ if(c == '-') f = -1; c = getchar();}
    while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}
namespace S1{
    void solve(){
        rep(i, 2, n) ans += 2.0 / i;
        printf("%.6f", ans);
    }
}
namespace S2{
    void solve(){
        p[2][1] = dp[1][0] = dp[2][1] = S[1][0] = S[2][1] = 1;
        rep(i, 3, n){
            rep(j, 1, i - 1){
                p[i][j] = p[i - 1][j - 1] * (1.0 * (j - 1) / (i - 1));
                p[i][j] += p[i - 1][j] * (1.0 * (i - j - 1) / (i - 1));
            }
            rep(j, 1, i - 1){
                rep(k, 1, i - 1) dp[i][j] += p[i][k] * dp[k][j - 1] * S[i - k][min(i - k - 1, j - 1)];
                rep(k, 1, i - 1) dp[i][j] += p[i][k] * dp[i - k][j - 1] * S[k][min(k - 1, j - 2)];
            }
            rep(j, 1, i - 1) S[i][j] = S[i][j - 1] + dp[i][j];
        } 
        rep(i, 1, n) ans += i * dp[n][i];
        printf("%.6f", ans);
    }
}
int main(){
    q = read(), n = read();
    if(q == 1) S1 :: solve();
    else S2 :: solve();
    return 0;
}

实际上我们还可以发现:对于任意的 \(1 \le j \le i - 1, p_{i, j} = \dfrac{1}{i - 1}\)

显然这个式子是可以直接使用上面的递推式归纳证明的,但还存在下面一个证法:

你会发现对于任意一颗树第一次操作一定会使得左右子树的叶子节点个数分别 \(+1\),接下来如果扩展左边的叶子就会让左边子树的叶子增加,反之亦然。那么实际上每次操作对我们来说就只用两种 \(L, R\) 分别表示在左边扩展和右边扩展。那么如果一共会有 \(i\) 个叶子,也就意味着有 \(i - 2\)\(L, R\) 操作;如果左子树有 \(j\) 个叶子,也就意味着会存在 \(j - 1\)\(L\),则排列 \(L, R\) 的方案就有 \(\dbinom{i - 2}{j - 1}\) 种。其次在左右子树中的操作中还可能选择不同的叶子节点,在左子树中的操作次数有 \((j - 1)!\) 种,右子树中有 \((i - j - 1)!\) 种,再除以总方案 \((i - 1)!\) 合起来写就是:

\[\frac{\dbinom{i - 2}{j - 1} \times (j - 1)! \times (i - j - 1)!}{(i - 1)!} = \frac{(i - 2)! \times (j - 1)! \times (i - j - 1)!}{(j - 1)! \times (i - j - 1)! \times (i - 1)!} = \frac{1}{i - 1} \]

值得一提的是,在直接计数时如果不好计很大可能是要考虑使用 \(dp\)。如果这个方向不好计数也可以考虑这个方向的对立面,不论是流程上还是对应计数的对象。同时也需要尽可能简化问题,简化到能表达所需状态的最简表达为止。

posted @ 2020-09-21 18:57  Achtoria  阅读(138)  评论(0编辑  收藏  举报