コンテスト中は迷走しまくってしまった
問題概要
マスがあって、最初はすべて白色である。以下の 種類の操作を好きな順序で好きな回数行うことができる。
- 種類目の操作: マス からマス までを黒色で塗る
マス目の状態を変化させるような操作の回数の最大値を求めよ。
制約
考えたこと
コンテスト中は嘘貪欲を考えていた。「長さ 1 の操作があればそれは必ず使って良くて、その操作の占めていた区間を縮約して、残りの問題を考えれば良い」までは正しい。
しかし、「長さ 1 の操作がない場合でも、長さが最小の操作を必ず使って良いと考えていいのではないか」と予想して、これが嘘だった。たとえばサンプル 4 に対して、正しくない。
そこで正攻法を考える。黒色マスが増えていく様子は、入れ子構造のようになっている。こういう構造を紐解く時は、確かに区間 DP が有効なのであった。
区間 DP
区間 内部について、操作列の最大値を求めたい。次のように考えることにした。
最後の操作によって黒色に変わるマス の位置で場合分けする
マス を含む操作であって、区間 内部におさまるものが存在すれば、それを最後の操作として、
- 区間 についての小問題
- 区間 についての小問題
へと分解できる。まさに区間 DP そのもの。もし、「マス を含む操作であって、区間 内部におさまるものが存在するか」どうかの判定を でできるならば、区間 DP の処理に要する計算量は となる。
その存在判定を高速でできるようにするために、次の配列を前処理で求めておくこととした。これは の計算量で容易に求められる。
minR[v][l]
:= 区間 を含む操作のうち、左端が 以上であるものの中での右端の最小値 (存在しない場合は )
全体として、計算量は となった。
コード
#include <bits/stdc++.h> using namespace std; #define COUT(x) cout << #x << " = " << (x) << " (L" << __LINE__ << ")" << endl template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; } template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; } int main() { int N, M; cin >> N >> M; vector<int> left(M), right(M); for (int i = 0; i < M; ++i) { cin >> left[i] >> right[i]; --left[i]; } // minR[v][l] := 区間 [v, v+1) を含む操作であって、左端が l 以上であるものの中での右端の最小値 (存在しない場合は N+1) vector<vector<int>> minR(N+1, vector<int>(N+1, N+1)); for (int v = 0; v < N; ++v) { for (int id = 0; id < M; ++id) { if (right[id] <= v || v+1 <= left[id]) continue; for (int l = 0; l <= left[id]; ++l) { chmin(minR[v][l], right[id]); } } } // 区間 DP vector<vector<int>> dp(N+1, vector<int>(N+1, -1)); auto rec = [&](auto self, int l, int r) -> int { if (dp[l][r] != -1) return dp[l][r]; if (l >= r) return 0; int res = 0; // 最後に置くマスで場合分け for (int v = l; v < r; ++v) { // 区間 [v, v+1) を含み、全体が [l, r) におさまるような操作が存在しない場合はスキップ if (minR[v][l] > r) continue; chmax(res, 1 + self(self, l, v) + self(self, v + 1, r)); } return dp[l][r] = res; }; int res = rec(rec, 0, N); cout << res << endl; }