你和你的朋友⾯前有⼀排⽯头堆, ⽤⼀个数组 piles 表⽰, piles[i] 表⽰第 i堆⽯⼦有多少个。 你们轮流拿⽯头, ⼀次拿⼀堆, 但是只能拿⾛最左边或者最右边的⽯头堆。 所有⽯头被拿完后, 谁拥有的⽯头多, 谁获胜。
⽯头的堆数可以是任意正整数, ⽯头的总数也可以是任意正整数, 这样就能打破先⼿必胜的局⾯了。 ⽐如有三堆⽯头 piles = [1, 100, 3] , 先⼿不管拿 1 还是 3, 能够决定胜负的 100 都会被后⼿拿⾛, 后⼿会获胜。
dp[i][j][fir]表示对于piles[i,....j]这部分石头,先手所能获得的最大分数
dp[i][j][sec]表示对于piles[i,....j]这部分石头,后手所能获得的最大分数
举例理解⼀下, 假设 piles = [3, 9, 1, 2], 索引从 0 开始
dp[0][1].fir = 9 意味着:⾯对⽯头堆 [3, 9], 先⼿最终能够获得 9 分。
dp[1][3].sec = 2 意味着:⾯对⽯头堆 [9, 1, 2], 后⼿最终能够获得 2 分
dp状态数组的含义一定要表示清楚,同时要注意先后手身份的转变,当先手取完石子后,先手的状态就应该变为后手
dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)
dp[i][j].fir = max( 选择最左边的⽯头堆 , 选择最右边的⽯头堆 )
# 解释:我作为先⼿, ⾯对 piles[i...j] 时, 有两种选择:
# 要么我选择最左边的那⼀堆⽯头, 然后⾯对 piles[i+1...j]
# 但是此时轮到对⽅, 相当于我变成了后⼿;
# 要么我选择最右边的那⼀堆⽯头, 然后⾯对 piles[i...j-1]
# 但是此时轮到对⽅, 相当于我变成了后⼿。
if 先⼿选择左边:
dp[i][j].sec = dp[i+1][j].fir
if 先⼿选择右边:
dp[i][j].sec = dp[i][j-1].fir
# 解释:我作为后⼿, 要等先⼿先选择, 有两种情况:
# 如果先⼿选择了最左边那堆, 给我剩下了 piles[i+1...j]
# 此时轮到我, 我变成了先⼿;
# 如果先⼿选择了最右边那堆, 给我剩下了 piles[i...j-1]
# 此时轮到我, 我变成了先⼿。
然后我们会发现只需要dp数组的右上办部分,对于dp[i][j]的计算依赖于dp[i-1][j]和dp[i][j-1].因此应该斜的遍历数组,同时对于初始情况初始化,应该只初始化对角线上的内容。
代码如下:
int stoneGame(int[] piles) {
int n = piles.length;
// 初始化 dp 数组
Pair[][] dp = new Pair[n][n];
for (int i = 0; i < n; i++)
for (int j = i; j < n; j++)
dp[i][j] = new Pair(0, 0);
// 填⼊ base case
for (int i = 0; i < n; i++) {
dp[i][i].fir = piles[i];
dp[i][i].sec = 0;
} /
/ 斜着遍历数组
for (int l = 2; l <= n; l++) {
for (int i = 0; i <= n - l; i++) {
int j = l + i - 1;
// 先⼿选择最左边或最右边的分数
int left = piles[i] + dp[i+1][j].sec;
int right = piles[j] + dp[i][j-1].sec;
// 套⽤状态转移⽅程
if (left > right) {
dp[i][j].fir = left;
dp[i][j].sec = dp[i+1][j].fir;
} else {
dp[i][j].fir = right;
dp[i][j].sec = dp[i][j-1].fir;
}
}
}
Pair res = dp[0][n-1];
return res.fir - res.sec;
}