题目描述
小F和朋友在一个 的棋盘上进行游戏,棋盘的初始状态是有一颗棋子放置在左上角 (1,1) 的位置。小F每次可以向右或向上移动棋子,移动的步数必须为奇数单位。当棋子无法继续移动到棋盘内的有效区域时,该玩家输掉游戏。小F先手出发,问你是否小F可以必胜。
例如:当棋盘的大小为 1×4 时,小F第一次可以向右移动 3 个单位,之后对手无法继续操作,小F获胜。如果胜利,那么输出 Yes,否则输出 No。
题目说的有点问题,这里应该是向右或者向下。
测试样例
样例1:
输入:
n = 1 ,m = 1
输出:'No'
样例2:
输入:
n = 1 ,m = 4
输出:'Yes'
样例3:
输入:
n = 4 ,m = 1
输出:'Yes'
样例4:
输入:
n = 4 ,m = 4
输出:'No'
题目分析:
这题很明显是一道类似博弈论的题目,突破点在于每次移动的步数必须是奇数,其实这里就是在引导我们奇偶性这一方向去思考,而事实的确如此,是否必胜就是由棋盘长宽的奇偶性决定的。小F是否能赢则取决于到达终点的最后一步是不是他走的,是则胜,不是则败。
解法1:模拟
我们可以先考虑比较简单的边界情况:
- 当棋盘大小为 1×1 时,小F无法移动,必败。
- 当棋盘大小为 1×n 时,小F是否必胜则取决于n的奇偶性,n为奇数则小F可以一步走到位,必胜;n为偶数则小F必败,因为不管怎么走,小F都不可能一步走到终点,但是小F走完后对手可以一步走完剩下的路程(奇+奇=偶)。
- 当棋盘大小为 m×1 时,情况跟2一样,只是棋子移动的方向换了一下。
下面我们讨论更加一般的情况,对于大小为 mxn 的棋盘,m和n的奇偶性搭配一共四种情况:
- m为奇,n为奇
- m为奇,n为偶
- m为偶,n为奇
- m为偶,n为偶
我们将棋子移动过程拆分成行移动和列移动两部分考虑,这里我们统一先走行再走列:
-
m为奇,n为奇时,
走完前面m行的过程必然是 小F -> 对手 -> .... -> 小F,这是由奇数性质决定的,一个奇数只能拆分成奇数个奇数的和,所以小F先手就意味着到达第m行的最后一步(当然最后一步也可以是第一步,也就是小F一步走到头)必然是由他走。但是此时剩下的n列刚好是奇数步,对手可以直接走完,所以小F必败。 -
m为奇,n为偶时,小F只需要第一步走完m行,剩下的n列随便走都能赢。因为对手不可能一步走完n列(步数限定为奇数步),并且对手走完后剩下的必然是奇数步(偶=奇+奇),小F必然可以一步到位,小F必胜。
-
m为偶,n为奇时,分析同情况1,
走完前面m行的过程必然是 小F -> 对手 -> .... -> 对手,因为一个偶数只能拆分成偶数个奇数的和,所以小F先手就意味着到达第m行的最后一步肯定是对手走的,而剩下的n列刚好是奇数步,小F可以直接走完,必胜。 -
m为偶,n为偶时,分析类似情况2和情况3,只是把两个情况的偶数走法拼到了一起。行列均为偶数步就意味着走完m行和n列的最后一步都是由对手走出的,因此小F必败。
综合上面的分析,我们的代码结构就很明显了:
def solution(n: int, m: int) -> str:
# 奇+偶必胜,偶+奇必胜,其余必败
if n % 2 == 0 and m % 2 == 0 or n % 2 != 0 and m % 2 != 0:
return "No"
return "Yes"
解法2:DP
这题除了模拟之外还可以用DP来解决,我们定义一个大小为 nxm 的 dp 数组,dp[i][j] 代表从棋盘起点到(i, j)小F是否必胜,dp[i][j]=True 代表必胜。因此我们求解的目标就变成了 dp[n][m]
关键是怎么确定状态转移方程?
我们还是考虑每一步的移动是怎么来的:对于点(i, j),它只有可能从水平和竖直两个方向移动过来,并且只能是通过奇数步移动过来。假设(i, j)可以通过(i - step, j)这个点移动得到[step%2 !=0],那么 dp[i][j] 的结果就取决于 dp[i - step][j],如果 dp[i - step][j]=False ,就说明从起点到(i - step, j)小F必败,这也意味着到终点的最后一步是对手走的。但是把终点延伸到(i, j)后,最后一步就变成小F来走了,因此小F就变成了必胜的局面。上面分析的是从竖直方向转移的结果,从水平方向转移的分析也是一样的。因此,我们状态转移方程的伪代码如下:
dp[i][j] = True if dp[i-step][j] or dp[i][j-step] = Fasle ( step%2 != 0 )
完整代码如下:
def solution(n: int, m: int) -> str:
# 初始化dp数组
dp = [[False] * (m + 1) for _ in range(n + 1)]
# 边界情况
if n == 1 and m == 1:
return "No"
# 动态规划填充dp数组
for i in range(1, n + 1):
for j in range(1, m + 1):
# 如果当前位置是起点,直接标记为False
if i == 1 and j == 1:
dp[i][j] = False
else:
# 如果 dp[i-step][j] 或者 dp[i][j-step] = Fasle,说明在 (i-step, j) 或者
# (i, j-step) 处小F必败,但是只要小F可以多走一步他就赢了,因此 dp[i][j] = True
for step in range(1, i + 1, 2):
if (i - step > 0 and not dp[i - step][j]):
dp[i][j] = True
break # 我们只需要一条必胜路径即可
for step in range(1, j + 1, 2):
if (j - step > 0 and not dp[i][j - step]):
dp[i][j] = True
break
# 返回结果
return "Yes" if dp[n][m] else "No"