第 6 回 ドワンゴからの挑戦状 本選 C - Tree Shrinking

C - Tree Shrinking

問題

木 $G = (V, E)$ が与えられる.この木に対して以下の操作を $n - 1$ 回行う.最初スコアは $0$ である.

  • $e = uv \in E$ を当確率でランダムに選ぶ.
  • $deg(u) \cdot deg(v)$ がスコアに加算される.
  • $e$ を縮約する.

最終的なスコアの期待値を求めよ.

制約

  • $2 \leq |V| \leq 10^5$

考察

期待値の積ということで,各辺対 $e_1, e_2 \in E$ の「スコアへの寄与の期待値」,言い換えると「操作中に $e_1, e_2$ 両方に接続する辺が選ばれる回数の期待値」が知りたくなる.辺対は順序対ではない,つまり $(e_1, e_2)$ と $(e_2, e_1)$ で 2 回カウントしないことに注意.

まず $e_1, e_2$ が隣接しているとき.この場合はどんな順番に操作をしても, $e_1$ か $e_2$ が操作対象になるときにちょうど 1 回だけカウントされる.よって期待値は $1$ .$e_1 = e_2$ のケースも考えることに注意.

そうでない場合.2 辺間の距離を $d$ とすると,2 辺を両端とするパス内で 2 辺間の辺が先に全て選ばれた場合のみちょうど 2 回 カウントされる.よって期待値は $\frac{2}{{}_{d + 2} C_2}$ .

このままでも進められるのだが,後々考えやすくするために「辺対」と「その外側の頂点対」の間で一対一対応を作る.すると,スコアの期待値は以下の式で求まる.

$$ \sum_{1 \leq u \lt v \leq |V|} e_{d(u, v)} \quad \left( e_d = \begin{cases} 1 & (d \leq 2) \\ \frac{2}{{}_d C_2} & (d \gt 2) \end{cases} \right) $$

よって 2 点対間の距離分布が分かれば,それに適当な係数 $e_d$ を掛けることで解が求まる.

距離分布については,蟻本 P.321 に部分問題である「距離 $k$ 以下の頂点対の個数」を重心分解で求める方法が載っている.今回も基本的な方針はこれと同じ.

ある 2 つの部分木の距離分布 $(a_d), (b_d)$ が与えられたとする.このとき,これら 2 つの部分木をマージしたときの距離分布を $(c_d)$ とすると,

$$ c_d = \sum_{0 \leq d' \leq d} a_{d'} b_{d - d'} $$

となる.これはまんま和に関する畳み込みなので,FFTで高速化できる.ただこれを全部分木対に対して計算すると遅いので,累積和を取りながらやる必要がある.なぜか次数が小さい方からマージすると速くなる.

実装例

1 つ 1 つの処理はライブラリに丸投げすることを意識すると,意外と考えることは少ない.

提出 #10090069 - 第6回 ドワンゴからの挑戦状 本選

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <tuple>

/* ----- Mod Int ----- */
template <int MOD>
struct ModInt { ... };
constexpr int MOD = 998244353;
using mint = ModInt<MOD>;
using mints = std::vector<mint>;

/* ----- NTT ----- */
template <int MOD, int Root>
struct NumberTheoreticalTransform { ... };
const NumberTheoreticalTransform<MOD, 3> NTT;

/* ----- Graph ----- */
template <class Cost = int>
struct Edge { ... };
template <class Cost = int>
using Graph = std::vector<std::vector<Edge<Cost>>>;

/* ----- Centroid Decomposition ----- */
template <class Cost = int>
struct Centroid { ... };

/* ----- main ----- */
// priority queue用の比較関数
struct Compare {
    bool operator()(const mints& lhs, const mints& rhs) {
        return lhs.size() > rhs.size();
    }
};

void solve() {
    int n;
    std::cin >> n;

    Graph<> graph(n);
    for (int i = 0; i < n - 1; ++i) {
        int u, v;
        std::cin >> u >> v;
        --u, --v;
        graph[u].emplace_back(u, v);
        graph[v].emplace_back(v, u);
    }

    Centroid<> cent(graph);
    std::queue<int> cents;  // 次見る部分木の代表点
    cents.push(0);
    std::vector<int> dist(n);  // BFS用

    mints distr(n, 0);
    while (!cents.empty()) {
        int r = cents.front();
        cents.pop();

        // 重心を探す
        r = cent.find(r);
        cent.deleted[r] = true;

        // 各部分木の距離分布 次数について昇順に格納
        std::priority_queue<mints, std::vector<mints>, Compare> polys;

        for (auto e : graph[r]) {
            if (cent.deleted[e.dst]) continue;
            cents.push(e.dst);

            // BFSで距離分布を求める
            mints poly(1, 0);
            std::queue<std::pair<int, int>> que;
            que.emplace(e.dst, r);
            dist[e.dst] = 1;

            while (!que.empty()) {
                int v, p;
                std::tie(v, p) = que.front();
                que.pop();

                while ((int)poly.size() <= dist[v]) poly.push_back(0);
                poly[dist[v]] += 1;

                for (auto f : graph[v]) {
                    if (p == f.dst || cent.deleted[f.dst]) continue;
                    dist[f.dst] = dist[v] + 1;
                    que.emplace(f.dst, v);
                }
            }
            polys.push(poly);
        }

        // 頂点数1の部分木を用意するとやりやすい
        mints acc(1, 1);

        while (!polys.empty()) {
            auto poly = polys.top();
            polys.pop();

            // 畳み込みして全体に加算
            auto prod = NTT.ntt(poly, acc);
            for (int d = 0; d < (int)prod.size(); ++d) {
                distr[d] += prod[d];
            }

            // 累積和にも加算
            while (acc.size() < poly.size()) acc.push_back(0);
            for (int d = 0; d < (int)poly.size(); ++d) {
                acc[d] += poly[d];
            }
        }
    }

    // 係数と共に畳み込み
    mint ans = 0;
    for (int d = 1; d < n; ++d) {
        ans += distr[d] * (d <= 2 ? 1 : mint(4) / (mint(d) * (d - 1)));
    }
    for (int d = 1; d < n; ++d) ans *= d;
    std::cout << ans << std::endl;
}
Built with Hugo
テーマ StackJimmy によって設計されています。