Alice and Bob continue their games with piles of stones. There are a number of piles arranged in a row, and each pile has a positive integer number of stones piles[i]
. The objective of the game is to end with the most stones.
Alice and Bob take turns, with Alice starting first. Initially, M = 1
.
On each player's turn, that player can take all the stones in the first X
remaining piles, where 1 <= X <= 2M
. Then, we set M = max(M, X)
.
The game continues until all the stones have been taken.
Assuming Alice and Bob play optimally, return the maximum number of stones Alice can get.
Example 1:
Input: piles = [2,7,9,4,4]
Output: 10
Explanation: If Alice takes one pile at the beginning, Bob takes two piles, then Alice takes 2 piles again. Alice can get 2 + 4 + 4 = 10 piles in total. If Alice takes two piles at the beginning, then Bob can take all three piles left. In this case, Alice get 2 + 7 = 9 piles in total. So we return 10 since it's larger.
Example 2:
Input: piles = [1,2,3,4,5,100]
Output: 104
Constraints:
1 <= piles.length <= 100
1 <= piles[i] <= 104
這道題是石頭游戲系列的第二道,跟之前那道 Stone Game 不同的是終於換回了 Alice 和 Bob!還有就是取石子的方法,不再是只能取首尾兩端的石子堆,而是可以取 [1, 2M] 范圍內的任意X堆,M是個變化的量,初始化為1,每次取完X堆后,更新為 M = max(M, X)
。這種取石子的方法比之前的要復雜很多,由於X的值非常的多,而且其不同的選擇還可能影響到M值,那么整體的情況就特別的多,暴力搜索基本上是行不通的。這種不同狀態之間轉移的問題用動態規划 Dynamic Programming 是比較合適的,首先來考慮 DP 數組的定義,題目要求的是 Alice 最多能拿到的石子個數,拿石子的方式是按順序的,不能跳着拿,所以決定某個狀態的是兩個變量,一個是當前還剩多少石子堆,可以通過當前位置坐標i來表示,另一個是當前的m值,只有知道了當前的m值,那么選手才知道能拿的堆數的范圍,所以 DP 就是個二維數組,其 dp[i][m] 表示的意義在上面已經解釋了。接下來考慮狀態轉移方程,由於在某個狀態時已經知道了m值,則當前選手能拿的堆數在范圍 [1, 2m] 之間,為了更新這個 dp 值,所有x的情況都要遍歷一遍,即在剩余堆數中拿x堆,但此時x堆必須小於等於剩余的堆數,即 i + x <= n
,i為當前的位置。由於每個選手都是默認選最優解的,若能知道下一個選手該拿的最大石子個數,就能知道當前選手能拿的最大石子個數了,因為二者之和為當前剩余的石子個數。由於當前選手拿了x堆,則下個選手的位置是 i+x,且m更新為 max(m,x),所以其 dp 值為 dp[i + x][max(m, x)])
。為了快速得知當前剩余的石子總數,需要建立累加和數組,注意這里是建立反向的累加和數組,其中 sums[i] 表示范圍 [i, n-1] 之和。分析到這里就可以寫出狀態狀態轉移方程如下:
dp[i][m] = max(dp[i][m], sums[i] - dp[i + x][max(m, x)])
接下來就是一些初始化和邊界定義的問題需要注意的了,dp 數組大小為 n+1 by n+1,因為選手是可能一次將n堆都拿了,比如 n=1 時,所以 dp[i][n] 是存在的,且需要用 sums[i] 來初始化。更新 dp 時需要用三個 for 循環,分別控制i,m,和 x,注意更新從后往前遍歷i和m,因為我們要先更新小區間,再更新大區間。x的范圍要設定為 x <= 2 * m && i + x <= n
,前面也講過原因了,最后的答案保存在 dp[0][1] 中返回即可,參見代碼如下:
解法一:
class Solution {
public:
int stoneGameII(vector<int>& piles) {
int n = piles.size();
vector<int> sums = piles;
vector<vector<int>> dp(n + 1, vector<int>(n + 1));
for (int i = n - 2; i >= 0; --i) {
sums[i] += sums[i + 1];
}
for (int i = 0; i < n; ++i) {
dp[i][n] = sums[i];
}
for (int i = n - 1; i >= 0; --i) {
for (int m = n - 1; m >= 1; --m) {
for (int x = 1; x <= 2 * m && i + x <= n; ++x) {
dp[i][m] = max(dp[i][m], sums[i] - dp[i + x][max(m, x)]);
}
}
}
return dp[0][1];
}
};
我們再來用遞歸加記憶數組的方式來實現一下,其實核心思想跟上面完全一樣,這里就不過多的講解了,直接看代碼吧:
解法二:
class Solution {
public:
int stoneGameII(vector<int>& piles) {
int n = piles.size();
vector<int> sums = piles;
vector<vector<int>> memo(n, vector<int>(n));
for (int i = n - 2; i >= 0; --i) {
sums[i] += sums[i + 1];
}
return helper(sums, 0, 1, memo);
}
int helper(vector<int>& sums, int i, int m, vector<vector<int>>& memo) {
if (i + 2 * m >= sums.size()) return sums[i];
if (memo[i][m] > 0) return memo[i][m];
int res = 0;
for (int x = 1; x <= 2 * m; ++x) {
int cur = sums[i] - sums[i + x];
res = max(res, cur + sums[i + x] - helper(sums, i + x, max(x, m), memo));
}
return memo[i][m] = res;
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/1140
類似題目:
參考資料:
https://leetcode.com/problems/stone-game-ii/
https://leetcode.com/problems/stone-game-ii/discuss/345247/C%2B%2B-DP-(Tabulation)
https://leetcode.com/problems/stone-game-ii/discuss/345230/JavaPython-DP-Solution