题目地址:
https://www.lintcode.com/problem/coins-in-a-line-iii/description
给定有 n n n个硬币排成一条线,硬币的价值为以数组 A A A表示。两个参赛者轮流从任意一边取一枚硬币,直到没有硬币为止。拿到硬币总价值更高的获胜。问先手是否有必胜策略。
参考https://blog.csdn.net/qq_46105170/article/details/104091040。
如果 n n n是偶数的话,我们可以将 A [ 0 ] , A [ 2 ] , . . . A[0],A[2],... A[0],A[2],...涂成红色, A [ 1 ] , A [ 3 ] , . . . A[1],A[3],... A[1],A[3],...涂成蓝色,如果红色硬币总价值高于蓝色,那么先手可以总是取红色,从而必胜;如果蓝色硬币总价值高于红色,先手也可以必胜;如果价值一样高,那么无论先手取哪个颜色,后手可以取不同的颜色,从而导致两个人各自始终取的是相同颜色的硬币,从而后手可以保持不败,也就是先手不会有必胜策略。
如果 n n n是奇数,则需要用动态规划或者记忆化搜索来解决了。
法1:记忆化搜索。设 f [ i ] [ j ] f[i][j] f[i][j]表示如果只剩下 A [ i : j ] A[i:j] A[i:j]这么多硬币的时候,先手与后手的分差最大值是多少(也就是先手能比后手多赢多少价值),那么有两种可能,分别是先手取 A [ i ] A[i] A[i],此时后手最多能赢 f [ i + 1 ] [ j ] f[i+1][j] f[i+1][j]分,那么先手最多能赢的分差是 A [ i ] − f [ i + 1 ] [ j ] A[i]-f[i+1][j] A[i]−f[i+1][j];类似的,先手取 A [ j ] A[j] A[j],分差最大值是 A [ j ] − f [ i ] [ j − 1 ] A[j]-f[i][j-1] A[j]−f[i][j−1]。base case是 i = j i=j i=j的时候, f [ i ] [ i ] = A [ i ] f[i][i]=A[i] f[i][i]=A[i]。最后只需要返回 f [ 0 ] [ l A − 1 ] f[0][l_A-1] f[0][lA−1]是否为正即可。代码如下:
public class Solution {
/**
* @param values: a vector of integers
* @return: a boolean which equals to true if the first player will win
*/
public boolean firstWillWin(int[] values) {
// write your code here
if (values.length % 2 == 0) {
int odd = 0, even = 0;
for (int i = 0; i < values.length; i++) {
if (i % 2 == 0) {
even += values[i];
} else {
odd += values[i];
}
}
return odd != even;
}
int len = values.length;
return dfs(0, len - 1, new int[len][len], values) > 0;
}
private int dfs(int i, int j, int[][] dp, int[] values) {
// 如果有记忆,则调取记忆
if (dp[i][j] != 0) {
return dp[i][j];
}
// 只有一个硬币,那么就返回其价值。返回之前做记忆
if (i == j) {
dp[i][i] = values[i];
return values[i];
}
// 分两种情况讨论,先手拿左边硬币还是拿右边硬币
int r1 = values[i] - dfs(i + 1, j, dp, values), r2 = values[j] - dfs(i, j - 1, dp, values);
// 返回之前做记忆
dp[i][j] = Math.max(r1, r2);
return dp[i][j];
}
}
时空复杂度 O ( n 2 ) O(n^2) O(n2)。
注意,这里if (dp[i][j] != 0) { return dp[i][j]; }
这句话,如果if判断成立则一定有记忆,但不代表有记忆这个判断就一定成立,因为有可能恰好dp[i][j] = 0
,这就是记忆。也就是说,如果恰好算出来dp[i][j] = 0
,那么这个结果是会被重复计算的。但这一点并不会有太大的影响,因为发生的概率很低。
法2:动态规划。思路和上面是一样的,只不过写成递推的形式。代码如下:
public class Solution {
/**
* @param values: a vector of integers
* @return: a boolean which equals to true if the first player will win
*/
public boolean firstWillWin(int[] values) {
// write your code here
if (values.length % 2 == 0) {
int odd = 0, even = 0;
for (int i = 0; i < values.length; i++) {
if (i % 2 == 0) {
even += values[i];
} else {
odd += values[i];
}
}
return odd != even;
}
int[][] dp = new int[values.length][values.length];
for (int len = 1; len <= values.length; len++) {
for (int i = 0; i + len - 1 < values.length; i++) {
int j = i + len - 1;
if (len == 1) {
dp[i][i] = values[i];
} else {
dp[i][j] = Math.max(values[i] - dp[i + 1][j], values[j] - dp[i][j - 1]);
}
}
}
return dp[0][values.length - 1] > 0;
}
}
时空复杂度一样。