本文记录各种各样的石子游戏题目和解法,石子游戏大多动态规划方法来处理,作为一个两人游戏,又可以从博弈的角度考虑。
题目中的英文名字太拗口,我们讨论时均以甲乙作为称呼
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i]
。游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true
,当李赢得比赛时返回 false
。
先不考虑偶数问题,假如石子堆可以是奇数的,使用动态规划的思想先定义dp数组
dp[i][j] 表示当前从石子堆的i--j中,可以获取的最大数量。
那么 dp[i][j] 就应该是下面两个情况下的较大者
1 选前面的, piles[i] + 剩下是石子中在乙选择后,下一步能甲拿到的石子数量
2选后面的,piles[j]+剩下是石子中在乙选择后,下一步能甲拿到的石子数量
由于乙也会发挥最佳水平,当剩下 plies[i+1]----piles[j] 时,乙也会做一次最佳决策,这样又陷入递归的噩梦里,因此这个递推公式还不完整.
引入一个数组sum[i][j], 代表piles[i] --- plies[j] 所有石子的和
那么当轮到乙挑选时,假设乙发挥最佳水平可以拿到x个石子,那么甲就可以获取剩下的sum[i+1][j] -x个。
因此上述地推公式变成
1 选前面的, piles[i] + sum[i+1][j] - dp[i+1][j]
2 选后面的, plies[j] +sum[i][j-1] - dp[i][j-1]
class Solution {
public:
bool stoneGame(vector& piles) {
int n = piles.size();
vector> dp(n,vector(n,0));
vector> sum(n,vector(n,0));
for(int i=0; isum[0][n-1];
}
};
另外当总数是偶数时, 第一个拿取的人一定能赢
亚历克斯和李继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子 piles[i]
。游戏以谁手中的石子最多来决出胜负。
亚历克斯和李轮流进行,亚历克斯先开始。最初,M = 1
。
在每个玩家的回合中,该玩家可以拿走剩下的 前 X
堆的所有石子,其中 1 <= X <= 2M
。然后,令 M = max(M, X)
。
游戏一直持续到所有石子都被拿走。
假设亚历克斯和李都发挥出最佳水平,返回亚历克斯可以得到的最大数量的石头。
同样采用动态规划的思路,正向思路即记忆化搜索,代码复杂但是好理解
反向思路需要逆向遍历数组
这里给出记忆化搜索方法,要特别注意边界值问题。
M的最大值应为数组长度的一半,因为 1+2+4+8<16 ,而且至少要为1,所以加1处理
class Solution {
public:
int stoneGameII(vector& piles) {
int n = piles.size();
K = n/2+1; // M的最大值
dp = vector>(n+1,vector(K+1,0));
sums = vector(n+1,0);
for(int i=n-1;i>=0;i--){
sums[i] = sums[i+1] + piles[i];
}
return dfs(piles, 0, 1);
}
int dfs(vector& piles, int i, int M) {
if (i > piles.size()-1)
return 0;
int res = 0;
int sum = 0;
for(int k = 0; k<2*M && i+k > dp;
vector sums;
int K; //m 的最大值
};
从后向前遍历
class Solution {
public:
int stoneGameII(vector& piles) {
int n = piles.size();
// 从后往前的累加和
int sum = 0;
int k = n/2+1;
vector> d(n,vector(k+1,0));
for (int i = n-1; i >= 0; --i)
{
sum += piles[i];
for (int j = 1; j <= k; ++j)
{
// 超过最大的堆数
if (i+2*j >= n)
{
d[i][j] = sum;
}
else
{ // 选1个,选2个,,, 选2M个
for (int x = 1; x <= 2*j; ++x)
{
d[i][j] = max(d[i][j], sum - d[i+x][max(j, x)]);
}
}
}
}
return d[0][1];
}
};
Alice 和 Bob 用几堆石子在做游戏。几堆石子排成一行,每堆石子都对应一个得分,由数组 stoneValue
给出。
Alice 和 Bob 轮流取石子,Alice 总是先开始。在每个玩家的回合中,该玩家可以拿走剩下石子中的的前 1、2 或 3 堆石子 。比赛一直持续到所有石头都被拿走。
每个玩家的最终得分为他所拿到的每堆石子的对应得分之和。每个玩家的初始分数都是 0 。比赛的目标是决出最高分,得分最高的选手将会赢得比赛,比赛也可能会出现平局。
假设 Alice 和 Bob 都采取 最优策略 。如果 Alice 赢了就返回 "Alice" ,Bob 赢了就返回 "Bob",平局(分数相同)返回 "Tie" 。
这题可以作为上一道题的简化版,需要注意的是数字里有负数,
1 初始化时需要将默认值设为一个较大的负数
2 因为有负数,所以最后三个数字不能采用贪心的方法全部取得,同样需要判断取一个取两个和全取的情况
本题还可以在dp数组后面添加三个0, 避免if判断
class Solution {
public:
string stoneGameIII(vector& stoneValue) {
int n = stoneValue.size();
vector dp(n,-1000000);
int sum = 0;
// dp[i] = sum[i+k] - dp[i+k] k = 1...3;
for(int i=n-1; i>=0; i--) {
sum += stoneValue[i];
if (n-i <=3) {
dp[i] = sum;
}
for(int k = 1; k <= 3 && i+k sum)
return "Alice";
else
return "Tie";
}
};
Alice 和 Bob 两个人轮流玩一个游戏,Alice 先手。
一开始,有 n 个石子堆在一起。每个人轮流操作,正在操作的玩家可以从石子堆里拿走 任意 非零 平方数 个石子。
如果石子堆里没有石子了,则无法操作的玩家输掉游戏。
给你正整数 n ,且已知两个人都采取最优策略。如果 Alice 会赢得比赛,那么返回 True ,否则返回 False 。
这里变成先拿光石子的人获胜,并不是在比大小,原理是一样的,对方赢我就输,对方输我就赢。
dp[i] 表示当前还剩 i 个石子时, 拿取的人会不会赢。
dp[i] |= !dp[i-k] (k 为完全平方数) 临界情况,dp[0] = false;
class Solution {
public:
bool winnerSquareGame(int n) {
vector dp(n+1,0);
for(int i = 1; i<=n; i++) {
for(int j =1; j*j <=i; j++) {
dp[i] = (dp[i] || (!dp[i-j*j])); // 对方赢了就是我输了,反之亦然
}
}
return dp[n];
}
};
几块石子 排成一行 ,每块石子都有一个关联值,关联值为整数,由数组 stoneValue 给出。
游戏中的每一轮:Alice 会将这行石子分成两个 非空行(即,左侧行和右侧行);Bob 负责计算每一行的值,即此行中所有石子的值的总和。Bob 会丢弃值最大的行,Alice 的得分为剩下那行的值(每轮累加)。如果两行的值相等,Bob 让 Alice 决定丢弃哪一行。下一轮从剩下的那一行开始。
只 剩下一块石子 时,游戏结束。Alice 的分数最初为 0 。
返回 Alice 能够获得的最大分数 。
分割数组,取和较小的那部分, 自然想到需要一个 sum[i][j] 数组保存i --- j的数组的元素和
dp[i][j] 表示 当剩余 i---j时, 能够获得的最大数量
使用 变量k作为分割的中点, i<=k 则 dp[i][j] = max { dp[i][k] + sum[i][k] if sum[i][k] dp[k+1][j] + sum[k+1][j] if sum[i][k] >sum[k+1][j] max(dp[i][k], dp[k+1][j]) +sum[i][k] if sum[i][k] >sum[k+1][j] } 因为较大的那部分丢弃后,较小的一部分立刻累加到结果上,所以需要加sum值 注意到我们的 i是逆序处理的,这是因为dp[i][j] 的值需要 dp[i][k] 和 dp[k+1][j], 计算i时 需要 i后面的值,所以需要逆序。举个例子 当计算 dp[0][8] 时 你可能需要 dp[0][4] 和 dp[5][8], 如果全按照正序, 此时dp[0][4]已经计算完成,但是dp[5][8]仍未计算,因此迭代无法进行下去 但可惜在 leetcode 1563 上 由于超时 有三个case没过, 1 可以优化的点是 求解sum数组时,其实不用两层循环,使用 sum[i] 表示 从 0 到 i 的所有元素之和,当需要 i -- j之间的元素之和时 使用 sum[j] -sum[i-1] 代替即可 2 据说把vector换成数组就可以通过了,不过没有兴趣尝试。 另外题解的评论区提出,使用记忆化搜索会减少很多状态的访问,可以减少耗时,这里暂时不知道原因,感觉没有剪枝的记忆化搜索和直接dp迭代应该是相同的。 Alice 和 Bob 轮流玩一个游戏,Alice 先手。 一堆石子里总共有 给你两个长度为 所有石子都被取完后,得分较高的人为胜者。如果两个玩家得分相同,那么为平局。两位玩家都会采用 最优策略 进行游戏。 请你推断游戏的结果,用如下的方式表示: 这一题改变了石子的权值,变成了甲乙双方有不同的权重,最终比双方的大小,我们来分析一下: 对于任意的 i , 假设第一个人选择这个位置,那么他的收益是自己能获得该位置的权值 a[i] ,以及对手拿不到对应的权值,即减少了对手的得分,因此总的收益是a[i] + b[i] 同样的对于第二个人来说收益也是a[i]+b[i], 这样甲乙就有相同的目标,就是尽量收益最大的位置,这就转变成了一个贪心问题。 但是又出现一个问题是,当几个位置的收益一样,即a[i]+b[i] 相同时,该怎么挑选?这时,甲会尽量选择得分高的,乙也会选择对他来说得分高的,在局部还是一个贪心问题,就是处理的时候有一点麻烦。 可以通过,但是耗时还比较高。 一部分题解上表示,当收益相同的时候,不需要考虑乙获得的分数,当按照收益从大到小排好之后,甲和乙从前往后挨个挑选即可,即甲总选下标为偶数的值。 但这一部分证明我还不是太明白。 石子游戏中,爱丽丝和鲍勃轮流进行自己的回合,爱丽丝先开始 。 有 n 块石子排成一排。每个玩家的回合中,可以从行中 移除 最左边的石头或最右边的石头,并获得与该行中剩余石头值之 和 相等的得分。当没有石头可移除时,得分较高者获胜。 鲍勃发现他总是输掉游戏(可怜的鲍勃,他总是输),所以他决定尽力 减小得分的差值 。爱丽丝的目标是最大限度地 扩大得分的差值 。 给你一个整数数组 stones ,其中 stones[i] 表示 从左边开始 的第 i 个石头的值,如果爱丽丝和鲍勃都 发挥出最佳水平 ,请返回他们 得分的差值 。 这一题与石子游戏一相似,是前后挑选,又与后面的几种相似,因为要求最后的差值。考虑到题目的描述,我们肯定是需要计算 i---j之间的石头的和,这里可以使用一个前缀和数组来计算,i---j 之间stones的和为 sum[j] - sum[i-1], 节省一维的空间和时间。 建立dp数组,令dp[i][j] 表示当剩下 i----j 时,挑选的人可获得的分数 和另一个获得的分数的最大差值,(当然也可能比不过第二个人,这时候代表的最小的差异,计算方法都是不变的) 当需要计算 dp[i][j] 时,有两个选择 1 选左边的,本次获得分数 sum[i+1][j] , 对手将领先 dp[i+1][j] 分, 2 选右边的,本次获得分数 sum[i][j-1], 对手将领先dp[i][j-1] 分, dp[i][j] 表示当前最佳选择后与对手的差值,因此无论是1还是2,使用的应该都是 sum - dp的值 dp[i][j] =std::max(sum[i+1][j] - dp[i+1][j], sum[i][j-1] - dp[i][j-1); 这里为了计算方便我们分别把sum数组和dp数组的范围扩展到 1----n;class Solution {
public:
int stoneGameV(vector
石子游戏 六
n
个石子,轮到某个玩家时,他可以 移出 一个石子并得到这个石子的价值。Alice 和 Bob 对石子价值有 不一样的的评判标准 。双方都知道对方的评判标准。n
的整数数组 aliceValues
和 bobValues
。aliceValues[i]
和 bobValues[i]
分别表示 Alice 和 Bob 认为第 i
个石子的价值。
1
。-1
。0
。class Solution {
public:
int stoneGameVI(vector
石子游戏 七
int stoneGameVII(vector