石子游戏
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。
定义二维数组 dp[i][j],其行数和列数都等于石子的堆数,dp[i][j]表示当剩下的石子堆为下标 i 到下标 j 时,当前玩家与另一个玩家的石子数量之差的最大值。
则有转移方程:
注意是轮流取,所以是减。
dp[i][j]=max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1])
class Solution {
public:
bool stoneGame(vector<int>& piles)
{
int s = piles.size();
vector< vector < int > > dp(s, vector<int >(s, 0));
for (int i = 0; i < s; i++)
{
dp[i][i] = piles[i];
}
for (int i = 0; i<s; i++)
{
//只有i<j才有意义
for (int j = i+1; j < s; j++)
{
dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
}
}
return dp[0][s - 1];
}
};
石子游戏 II
动态规划
用动态规划 ,首先我们要存储状态,那么肯定需要存储的状态即为当前的下标,和M的值。
如果只有一堆的话,自然是很好计算的,全部取完即可。但是剩很多堆时不好算(依赖后面的状态)。所以我们选择从后往前递推。
那么定义dp[i][j]表示,剩余i堆时,M=j时,先取的人能获得的最多石子数。
自然有 i+2*M>=len,可以取完剩下的,那就取完。
剩下的堆数不能全部取走,那么最优情况就是让下一个人取的更少。
dp[i][M] = max(dp[i][M], presum - dp[i + x][max(x, M)])
class Solution {
public:
int stoneGameII(vector<int>& piles) {
int n = piles.size();
vector<vector<int>>dp(n, vector<int>(n + 1, 0));
int presum;
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(x, M)]);
}
}
}
}
return dp[0][1];
}
};
递归
class Solution {
private:
unordered_map<pair< int, int>, int > m;
vector<int> s;//后缀和
int n;
int dfs(int i, int M)
{
//从i开始取
if (m.find({
i,M }) != m.end())
return m[{
i, M}];
if (i >= n)
return 0;
if (i + M * 2 >= n)
return s[i];
int best = 0;
for (int x = 1; x <=2 * M; x++)
{
//减去对方最优策略
best = max(best, s[i] - dfs(i+x, max(x, M)));
}
m[{
i, M}] = best;
return best;
}
public:
int stoneGameII(vector<int>& piles) {
this->n = piles.size();
s = vector<int>(n+1, 0);
for (int i = n - 1; i >= 0; i--)
s[i] = s[i + 1] + piles[i];
return dfs(0, 1);
}
};
石子游戏III
定义dp[i]为剩余i个时,先手获得的最大分数。那么留给下一位的最大分数只能为dp[i+1],dp[i+2],dp[i+3]中的一个。
presum为i到数组末尾的和。
dp[i]=max(presum[i]-dp[j]), i+1<=j<=i+3
所以 dp[i]=presum[i]-min(dp[j])
class Solution {
public:
string stoneGameIII(vector<int>& stoneValue) {
int n = stoneValue.size();
vector<int> presum(n, 0);
presum[n - 1] = stoneValue[n - 1];
for (int i = n - 2; i >= 0; i--)
{
presum[i] = presum[i + 1] + stoneValue[i];
}
vector<int> dp(n+1, 0);
dp[n] = 0;
for (int i = n - 1; i >= 0; i--)
{
int best = dp[i+1];
for (int j = i + 2; j <= i + 3 && j <=n; j++)
best = min(best, dp[j]);
dp[i] = presum[i] - best;
}
if (dp[0] * 2 == presum[0])
return "Tie";
return dp[0]*2>presum[0]? "Alice" : "Bob";
}
};
石子游戏IV
假设dp[i]时先手处于必胜态,则一定有dp[i-k*k]为必败态。也就是说,当先手在面对 i 颗石子时,可以选择取走 k * k颗,剩余的 i-k * k 颗对于后手来说是必败态,因此先手会获胜。
边界条件为 f[0]=false,即没有石子时,先手会输掉游戏。
动态规划
class Solution {
public:
bool winnerSquareGame(int n) {
vector<int>dp(n + 1, 0);
for (int i = 0; i <= n; i++)
{
//从剩余0个开始递推
for (int k = 1; k * k <= i; k++)
{
if (!dp[i-k*k])
{
dp[i] = 1;
}
}
}
return dp[n];
}
};
DFS
class Solution {
private:
unordered_map<int, int> cache;
bool dfs(int n)//剩余n个时
{
if (n == 1) return true;
if (cache.find(n) != cache.end())return cache[n];
for (int i = 1; i * i <= n; i++)
{
if (!dfs(n - i * i))//另一个人剩余n-i*i必输时,n就赢
{
cache[n] = 1;
return true;
}
}
cache[n] = 0;
return false;
}
public:
bool winnerSquareGame(int n) {
return dfs(n);
}
};
石子游戏V
用dp[i][j]表示当 Alice 面对数组 stoneValue中从位置 i 到 j 这一段连续的石子时,她能获得的最大分数。由于 Alice 需要选择将这一段石子分成两部分,因此我们可以枚举分隔位置 。
对于左右两边的和,记为suml,sumr
suml>sumj,那么肯定丢弃左侧部分,对应的,小于则丢弃右边部分。
如果两行的值相等,Bob 让 Alice 决定丢弃哪一行。那就两边继续搜索比谁大。
class Solution {
private:
vector<vector<int>> cache;
int dfs(vector<int>& sum, int l, int r)
{
if(l == r)return 0;
if (cache[l][r]!=-1)return cache[l][r];
cache[l][r] = 0;
for (int i = l + 1; i <= r; i++)
{
int lsum = sum[i] - sum[l];
int rsum = sum[r+1] - sum[i];
if (lsum < rsum)
cache[l][r] = max(cache[l][r], lsum + dfs(sum, l, i - 1));
else if (lsum > rsum)
cache[l][r] = max(cache[l][r], rsum + dfs(sum, i, r));
else
cache[l][r] = max(cache[l][r],lsum+max(dfs(sum, l, i - 1), dfs(sum, i, r)));
}
return cache[l][r];
}
public:
int stoneGameV(vector<int>& stoneValue) {
int size = stoneValue.size();
cache = vector<vector<int>>(size + 1, vector<int>(size + 1, -1));
vector<int> sum(size + 1, 0);//[n,m)
for (int i = 0; i < size; i++)
sum[i + 1] = sum[i] + stoneValue[i];
return dfs(sum, 0, size - 1);
}
};
石子游戏 VI
我们可以这么想:石子分数高我们需要拿,有石子对于bob来说分数也很高,我们也需要拿(因为尽可能要让bob的分数最少)。
存在两个方案 :
alice的 a1,b1
bob的 a2,b2
我们比较a1-b2(alice拿了a1,bob只能拿b2)与b1-a2的差值,即比较a1+a2和b1+b2。
由此 将两数组合并,排序,偶数位 alice的 ,获得排序后对应下标的石头价值即可。
class Solution {
public:
int stoneGameVI(vector<int>& aliceValues, vector<int>& bobValues) {
int size = aliceValues.size();
int sum_a = 0, sum_b = 0;
vector<pair<int, int>> s(size, {
0,0});
for (int i = 0; i < size; i++)
{
s[i] = {
aliceValues[i] + bobValues[i],i };
}
sort(s.begin(), s.end(), [](const pair<int, int> a, const pair<int, int> b)
{
return a.first > b.first;
});
for (int i = 0; i < size; i++)
{
if (i % 2)
sum_b += bobValues[s[i].second];
else
sum_a += aliceValues[s[i].second];
}
if (sum_a == sum_b)
return 0;
return sum_a > sum_b? 1:-1;
}
};
石子游戏 VII
所以 最大得分差 可以理解为此次操作之后,A 所收获的价值 - 下次B 比A的得分差的最大值。
如果是 B 操作,那么就是 B 所收获的价值 - 下次A比B得分差的最大值
所以这次 DP 也用类似的状态。维护一个sum前缀和数组。
dp[i][j]为i-j的当前玩家与另一个玩家得分差的最大值。
当j-i==1,肯定删掉一个较小的石头,取最大得分。
剩下的情况 要么左边删,要么右边删。
class Solution {
public:
int stoneGameVII(vector<int>& stones) {
int size = stones.size();
vector<int> sum(size + 1, 0);
for (int i = 0; i < size; i++)
{
sum[i + 1] = sum[i] + stones[i];
//[0,i);
}
vector<vector<int>>dp(size, vector<int>(size, 0));
for (int i = size - 1; i >= 0; i--)
{
for (int j = i + 1; j < size; j++)
{
if (j - i == 1)
dp[i][j] = max(stones[i], stones[j]);
else//[0,j+1)-[0,i+1)=[i+1,j]
dp[i][j] = max(sum[j + 1] - sum[i + 1] - dp[i + 1][j],
//[0,j)-[0,i) =[i,j-1]
sum[j] - sum[i] - dp[i][j - 1]);
}
}
return dp[0][size - 1];
}
};