整理一下亚历克斯和李用两个人闲来无聊玩的7场石子游戏(╯‵□′)╯︵┻━┻
总结
双方博弈的题一般都是动态规划解决。也不排除像第一场游戏有讨巧的解法,或者第6场用的贪心方法。
一般来讲,两端都可以取,考虑二维dp;只有一端,考虑一维dp;任意取,大概率贪心。
两端的情况,推荐顺序(游戏7->游戏5)
一端的情况,推荐顺序(游戏3->游戏4->游戏2)
对dp的设定有两种,一种是对于这部分石子,当前选手能拿到的最高分数,另一种是对于这部分石子,当前选手能拿到的最大差值。一般设为最大差值可以减少一些不必要的运算。
这里讲一下为什么用差值比较好
假设当前选手A取i位置的石子
最大差值
dp[i] = 新增的分数-dp[i+1]
解释dp[i+1]表示下一个选手B取得最大差值 = b-a
新增的分数-(b-a) = a0+a-b = 当前选手A能得到的最大差值
最高分数
dp[i] = 新增的分数+(dp[i+1]之后能给双方选手带来的最大分数-dp[i+1])
解释dp[i+1]表示下一个选手B取得最大分数 = b
新增的分数+(dp[i+1]之后能给双方选手带来的分数之和-dp[i+1]) = a0+(a+b-b) = a0+a=当前选手A能得到的最高分数
很明显可以看到对于dp[i+1]之后能给双方选手带来的分数之和若是条件稍微变一下,是不太容易求的。
具体题解
877. 石子游戏
本题用动态规划肯定是可以解的,但是本题有几个限制,让这题有了更简单的解法。一则,石子为奇数(肯定有胜负);二则,只需要判断胜负(不需要找到最优解),三则,石子堆数为偶数(每人拿的堆数都一样)。
这么解释,把一堆石子按奇偶数分,奇数位置的和,偶数位置的和,两者肯定有一个更大的,作为先手的话,他可以选取大一些的那个。
比如说,先手A想拿偶数位置的,只要上来拿0编号,对于后手B,只能拿1编号和n-1编号(均为奇数),然后B任意拿一个,肯定会暴露出一个偶数编号,A继续拿这个偶数编号的。
所以说先手总能选大一些的那一堆,即肯定会赢。
class Solution {
public:
bool stoneGame(vector<int>& piles) {
return true;
}
};
1140. 石子游戏 II
// dp[i][j]表示剩余[i : len - 1]堆时,M = j的情况下,先取的人能获得的最多石子数
// dp[i][M]=max{dp[i][M],sum[i:n-1]-dp[i+x][max(M,x)]}
class Solution {
public:
int stoneGameII(vector<int>& piles) {
int n = piles.size();
int preSum = 0;
vector<vector<int>> dp(n, vector<int>(n+1));
for(int i = n-1; i >= 0; -- i){
preSum += piles[i];
for(int M = 1; M <= n; ++ M){
if(i + 2*M >= n)
dp[i][M] = preSum;
else{
for(int x = 1; x <= 2*M; ++ x)
dp[i][M] = max(dp[i][M], preSum - dp[i+x][max(M, x)]);
}
}
}
return dp[0][1];
}
};
1406. 石子游戏 III
这个是最经典的石子游戏的动态规划解决办法。
对于当前选手来说,我有三种取法,那当前选手选择的应该是能让他与下一个选手差值最大的。
//dp[i]: 对于石子stones[i..n-1]序列,当前选手可以取到的最大差值
//dp[i] = max(
// stone[i]-dp[i+1],
// stone[i]+stone[i+1]-dp[i+2],
// stone[i]+stone[i+1]+stone[i+2]-dp[i+3],
//)
class Solution {
public:
string stoneGameIII(vector<int>& stoneValue) {
int n = stoneValue.size();
vector<int> dp(n+1, INT_MIN);
dp[n] = 0;
for(int i = n-1; i >= 0; -- i){
int pre = 0;
for(int j = i+1; j <= i+3 && j <= n; ++ j){
pre += stoneValue[j-1];
dp[i] = max(dp[i], pre - dp[j]);
}
}
return dp[0] == 0 ? "Tie" : dp[0] > 0 ? "Alice" : "Bob";
}
};
1510. 石子游戏 IV
这题石子没有分数,其实相当于也是从一端开始拿,也是一位dp。不难想象对于石子数为n,当前选手有K种拿法, k 2 < = n , k ϵ [ 1 , K ] k^2<=n, k\epsilon[1,K] k2<=n,kϵ[1,K],那当前选手想赢的话,只要有一个k满足dp[n-k*k]必输,那当前选手就必赢。
//dp[i]: i个石子当前选手的输赢状态
class Solution {
public:
bool winnerSquareGame(int n) {
vector<bool> dp(n+1, false);
for(int i = 1; i <= n; ++ i){
for(int k = 1; k*k <= i; ++ k)
if(!dp[i-k*k]){
dp[i] = true;
break;
}
}
return dp[n];
}
};
1563. 石子游戏 V
本题也是二维dp的问题,你可以把他理解成从左端或者从右端拿掉一堆石子的问题。
只不过这里用一个要遍历k(分隔线),至于是拿k的左半部分,还是k的右半部分就取决于左半部分的和相比右半部分的和哪个更大了。计算一段序列的和可以考虑用前缀和数组。
不过本题其实可以优化一下,复杂度降为n^2,这里暂时不讨论。
/*
dp[i][j]: 对于石子stone[i...j],Alice能得到的最大分数
在[i+1,j] 中遍历k,找到分数最大的那个作为dp[i][j]
对于分隔线k,将[i...j]分隔为[i..k-1]和[k...j]
当sum[i...k-1]>sum[k...j] 分数为sum[k...j]+dp[k][j]
当sum[i...k-1]<sum[k...j] 分数为sum[i...k-1]+dp[i][k-1]
当sum[i...k-1]=sum[k...j] 分数为sum[k...j]+max(dp[k][j],dp[i][k-1])
*/
class Solution {
public:
int stoneGameV(vector<int>& stoneValue) {
int n = stoneValue.size();
int pre[505]={0};
int dp[505][505];
for(int i = 1; i <= n; ++ i)
pre[i] += pre[i-1] + stoneValue[i-1];
for(int i = n-2; i >= 0; -- i){
for(int j = i+1; j < n; ++ j){
dp[i][j] = INT_MIN;
for(int k = i+1; k <= j; ++ k){
int L = pre[k] - pre[i];
int R = pre[j+1] - pre[k];
if(L > R)
dp[i][j] = max(dp[i][j], R + dp[k][j]);
else if(L < R)
dp[i][j] = max(dp[i][j], L + dp[i][k-1]);
else
dp[i][j] = max(dp[i][j], R + max(dp[i][k-1], dp[k][j]));
}
}
}
return dp[0][n-1];
}
};
1686. 石子游戏 VI
本题是贪心解法。
假设有两个石子,对a来说价值是a1, a2,对b来说价值是b1, b2。
若a取1,价值差为a1-b2
若a取2,价值差为a2-b1
那取哪个只需要判断a1-b2 与a2-b1的大小,移项后即只要比较a1+b1与a2+b2的大小。
对先手来说,尽可能取这个石头总价值高的。
整个思路就有了,把石头按总价值排序,依次取就可以。
class Solution {
public:
int stoneGameVI(vector<int>& aliceValues, vector<int>& bobValues) {
vector<vector<int>> Stones;
int n = aliceValues.size();
for(int i = 0; i < n; ++ i)
Stones.push_back(vector<int>{aliceValues[i]+bobValues[i], aliceValues[i], bobValues[i]});
sort(Stones.begin(), Stones.end(), [](vector<int>& a, vector<int>& b){return a[0] > b[0];});
int aliceSum = 0, bobSum = 0;
for(int i = 0; i < n; ++ i){
if(i % 2)
bobSum += Stones[i][2];
else
aliceSum += Stones[i][1];
}
return aliceSum == bobSum ? 0 : (aliceSum > bobSum ? 1 : -1);
}
};
1690. 石子游戏 VII
两端都可以取,二维dp;用到一段序列的和,前缀和数组。
/*
dp[i][j]:对于石子序列stone[i...j],当前选手能获得的最大分数差值
对于dp[i][j]
当前选手有两种取法
sum[i+1...j]-dp[i+1][j]
sum[i...j-1]-dp[i][j-1]
*/
class Solution {
public:
int stoneGameVII(vector<int>& stones) {
int n = stones.size();
vector<int> sum(n+1, 0);
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = 1; i <= n; ++ i)
sum[i] = sum[i-1] + stones[i-1];
for(int i = n-2; i >= 0; -- i){
for(int j = i + 1; j < n; ++ j){
if(i == j-1) dp[i][j] = max(stones[i], stones[j]);
else{
dp[i][j] = max(
sum[j+1]-sum[i+1]-dp[i+1][j],
sum[j]-sum[i]-dp[i][j-1]
);
}
}
}
return dp[0][n-1];
}
};