292 Nim游戏
题目描述: 你和你的朋友,两个人一起玩 Nim 游戏:桌子上有一堆石头,每次你们轮流拿掉 1 - 3 块石头。 拿掉最后一块石头的人就是获胜者。你作为先手。
你们是聪明人,每一步都是最优解。 编写一个函数,来判断你是否可以在给定石头数量的情况下赢得游戏。
思路1: dfs方法 (超时)。 dfs由底向上,博弈,三种情况中任一种使当前人失败,则另一人采用最优策略可以在其上一步成功。
思路2: 动态规划 (超时)。 思路同思路一相同,可以用DP方法实现。 状态转移方程: dp[i] = (!dp[i - 1]) || (!dp[i - 2]) || (!dp[i - 3]);
思路3: 找规律 (通过)。 可以尝试从1开始枚举归纳来寻找题解。1,2,3,4中只有4是输的;然后在5,6,7,8时对手可以通过最优解得到4,可推知8的情况下同样失败;由此可推知,所有4的倍数都会导致先手玩家失败。
class Solution {
public:
// dfs 思路一
bool canWinNim(int n) {
if(n < 0)
return false;
if(n <= 3)
return true;
bool ans1 = canWinNim(n - 1);
bool ans2 = canWinNim(n - 2);
bool ans3 = canWinNim(n - 3);
return !ans1 || !ans2 || !ans3;
}
};
class Solution {
public:
// 动态规划 博弈 三种情况中任一种使当前人失败,则另一人采用最优策略可以在其上一步成功。
// 思路二
bool canWinNim(int n) {
vector<bool> dp(n, false);
dp[0] = true, dp[1] = true, dp[2] = true;
for(int i = 3; i < n; i++)
dp[i] = (!dp[i - 1]) || (!dp[i - 2]) || (!dp[i - 3]);
return dp[n -1];
}
};
class Solution {
public:
// 归纳分析 4的倍数
// 思路三
bool canWinNim(int n) {
if(n % 4 == 0)
return false;
return true;
}
};
486 预测赢家
题目描述: 给定一个表示分数的非负整数数组。 玩家1从数组任意一端拿取一个分数,随后玩家2继续从剩余数组任意一端拿取分数,然后玩家1拿,……。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。
给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。
思路: 本题是典型的解决博弈问题的动态规划。 即“假设两个人都足够聪明,最后谁会获得胜利。” 基本思路见leetcode 题解。所有类似的俩海盗分宝石,俩人拿硬币的问题,都可以用该方法解答。
class Solution {
public:
bool PredictTheWinner(vector<int>& nums) {
int n = nums.size();
vector<vector<vector<int>>> dp(n, vector<vector<int>>(n, vector<int>(2, 0)));
for(int i = 0; i < n; i++)
dp[i][i][0] = nums[i];
for(int len = 2; len <= n; len++)
for(int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
int left = dp[i + 1][j][1] + nums[i];
int right = dp[i][j - 1][1] + nums[j];
if(left > right) {
dp[i][j][0] = left;
dp[i][j][1] = dp[i + 1][j][0];
}
else {
dp[i][j][0] = right;
dp[i][j][1] = dp[i][j - 1][0];
}
}
if(dp[0][n - 1][0] >= dp[0][n - 1][1])
return true;
return false;
}
};
877 石子游戏
题目描述: 亚历克斯和李用几堆石子在做游戏。偶数堆 石子排成一行,每堆都有正整数颗石子 piles[i] 。游戏以谁手中的石子最多来决出胜负。石子的总数是奇数 ,所以没有平局。亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。
思路1: 同上一题(486题)。用区间动态规划解决博弈问题。
思路2: 一种“取巧”的解法:将石堆排序,奇数编号的石堆拥有的石子总数和偶数编号石堆拥有的石子总数相比,总有一个大于另外一个。先手玩家可以选择永远拿奇数编号石堆或者偶数编号石堆(由两堆->四堆->八堆…)。结合上面的结果可知先手玩家必胜,因此次此道题目可以直接 return true.
class Solution {
public:
// 博弈 区间动态规划
bool stoneGame(vector<int>& piles) {
int n = piles.size();
vector<vector<vector<int>>> dp(n, vector<vector<int>>(n, vector<int>(2, 0)));
for(int i = 0; i < n; i++)
dp[i][i][0] = piles[i];
// 已知dp数组中对角线的值,根据区间长度,斜着遍历
for(int len = 2; len <= n; len++) {
for(int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
int left = dp[i + 1][j][1] + piles[i]; // 先手取左边那堆
int right = dp[i][j - 1][1] + piles[j]; // 先手取右边那堆
if(left > right) {
dp[i][j][0] = left; // 取较大的
dp[i][j][1] = dp[i + 1][j][0]; // 在ij区间内,先手已经决定了取左边还是右边,即后手等于剩下区间的先手
}
else {
dp[i][j][0] = right;
dp[i][j][1] = dp[i][j - 1][0];
}
}
}
if(dp[0][n - 1][0] > dp[0][n - 1][1])
return true;
return false;
}
};