目录
- [1 技巧](#1 技巧)
- [2 例题](#2 例题)
-
- [2.1 Nim 游戏](#2.1 Nim 游戏)
- [2.2 石子游戏](#2.2 石子游戏)
- [2.3 灯泡开关](#2.3 灯泡开关)
1 技巧
稍加思考,找到规律
2 例题
2.1 Nim 游戏
你和你的朋友,两个人一起玩 Nim 游戏:
桌子上有一堆石头。
你们轮流进行自己的回合, 你作为先手 。
每一回合,轮到的人拿掉 1 - 3 块石头。
拿掉最后一块石头的人就是获胜者。
假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 n 的情况下赢得游戏。如果可以赢,返回 true;否则,返回 false 。
示例 1:
输入:n = 4
输出:false
解释:以下是可能的结果:
- 移除1颗石头。你的朋友移走了3块石头,包括最后一块。你的朋友赢了。
- 移除2个石子。你的朋友移走2块石头,包括最后一块。你的朋友赢了。
3.你移走3颗石子。你的朋友移走了最后一块石头。你的朋友赢了。
在所有结果中,你的朋友是赢家。
示例 2:
输入:n = 1
输出:true
示例 3:
输入:n = 2
输出:true
思路以及代码:
我们解决这种问题的思路一般都是反着思考:
如果我能赢,那么最后轮到我取石子的时候必须要剩下 1~3 颗石子,这样我才能一把拿完。
如何营造这样的一个局面呢?显然,如果对手拿的时候只剩 4 颗石子,那么无论他怎么拿,总会剩下 1~3 颗石子,我就能赢。
如何逼迫对手面对 4 颗石子呢?要想办法,让我选择的时候还有 5~7 颗石子,这样的话我就有把握让对方不得不面对 4 颗石子。
如何营造 5~7 颗石子的局面呢?让对手面对 8 颗石子,无论他怎么拿,都会给我剩下 5~7 颗,我就能赢。
这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输。所以这道题的解法非常简单:
java
class Solution {
boolean canWinNim(int n) {
// 如果上来就踩到 4 的倍数,那就认输吧
// 否则,可以把对方控制在 4 的倍数,必胜
return n % 4 != 0;
}
}
2.2 石子游戏
Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行;每堆都有 正 整数颗石子,数目为 piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的 总数 是 奇数 ,所以没有平局。
Alice 和 Bob 轮流进行,Alice 先开始 。 每回合,玩家从行的 开始 或 结束 处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中 石子最多 的玩家 获胜 。
假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true ,当 Bob 赢得比赛时返回 false 。
示例 1:
输入:piles = [5,3,4,5]
输出:true
解释:
Alice 先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果 Bob 拿走前 3 颗,那么剩下的是 [4,5],Alice 拿走后 5 颗赢得 10 分。
如果 Bob 拿走后 5 颗,那么剩下的是 [3,4],Alice 拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对 Alice 来说是一个胜利的举动,所以返回 true 。
示例 2:
输入:piles = [3,7,2,3]
输出:true
思路以及代码:
注意,石头的堆的数量为偶数,所以你们两人拿走的堆数一定是相同的。石头的总数为奇数,也就是你们最后不可能拥有相同多的石头,一定有胜负之分。
而且注意,并不是简单的挑数字大的选
这道题又涉及到两人的博弈,也可以用动态规划算法暴力试,比较麻烦。但我们只要对规则深入思考,就会大惊失色:只要你足够聪明,你是必胜无疑的,因为你是先手。
java
class Solution {
public boolean stoneGame(int[] piles) {
return true;
}
}
动态规划解法:
dp[i][j] 表示当剩下的石子堆为下标 i到下标 j时,即在下标范围 [i,j] 中,在双方都做最好选择的情况下,先手与后手的最大得分差值为多少。
那么 f[1][n]为考虑所有石子,先手与后手的得分差值:
f[1][n]>0,则先手必胜,返回 True
f[1][n]<0,则先手必败,返回 False
不失一般性的考虑 f[l][r] 如何转移,根据题意,只能从两端取石子(令 piles 下标从 1 开始),共两种情况:
1.Alice从左端取,那么取到的价值为piles[l-1],这时,Alice变为了后手,Bob为先手,从f[l-1][r]之间做决策,那么基于这种情况,双方差值为:piles[l−1]−f[l+1][r]
2.Alice从右端取,同理,双方的差值为:piles[r−1]−f[l][r−1]
好,在双方都做出最优决策的情况下,f[l][r]为上述两种情况中的最大值。
因此,我们可以的到状态转移方程为
dp[i][j]=max(piles[i]−dp[i+1][j],piles[j]−dp[i][j−1])
对于区间 dp 来说,将 i 从 n−1 往前遍历到 0,而 j从 i 位置往后遍历到 n−1,这样能够方便 i<j ,将大区间划分成小区间。从小区间开始判断,不断的扩大我们的判断范围看会不会赢
java
class Solution {
public boolean stoneGame(int[] piles) {
int n = piles.length;
if (n == 0) return false;
int[][] dp = new int[n][n];
for (int i=0;i<n;i++) {
dp[i][i] = piles[i];
}
for (int i=n-1;i>=0;i--) {
for (int j=i+1;j<n;j++) {
dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
}
}
return dp[0][n - 1] >= 0;
}
}
2.3 灯泡开关
初始时有 n 个灯泡处于关闭状态。第一轮,你将会打开所有灯泡。接下来的第二轮,你将会每两个灯泡关闭第二个。
第三轮,你每三个灯泡就切换第三个灯泡的开关(即,打开变关闭,关闭变打开)。第 i 轮,你每 i 个灯泡就切换第 i 个灯泡的开关。直到第 n 轮,你只需要切换最后一个灯泡的开关。
找出并返回 n 轮后有多少个亮着的灯泡。
示例1:
输入:n = 3
输出:1
解释:
初始时, 灯泡状态 [关闭, 关闭, 关闭].
第一轮后, 灯泡状态 [开启, 开启, 开启].
第二轮后, 灯泡状态 [开启, 关闭, 开启].
第三轮后, 灯泡状态 [开启, 关闭, 关闭].
你应该返回 1,因为只有一个灯泡还亮着。
示例 2:
输入:n = 0
输出:0
示例 3:
输入:n = 1
输出:1
题目解读:
第 1 轮操作是把每一盏电灯的开关按一下(全部打开)。
第 2 轮操作是把每两盏灯的开关按一下(就是按第 2,4,6... 盏灯的开关,它们被关闭)。
第 3 轮操作是把每三盏灯的开关按一下(就是按第 3,6,9... 盏灯的开关,有的被关闭,比如 3,有的被打开,比如 6)...
如此往复,直到第 n 轮,即只按一下第 n 盏灯的开关。
代码以及思路:
首先,因为电灯一开始都是关闭的,所以某一盏灯最后如果是点亮的,必然要被按奇数次开关。
我们假设只有 6 盏灯,而且我们只看第 6 盏灯。需要进行 6 轮操作对吧,请问对于第 6 盏灯,会被按下几次开关呢?这不难得出,第 1 轮会被按,第 2 轮,第 3 轮,第 6 轮都会被按。
为什么第 1、2、3、6 轮会被按呢?因为 6=16=23
一般情况下,因子都是成对出现的,也就是说开关被按的次数一般是偶数次。
但是有特殊情况,比如说总共有 16 盏灯,那么第 16 盏灯会被按几次?
16 = 116 = 28 = 4*4
其中因子 4 重复出现,所以第 16 盏灯会被按 5 次,奇数次。现在你应该理解这个问题为什么和平方根有关了吧?
所以这道的base case就是找 1-n之间有多少个平方数
因此代码如下:
java
int bulbSwitch(int n) {
return (int)Math.sqrt(n);
}