除数博弈
除数博弈(Divisor Game)是我在leetcode上遇到的一个题目,它的描述如下:
Alice and Bob take turns playing a game, with Alice starting first.
Initially, there is a number N on the chalkboard. On each player's turn, that player makes a move consisting of:
Choosing any x with 0 < x < N and N % x == 0.
Replacing the number N on the chalkboard with N - x.
Also, if a player cannot make a move, they lose the game.Return True if and only if Alice wins the game, assuming both players play optimally.
来源:力扣(LeetCode)
链接:单击这里
中文是:
爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。
最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:
选出任一 x,满足 0 < x < N 且 N % x == 0 。
用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。
给出的示例是:
输入:2 输出:true 解释:爱丽丝选择 1,鲍勃无法进行操作。
输入:3 输出:false 解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。
题目理解
做题的第一步肯定是要理解题意。
首先,游戏本身规则只有两条,是很简单易懂的,关键在于如何选择一个数x使自己能够获胜。
其中,题目给出了一个限定条件:假设两个玩家都以最佳状态参与游戏(play optimally)。这是一个很重要的条件,这告诉我们玩家在选择x时不是随机的(都选1 或者 选尽可能大的因数 等等),因为这么选似乎与自己的胜利没多大关系。他们是有针对性地选择一个有利于自己的数字。
从上述角度思考,可以大致得到解决题目的方向。
动态规划解法
如果假设给定任意一个整数N,我们要想取得胜利,必须找到一个数x,它既是N的因数,又使得N-x的结果是输的。
也就是说,我们只要找到了这个数x,就可以选择它,然后扔给对方N-x让他输掉。相反地,如果我们找不到,那么我们就是输家。
由于要用到以前的状态(即N-x是赢还是输),所以我们自然想到了动态规划。做法如下:
1. 动态规划的各状态为拿到该数会输还是赢,对应False和True。易知dp[1] = False。由此建立了边界条件。
2. 对于一个数N,可以先初始化其状态为False。要想求得它的真实状态,则要从1到N-1的循环中寻找它的因数( N%x==0 ? ),并判断N-x的状态是否为False,若是,这个数N的状态为True。若没有,则维持原状态False。
最后可以求得N的状态,通过判断N的状态是True还是False来给出胜负与否。
代码如下:
bool divisorGame(int N)
{
bool dp[N + 1]; // 多开一个好看一点
dp[1] = false;
for (int i = 2; i <= N; i++) // 建立状态数组
{
dp[i] = false;
for (int j = 1; j < i; j++)
{
if (i % j == 0 && dp[i - j] == false) //满足那两个条件才能获胜
{
dp[i] = true;
break;
}
}
}
return dp[N]; // 返回第N个状态
}
代码的时间复杂度较高,只能击败大概20-30%的提交者。那么是否还有一些潜在规律还没有被发现?是否能够进一步优化算法呢?答案是肯定的,要想击败100%,你只需要一行代码。
归纳法
归纳法的思路跟动态规划很像,也是从第1个状态往后推,一直推到第N个,唯一不同的是,它发现了两条潜在的定律:
- 任何奇数的因数都是奇数,不可能有偶数,因此奇数减去任意一个因数必然得到偶数。
- 任何偶数的因数可以是奇数也可以是偶数,但是我们只要取因数1,减去1就必然得到一个奇数。
这两条定律有什么用呢?
考虑到边界状态1是False,那么我拿到2,我就可以减1让你输,因此2是True。
再考虑3,根据规律一,你必然扔给给对方一个偶数,偶数目前只有2,对方拿到2是必然赢的,所以你会输。
再考虑4,因为3是肯定输的,那我就故意减1,让你输,所以4是True。
再考虑5,因为处理后会给对方偶数,偶数又是必胜的,所以5是会输的。
……
由此,我们就发现了这个游戏的奥秘,你拿到奇数,你必然会输(若对方知道规律),因为你处理后只能给对方偶数,如果对方知道奇数会输,偶数会赢,必然返你一个奇数(减1必然是奇数),你无法逃出规律一的魔咒,只能等着输。反之亦然。
因为题目中说,双方都会play optimally。那么代码就很简单了:
bool divisorGame(int N)
{
return N % 2 == 0; // 判断是否为偶数
}
不妨在生活中玩一下
当你知道归纳法的规律,你就能对一个不知道规律的对手操纵游戏的进行,让对方一直输下去……直到他也可能摸索出了规律哈哈。