题目描述:
亚历克斯和李用偶数堆石子做游戏。石子排成一行,每堆都有正整数颗石子
p
i
l
e
s
[
i
]
piles[i]
piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。
分析:
这道题问先行动者能不能获胜,翻译为定量的数学语言,就是问先行动者能不能确保自己游戏结束时获得的石子个数一定超过总个数的一半。
设
d
p
[
s
t
a
r
t
]
[
e
n
d
]
dp[start][end]
dp[start][end]代表这一步的行动者(可能为阿历克斯也可能为李)在游戏结束时最多能确保拿到的石子个数,其中
s
t
a
r
t
start
start和
e
n
d
end
end 分别代表目前剩下的石子堆的头尾序号。那么游戏开始的状态是
d
p
[
s
t
a
r
t
]
[
e
n
d
]
dp[start][end]
dp[start][end] ,问题在于是否能确保此状态对应的数值大于石子总数的一半。
该从哪里找到这个状态方程的起点呢?所谓起点,就是可以直接断定的值,这个状态方程里有什么初始值可以直接断定吗?游戏开始时的状态就是要求解的问题,很明显不可能从这头入手。换一个方向,能不能从游戏结局入手呢?
答案是肯定的。当只剩最后一堆石子的时候,这一步的行动者别无选择,只能拿走最后这堆石子!因此,对于任意的
d
p
[
i
]
[
i
]
dp[i][i]
dp[i][i] , 有
d
p
[
i
]
[
i
]
=
i
dp[i][i]=i
dp[i][i]=i 。这就是状态方程的起点。
一步步倒推回初始状态,就能算出所求状态对应的值。
代码实现:
boolean stoneGame(int[] piles) {
int[][] dp = new int[piles.length][piles.length];
int[][] sumOfStonesLeft = new int[piles.length][piles.length];//目前剩下的石子总个数
for (int d = 0; d < piles.length; d++) {//剩余石子堆头尾编号的距离,0代表只剩最后一堆石子
for (int i = 0; i + d < piles.length; i++) {
if (d == 0) {//只剩最后一堆石子时的状态作为初始值
dp[i][i] = piles[i];
sumOfStonesLeft[i][i] = piles[i];
} else {
sumOfStonesLeft[i][i + d] = sumOfStonesLeft[i][i + d - 1] + piles[i + d];
//从这一步开始自己最终获得的石子数尽量多,等价于让对方从下一步开始最终可获得的石子数尽量少
dp[i][i + d] = Math.max(sumOfStonesLeft[i][i + d] - dp[i + 1][i + d],
sumOfStonesLeft[i][i + d] - dp[i][i + d - 1]);
}
}
}
return dp[0][piles.length - 1] * 2 > sumOfStonesLeft[0][piles.length - 1];//超过一半即获胜
}
思考:
1.DP算法里状态方程的寻找常常是困难的,但通用的入手角度往往是从求解目标入手,要搜索什么,就把什么翻译为定量语言设置为状态。
2.寻找到状态之后,需要设置一个推导的起点,这个起点应该是可以直接断定的简单状态,因此需要思考过程中不发生变化的量出现在哪里。例如这道题里,只剩最后一堆石子时的状态是确定的,和前面的操作过程无关,因此可以作为状态方程的起点。