Leetcode464. 我能赢吗
博弈问题
递归+备忘录+压缩状态
题目
在 “100 game” 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和达到 100 的玩家,即为胜者。
如果我们将游戏规则改为 “玩家不能重复使用整数” 呢?
例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。
给定一个整数 maxChoosableInteger (整数池中可选择的最大数)和另一个整数 desiredTotal(累计和),判断先出手的玩家是否能稳赢(假设两位玩家游戏时都表现最佳)?
你可以假设 maxChoosableInteger 不会大于 20, desiredTotal 不会大于 300。
示例
输入:
maxChoosableInteger = 10
desiredTotal = 11
输出:
false
解释:
无论第一个玩家选择哪个整数,他都会失败。
第一个玩家可以选择从 1 到 10 的整数。
如果第一个玩家选择 1,那么第二个玩家只能选择从 2 到 10 的整数。
第二个玩家可以通过选择整数 10(那么累积和为 11 >= desiredTotal),从而取得胜利.
同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。
法1:递归法
- 这是一道博弈类问题:“稳赢”指的是,假设有两个人A, B,A先抽整数然后轮到B,再轮到A…,每次都根据要凑的和选择最有利于自己赢的数。
首先要理解什么是稳赢:在AB都各自执行最优策略达到某个状态时,A存在一种走法,在走完后,无论B怎么走,A都能取胜,因此我们需要不断地递归,遍历A走一步后的各种情况,从而判断A能否获胜。 - 传统的做法可能想标记A和B,即表示当前状态下,该轮到谁进行操作。但实际上并不需要进行标记,其实这就是这类问题的关键: 因为是两个人在博弈, 所以从当前状态转移到下一个状态时, 就体现了动作执行这的变化。如说当前状态是A, 因为是两个人在玩, 下一个状态就是B. 这里很关键, 希望大家好好思考一下。
- 递归函数dfs的含义是,某人(与AB无关)先手操作到达当前状态,能否稳赢。
class Solution {
boolean[] visited; //用来标记某个数是否被使用过
int maxChoosableInteger, desiredTotal;
// 当前这个状态下, 能否稳赢.
bool dfs(int total_sum) {
// 递归的边界
if (total_sum >= desiredTotal) return true; //这种情况是稳赢的
//递归遍历各种情况
for (int i = 1; i <= maxChoosableInteger; ++i) {
if (visited[i]) continue;
vis[i] = true;
if (dfs(total_sum + i)) {
visited[i] = false;
return false; // 如果在当前状况下,对手通过取i能够获得胜利,说明自己无法稳赢,返回false
}
visited[i] = false; //当前这轮循环完成,换源i标记
}
//当所有情况遍历完,dfs都是false的时候,可以退出循环,表示对手无论取哪个数都无法胜利,这说明自己可以稳赢,返回true。
return true;
}
bool canIWin(int maxChoosableInteger_, int desiredTotal_) {
visited = new int[maxChoosableInteger+1];
maxChoosableInteger = maxChoosableInteger_;
desiredTotal = desiredTotal_;
for (int i = 1; i <= maxChoosableInteger; ++i) {
visited[i] = true; // A先手,从第i个数开始
if (dfs(i)) return true; //表示判断在A先手达到状态i的情况下,能否稳赢。
}
return false;
}
};
法二:状态压缩+递归+记忆化(动态规划)
在递归法中,进行了大量的重复计算,比如[a选3,b选2]、[a选2,b选3],因此我们需要对状态进行记忆,这里就用到了状态压缩。
状态压缩指的是用位来表示所有的情况,在该题目中,如果maxChooseableInteger = 4,那么我们可以使用“0000”四位表示状态,“0100”表示3已被选,“0101”表示3和1已被选。
class Solution {
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
if(maxChoosableInteger > 20 || maxChoosableInteger < 0 || desiredTotal > 300 || desiredTotal <0){
return false;
}
if((1+maxChoosableInteger)*maxChoosableInteger/2 < desiredTotal){
return false;
}
Boolean[] dp = new Boolean[1<<maxChoosableInteger]; //要用Boolean初始化,这样每个元素才能为null
return backtrack(maxChoosableInteger, desiredTotal, dp, 0);
}
// 表示从状态state开始,先手能否获胜。
public boolean backtrack(int maxChoosableInteger, int desiredTotal, Boolean[] dp, int state){
if(dp[state] != null){ //无需重复计算
return dp[state];
}
for(int i = 1; i <= maxChoosableInteger; i++){
if((state & (1 << (i-1))) == 0){ //若第i个数未被使用过,查看选i能否获胜。
if(desiredTotal - i <= 0 || backtrack(maxChoosableInteger, desiredTotal-i, dp, state | (1<<(i-1))) == false){ //当选i大于目标值,或者接下来对手无法获胜时,该状态先手能够稳赢。
dp[state] = true;
return true;
}
}
}
//否则,不能稳赢
dp[state] = false;
return false;
}
}