L – Deque 解説 (Educational DP Contest / DP まとめコンテスト)
問題概要
数列 \(a = a_1, a_2, \ldots, a_N \) がある。二人で以下の操作を交互に行う。
- a の先頭要素または末尾要素を取り除く。 取り除いた要素を x とすると、操作を行った人は x 点を得る。
先手の得点を X, 後手の得点を Y とする時、先手は X-Y を最大化しようとし、後手は X-Yを最小化しようとする。
最終的な X-Y の値は何か?
制約
- \(1 \leq N \leq 3000\)
- \(1 \leq a_i \leq 10^9\)
考え方
問題K と同じくゲームに関する問題です。後退解析という考え方が使えます。
後退解析とは、「ゲームの終了状態から出発して、ゲームの開始状態まで逆方向に解析していくこと」です。
ゲームの終了状態(数列が全て取り除かれた時)から逆に考えて、ゲーム開始時点までの状態を探索していきます。
dp[ i ][ j ] := 区間 [ i, j ) 番目の数列から始めて最善手を取った時の、最終的な X-Y の値
遷移の方法としては、まずどちらの手番なのかを考える必要があります。実は残り個数 (j-i) の時は、今までに取った数は N -(j-i) になるので、
- N – (j-i) が偶数のとき:先手の手番
- N – (j-i) が奇数のとき:後手の手番
になることが分かります。先手の時は最終スコアが増加し、後手の時は最終スコアが減少するようにしなければなりません。
数列の取り方としては、
- \(a_i\) を取る時
- \(a_j\) を取る時
の2種類があり、この2つの場合を考えてやれば良いです。
初期化
- dp[ i ][ i ] = 0
残り個数が 0個の時はどちらも得点が 0 なので、 X-Y も 0 になります。
更新
- N – (j-i) が偶数のとき:先手の手番
- dp[ i ][ j ] = max(dp[ i ][ j – 1 ] + A[ j – 1 ], dp[ i + 1 ][ j ] + A[ i ]);
- N – (j-i) が奇数のとき:後手の手番
- dp[ i ][ j ] = min(dp[ i ][ j – 1 ] – A[ j – 1 ], dp[ i + 1 ][ j ] – A[ i ]);
更新の順序には注意が必要です。既に更新が終わった部分を利用して、次の更新を行う必要があります。
メモ化再帰で実装したほうがやりやすいかもしれません。
C++での実装例
メモ化再帰
今回のように dp の更新を for 文で書きにくい場合は、メモ化再帰の方が実装しやすいです。
#include <bits/stdc++.h> #define ALL(obj) begin(obj), end(obj) using namespace std; using ll = long long; using ull = unsigned long long; const ll INF = 1e18; int N; vector<vector<ll>> dp; vector<ll> a; ll rec(int l = 0, int r = N) { if (r == l) return 0; ll &ret = dp[l][r]; if (ret != INF) return ret; if ((N - (r - l)) % 2 == 0) { // 先手 ret = max(a[l] + rec(l + 1, r), rec(l, r - 1) + a[r - 1]); } else { // 後手 ret = min(-a[l] + rec(l + 1, r), rec(l, r - 1) - a[r - 1]); } return ret; } int main() { cin >> N; a.assign(N, 0); dp.assign(N + 5, vector<ll>(N + 5, INF)); for (int i = 0; i < N; i++) { cin >> a.at(i); } cout << rec() << endl; return 0; }
dp で更新
dp配列の対角線に並行に更新しました。
#include <bits/stdc++.h> #define LOOP(n) for (int _i = 0; _i < (n); _i++) #define REP(i, n) for (int i = 0; i < (n); ++i) #define FOR(i, r, n) for (int i = (r); i < (n); ++i) #define ALL(obj) begin(obj), end(obj) using namespace std; using ll = long long; using ull = unsigned long long; int N, K; ll dp[3005][3005]; int main() { cin >> N; vector<int> A(N + 1); REP(i, N) cin >> A.at(i + 1); FOR(s, 2, N + 2) { // jのstart位置 FOR(j, s, N + 2) { int i = j - s + 1; if ((N - (j - i)) % 2 == 0) { // 先手 dp[i][j] = max(dp[i][j - 1] + A[j - 1], dp[i + 1][j] + A[i]); } else { // 後手 dp[i][j] = min(dp[i][j - 1] - A[j - 1], dp[i + 1][j] - A[i]); } } } cout << dp[1][N + 1] << endl; return 0; }
ディスカッション
コメント一覧
まだ、コメントがありません