来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/stone-game
题目:甲、乙两人用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
甲和乙轮流进行,甲先开始。 每回合,玩家甲从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设甲、乙二人都发挥出最佳水平,当甲赢得比赛时返回 true ,当乙赢得比赛时返回 false 。
示例:
输入:[5,3,4,5]
输出:true
解释:
甲先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果乙拿走前 3 颗,那么剩下的是 [4,5],甲拿走后 5 颗赢得 10 分。
如果乙拿走后 5 颗,那么剩下的是 [3,4],甲拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对甲来说是一个胜利的举动,所以我们返回 true 。
关键字:【动态规划】【博弈】【极小化极大】
答案:return true(即先手的始终胜利)
思路:枚举是当然可以的,但是这肯定不是我们想要的。
这道题的我理解是每一步只考虑当前挑选石子的人的最优解。
比如:
当石子只剩下[4,5]的时候,此时(甲)的最优解拿5,记为dp甲[4,5]=[5],dp乙[4,5]=[4];
当石子只剩下[3,4]的时候,此时(甲)的最优解拿4,记为dp甲[3,4]=[4],dp乙[3,4]=[3];
当石子只剩下[3,4,5]的时候,此时(乙)的最优解是拿5、3,记为dp乙[3,4,5]=[5,3],dp甲[3,4,5]=[4];
【这里我们只考虑当前场景下拿石子的人,而不去考虑当前是谁去拿这个石子。】
- 前两步都很好理解,因为只剩下两个石子,所以直接拿大的那个即可,即 if a > b then a else b
- 第三步是怎么来的呢:因为当前选手拿石子只能从开头或者结尾去拿,所以当前选选手(乙)只有两个选择,即要么拿3,要么拿4。
- 如果(乙)拿3,那么(甲)肯定拿5,因为(甲)必须保证自己发挥最佳水平,最后(乙)拿走最后剩下的4;
- 如果(乙)拿5,那么(甲)肯定拿4,因为(甲)必须保证自己发挥最佳水平,最后(乙)拿走最后剩下的3;;
- 当前选手(乙)的两种选择已经有了:
dp乙[3,4,5]=[3,4] , dp甲[3,4,5]=[5]
dp乙[3,4,5]=[5,3] , dp甲[3,4,5]=[4]
这两种情况肯定是第二种更好,因为5+3-4>3+4-5,第二种情况乙能比甲多4个石子,明显好于第一种情况,因此当前选手(乙)的最优解是[5,3],即dp乙[3,4,5]=[5,3]
1)我们不妨改变一下上面的写法:
当石子只剩下[4,5]的时候,此时(甲)的最优解拿5,记为dp甲[4,5]=5,dp乙[4,5]=4;
当石子只剩下[3,4]的时候,此时(甲)的最优解拿4,记为dp甲[3,4]=4,dp乙[3,4]=3;
当石子只剩下[3,4,5]的时候,此时(乙)的最优解是拿5、3,记为dp乙[3,4,5]=8,dp甲[3,4,5]=4;
等式右边表示选手手中石子的个数。
2)我们可以再改变一下写法:
当石子只剩下[4,5]的时候,此时(甲)的最优解拿5,记为dp甲[2,3]=5,dp乙[2,3]=4;
当石子只剩下[3,4]的时候,此时(甲)的最优解拿4,记为dp甲[1,2]=4,dp乙[1,2]=3;
当石子只剩下[3,4,5]的时候,此时(乙)的最优解是拿5、3,记为dp乙[1,3]=8,dp甲[1,3]=4;
等式左边中括号里面是对应石子在数组中的索引,这样子,矩阵模型就出来了。
3)我们最后再优化一下这个矩阵:
当石子只剩下[4,5]的时候,dp[2,3]=1;
当石子只剩下[3,4]的时候,dp[1,2]=1;
当石子只剩下[3,4,5]的时候,dp[1,3]=4;
dp[i][j]表示场上剩下第i-j堆石子的时候,当前选手最多能比对手多出几个石子(最优解)。
dp[0][石子堆数-1] 即表示从第0堆石子一直到最后一堆石子的时候,甲比乙最多能多出多少个石子,即全盘最优解。
当i=j的时候:
dp[ i ] [ j ] = piles[ i ],(场上仅剩一堆石子的时候最优解就为这堆石子包含的石头的个数)
当i !=j的时候:
dp[ i ] [ j ] = Max (piles[ i ] - ( dp[ i+1 ] [ j ] ) , piles[ j ] - ( dp[ i ] [ j-1 ] ))
从两端选石子的时候,
假如选左端的石子堆,piles[i] - (dp[i+1][j]) 表示第i堆石子减去右边剩下石子堆的对手的最优解所对应的石子个数
假如选右端的石子堆,piles[j] - (dp[i][j-1] ) 表示第j堆石子减去左边剩下石子堆的对手的最优解所对应的石子个数
这样动态方程就出来了(我觉得这样思路也很清晰了)
代码:
class Solution {
public:
bool stoneGame(vector<int>& piles) {
// 如果是2个石子堆,那么先手的人必胜
if (piles.size() == 2) return true;
// 初始化dp矩阵
int dp[piles.size() + 1][piles.size() + 1];
// 初始化矩阵对角线,对应表示仅剩一个石子的时候,最优解就为对应的石子堆包含石子的个数
for (int i=0; i<piles.size(); ++i) {
dp[i][i] = piles[i];
}
// 从后往前遍历,求出每一个格子的值
for (int i=piles.size() - 1; i>=0; --i) {
for (int j=i+1; j<piles.size(); ++j) {
int leftMax = piles[i] - dp[i+1][j];
int rightMax = piles[j] - dp[i][j-1];
dp[i][j] = (leftMax > rightMax) ? leftMax : rightMax;
}
}
// 如果最右上角的那个位置的值 > 0 说明第一个拿的人(甲)必胜,返回true,否则返回false
if (dp[0][piles.size() - 1] > 0) {
return true;
}
else {
return false;
}
}
};
这道题还有另外一个比较玄学的解法:
两个石子堆,我可以讲在偶数位上的石子堆分成一组,奇数位的石子堆分成一组,因为石子总和为奇数,所以这两个组石子堆必定有比较多的那一组,然后我们就会发现(我也不知道怎么发现的,别人就是这么发现了),先手的人总能够精准拿到其中的一组,所以他只要拿比较多的那一组就行。就比如,先手的人必定能拿到[0,2,4,6,8.....],或者必定能拿到[1,3,5,7...]
因此先手的人必胜,这里直接return true即可
class Solution {
public:
bool stoneGame(vector<int>& piles) {
return true;
}
};