力扣刷题之区间动态规划类题目

         区间动态规划是一种解决涉及区间或序列问题的动态规划策略。这类问题通常要求在一个序列或者数组上进行操作,以达到某种优化目标,例如最大化或最小化某个量。区间动态规划在很多场景下非常有用,比如在游戏理论、字符串处理、以及序列分析等领域。

基本策略

区间动态规划的基本策略涉及以下几个关键步骤:

  1. 定义状态: 首先定义状态 dp[i][j],表示对于序列的一个子区间 [i, j](包括端点 ij),问题的解(如最大值、最小值、最优方案等)。状态的定义根据具体问题的需求来定。

  2. 状态转移方程: 接着是确定状态转移方程,即如何从小区间的解推导出大区间的解。这通常涉及到枚举区间内某个分割点,将大区间分为两个或多个小区间,然后根据小区间的解来构造大区间的解。状态转移方程的形式依赖于问题的具体性质。

  3. 初始化: 对于一些基本情况(如最小的区间),直接给出 dp 数组的初始值。例如,在很多问题中,长度为 1 的区间(即 i == j 的情况)的解是显而易见的,可以直接初始化。

  4. 计算顺序: 确定计算 dp 数组的顺序。由于大区间的解依赖于小区间的解,因此需要从最小的区间开始计算,逐渐扩大区间的长度,直到计算出整个序列的解。这通常意味着使用两层循环,外层循环控制区间长度,内层循环控制区间的起点。

  5. 结果提取: 最后,根据问题的需求从 dp 数组中提取结果。在很多情况下,整个序列的最优解就存储在 dp[0][n-1] 中,其中 n 是序列的长度。

技巧和注意事项

  • 边界处理: 在定义状态转移方程时,要注意处理好边界条件,确保所有可能的情况都被考虑到,避免出现数组越界等错误。
  • 空间优化: 在某些情况下,当前状态可能只依赖于有限的几个其他状态,因此可以通过滚动数组等技巧来减少空间复杂度。
  • 复杂度分析: 在设计区间动态规划算法时,需要分析时间和空间复杂度,以确保算法的效率。通常情况下,如果有两层循环遍历所有区间,并且在每个区间内部还有枚举操作,则时间复杂度可能会达到O(n3),这在 n 较大时可能不够高效。

例子:

 2024.2.3 每日一题 1690. 石子游戏 VII

在这个游戏中,两个玩家轮流从石子数组的开始或结束处移除石子。每次移除石子后,玩家的得分增加剩余石子的总和。目标是最大化当前玩家与对手的得分差。

状态定义

定义 dp[i][j] 为在石子数组的子区间 [i, j] 内,当前玩家相对于对手可以获得的最大分数差。注意,这里的最大分数差考虑了双方都采取最优策略的情况。

状态转移

对于任意区间 [i, j],当前玩家有两种选择:

  1. 移除左端的石子:这时,区间变为 [i+1, j]。当前玩家获得的得分是剩余石子的总和,即 sum[i+1][j](prefix_sum[j + 1] - prefix_sum[i + 1])。之后,对手在 [i+1, j] 区间内相对于当前玩家的最大分数差是 dp[i+1][j]。因此,如果当前玩家选择这个策略,他与对手的分数差将是 sum[i+1][j] - dp[i+1][j]

  2. 移除右端的石子:这时,区间变为 [i, j-1]。类似地,当前玩家获得的得分是 sum[i][j-1](prefix_sum[j] - prefix_sum[i]),对手在 [i, j-1] 区间内相对于当前玩家的最大分数差是 dp[i][j-1]。因此,当前玩家与对手的分数差将是 sum[i][j-1] - dp[i][j-1]

最优策略

因为每个玩家都在尽可能地最大化自己相对于对手的分数差,所以当前玩家会选择以上两种策略中使自己分数差最大的一个。因此,状态转移方程为:

dp[i][j]=max(sum[i+1][j]−dp[i+1][j],sum[i][j−1]−dp[i][j−1])

这个方程考虑了当前玩家在区间 [i, j] 内的每一步都是最优的,即最大化其相对于对手的分数差。通过这种方式,我们可以确保无论对手如何响应,当前玩家都能获得该区间内可能的最大分数差。

初始化

对于任何区间 [i, i](即区间长度为 1),dp[i][i] = 0,因为没有剩余的石子可以加到玩家的得分上。

通过这种方法,我们可以填充整个 dp 表,并最终找到在整个石子数组中,第一个玩家相对于第二个玩家的最大分数差,即 dp[0][n-1],其中 n 是石子数组的长度。

class Solution:
    def stoneGameVII(stones):
        n = len(stones)
        prefix_sum = [0] * (n + 1)
        for i in range(n):
            prefix_sum[i + 1] = prefix_sum[i] + stones[i]

        dp = [[0] * n for _ in range(n)]
        for length in range(2, n + 1):
            for i in range(n - length + 1):
                j = i + length - 1
                left = prefix_sum[j + 1] - prefix_sum[i + 1] - dp[i + 1][j]
                right = prefix_sum[j] - prefix_sum[i] - dp[i][j - 1]
                dp[i][j] = max(left, right)
    
        return dp[0][n - 1]

        在石子游戏 VII 问题中,区间 [i, j] 的最优解依赖于 [i+1, j][i, j-1](即移除左端或右端石子后剩下的区间)的解。因此,我们必须先解决这些较小的区间,才能解决较大的区间。

动态规划表的填充

在实际的动态规划实现中,通常会使用一个表(二维数组)来存储子问题的解。从小区间到大区间的顺序确保当我们尝试填充表中的一个条目(即解决一个子问题)时,该条目所依赖的所有其他条目(即较小的子问题的解)都已经被计算并填充好了。这避免了重复计算,并确保了我们在解决当前子问题时可以直接引用这些依赖项。

计算效率

从小区间开始,逐步扩大区间的计算方式,不仅符合问题的依赖结构,而且提高了计算效率。因为较小的子问题只需要计算一次,其结果就可以被多次复用来解决更大的问题。这大大减少了计算量,因为不需要对相同的小区间重复进行计算。

确保正确性

这种方法还有助于确保解决方案的正确性。通过先解决所有可能的小问题,我们可以构建一个坚实的基础,确保在解决更大和更复杂的问题时,所需的所有基本构建块都是正确和可用的。

在“石子游戏 VII”的例子中,从长度为 2 的区间开始,然后逐渐增加区间长度,这保证了当我们计算一个区间 [i, j] 的最大分数差时,我们已经知道了所有可能影响这个决策的较小区间的最优解。这种方法简化了问题的复杂性,使得动态规划成为解决此类问题的一种有效工具。

更多练习:

87. 石子游戏

132. 分割回文串 II

312.戳气球

486.预测赢家

1000.合并石头的最低成本

1043. 分隔数组以得到最大和

  • 22
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值