算法心得体会(二)动态规划

378 阅读3分钟

本篇文章是动态规划解题套路框架 :: labuladong的算法小抄的笔记。

动态规划

动态规划目的:求最值

动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离等等。

动态规划核心问题:穷举

因为要求最值,所以把所有可行的答案穷举出来,然后在其中找最值。

动态规划三要素

  • 只有列出正确的「状态转移方程」,才能正确地穷举;

  • 需要判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值;

  • 动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

以上提到的重叠子问题最优子结构状态转移方程就是动态规划三要素

动态规划难点:写出状态转移方程

通过思维框架:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义dp数组/函数的含义,辅助思考状态转移方程。

按上面的套路,最后的解法代码就会是如下的框架:

# 自顶向下递归的动态规划 
def dp(状态1, 状态2, ...): 
    for 选择 in 所有可能的选择: 
    # 此时的状态已经因为做了选择而改变 
        result = 求最值(result, dp(状态1, 状态2, ...)) 
        return result 
        
# 自底向上迭代的动态规划 
# 初始化 
base case dp[0][0][...] = base case 
# 进行状态转移 
for 状态1 in 状态1的所有取值: 
    for 状态2 in 状态2的所有取值: 
        for ... 
            dp[状态1][状态2][...] = 求最值(选择1,选择2...)

练习题

题目一:斐波那契数 - 力扣(LeetCode)

斐波那契数列虽然简单,但可以很好的帮助我们理解动态规划

题目描述:

0a10eeb15411fb202e3446027e6dbab.png

暴力解法:穷举法。从头到尾依次计算。

穷举法

class Solution:
    def fib(self, n: int) -> int:
        if n == 0 or n == 1:
            return n
        return self.fib(n-1) + self.fib(n-2)

写出穷举法就是成功的第一步,接下来就是考虑如何进行优化。

优化一:通过带备忘录的递归方法,避免重复的计算

带备忘录的递归方法

class Solution: 
     def fib(self, n: int) -> int:
         memo = dict()    # 开启备忘录  其实创建了字典用来存会重复计算的结果
         def my_fib(x):
             if x < 2:
                 return x
             if x in memo:
                 return memo[x]
             res = my_fib(x-1) + my_fib(x-2)
            # 将其结果计入备忘录,下次遇到相同的 直接从字典里进行返回
             memo[x] = res
             return memo[x]
         return my_fib(n)

有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,通常叫做 DP table

dp 数组的迭代(递推)解法

class Solution: 
     def fib(self, n: int) -> int:
        self.dp=[-1] * (n+1)

        if n == 0:
            return 0

        self.dp[0] = 0
        self.dp[1] = 1

        for i in range(2, n+1):
            self.dp[i] = self.dp[i-1] + self.dp[i-2]
        return self.dp[n]

题目二:凑零钱问题

零钱兑换 - 力扣(LeetCode)

题目描述:

bf6ae209119e9bae8d18f7f25bcf355.png

dp数组解法

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = []
        for _ in range(amount + 1):
            dp.append(amount + 1)
        # 最差结果是只用 1 元硬币凑整,硬币数量为 amount
        # dp 数组中全写 amount + 1 的原因是:amount + 1 可以选出 amount
        
        dp[0] = 0
        for i in range(len(dp)):
            for coin in coins:
                if i - coin < 0:
                    continue
                dp[i] = min(dp[i], dp[i-coin] + 1)
                # dp[i-coin] + 1:这里的 +1 是因为要加上 coin
                # 比较当前策略 dp[i] 和 选择 coin 策略

        if dp[amount] == amount + 1:
            return -1
            # 无变化,说明凑不齐
        else:
            return dp[amount]

总结

计算机解决问题其实没有任何奇技淫巧,它唯一的解决办法就是穷举,穷举所有可能性。算法设计无非就是先思考“如何穷举”,然后再追求“如何聪明地穷举”。

列出状态转移方程,就是在解决“如何穷举”的问题。之所以说它难,一是因为很多穷举需要递归实现,二是因为有的问题本身的解空间复杂,不那么容易穷举完整。

备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门。


最后,再次推荐动态规划解题套路框架 :: labuladong的算法小抄