AOJ 2611 Ordering

解法

問題文を丁寧に読まなければならない問題です.
「それぞれの塔は自分自身よりも大きな塔とは高々1回しか比較されていない」とあるので,与えられた関係 (a, b) を有向辺とみるとこれは木になっています.

この時点で木DPかなあという見通しが立ちます.
ある頂点のそれぞれの部分木は独立に考えることができるので,あとはそれらをいい感じにマージしていくことになります.

引き分けがなければ単純に挿入していくDPなんですが,今回は引き分けがあるのでうまく処理しなければなりません.
(高さの異なる塔を高さ順に並べていくイメージだとすると,引き分けは同じところに複数の塔を置くということになります.)
ここで以下のDPを考えます.
dp2[i][j][k] := 高さが異なる i 個の集合 A と,高さが異なる j 個の集合 B をマージして,最終的に高さが異なる k 個の集合にする場合の数
遷移は以下の通り,3通りあります.

  • A の i 番目の塔が B の j 番目よりも高い => dp2[i - 1][j][k - 1]
  • A の i 番目と B の j 番目の高さが等しい => dp2[i - 1][j - 1][k - 1]
  • A の i 番目より B の j 番目のほうが高い => dp2[i][j - 1][k - 1]

dp2[i][j][k] はこれらの和になります.
このDPテーブルは前もって計算できるのでそうします.

あとはマージしていくだけで,これは難しくありません.以下のDPでできます.
dp[v][i] := 根が v の部分木で,高さが異なる塔が i 個あるような関係の作り方の総数
この dp テーブルを先程の dp2 を使って計算していきます.
今構築中の高さの関係を
tmp[i][j] := i 番目の子まで見たときに,高さが異なる塔が j 個であるような総数
とすると,
tmp[i + 1][l] += tmp[i][j] * dp[ch[i]][k] * dp2[j][k][l]
という式が書けるので,各 i, j, k, l についてループを回します.

各頂点でこれをやるので,ぱっと見た感じでは(ch の i は無視できるので)j, k, l の3重ループと合わせて O(N^4) っぽいですが,計算量の漸化式を書いてみると実は O(N^3) になっています.

ソースコード

#include <bits/stdc++.h>
using namespace std;

using ll = long long;

constexpr ll M = 1e9 + 7;
ll dp2[256][256][256];
int sz[256];

int dfs(int v, vector<vector<int>>& g, vector<vector<ll>>& dp) {
    if(g[v].size() == 0) {
        dp[v][1] = 1;
        return sz[v] = 1;
    }

    sz[v] = 1;
    for(auto ch : g[v]) {
        sz[v] += dfs(ch, g, dp);
    }

    vector<vector<ll>> tmp(g[v].size() + 1, vector<ll>(sum));
    tmp[0][0] = 1;
    for(int i = 0; i < (int)g[v].size(); ++i) {
        int ch = g[v][i];
        for(int l = 0; l < sz[v]; ++l) {
            for(int j = 0; j <= l; ++j) {
                for(int k = 1; k <= sz[ch]; ++k) {
                    (tmp[i + 1][l] += (tmp[i][j] * dp[ch][k] % M) * dp2[j][k][l]) %= M;
                }
            }
        }
    }
    for(int i = 0; i < sum; ++i) {
        dp[v][i + 1] = tmp[g[v].size()][i];
    }
    return sz[v];
}

int main() {
    for(int i = 0; i < 201; ++i) {
        dp2[i][0][i] = 1;
        dp2[0][i][i] = 1;
    }
    for(int k = 1; k < 201; ++k) {
        for(int i = 1; i < 201; ++i) {
            for(int j = 1; j < 201; ++j) {
                (dp2[i][j][k] += dp2[i - 1][j - 1][k - 1] + dp2[i - 1][j][k - 1] + dp2[i][j - 1][k - 1]) %= M;
            }
        }
    }

    int N;
    cin >> N;
    vector<vector<int>> g(N);
    vector<bool> in(N);
    for(int i = 0; i < N - 1; ++i) {
        int a, b;
        cin >> a >> b;
        g[a].push_back(b);
        in[b] = true;
    }
    int root = -1;
    for(int i = 0; i < N; ++i) {
        if(!in[i]) {
            root = i;
            break;
        }
    }
    vector<vector<ll>> dp(N + 1, vector<ll>(N + 1));
    dfs(root, g, dp);

    ll res = 0;
    for(int i = 0; i <= N; ++i) {
        (res += dp[root][i]) %= M;
    }
    cout << res << endl;
}

感想

O(N^4) から落ちへんやんけ!と言いながらヤケクソで投げたら通り,計算量の見積もりが甘かったと気付かされた.
実はオーダーが落ちて(?)いるというのは,結構面白かったし今後も使えそうなので覚えておきたい.