涉及game的问题,2个player,
现有几堆石头,每堆石头个数为piles[i],
刚开始M=1,
player1先拿石头,可以拿走前 x 堆(假设从第 i 堆开始拿,可以拿 i ~ i+x-1 堆),1 <= x <= 2M,
拿完之后,重置M=max(x, M),
然后player2开始拿,重复这个过程,直到所有的石头被拿完。
问player1最多可以拿多少个石头。
思路:
game理论就是min, max, 尽量做到自己max, 对手min。
枚举所有可能的情况,找到player1最多的石头。
假设从第 i 堆开始拿,
player1拿x堆,那么player1将会得到piles[i] + … + piles[i+x-1]个石头,
M更新为max(x, M),
那么下一次player2从 i+x 堆开始拿,最多拿 2*max(x,M) 堆。
直到石头被拿完。
要枚举所有的 1 <= x <= 2M,
同时M <= n (n为石头堆数),2M >= n-x时,可以一次性拿走剩下所有石头(x=2M时,可以拿完)。
方法1:DFS
用DFS模拟一次player1, player2交替拿石头的过程,
因为中间涉及到计算和的过程:piles[i] + … + piles[i+x-1],
提前计算积分数组pileSum,那么piles[i] + … + piles[i+x-1] = pileSum[i] - pileSum[i+x] (从右到左求和)
dfs(pileSum, i, m, dp)表示player从第 i 堆开始拿x堆石头(遍历所有x),m为当前的M,
保证调用它的player能石头最大化。
dp[i][m]保存从第i堆开始拿,限制为m的最大可拿石头数,可以避免重复计算。
因为最后求的是player1最多拿多少个石头,所以用player1的视角来调用DFS.
player1先拿,从第0堆开始拿,M=1。
所以调用dfs(pileSum, 0, 1, dp)
现在假设到了第 i 堆,
player1从第i堆开始拿走x堆石头后,会得到piles[i] + … + piles[i+x-1]个石头,
pileSum计算的是从右到左的和,因此piles[i] + … + piles[i+x-1] = pileSum[i]-pileSum[i+x]
M更新为max(x, M)
player1拿完之后,player2从i+x堆开始拿,限制为新的M=max(x, M),
根据game理论,对于player2来说,它自己也要石头最大化,
所以从player2的视角调用DFS, dfs(pileSum, i+x, max(x,M), dp)
player2拿完之后,player1可以拿pileSum[i+x] - player2拿的部分,
这就得到了player1最后拿的石头数。
用player1的视角调用DFS时,DFS函数的内部也有player2视角调用的DFS,
反之player2的视角调用DFS时,内部也有player1调用的DFS,
这就解释了game理论中的min, max, 最大化自己,最小化对手。
class Solution {
int n = 0;
public int stoneGameII(int[] piles) {
n = piles.length;
int[][] dp = new int[n][n];
for (int i = n - 2; i >= 0; i--)
piles[i] += piles[i + 1];
return dfs(piles, 0, 1, dp);
}
private int dfs(int[] pileSum, int i, int m, int[][] dp) {
//x<=2m,如果i+2m>=n,说明可以一次取走剩下所有堆
if (i + 2 * m >= n)
return pileSum[i];
if (dp[i][m] > 0)
return dp[i][m];
int result = 0, take = 0;
for (int x = 1; x <= 2 * m; x++) {
take = pileSum[i] - pileSum[i + x]; //拿走前x堆
result = Math.max(result, take + pileSum[i + x] - dfs(pileSum, i+x, Math.max(x, m), dp));
}
dp[i][m] = result;
return result;
}
}
方法二:DP
上面的DFS可以看作top-down的过程,
那么DP就是一个bottom-up的过程。
效率的话DFS更快。
public int stoneGameII(int[] piles) {
final int n = piles.length;
if(n < 2) return piles[0];
int[][] dp = new int[n][n+1];
int sum = 0;
//dp[i][m]: i表示从第i堆开始拿,m表示当前M
for(int i = n-1; i >= 0; i--) {
sum += piles[i];
for(int m = 1; m <= n; m++) {
if(i + 2*m >=n) dp[i][m] = sum;
else {
for(int x = 1; x <= 2* m; x++)
dp[i][m] = Math.max(dp[i][m], sum - dp[i+x][Math.max(x,m)]);
}
}
}
return dp[0][1];
}