coverPiccoverPic

Leetcode 486 Predict the Winner 一些关于博弈的题目的笔记

思路

Leetcode 的 486 题:给定数组nums,玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0]nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1)。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。玩家都会使用最优的取法,玩家 1 分数 >= 玩家 2 的的时候,返回true,否则返回false,换句话说,求先手是否能必胜。

nums = [1,5,2]为例,记玩家 1 和 2 分数之差为 ansans,枚举一下:

graph TB
A("[1,5,2] ans = 0") -->|Player 1 select 1|B("[5,2] ans = 1")
A-->|Player 1 select 2|C("[1,5] ans = 2")
B-->|Player 2 select 5|D("[2] ans = -4")-->|Player 1 select 2|E("[] ans = -2")
B-->|Player 2 select 2|F("[5] ans = -1")-->|Player 1 select 5|G("[] ans = 4")
C-->|Player 2 select 1|J("[5] ans = 1")-->|Player 1 select 5|K("[] ans = 6")
C-->|Player 2 select 5|M("[1] ans = -3")-->|Player 1 select 1|N("[] ans = -2")

很明显,这种情况下,先手不能必胜,即 ans>=0ans >= 0

感觉解这种博弈的题目,需要关注的是怎么利用递归模拟玩家的选择

递归

两个玩家在下标 llrr 之间的数组进行选择。先手是否必胜,我们只需关心玩家 1、2 分数之差是否 >=0。

这里一开始我写的时候,递归会把累计的分数之差也作为入参,这就给记忆化搜索和推导出递推式造成了麻烦…

在看了题解之后得到了以下思路:假如玩家 1 选择了下标 ll,此时分数之差是 ll 位置的数字减去 l+1l + 1rr 之间玩家 2 能得到的最大分数 slsl。选择了 rr 同理,得分记为 srsr,因此这一步玩家 1 的得分为 max(sl,sr)\max(sl,sr)。这样子这个问题被拆分为多个子问题,这里使用 DFS 的方法进行求解,每一步返回的是当前玩家所能获取的最大分数。

因为是玩家 1 先手,所以 DFS 返回的是玩家 1 - 玩家 2 的分数。

ts
  1. function predictTheWinner(nums: number[]): boolean {
  2. const dfs = (l, r) => {
  3. if (l >= r) {
  4. return nums[l]
  5. } else {
  6. return Math.max(
  7. nums[l] - dfs(l + 1, r),
  8. nums[r] - dfs(l, r - 1)
  9. )
  10. }
  11. }
  12. return dfs(0, nums.length - 1) >= 0
  13. };

时间复杂度 O(2n)O(2^n),其中 nn 是数组的长度,空间复杂度 O(n)O(n)

很显然会有重复的计算,所以加上缓存:

ts
  1. function predictTheWinner(nums: number[]): boolean {
  2. const cache = [], len = nums.length
  3. const dfs = (l, r) => {
  4. const index = l * len + r
  5. if (cache[index] !== undefined) {
  6. return cache[index]
  7. }
  8. if (l >= r) {
  9. return cache[index] = nums[l]
  10. } else {
  11. return cache[index] = Math.max(
  12. nums[l] - dfs(l + 1, r),
  13. nums[r] - dfs(l, r - 1)
  14. )
  15. }
  16. }
  17. return dfs(0, nums.length - 1) >= 0
  18. };

时间复杂度 O(n2)O(n^2),空间复杂度 O(n2)O(n^2)

递推

把记忆化搜索转为递推式动态规划,利用二维数组记录当前索引分别为 llrr 时,当前玩家在这个区间获得的最大分数之差。

在这里画图好不方便,有空把外链导入的功能加上

nums = [1,5,2]为例,很显然,当区间长度为 1 时,能获得的分数之差就是对应的数字,有:

l\r 0 1 2
0 1 ? ?
1 - 5 ?
2 - - 2

有状态转移方程:

dp[i][j]=max(nums[i]dp[i+1][j],nums[j]dp[j1][i])dp[i][j] = \max(nums[i]-dp[i + 1][j],nums[j]-dp[j - 1][i])

这里我们选择从右下角开始迭代,结果为dp[0][nums.length - 1],也就是在整个nums数组上的最大分数差:

ts
  1. function predictTheWinner(nums: number[]): boolean {
  2. const dp = [], len = nums.length
  3. for (let i = 0; i < len; i++) {
  4. dp[i] = []
  5. dp[i][i] = nums[i]
  6. }
  7. for (let i = len - 2; i >= 0; i--) {
  8. for (let j = i + 1; j < len; j++) {
  9. dp[i][j] = Math.max(nums[j] - dp[i][j - 1], nums[i] - dp[i + 1][j])
  10. }
  11. }
  12. return dp[0][len - 1] >= 0
  13. };

时间复杂度 O(n2)O(n^2),空间复杂度 O(n2)O(n^2)

然后,可以发现,每次迭代都只依赖dp[i + 1][j]dp[j - 1][i]这两个 dp 中的元素,可以把二维的dp优化掉:

其实这里每一轮,操作列的时候,只依赖每一列的(从上到下数)第一个已知元素,计算出新的元素后,覆盖掉dp数组中原来的元素就好了。

ts
  1. function predictTheWinner(nums: number[]): boolean {
  2. const dp = [...nums], len = nums.length
  3. for (let i = len - 1; i >= 0; i--) {
  4. for (let j = i + 1; j < len; j++) {
  5. dp[j] = Math.max(nums[j] - dp[j - 1], nums[i] - dp[j])
  6. }
  7. }
  8. return dp[len - 1] >= 0
  9. };

时间复杂度 O(n2)O(n^2),空间复杂度 O(n)O(n)

0 条评论未登录用户
Ctrl or + Enter 评论
© 2023-2025 LittleRangiferTarandus, All rights reserved.
🌸 Run