数据结构与算法之高级动态规划

131 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 19 天,点击查看活动详情

作者: 千石
支持:点赞、收藏、评论
欢迎各位在评论区交流

前言

本文内容来自我平时学习的一些积累,如有错误,还请指正

在题目实战部分,我将代码实现和代码解释设置在了解题思路的下方,方便各位作为参考刷题

一些话

本文内容来自我平时学习的一些积累,如有错误,还请指正

前置知识

动态规划

动态规划(Dynamic Programming,简称 DP)是一种求解最优化问题的算法思想。动态规划通过将问题分解成更小的子问题,并将子问题的解缓存起来,最终得到原问题的解。

状态转移方程是动态规划的核心,它描述了问题的状态如何从之前的状态转移而来。通常情况下,状态转移方程可以用递推的方式来求解。

以求解最长上升子序列(LIS)问题为例,状态转移方程可以定义为:

设 dp[i] 表示以第 i 个数结尾的最长上升子序列的长度,状态转移方程为:

dp[i] = max{dp[j] + 1 | j < i, nums[j] < nums[i]}

其中,nums 是原序列,dp[j] 表示以第 j 个数结尾的最长上升子序列的长度。状态转移方程的意义是,以第 i 个数结尾的最长上升子序列的长度,等于在前面小于第 i 个数的数中,以最长上升子序列结尾的长度加 1 的最大值。

根据状态转移方程,可以使用递推的方式,从前往后求解 dp 数组。最终,dp[n] 就是原序列的最长上升子序列的长度。

动态规划进阶

  1. 状态压缩

当状态较多且状态之间存在相关性时,可以考虑使用状态压缩技巧,将状态用一个整数表示。例如,在一个图上找最短路,可以用二进制数表示经过的点的集合。

  1. 优化状态转移方程

有时候状态转移方程的计算可以进行优化,比如通过前缀和、后缀和等方式,减少计算量。

  1. 记忆化搜索

当递归搜索中存在重复的状态时,可以使用记忆化搜索技巧,将计算过的状态结果保存下来,下次查询时直接返回结果。

  1. 状态转移矩阵快速幂

在一些特殊的动态规划问题中,状态转移可以用矩阵乘法表示。此时可以使用矩阵快速幂技巧,快速求出状态矩阵的高次幂,从而得到最终的状态。

  1. 单调队列优化

当状态转移方程中存在单调性时,可以使用单调队列优化技巧,减少不必要的计算。例如,在求最长不降子序列时,可以使用单调递增的队列,从而省略一些不必要的计算。

  1. 斜率优化

当状态转移方程中存在斜率变化时,可以使用斜率优化技巧,通过维护凸壳来快速求出最优解。例如,在一些背包问题中,物品价值和重量之间的关系是线性的,此时可以使用斜率优化技巧。

  1. 二分答案优化

当问题的答案具有单调性时,可以使用二分答案优化技巧,通过二分答案来减少计算次数。例如,在一些最优化问题中,可以通过二分答案来快速求解最优解。

题目

题目一:70. 爬楼梯 - 力扣(LeetCode)

image.png

思路

我们可以定义一个状态数组 dp,其中 dp[i] 表示到达第 i 阶台阶的不同方法数。则有状态转移方程:

dp[i] = dp[i-1] + dp[i-2]

其中 dp[i-1] 表示从第 i-1 阶台阶爬一步到达第 i 阶台阶的方法数,dp[i-2] 表示从第 i-2 阶台阶爬两步到达第 i 阶台阶的方法数。

注意到这是一个斐波那契数列,我们可以将初始值 dp[0] 和 dp[1] 分别设置为 1 和 1,然后从 i=2 开始循环计算 dp 数组即可。

代码实现

def climbStairs(n: int) -> int:
    if n <= 1:
        return 1
    dp = [1, 1]
    for i in range(2, n+1):
        dp.append(dp[i-1] + dp[i-2])
    return dp[n]

题目二:[746. 使用最小花费爬楼梯]

(leetcode.cn/problems/mi…)

image.png

思路

我们可以定义一个状态数组 dp,其中 dp[i] 表示到达第 i 阶台阶的最小花费。则有状态转移方程:

dp[i] = min(dp[i-1], dp[i-2]) + cost[i]

其中 dp[i-1] 表示从第 i-1 阶台阶爬一步到达第 i 阶台阶的最小花费,dp[i-2] 表示从第 i-2 阶台阶爬两步到达第 i 阶台阶的最小花费,cost[i] 表示到达第 i 阶台阶需要支付的费用。

注意到题目中要求我们可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,因此需要对初始值进行特殊处理。我们可以将初始值 dp[0] 和 dp[1] 分别设置为 cost[0] 和 cost[1],然后从 i=2 开始循环计算 dp 数组即可。

代码实现

def minCostClimbingStairs(cost) -> int:
    if len(cost) <= 1:
        return 0
    dp = [cost[0], cost[1]]
    for i in range(2, len(cost)):
        dp.append(min(dp[i-1], dp[i-2]) + cost[i])
    return min(dp[-1], dp[-2])