博弈型动态规划模板——精髓:把两个选手当成一个人,每次面对a[i…j]选最优解,用dfs+cache做最直观,再考虑修改为dp数组

算法笔记D4-博弈型动态规划

D4 博弈型动态规划

Coins in a Line

Problems:

有 n 个硬币排成一条线。两个参赛者轮流从右边依次拿走 1 或 2 个硬币,直到没有硬币为止。拿到最后一枚硬币的人获胜。

请判定 先手玩家 必胜还是必败?

若必胜, 返回 true, 否则返回 false.

样例 :

plain
 
1
2
3
4
5
输入: 4
输出: true
解释:
先手玩家第一轮拿走一个硬币, 此时还剩三个.
这时无论后手玩家拿一个还是两个, 下一次先手玩家都可以把剩下的硬币拿完.

确定状态:

博弈型动态规划通常从第一步 分析,而不是最后一步(因为局面越来越简单,硬币数越来越少)

知识点:

  1. 如果取1个或2个硬币后,能让剩下的局面先手必败,则当前先手必胜
  2. 如果不管怎么走,剩下的局面都是先手必胜,则当前先手必败
  3. 总之:
    • 必胜: 在当下的局面走出一步,让对手无路可逃
    • 必败: 自己无路可逃
java   ==》下面这个代码修改为动态规划是再简单不过的了!!!所以在思考博弈类问题时候,优先使用DFS。
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean firstWillWin(int n) {
if (n == 0)
return false;
if (n == 1)
return true;
boolean[] f = new boolean[n + 1];
int i;
f[0] = false;
f[1] = true;
for (i = 2; i <= n; i++) {
f[i] = (!f[i - 1]) || (!f[i - 2]);
}
/*可以证明, 当硬币数目是3的倍数的时候, 先手玩家必败, 否则他必胜.
当硬币数目是3的倍数时, 每一轮先手者拿a个, 后手者拿3-a个即可, 后手必胜.
若不是3的倍数, 先手者可以拿1或2个, 此时剩余硬币个数就变成了3的倍数
*/
return f[n];
}

dfs+cache做:

class Solution:
    """
    @param n: An integer
    @return: A boolean which equals to true if the first player will win
    """
    def first_will_win(self, n: int) -> bool:
        # write your code here
        self.cache = {0: False, 1: True}

        def dfs(n):
            if n in self.cache:
                return self.cache[n]                     
            
            ans = not dfs(n-1) or not dfs(n-2)
            self.cache[n] = ans
            return ans
        
        return dfs(n)

 

修改为dp:

class Solution:
    """
    @param n: An integer
    @return: A boolean which equals to true if the first player will win
    """
    def first_will_win(self, n: int) -> bool:
        # write your code here
        if n == 0:
            return False
        if n <= 2:
            return True

        dp = [False] * (n+1)
        dp[1] = True
        dp[2] = True

        for i in range(3, n+1):
            dp[n] = not dp[n-1] or not dp[n-2]
        
        return dp[n]

  

优化存储:

class Solution:
    """
    @param n: An integer
    @return: A boolean which equals to true if the first player will win
    """
    def first_will_win(self, n: int) -> bool:
        # write your code here
        if n == 0:
            return False
        if n <= 2:
            return True
        
        dp_1 = True
        dp_2 = True

        for i in range(3, n+1):
            dp = not dp_1 or not dp_2
            dp_1 = dp_2
            dp_2 = dp
        
        return dp

  

486. 预测赢家

给你一个整数数组 nums 。玩家 1 和玩家 2 基于这个数组设计了一个游戏。

玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0] 或 nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1 )。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。

如果玩家 1 能成为赢家,返回 true 。如果两个玩家得分相等,同样认为玩家 1 是游戏的赢家,也返回 true 。你可以假设每个玩家的玩法都会使他的分数最大化。

 

示例 1:

输入:nums = [1,5,2]
输出:false
解释:一开始,玩家 1 可以从 1 和 2 中进行选择。
如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。 
所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
因此,玩家 1 永远不会成为赢家,返回 false 。

示例 2:

输入:nums = [1,5,233,7]
输出:true
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。
最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 true,表示玩家 1 可以成为赢家。

 

