AOJ 2646 - Tournament
解法
全探索を考えるところから。
n が小さくで列を愚直に持てるとしたら、次の全探索を考えるはず。
res(l, r, rank) := 区間 [l, r) における勝者の最終順位が rank になるときの最適解
遷移は、m = (l + r) / 2, d = この区間の最終決戦の高さ(真の決勝からの高さ)として
res(l, r, rank) = min(res(l, m, rank) + res(m, r, d + 1), res(l, m, d + 1) + res(m, r, rank))
となる。
メモ化してもこのままだと間に合わない…
と思いきや、この探索は別に数列を愚直に持たなくてもできる。
今見ている区間が、ある区間 [a[i], a[i + 1]) に完全に内包されるなら、その瞬間に O(1) で計算できるからである。
このとき、メモ化するのは、rank == d のときだけでOK。
遷移を考えればわかるが、d == rank のときをメモ化しておけば、決勝からの高さ d のところで d != rank となるような探索は全体では d 回しか起こらない(はず、厳密に示してない)。
探索ノード数は、各区間 [a[i], a[i + 1])に対して、2べきサイズでごっそりとっていくイメージ(セグツリと似てる)なので、それぞれ log ぐらいのサイズ (n 以下) しかない。
なので、どんなに悪く見積もっても O(n^2 m) で解けている。
以下は map 使ってるので余計な log が付いてる。
定数倍厳しいときは気をつけないと普通に TLE しそう。
ソースコード
#include <bits/stdc++.h> using namespace std; using pii = pair<int, int>; int main() { int n, m; cin >> n >> m; vector<int> a(m + 1), b(m); for(auto& x : a) cin >> x; for(auto& x : b) cin >> x; vector<map<pii, int>> dp(n + 1); // consider ['l', 'r'), 'd' is depth, 'final_rank' represents this block winner's rank (2^r) function<int(int, int, int, int)> solve = [&] (int l, int r, int d, int final_rank) { if(final_rank == d && dp[final_rank].count({l, r})) return dp[final_rank][{l, r}]; const int i = lower_bound(begin(a), end(a), r) - begin(a) - 1; int res = 0; if(0 <= i && a[i] <= l && r <= a[i + 1]) { // contain completely if(b[i] >= d + 1) { res = r - l - (r - l) / (1 << (n - b[i] + 1)); } else { res = r - l - (b[i] == final_rank); } } else { // choose which block win const int mid = (l + r) / 2; res = min(solve(l, mid, d + 1, final_rank) + solve(mid, r, d + 1, d + 1), solve(l, mid, d + 1, d + 1) + solve(mid, r, d + 1, final_rank)); } if(final_rank == d) return dp[final_rank][{l, r}] = res; else return res; }; cout << solve(0, 1 << n, 0, 0) << endl; }
感想
なんか頭いいやり方があるのかと思って無限に悩んだ。
結局ただのメモ化再帰なんだなあ。ただ計算量解析が怪しい。