【动态规划】博弈问题
博弈问题
简介
具有竞争或对抗性质的行为称为博弈行为,在这类行为中,参加斗争或竞争的各方各自具有不同的目标或利益。博弈论是二人在平等的对局中各自利用对方的策略变换自己的对抗策略,达到取胜的目的。
为了达到各自的目标和利益,各方必须考虑对手的各种可能的行动方案,并力图选取对自己最为有利或最为合理的方案,比如日常生活中的下棋,打牌等。博弈论就是研究博弈行为中斗争各方是否存在着最合理的行为方案,以及如何找到这个合理的行为方案的数学理论和方法。
应用
应用1:Leetcode.486
题目
方法一:动态规划(自底向上)
解题思路
设
同时,我们假设数组
对应的状态,就是数组 的初始状态,即对应先手玩家的分数。
因此,当所有的状态枚举完了之后,如果
边界条件
显然,
并且,当
也就是说,如果只有一个数字,先手的玩家始终会成为赢家。
状态转移
假设子数组中的元素大于等于两个时,那么,状态
此时,对于当前玩家,他有两种选择,他可以选择
- 如果当前玩家选择
,那么,当前两者分数的差值,就是当前选择的数字 减去上一个状态的差值,即 ; - 如果当前玩家选择
,那么,当前两者分数的差值,就是当前选择的数字 减去上一个状态的差值,即 ;
在两种方案中,当前玩家一定会选择最优的方案,使得自己的分数最大化,即上述两种选择之中的最大值,因此,状态转移方程:
这里,我们不用考虑某个位置是谁在做选择,因为每个位置,对应的玩家都会做最优选择。
我们只需要考虑最终状态状态
注意,枚举
和 的时候,需要注意枚举的方向,需要从边界条件(只有单个元素的时候)开始,依次增加子数组的长度。
代码实现
- 逆序枚举:
class Solution { public boolean predictTheWinner(int[] nums) { int n = nums.length; int [][] dp = new int [n][n]; for (int i = 0; i < n; i++) { dp[i][i] = nums[i]; } for (int i = n - 2; i >= 0; i--) { for (int j = i + 1; j < n; j++) { dp[i][j] = Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]); } } return dp[0][n - 1] >= 0; } }
- 顺序枚举也是一样的,代码实现如下:
class Solution { public boolean predictTheWinner(int[] nums) { int n = nums.length; int [][] dp = new int [n][n]; for (int i = 0; i < n; i++) { dp[i][i] = nums[i]; } for (int j = 1; j < n; j++) { for (int i = j - 1; i >= 0; i--) { dp[i][j] = Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]); } } return dp[0][n - 1] >= 0; } }
枚举的方向并不重要,核心思想在于:从边界条件开始,依次顺序枚举完所有的子数组即可。
方法二:动态规划(自顶向下)
解题思路
这里我们利用分治的思想:通过定义一个带返回值的递归函数,将问题分解为子问题(子树),通过递归推导出答案。
这里,我们定义一个play(nums, left, right, memory)
函数,表示当从子数组
递归的终止条件就是子数组长度为零时,即
代码实现
class Solution: def PredictTheWinner(self, nums: List[int]) -> bool: n = len(nums) memory = [ [None for _ in range(n) ] for _ in range(n)] return self.play(nums, 0, n - 1, memory) >= 0 def play(self, nums: List[int], left: int, right: int, memory: List[List[int]]) -> int: if left > right: return 0 if memory[left][right] is not None: return memory[left][right] plan_a = nums[left] - self.play(nums, left + 1, right, memory) plan_b = nums[right] - self.play(nums, left, right - 1, memory) result = max(plan_a, plan_b) memory[left][right] = result return result
方法三:动态规划
解题思路
这里我们定一个了一个对象
设
表示先手玩家的最大分数; 表示后手玩家的最大分数。
因此,当所有的状态枚举完了之后,如果
边界条件
显然,
并且,当
也就是说,如果只有一个数字,先手的玩家始终会成为赢家。
状态转移
假设子数组中的元素大于等于两个时,那么,状态
此时,对于这一轮选择,当前玩家就是先手玩家,他有两种选择,他可以选择
-
如果当前玩家选择
,那么:- 他可以获取的大分数
,就是上一个状态的他分数 加上当前的数字 ,即 ; - 另一个玩家的分数保持不变,即还是上一个状态的分数
。
- 他可以获取的大分数
-
如果当前玩家选择
,那么:- 他可以获取的大分数
,就是上一个状态的他分数 加上当前的数字 ,即 ; - 另一个玩家的分数保持不变,即还是上一个状态的分数
。
- 他可以获取的大分数
因此,对于当前的这一轮选择,当前玩家一定会选择对自己有利的方案,即选择
注意,对于当前的这一轮选择,当前玩家就是先手玩家,显然,在上一轮他就是后手玩家。
代码实现
class Score(object): def __init__(self, first: int = 0, second: int = 0): self.first = first # 先手玩家获得的分数 self.second = second # 后手玩家获取的分数 def diff(self): return self.first - self.second class Solution: def PredictTheWinner(self, nums: List[int]) -> bool: n = len(nums) dp = [ [Score() for _ in range(n)] for _ in range(n)] for i in range(n): dp[i][i] = Score(nums[i], 0) for i in range(n - 2, -1, -1): for j in range(i + 1, n): plan_a = nums[i] + dp[i + 1][j].second plan_b = nums[j] + dp[i][j - 1].second # 先手玩家选择对其有利的方案,后手玩家的分数不变,还是上一个状态的分数 if plan_a > plan_b: dp[i][j].first = plan_a dp[i][j].second = dp[i + 1][j].first else: dp[i][j].first = plan_b dp[i][j].second = dp[i][j - 1].first return dp[0][n - 1].diff() >= 0
应用2:Leetcode.877
题目
分析
这道题可以直接套用前面的结论,这里就不再重复分析了,我们直接给出代码实现。
另外,这道题可以通过数学结论,直接返回结果,即当数组长度为偶数时,先手玩家必获胜。由于数学解法不具有通用性,就不在此深入分析了。
感兴趣的话,可以参考力扣官方题解:石子游戏
代码实现
class Solution: def stoneGame(self, piles: List[int]) -> bool: n = len(piles) dp = [ [0] * n for _ in range(n)] for i in range(n): dp[i][i] = piles[i] # 枚举所有的状态 for i in range(n - 2, -1, -1): for j in range(i + 1, n): dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]) return dp[0][n - 1] >= 0
class Score(object): def __init__(self, first: int = 0, second: int = 0): self.first = first # 先手玩家获得的分数 self.second = second # 后手玩家获取的分数 def diff(self): return self.first - self.second class Solution: def stoneGame(self, piles: List[int]) -> bool: n = len(piles) dp = [ [Score() for _ in range(n)] for _ in range(n)] for i in range(n): dp[i][i] = Score(piles[i], 0) for i in range(n - 2, -1, -1): for j in range(i + 1, n): plan_a = piles[i] + dp[i + 1][j].second plan_b = piles[j] + dp[i][j - 1].second # 先手玩家选择对其有利的方案,后手玩家的分数不变,还是上一个状态的分数 if plan_a > plan_b: dp[i][j].first = plan_a dp[i][j].second = dp[i + 1][j].first else: dp[i][j].first = plan_b dp[i][j].second = dp[i][j - 1].first return dp[0][n - 1].diff() >= 0
参考:
本文作者:LARRY1024
本文链接:https://www.cnblogs.com/larry1024/p/17047356.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步