AOJ 2344 Multi Ending Story

解法

木DPで解きます.
まず部分木に注目しましょう.
冷静に考えると,この部分木のすべての葉を回収するのには,以下の3通りだけしかありません.この部分木の根を $v$,左右の子をそれぞれ $l, r$ とします.

  • $v$ で quick save を使わず,それぞれの子の部分木でいい感じに quick save を用いて最適に回収する
  • $v$ で quick save した後,まず $l$ の部分木の葉をすべて回収(この時 quick save は用いない)して,その後 $r$ の部分木で quick save を用いつつ最適に回収する
  • 2つ目のパターンで左右を入れ替えたパターン

そこで以下のように定めます.
$dp[v] := v$ が根の部分木にあるすべての葉を quick save をいい感じに用いて最適に回収したときのコスト
$dp_2[v] := v$が根の部分木にあるすべての葉を, $v$ で quick save をしたとき(子の部分木では一切 quick save しない)の回収のコスト.ただし真の根から $v$ までのコストは除くものとする.
$cnt[v] := v$ の部分木に含まれる葉の個数

1つ目のパターンは,$v$ で quick save しないので真の根から $v$ までのコスト $depth[v]$ が余計にかかります.なので,このときのコストは
$dp[l] + dp[r] + depth[v] + 2$
となります.

2つ目(3つ目も同様)のパターンでは,$l$ の部分木の回収に $dp_2[l]$ と,
$v$ から $l$ に $cnt[l]$ 回移動することになるのでその分だけのコストがまずかかります.その後は,$r$ にうつって最適に回収するので,そのコストは $dp[r] + 1$ です.よって総コストは
$dp_2[l] + cnt[l] + dp[r] + 1$
となります.

あとはこれらのうちで最小のものが $dp[v]$ の値となります.

ソースコード

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

using ll = long long;

// スタック拡張します

int N;
ll dp[500001], dp2[500001];
int leaf_cnt[500001];
int g[500001][2];

ll dfs(int v, int depth) {
    for(int i = 0; i < 2; ++i) {
        int to = g[v][i];
        if(to == N) {
            leaf_cnt[v]++;
        } else {
            dfs(to, depth + 1);
            leaf_cnt[v] += leaf_cnt[to];
            dp2[v] += dp2[to];
        }
    }
    dp2[v] += leaf_cnt[v];

    ll& res = dp[v];
    int l = g[v][0], r = g[v][1];
    dp[v] = min({dp2[l] + (leaf_cnt[l] == 0 ? 1 : leaf_cnt[l]) + 1 + dp[r],
                 dp[l] + dp2[r] + (leaf_cnt[r] == 0 ? 1 : leaf_cnt[r]) + 1,
                 dp[l] + depth + dp[r] + 2});
    return dp[v];
}

int main() {
    cin >> N;
    for(int i = 0; i < N - 1; ++i) {
        int a, b;
        cin >> a >> b;
        a -= (a != N);
        b -= (b != N);
        g[i][0] = a;
        g[i][1] = b;
    }
    cout << dfs(0, 0) << endl;    
}

感想

メモリの制約が厳しい上に,普通にスタックオーバーフローするので†スタック拡張†します.
DP部分は普通に難しかった…….