[数学] 292. Nim 游戏(递归 → 动态规划 → 数学规律)
292. Nim 游戏
题目链接:https://leetcode-cn.com/problems/nim-game/
分类:
- 数学(根据新概念“Nim游戏”总结数学规律:必胜态、必输态)
题目分析
本题提出了一个新概念“Nim游戏”,我们可以将其抽象成数学问题,寻找其中的数学规律。这里参考下面的题解,从状态转移的角度来分析数学规律,从而得出几个解题思路:
- 思路1:问题分析 + 递归实现;
- 思路2:动态规划,是对思路1的优化,使用辅助数组记录计算过的结果;空间优化:将dp[n+1]优化为滚动数组dp[4];
- 思路3:数学规律,基于思路1的分析或举例归纳可以发现,石堆数量 n % 4 == 0,则先手必败,n % 4 != 0,则先手必胜。
参考题解:
.
.
思路1:问题分析(关键) + 递归实现
参考“原理讲解”,可以这样理解本题的规律:
我们可以把两个人从一堆石头里轮流拿石头的过程看做是状态传递的过程,每一时刻剩余的石头个数就是一个状态,状态之间的传递就是两个人轮流拿石头。
例如:初始时石碓个数为20,a,b两人轮流拿1-3块石头,且a是先手,则初始状态是20,属于a的状态,下一个状态就是a拿掉石头后剩余的石头个数,a可以拿1-3块石头,所以下一个状态有19,18,17,都是b的状态,然后每个状态又有3个后继状态都是a的状态,以此类推。
设置两个状态:必输态和必胜态,必胜态例如剩下1-3块石头,这个人无论怎么拿都一定是胜利的,必输态例如剩下4块石头,这个人无论怎么拿,最后输的人一定是他自己。
所有状态要么是必胜态要么是必输态,题目说双方的每一步都是最优解,也就是说双方都会尽可能让对方处于必输态,所以:
- 如果一个状态的下一个状态存在必输态,那么当前状态就是必胜态,因为每一步都是最优解,所以处于当前状态的人一定会选择拿掉会让对方处于必输态的石头数量,对方处于必输态,相应的己方就处于必胜态。
- 如果一个状态的下一个状态都是必胜态,那么处于当前状态的人无论拿掉多少石头,对方都一定处于必胜态,相应的己方就处于必输态。
如图所示:(图片来源)
可以看出上述过程就是一个递归的过程:
- 递归函数helper()的功能:对于一堆数量为n的石头,当前玩家所处的状态是必输(返回false)或必胜状态(返回true)
- 递归出口:n <= 3,return true;
- 递归主体:当前状态的下一个状态有:helper(n-1),helper(n-2),helper(n-3):
- 这些状态都为true,则返回false;
- 这些状态存在false,就返回true;
实现代码:
class Solution {
public boolean canWinNim(int n) {
if(n <= 3) return true;
else{
if(canWinNim(n-1) && canWinNim(n-2) && canWinNim(n-3)) return false;
else return true;
}
}
}
- 存在的问题:效率低,超时。
思路2:动态规划
基于思路1的分析,我们可以使用辅助数组记录计算过的状态,用动态规划减少递归的重复计算。
dp数组:
- dp[1] = true,dp[2]=true,dp[3]=true,
- dp[i] = (!dp[i-1]) || (!dp[i-2]) || (!dp[i-3])
实现代码:
class Solution {
public boolean canWinNim(int n) {
if(n < 4) return true;
boolean[] dp = new boolean[n + 1];
dp[1] = true;
dp[2] = true;
dp[3] = true;
for(int i = 4; i <= n; i++){
dp[i] = !dp[i-1] || !dp[i-2] || !dp[i-3];
}
return dp[n];
}
}
- 存在的问题:
用例:1348820612,超出内存限制。
空间优化:使用滚动数组
使用固定大小的dp数组作为滚动数组,将空间复杂度降低为O(1):
class Solution {
public boolean canWinNim(int n) {
if(n < 4) return true;
boolean[] dp = new boolean[4];
dp[1] = true;
dp[2] = true;
dp[3] = true;
for(int i = 4; i <= n; i++){
dp[i % 4] = !dp[(i-1) % 4] || !dp[(i-2) % 4] || !dp[(i-3) % 4];
}
return dp[n % 4];
}
}
- 存在的问题:由超出内存限制变为超过时间限制,说明题目设定好了只能使用数学方法求解,见思路3。
思路3:数学规律
基于思路1的分析,对于状态i(i表示剩余的石头数量),如果它是必败态,则它的上一个状态(拿走石头之前):i+1,i+2,i+3都是必胜态,这些必胜态都是i+4的下一个状态,所以i+4就是必败态,以此类推,必败态对应的石头数量是4的倍数。
所以对于石头数量n,直接判断它是不是4的倍数即可。
return n % 4 != 0;
推广到更一般的情况,石头数量为n,每次可取的石头数量为[l,r],则:
- 如果 n % (l+r) == 0,先手必败,
- 如果 n % (l+r) != 0,先手必胜。
上面的思路1、思路2如果推广到一般情况也是相同的处理流程。
实现代码:
class Solution {
public boolean canWinNim(int n) {
return (n % 4) != 0;
}
}