【数据结构和算法】三、动态规划原理讲解与实战演练

目录

1、什么是动态规划?

2、动态规划实战演练

[2.1 力扣题之爬楼梯问题](#2.1 力扣题之爬楼梯问题)

(1)解题思路1:

(2)解题思路2:

(3)动态规划(DP):解题思路

(4)动态规划理论

[2.2 力扣题之海盗船长](#2.2 力扣题之海盗船长)

(1)解题思路1:

(2)解题思路2:

(3)动态规划:解题思路3:


1、什么是动态规划?

动态规划(Dynamic Programming,DP)是把复杂问题分解为简单的子问题的求解方法。

动态规划的基本思想虽然简单,但分解的子问题很多都不一样。所以需要多做一些练习,接触并了解各种子问题及分解的思路,结合着不同数据结构,才能逐渐掌握DP的技巧。

所以,DP归纳起来就是多做练习,多思考,逐渐摸索题目的思考逻辑和优化思路。才能掌握使用DP解题的技巧。


从实际问题出发,分几步走,不断练习、迭代熟练:

  1. 暴力求解(枚举)
  2. 拆分子问题(DP)解法
  3. DP原理与题型相结合的分析

2、动态规划实战演练

2.1 力扣题之爬楼梯问题

以力扣著名的题"爬楼梯"为例子:

  • 爬楼梯问题: . - 力扣(LeetCode)

    假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

    每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

    示例 1:

    复制代码
    输入:n = 2
    输出:2
    解释:有两种方法可以爬到楼顶。
    1. 1 阶 + 1 阶
    2. 2 阶

    示例 2:

    复制代码
    输入:n = 3
    输出:3
    解释:有三种方法可以爬到楼顶。
    
    1. 1 阶 + 1 阶 + 1 阶
    2. 1 阶 + 2 阶
    3. 2 阶 + 1 阶

    提示:

    • 1 <= n <= 45

    (1)解题思路1:

    暴力求解,排列所有1,2的组合。

    寻找总和为n的,不同数字长度的组合。

    python 复制代码
    from itertools import product
    
    def climbStairs(n):
        pms = []
        for rep in range(1,n+1):
            permutations = list(product([1,2],repeat=rep))
            for pm in permutations:
                if sum(pm) == n:
                    pms.append(pm)
        return len(pms)

    itertools是python为高效循环而创建迭代器的函数库 。product就是一个笛卡尔积(嵌套for循环)功能实现的函数。

    • 时间复杂度O(n^3)
    • 空间复杂度O(n)

    (2)解题思路2:

    问题寻找的是爬楼梯的方法数量,而不是具体统计每次走几阶。

    所以问题关注的是每次爬1阶、爬2阶两种走法的组合上。

    **假设n=3,**可以直接排列一下相应的组合

    python 复制代码
    3 = 1 + 1 + 1
    3 = 2 + 1
    3 = 1 + 2

    简单直观,但我们要寻找的爬楼梯的规律。似乎还没有发现,继续增加n的数量~

    当n=4:

    python 复制代码
    4 = 1 + 1 + 1 + 1
    4 = 2 + 1 + 1
    4 = 1 + 2 + 1
    
    4 = 1 + 1 + 2
    4 = 2 + 2

    当n=5时:

    python 复制代码
    5 = 1 + 1 + 1 + 1 + 1
    5 = 2 + 1 + 1 + 1
    5 = 1 + 2 + 1 + 1
    
    5 = 1 + 1 + 2 + 1
    5 = 2 + 2 + 1
    
    5 = 1 + 1 + 1 + 2
    5 = 2 + 1 + 2
    5 = 1 + 2 + 2

    排列组合的方式,让我们似乎观察到了数值的规律。那就是n阶的走法,包含了n-1阶和n-2阶的走法。且 n阶的走法 = (n-1)阶走法 + (n-2)阶走法

    简单的循环遍历就可以完成走法的累加:

    复制代码
    n = 5
    n1,n2 = 1,2  # n=1和n=2情况下的走法
    res = 0
    for i in range(3, n+1):  # 从n=3时开始,直到n
        res = n1 + n2
        n1 = n2
        n2 = res
    print(res)

    测试下n=6,结果(n-1) + (n-2) = 8 + 5 = 13

    还可以转换为递归写法

    复制代码
    def climbStairs(n):
        if n <= 1:
    		    return 1
        return climbStairs(n-1) + climbStairs(n-2) 

    至此,就是暴力算法的解题思路,所有可能的组合都做一遍。

    (3)动态规划(DP):解题思路

    依据上面算法特征分析的基础上,使用一个数组动态存储n的计算结果。

    初始化数组,预先存入n-1和n-2的结果

    复制代码
    def climbStairs(n):
        if n <= 1:
            return n
        dp = [i for i in range(n+1)]
        for i in range(3,n+1):
            dp[i] = dp[i-1] + dp[i-2]
        return dp[-1]

    上面代码中的dp就是我们提到的数组,其中**dp[i] = dp[i-1] + dp[i-2]**。

    循环执行完成后,数组中最后一次计算的结果就是爬n阶楼梯的方法数。

    • 时间复杂度:O(n)
    • 空间复杂度:O(n)

    对于空间复杂度,还可以进一步优化。由于我们只考虑n以及n-1、n-2的值。所以,中间的过程值可以丢弃。定义一个固定长度的数组,每次计算结果动态覆盖,如下:

    复制代码
    def climbStairs(n):
        if n <= 1:
            return n
        dp = [0,1,2]
        for i in range(3,n+1):
            sum = dp[1] + dp[2]
            dp[1] = dp[2]
            dp[2] = sum
        return dp[2]
    • 时间复杂度:O(n)
    • 空间复杂度:O(1)

    (4)动态规划理论

    学习动态规划算法,是从多种题解的思路中学习和发现。众多技术高手都给出了思路和技巧,但从实际出发,建议还是先埋头做题,积累了练习和分析思考后,再进行看高手的总结和归纳才会有"英雄所见略同"的共识。

    在这之前,针对每道题目的分析,希望能思考如下的几个问题:

    • 题目计算的目标是什么

      走台阶问题的目标是求走法组合总数

    • 计算分解的子问题是什么

      (n-1)阶走法 、(n-2)阶走法就是n阶走法的子问题 ****

    • 转移方程是否是子问题的组合

      转移方程上面已经写出来了 f(n) = f(n-1)+f(n-2) 。

    • 能否优化

      使用动态数组或字典来存储中间结果,可以有效的减少重复计算的复杂度。

      当然,涉及到使用数组等容器来实现计算和优化时,还会接触到背包问题。

2.2 力扣题之海盗船长

问题:海盗船长. - 力扣(LeetCode)

海盗船长:船长从坐标为(0,0)的位置出发,每次只能向x轴,y轴正方向走一步,求走到x,y点有几种走法?

示例 1:

复制代码
输入:m = 3, n = 7
输出:28

示例 2:

复制代码
输入:n = 3, n = 2
输出:3
解释:从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

复制代码
输入:m = 7, n = 3
输出:28

示例 4:

复制代码
输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 10^9

(1)解题思路1:

0,0x,y,是一个从矩阵左上角向右、向下探索的过程。

想要到达x,y位置,必然经过x-1,y或者x,y-1

所以,路径问题的分解就是求x , y = x-1,y + x,y-1

设 m,n = 3,7 暴力解法

python 复制代码
results = {}

def path_count(x, y):
    # results = {}
    if x == 1 or y == 1:
        results[(x,y)] = 1
        return 1

      results[(x,y)] = path_count(x-1,y) + path_count(x,y-1)
      return results[(x,y)]

print(path_count(3,7))

时间复杂度:O(mn)

空间复杂度:O(n)

(2)解题思路2:

观察上面的方案,可以发现分解的路径实际上存在着大量重复的计算

复制代码
print(results)

我们可以思考:走到results中(m,n)位置的上一步,一定是(m-1,n),(m,n-1),(m-1,n-1)的位置。

这其中(m-1,n-1)走过的路径中,就包含了(m-1,n)和(m,n-1)走过的路径。

以此类推,所有(m-2,n-2)的路径中,也就包含了(m-1,n-1), (m-1,n), (m,n-1) ...... 如此嵌套。我们把计算结果存储在results里面,再次遇到相同的计算时,就直接把结果提取出来。

python 复制代码
def path_count(x, y):
    if x == 1 or y == 1:
        results[(x,y)] = 1
        return 1
    # 查找重复节点,直接返回计算后结果
    if (x,y) in results:
        return results[(x,y)]
    
    results[(x,y)] = path_count(x-1,y) + path_count(x,y-1)
    return results[(x,y)]

(3)动态规划:解题思路3:

动态规划方式实现:

转移方程:f(x,y) = f(x-1,y) + f(x, y-1)

分两步实现:

  1. 计算"到达"每个位置所需要的步数(初始为1)

    python 复制代码
    m,n = 3,7
    path_counts = [[1] * n for j in range(m)]
    for i in range(m):
        for j in range(n):
            print(path_counts[i][j], end=' ')
        print()
  2. 计算从起始位置,到达当前位置,步数的累加和

    python 复制代码
    for x in range(1,m):
        for y in range(1,n):
            path_counts[x][y] = path_counts[x-1][y] + path_counts[x][y-1]
    
    print(path_counts[m-1][n-1])

完整代码:

python 复制代码
def path_count(x, y):
    path_counts = [[1] * y for j in range(x)]
    for i in range(1,x):
        for j in range(1,y):
            path_counts[i][j] = path_counts[i-1][j] + path_counts[i][j-1]
    return path_counts[x-1][y-1]
相关推荐
Jerry2 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry4 小时前
LeetCode 459. 重复的子字符串
算法
海石7 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石7 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry9 小时前
LeetCode 151. 反转字符串中的单词
算法
gugucoding10 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表
ChaoZiLL11 小时前
我的数据结构3——链表(link list)
数据结构·链表
a11177611 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜11 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai
Jerry12 小时前
LeetCode 541. 反转字符串 II
算法