目录
博弈问题
具有竞争或对抗性质的行为称为博弈行为,在这类行为中,参加斗争或竞争的各方各自具有不同的目标或利益。博弈论是二人在平等的对局中各自利用对方的策略变换自己的对抗策略,达到取胜的目的。
为了达到各自的目标和利益,各方必须考虑对手的各种可能的行动方案,并力图选取对自己最为有利或最为合理的方案,比如日常生活中的下棋,打牌等。博弈论就是研究博弈行为中斗争各方是否存在着最合理的行为方案,以及如何找到这个合理的行为方案的数学理论和方法。
应用
应用1:Leetcode.486
题目
方法一:动态规划(自底向上)
解题思路
设 \(dp[i][j]\) 表示从子数组 \(nums[i], \cdots, nums[j]\) 中做选择,当前玩家与另一个玩家的分数差值的最大值。
同时,我们假设数组 \(nums\) 的长度为 \(n\) ,因为两位选手是轮流选择的,我们只关心最终的状态,也就是 \(dp[0[n - 1]\) 的状态,所以,我们只需要通过状态转移记录每次选择的方案就行了。
因此,当所有的状态枚举完了之后,如果 \(dp[0[n - 1]\) 的值大于零,那么当前玩家(先手的玩家)的分数大于另一个玩家的得分,则先手赢,否则,后手赢。
边界条件
显然,\(i\), \(j\)需要满足:
\[\begin{aligned} &0 \le i \le n - 1 \\ &0 \le j \le n - 1 \end{aligned} \]并且,当 \(i = j\) 时,即子数组的长度为 \(1\) 时,当前玩家只能选择这个数字,两人的分数差值的最大值就是当前的数字,即
\[dp[i][i] = nums[i], \quad 0 \le i \le n -1 \]也就是说,如果只有一个数字,先手的玩家始终会成为赢家。
状态转移
假设子数组中的元素大于等于两个时,那么,状态 \(dp[i][j]\) 对应的子数组中的元素可以表示为:
\[nums[i], \ nums[i + 1], \ \cdots, \ nums[j - 1], \ nums[j] \]此时,对于当前玩家,他有两种选择,他可以选择 \(nums[i]\) 或者 \(nums[j]\) :
- 如果当前玩家选择 \(nums[i]\),那么,当前两者分数的差值,就是当前选择的数字 \(nums[i]\) 减去上一个状态的差值,即 \(nums[i] - dp[i + 1][j]\);
- 如果当前玩家选择 \(nums[j]\),那么,当前两者分数的差值,就是当前选择的数字 \(nums[j]\) 减去上一个状态的差值,即 \(nums[j] - dp[i][j - 1]\);
在两种方案中,当前玩家一定会选择最优的方案,使得自己的分数最大化,即上述两种选择之中的最大值,因此,状态转移方程:
\[dp[i][j] = \max(nums[i] - dp[i + 1][j], \ nums[j] - dp[i][j - 1]), \quad i \lt j \]这里,我们不用考虑某个位置是谁在做选择,因为每个位置,对应的玩家都会做最优选择。
我们只需要考虑最终状态状态 \(dp[0[n - 1]\) 就行了,因为只有先手的玩家会对应状态 \(dp[0[n - 1]\)。
代码实现
class Solution:
def PredictTheWinner(self, nums: List[int]) -> bool:
n = len(nums)
dp = [ [0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = nums[i]
# 枚举所有的状态
for i in range(n - 2, -1, -1):
for j in range(i + 1, n):
dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1])
return dp[0][n - 1] >= 0
方法二:动态规划(自顶向下)
解题思路
这里我们利用分治的思想:通过定义一个带返回值的递归函数,将问题分解为子问题(子树),通过递归推导出答案。
这里,我们定义一个play(nums, left, right)
函数,表示当从子数组 \(nums[left]\) , \(\cdots\), \(nums[right]\) 中做选择时,当前玩家与另一个玩家的分数差值的最大值。
递归的终止条件就是子数组长度为零时,即 \(left \gt right\),两者分数的差值为零。
代码实现
class Solution:
def PredictTheWinner(self, nums: List[int]) -> bool:
return self.play(nums, 0, len(nums) - 1) >= 0
def play(self, nums, left, right):
if left > right:
return 0
plan_a = nums[left] - self.play(nums, left + 1, right)
plan_b = nums[right] - self.play(nums, left, right - 1)
return max(plan_a, plan_b)
方法三:动态规划
解题思路
这里我们定一个了一个对象 \(Score\),它有两个属性\(first\)、\(second\),分别表示先手和后手玩家的分数,用于辅助记录状态转移过程。
我们用令 \(dp[i][j]\) 表示从子数组 \(nums[i] , \cdots, nums[j]\) 中做选择,两个玩家可以获取的分数的最大值,即:
- \(dp[i][j].first\) 表示先手玩家的最大分数;
- \(dp[i][j].second\) 表示后手玩家的最大分数。
因此,当所有的状态枚举完了之后,如果 \(dp[0[n - 1].first\) 的值大于\(dp[0[n - 1].second\),则先手赢,否则,后手赢。
边界条件
显然,\(i\), \(j\)需要满足:
\[\begin{aligned} &0 \le i \le n - 1 \\ &0 \le j \le n - 1 \end{aligned} \]并且,当 \(i = j\) 时,即子数组的长度为 \(1\) 时,先手玩家只能选择这个数字,那么先手玩家的分数就是\(nums[i]\),后手玩家的分数就是零,即
\[dp[i][i] = Score(nums[i], 0), \quad 0 \le i \le n -1 \]也就是说,如果只有一个数字,先手的玩家始终会成为赢家。
状态转移
假设子数组中的元素大于等于两个时,那么,状态 \(dp[i][j]\) 对应的子数组中的元素可以表示为:
\[nums[i], \ nums[i + 1], \ \cdots, \ nums[j - 1], \ nums[j] \]此时,对于这一轮选择,当前玩家就是先手玩家,他有两种选择,他可以选择 \(nums[i]\) 或者 \(nums[j]\) :
-
如果当前玩家选择 \(nums[i]\),那么:
- 他可以获取的大分数 \(y_1\),就是上一个状态的他分数 \(dp[i + 1][j].second\) 加上当前的数字 \(nums[i]\) ,即 \(y_1 = nums[i] + dp[i + 1][j].second\);
- 另一个玩家的分数保持不变,即还是上一个状态的分数 \(dp[i + 1][j].first\) 。
-
如果当前玩家选择 \(nums[j]\),那么:
- 他可以获取的大分数 \(y_2\),就是上一个状态的他分数 \(dp[i][j - 1].second\) 加上当前的数字 \(nums[i]\) ,即 \(y_2 = nums[i] + dp[i][j - 1].second\);
- 另一个玩家的分数保持不变,即还是上一个状态的分数 \(dp[i][j - 1].first\)。
因此,对于当前的这一轮选择,当前玩家一定会选择对自己有利的方案,即选择 \(y_1\) 和 \(y_2\) 较大的方案,那么
\[dp[i][j]=\begin{cases} Score(nums[i] + dp[i + 1][j].second, \ dp[i + 1][j].first), & y_1 \gt y_2 \\ Score(nums[i] + dp[i][j - 1].second, \ dp[i][j - 1].first), & y_1 \le y_2 \end{cases} \]注意,对于当前的这一轮选择,当前玩家就是先手玩家,显然,在上一轮他就是后手玩家。
代码实现
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
标签:分数,博弈,nums,玩家,second,动态,规划,dp,first
From: https://www.cnblogs.com/larry1024/p/17047356.html