いわゆる「相手との得点差を最大化したい」タイプのゲーム DP ですね!
問題概要
太郎君と次郎君が数列 を使ってゲームをします。太郎君を先手として、交互に次の操作を行います。
- 数列の先頭要素または末尾要素を除去する
- 除去した値の分だけ得点が得られる
数列が空になった瞬間にゲームは終了します。
ゲーム終了時の太郎君の総得点を 、次郎君の総得点を とします。 太郎君は を最大化しようとし、次郎君は を最小化しようとします。
二人が最適に行動すると仮定したとき、 を求めてください。
制約
解法
いわゆる「ゲームを DP で解く」系の問題ですね!
そのタイプの問題にまったく馴染みのない方は、次の記事を読んでみてください。
さて、今回のゲームは、お互いに「自分の得点から相手の得点を引いた値」を最大化したい構造になっています。実際、今回の問題設定は、
- 太郎君は を最大化したい
- 次郎君は を最大化したい
というように考えることができます。
このようなゲームをゼロサムであると言います。ゼロサムゲームを解くためには、次のような再帰関数が有効です。
// 盤面の状態が state である状態からスタートして、 // その状態で手番であるプレイヤー視点での相手との得点差の最大値を返す int dfs(State state) { // 終端条件 if (state が終局である) return (終局に応じた得点); // 打てる手をすべて試す int res = -INF; for (state2 : state から遷移できる局面全て) { // dfs(state2) によって、相手視点の得点が得られるので -1 倍する res = max(res, -dfs(state2) + (その手で得る利得)); } return res; }
なお、このような形式の再帰関数を用いたゲーム探索は、ミニマックス法や、ネガマックス法などと呼ばれます。
競プロでは、この再帰関数をこのまま実装するだけだと TLE になることが多いです。それは、同一局面を何度も何度も探索し得ることが原因です。
そこで、メモ化しましょう。そうすると、いわゆるメモ化再帰と呼ばれる DP になりますね!
今回の問題
今回の問題では、盤面の状態は、残っている数列の区間 によって表せます。これを盤面 と表すことにします。
よって、次のような再帰関数で解けます。この再帰関数では、メモ化も実施しています。
// 盤面 (l, r) からスタートして、 // その状態で手番であるプレイヤー視点での相手との得点差の最大値を返す long long dfs(int l, int r, vector<vector<long long>> &dp) { // メモ化済みならば、メモ化された値を返す if (dp[l][r] != -INF) return dp[l][r]; // 終端条件 if (l == r) return 0; // 打てる手をすべて試す long long res = -INF; res = max(res, -dfs(l+1, r, dp) + A[l]); // 先頭を除去 res = max(res, -dfs(l, r-1, dp) + A[r-1]); // 末尾を除去 // メモ化しながら答えを返す return dp[l][r] = res; }
最後に計算量を見積りましょう。
- ありうる盤面の個数: 通り
- 各盤面で打てる手の個数: 通り (高々 2 通り)
ですので、全体の計算量は となります。
コード
#include <bits/stdc++.h> using namespace std; const long long INF = 1LL<<60; // 入力 vector<long long> A; // 盤面 (l, r) からスタートして、 // その状態で手番であるプレイヤー視点での相手との得点差の最大値を返す long long dfs(int l, int r, vector<vector<long long>> &dp) { // メモ化済みならば、メモ化された値を返す if (dp[l][r] != -INF) return dp[l][r]; // 終端条件 if (l == r) return 0; // 打てる手をすべて試す long long res = -INF; res = max(res, -dfs(l+1, r, dp) + A[l]); // 先頭を除去 res = max(res, -dfs(l, r-1, dp) + A[r-1]); // 末尾を除去 // メモ化しながら答えを返す return dp[l][r] = res; } int main() { // 入力 int N; cin >> N; A.resize(N); for (int i = 0; i < N; ++i) cin >> A[i]; // メモ化再帰 vector<vector<long long>> dp(N+1, vector<long long>(N+1, -INF)); cout << dfs(0, N, dp) << endl; }