题目
动态规划
动态规划三步走
- 定义dp数组意义
- 找出状态转移方程
- 明确基本情况
定义dp数组意义
dp[i][j]表示M = i的情况下,剩余piles[j : len - 1]堆时,先取的人能获得的最多石子数
行为i 列为j 因为行多了一行j为0的情况 这里先不考虑优化数组空间,重在思路
找出状态转移方程
一、 len - i <= 2M
剩下的堆数能够直接全部取走,那么最优的情况就是剩下的石子总和
dp[i][M] = sum[i : len - 1]
二、 i + 2M < len
其中 1 <= x <= 2M,剩下的堆数不能全部取走,那么最优情况就是让下一个人取的更少。
对于所有的x取值,下一个人从x开始取起,M变为max(M, x)
下一个人能取dp[i + x][max(M, x)]
我最多能取sum[i : len - 1] - dp[i + x][max(M, x)]。
dp[i][M] = max(dp[i][M], sum[i : len - 1] - dp[i + x][max(M, x)])
base case
刚开始想的时候,就理所当然地认为最后一列全都等于piles[len-1],因为这种情况下,无论怎么拿,都是得拿完的。
// 基本情况 最后一列 此时i=len-1,即只剩下一堆
for (int row = 1; row < len + 1; row++) {
dp[row][len - 1] = piles[len - 1];
}
不过其实,只要是剩下的堆数能够直接全部取走,这种都属于基本情况。
那么最优的情况就是剩下的石子总和
dp[i][M] = sum[i : len - 1]
代码
class Solution {
public int stoneGameII(int[] piles) {
int len = piles.length;
if(len < 1){
return 0;
}
if(len == 1){
return piles[0];
}
int sum = 0;
// dp[i][j]表示M = i的情况下,剩余piles[j : len - 1]堆时,先取的人能获得的最多石子数
// 行为i 列为j 因为行多了一行j为0的情况 这里先不考虑优化数组空间,重在思路
int[][] dp = new int[len+1][len];
// 基本情况 最后一列 此时i=len-1,即只剩下一堆
// for (int row = 1; row < len + 1; row++) {
// dp[row][len - 1] = piles[len - 1];
// }
// 从后往前推,推到第一列
for (int j = len - 1; j >= 0; j--) {
sum += piles[j];
for (int M = 1; M <= len; M++) {
// 可以一次性全部拿完 剩余堆数小于2*M时
if (len - j <= 2 * M) {
dp[M][j] = sum;
} else {
for (int x = 1; x <= 2 * M; x++) {
dp[M][j] = Math.max(
dp[M][j],
sum - dp[Math.max(M, x)][j + x]
);
}
}
}
}
// 答案为dp[0][1]
// 剩余piles[0 : len - 1]堆时,M = 1的情况下,先取的人能获得的最多石子数
return dp[1][0];
}
}
其它做法
一般是回溯法+备忘录
这种用python写思路比较清晰。
class Solution:
def stoneGameII(self, piles: List[int]) -> int:
dp = {}
N = len(piles)
# 输入当前的M以及当前剩余石子堆的位置,返回先手能获得的最大积分及该情况下后手积分
def max_get(M, start):
if (M, start) in dp:
return dp[M, start]
count = N-start # 剩余堆数
if count <= 2*M: # 全部拿完
dp[M, start] = sum(piles[start:]), 0
else:
# 记录先手能获得的最大积分及该情况下后手积分,get为本次拿的积分
max_pre, max_post, get = 0, 0, 0
for x in range(min(2*M, count)):
# 实际拿的堆数为x+1
_M = max(M, x+1)
get += piles[start+x]
pre, post = max_get(_M, start+x+1) # 回溯
# get+post为当前拿到的积分+回溯下的后手积分
if get+post > max_pre:
# 替换最大值
max_pre, max_post = get+post, pre
dp[M, start] = max_pre, max_post
return dp[M, start]
return max_get(1, 0)[0]
class Solution:
def stoneGameII(self, piles: List[int]) -> int:
# 数据规模与记忆化
n, memo = len(piles), dict()
# s[i] 表示第 i 堆石子到最后一堆石子的总石子数
s = [0] * (n + 1)
for i in range(n - 1, -1, -1):
s[i] = s[i + 1] + piles[i]
# dfs(i, M) 表示从第 i 堆石子开始取,最多能取 M 堆石子所能得到的最优值
def dfs(i, M):
# 如果已经搜索过,直接返回
if (i, M) in memo:
return memo[(i, M)]
# 溢出拿不到任何石子
if i >= n:
return 0
# 如果剩余堆数小于等于 2M, 那么可以全拿走
if i + M * 2 >= n:
return s[i]
# 枚举拿 x 堆的最优值
best = 0
for x in range(1, M * 2 + 1):
# 剩余石子减去对方最优策略
best = max(best, s[i] - dfs(i + x, max(x, M)))
# 记忆化
memo[(i, M)] = best
return best
return dfs(0, 1)
结语
博弈问题的前提一般都是在两个聪明人之间进行,编程描述这种游戏的一般方法是二维 dp 数组,数组中通过元组分别表示两人的最优决策。
之所以这样设计,是因为先手在做出选择之后,就成了后手,后手在对方做完选择后,就变成了先手。这种角色转换使得我们可以重用之前的结果,典型的动态规划标志。
参考资料
《动态规划之博弈问题》 labuladong