原题链接: 464. 我能赢吗
很少做博弈论的题目,看了题解后很受启发,写了点总结。
题目大意
在 “100 game” 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和 达到或超过 100 的玩家,即为胜者。
如果我们将游戏规则改为 “玩家 不能 重复使用整数” 呢?
例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。
给定两个整数 maxChoosableInteger
(整数池中可选择的最大数)和 desiredTotal
(累计和),若先出手的玩家是否能稳赢则返回 true ,否则返回 false 。假设两位玩家游戏时都表现 最佳 。
示例:
输入:maxChoosableInteger = 10, desiredTotal = 11
输出:false
解释:
无论第一个玩家选择哪个整数,他都会失败。
第一个玩家可以选择从 1 到 10 的整数。
如果第一个玩家选择 1,那么第二个玩家只能选择从 2 到 10 的整数。
第二个玩家可以通过选择整数 10(那么累积和为 11 >= desiredTotal),从而取得胜利.
同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。
输入:maxChoosableInteger = 10, desiredTotal = 0
输出:true
输入:maxChoosableInteger = 10, desiredTotal = 1
输出:true
数据范围:
1 <= maxChoosableInteger <= 20
0 <= desiredTotal <= 300
思路
本题可用记忆化搜索来递归,每一次递归时的状态为当前已经选择的整数池,该整数池可用unordered_set来存储,但在递归过程中需要多次复制,复杂度较高。考虑到数据范围为1~20,而一个int占用32位,因此可以用一个整数x来表示状态,即当前已选整数池。在状态转移中,可借助位运算。
具体地,整数x的低20位可表示当前状态,从右往左为第0-19位,可以表示 2 20 2^{20} 220 个状态,可用f数组来存储各个状态是否必胜。初始时都不选,则x为0;当选择某个数i后,则x的右边第i-1位置为1。
对于该博弈问题,某人是否必胜取决于当前的棋局,也即x的大小,通过棋局下一步的状态是否必胜可得到当前棋局是否必胜:从未选的整数池中选择某个数i转移到下一状态,若在下一状态下能够必败,则当前棋局必胜;若遍历未选的整数池,任意下一状态下都必胜,则当前棋局必败。递归的边界条件为求和大于等于desiredTotal
:sum + i + 1 >= m。
其中,f表示某个x状态下是否必胜,初始为-1,必胜标记为1,必败标记为0.用来减小重复运算,例如先选1再选2,与先选2再选1,其状态一致,而某时刻能否必胜取决于棋局状态,因此查f,若已经计算过该状态下的胜负情况则可直接返回。
代码
class Solution {
public:
vector<int> f;
int n, m;
int dp(int x) {
// 该状态x已经搜索过,确认过能否必胜
if (f[x] != -1) return f[x];
int sum = 0; // 求和该状态下的总和sum
for (int i = 0; i < n; i ++ )
if (x >> i & 1)
sum += i + 1;
for (int i = 0; i < n; i ++ ) { // 遍历未选的整数池
if (x >> i & 1) continue;
if (sum + i + 1 >= m) return f[x] = 1; // 边界条件
// 博弈论中的状态转移,若存在某个下一状态必败,则当前状态必胜
if (!dp(x + (1 << i))) return f[x] = 1;
}
// 任意下一状态都必胜,则该状态必败
return f[x] = 0;
}
bool canIWin(int _n, int _m) {
n = _n, m = _m;
if (n * (n + 1) / 2 < m) return false; // 特判
f.resize(1 << n, -1); // 开2^20的数组来存储某棋局状态x下,是否必胜
return dp(0); // 先手面对的状态为0,即未选任何数,x为0
}
};
复杂度
时间复杂度:需要搜索
2
20
2^{20}
220种状态,每次都需要遍历未选的整数池,因此时间复杂度为
O
(
20
∗
2
20
)
O(20*2^{20})
O(20∗220)
空间复杂度:开了数组f来存储当前状态x的胜负情况,
O
(
2
20
)
O(2^{20})
O(220)