题目地址:
https://www.lintcode.com/problem/can-i-win/description
有一个游戏,两个人轮流从一个数字池里取数,数字池里一开始的数字是 1 ∼ m 1\sim m 1∼m,每个数只能被取一次。另给定一个数 d d d,如果某人取完数之后,两个人取的数的总和达到或超过了 d d d,则最后取数的那个人就赢了。问先手是否有必胜策略。
思路是记忆化搜索。这里可以考虑状态压缩,设 s s s从左向右的第 k k k个二进制位是 1 1 1的话表示 k k k这个数还没被取,否则表示这个数被取了。设 f [ s ] f[s] f[s]表示当前先手面对的局面是 s s s的情况下,先手是否必胜。那么,当他面对的局面里,已经取的数总和已经达到了 d d d的话,则必输(意味着对方上次取完数之后他就已经赢了)。否则可以枚举先手取哪些数,则有: f [ s ] = ⋁ i = 2 k ∧ ( s > > k & 1 = 0 ) ¬ f [ s + i ] f[s]=\bigvee_{i=2^k \land (s>>k\&1=0)} \neg f[s+i] f[s]=i=2k∧(s>>k&1=0)⋁¬f[s+i]这里的意思是只要先手取某个数就能导致对方必输,那么他就必赢。代码如下:
import java.util.Arrays;
public class Solution {
/**
* @param maxChoosableInteger: a Integer
* @param desiredTotal: a Integer
* @return: if the first player to move can force a win
*/
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
// Write your code here
// 特判两个必赢和必输的局面
if (maxChoosableInteger >= desiredTotal) {
return true;
}
if ((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal) {
return false;
}
// dp[s]表示先手面对的局面是s的情况下是否必胜,如果是则取1,否则取0
int[] dp = new int[1 << maxChoosableInteger];
// 一开始赋值为-1表示还没求出来
Arrays.fill(dp, -1);
return dfs(dp, 0, 0, maxChoosableInteger, desiredTotal);
}
// 返回先手面对的局面是state,已经取的数总和是curSum的情况下,他是否必胜
private boolean dfs(int[] dp, int state, int curSum, int maxInt, int desiredTotal) {
// 如果有记忆了则调取记忆
if (dp[state] != -1) {
return dp[state] == 1;
}
// 如果已经取的数超过了desiredTotal,那先手已经输了,返回false
if (curSum >= desiredTotal) {
dp[state] = 0;
return false;
}
// 开始枚举先手取哪个数
for (int i = 1; i <= maxInt; i++) {
// 如果枚举到已经取了的数则跳过
if (((state >> i - 1) & 1) != 0) {
continue;
}
// 枚举取该数。如果先手取了i,会导致后手(这个时候后手变成了先手)
// 面对state | (1 << i - 1)这个局面的时候必输,那么先手就赢了
if (!dfs(dp, state | (1 << i - 1), curSum + i, maxInt, desiredTotal)) {
return true;
}
}
// 如果枚举完所有情况先手仍然没法赢,则先手必输,返回之前记忆一下
dp[state] = 0;
return false;
}
}
时空复杂度 O ( 2 m ) O(2^m) O(2m)。