用dfs+cache的思路:【可作为模板】

class Solution:
    def PredictTheWinner(self, nums: List[int]) -> bool:
        self.cache = {}
        def dfs(l, r):
            if (l, r) in self.cache:
                return self.cache[(l, r)]
            if l >= r:
                self.cache[(l, r)] = nums[r]
                return nums[r]
            
            choice1 = nums[l] - dfs(l+1, r)
            choice2 = nums[r] - dfs(l, r-1)
            ans = max(choice1, choice2)
            self.cache[(l, r)] = ans
            return ans
        
        return dfs(0, len(nums)-1) >= 0

  

 修改为dp:

class Solution:
    def PredictTheWinner(self, nums: List[int]) -> bool:
        n = len(nums)
        dp = [[0]*n for _ in range(n)]
        dp[0][0] = nums[0]

        for i in range(n-1, -1, -1):
            for j in range(i+1, n):
                choice1 = nums[i] - dp[i+1][j]
                choice2 = nums[j] - dp[i][j-1]
                dp[i][j] = max(choice1, choice2)
        
        return dp[0][n-1] >= 0

  

上面代码的思考过程见下。

上面题目和下面这个一样。

Coins in a Line III

Problem

There are n coins in a line. Two players take turns to take a coin from one of the ends of the line until there are no more coins left. The player with the larger amount of money wins.

Could you please decide the first player will win or lose?

Example

plain
 
1
2
3
Given array A = [3,2,2], return true.
Given array A = [1,2,4], return true.
Given array A = [1,20,4], return false.

分析

设己方数字和是A,对手数字和是B,即目标是A>=B,由于记录两个变量的状态太复杂,所以还是把两个选手当成一个人(精髓!!!),每次面对a[i…j]选最优解。这里dp[i][j]是当前选手对a[i…j]所选的coins与对手将要选的coins的最大差值。

初始条件:dp[i][i] = values[i]。因为只有1个coin,肯定是全选。

 

dp[i][i]含义补充:相对分数 说成 净胜分 ,语义会更强一些。

甲乙比赛,甲先手面对区间[i...j]时,dp[i][j]表示甲对乙的净胜分。

最终求的就是,甲先手面对区间[0...n-1]时,甲对乙的净胜分dp[0][n-1]是否>=0

甲先手面对区间[i...j]时,

  • 如果甲拿nums[i],那么变成乙先手面对区间[i+1...j],这段区间内乙对甲的净胜分为dp[i+1][j];那么甲对乙的净胜分就应该是nums[i] - dp[i+1][j]
  • 如果甲拿nums[j],同理可得甲对乙的净胜分为是nums[j] - dp[i][j-1]

以上两种情况二者取大即可。

如果上面题目用dfs,则写成:

import java.util.Arrays;

public class Solution {

    public boolean PredictTheWinner(int[] nums) {
        int len = nums.length;
        int[][] memo = new int[len][len];

        for (int i = 0; i < len; i++) {
            Arrays.fill(memo[i], Integer.MIN_VALUE);
        }
        return dfs(nums, 0, len - 1, memo) >= 0;
    }

    private int dfs(int[] nums, int i, int j, int[][] memo) {
        if (i > j) {
            return 0;
        }

        if (memo[i][j] != Integer.MIN_VALUE) {
            return memo[i][j];
        }
        int chooseLeft = nums[i] - dfs(nums, i + 1, j, memo);
        int chooseRight = nums[j] - dfs(nums, i, j - 1, memo);
        memo[i][j] = Math.max(chooseLeft, chooseRight);
        return memo[i][j];
    }
}

作者:liweiwei1419
链接:https://leetcode.cn/problems/predict-the-winner/solution/ji-yi-hua-di-gui-dong-tai-gui-hua-java-by-liweiwei/
 

   

转移方程:f[i][j] = max{a[i] - f[i+1][j], a[j] - f[i][j-1]}

  1. f[i][j]:为一方先手在面对a[i...j]这些数字时,能得到的最大的与对手的数字差
  2. a[i] - f[i+1][j]:选择a[i],对手采取最优策略时自己能得到的最大的与对手的数字差
  3. a[j] - f[i][j-1]:选择a[j],对手采取最优策略时自己能得到的最大的与对手的数字差

计算顺序

f[0][0]f[1][1]f[2][2], …, f[n-1][n-1] //len = 0, 1 coin
f[0][1], …, f[n-2][n-1] //len = 1, 2 coins

f[0][n-1] //len = n - 1, n coins

java
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Solution{
public boolean firstWillWin(int[] A){
int n = A.length;
int[][] f = new int[n][n];
int i,j,len;
for(i=0; i < n; i++) f[i][i] = A[i];
for(len = 2; len <= n; len++)
for(i = 0; i <= n; i++){
j = i + len -1;
f[i][j] = Math.max(A[i] - f[i+1][j],A[j] - f[i][j -1];
}
return f[0][n-1]>=0;
}
}

 
总结:

博弈型动态规划模板——精髓:把两个选手当成一个人,每次面对a[i…j]选最优解,用dfs+cache做最直观,再考虑修改为dp数组

 

 

1406. 石子游戏 III

难度困难

Alice 和 Bob 用几堆石子在做游戏。几堆石子排成一行,每堆石子都对应一个得分,由数组 stoneValue 给出。

Alice 和 Bob 轮流取石子,Alice 总是先开始。在每个玩家的回合中,该玩家可以拿走剩下石子中的的前 1、2 或 3 堆石子 。比赛一直持续到所有石头都被拿走。

每个玩家的最终得分为他所拿到的每堆石子的对应得分之和。每个玩家的初始分数都是 0 。比赛的目标是决出最高分,得分最高的选手将会赢得比赛,比赛也可能会出现平局。

假设 Alice 和 Bob 都采取 最优策略 。如果 Alice 赢了就返回 "Alice" Bob 赢了就返回 "Bob",平局(分数相同)返回 "Tie" 。

 

示例 1:

输入:values = [1,2,3,7]
输出:"Bob"
解释:Alice 总是会输,她的最佳选择是拿走前三堆,得分变成 6 。但是 Bob 的得分为 7,Bob 获胜。

示例 2:

输入:values = [1,2,3,-9]
输出:"Alice"
解释:Alice 要想获胜就必须在第一个回合拿走前三堆石子,给 Bob 留下负分。
如果 Alice 只拿走第一堆,那么她的得分为 1,接下来 Bob 拿走第二、三堆,得分为 5 。之后 Alice 只能拿到分数 -9 的石子堆,输掉比赛。
如果 Alice 拿走前两堆,那么她的得分为 3,接下来 Bob 拿走第三堆,得分为 3 。之后 Alice 只能拿到分数 -9 的石子堆,同样会输掉比赛。
注意,他们都应该采取 最优策略 ,所以在这里 Alice 将选择能够使她获胜的方案。

示例 3:

输入:values = [1,2,3,6]
输出:"Tie"
解释:Alice 无法赢得比赛。如果她决定选择前三堆,她可以以平局结束比赛,否则她就会输。

示例 4:

输入:values = [1,2,3,-1,-2,-3,7]
输出:"Alice"

示例 5:

输入:values = [-1,-2,-3]
输出:"Tie"


使用dfs+cache还是非常好写的!
class Solution:
    def stoneGameIII(self, nums: List[int]) -> str:
        self.cache = {}
        n = len(nums)

        def dfs(l, r):
            if (l, r) in self.cache:
                return self.cache[(l, r)]

            if l > r:
                return 0
            
            if l == r:
                return nums[l]
             
            choice1 = nums[l] - dfs(l+1, r)
            choice2 = choice3 = float('-inf')
            if l + 1 <= r:
                choice2 = nums[l]+nums[l+1] - dfs(l+2, r)
            if l + 2 <= r:
                choice3 = nums[l]+nums[l+1]+nums[l+2] - dfs(l+3, r)

            self.cache[(l, r)] = max(choice1, choice2, choice3)
            return self.cache[(l, r)]
         
        winned = dfs(0, n-1) 
        if winned == 0:
            return "Tie"
        elif winned > 0:
            return "Alice"
        else:
            return "Bob"

  

 

posted @ 2023-01-30 15:18  bonelee  阅读(50)  评论(0编辑  收藏  举报