Python 最最最使用的动态规划入门教程 + 10道经典例题

不多废话,直接开讲

动态规划三大步骤

动态规划是一种将问题分解为若干个子问题,并存储这些子问题的解(通常使用数组或矩阵等数据结构),以便在后续计算中重复使用,从而避免了重复计算,提高了算法的效率。

需要注意的是,动态规划并非一种特定的算法,而是一种解决问题的思想和方法。在实际应用中,需要根据具体问题的特点来设计合适的动态规划算法。

动态规划的根本在于用已知项的求出未知项,并再次调用已经求出的未知项来解决更多未知项,可以将其具象化的理解为“造房子”:造房子需要木材,而木材得用电锯砍伐才能获得,所有得现拥有电锯,后得到木材才能解锁房子

我们可以将其分为以下三大步骤

定义数组

一般的,我们会将问题分解之后需要存储这些问题的解(注意:是问题的解),因此,我们需要定义所存储的量,一般起名为数组 or 列表 dp (dynamic programing 的缩写)。dp 的定义不同,所存储的量也将不同,所以必须明确 dp 的定义

动规方程

问题的中已知量与所要求得的未知量之间是存在一定的逻辑关系,我们可以运用归纳法等数学方法来列出已知量与未知量直接的某种关系,这种关系一般可以用方程表示,故被称为 动规方程

数组初始化

任何一个方程都是已知量求未知量的过程,我们在定义完数组 dp 之后就得对其初始化,并且初始化的内容要与数组的定义相关

例题讲解

动态规划并不是一个指定的方法,她是一种思想,一种思维方式,虽然步骤都相同,但问题却会千姿百态,并且光凭几段文字是难以掌握动态规划的,学动态规划的最好方法就是以例题讲解为主的去理解和运用,也就是说,学动态规划的最好方法是结合答案做动态规划的题

斐波那契数列

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
def function(n):
    dp = [0, 1]
    if n == 0:
        return dp[0]
    if n == 1:
        return dp[1]
    for i in range(2, n + 1):
        dp.append(dp[i - 2] + dp[i - 1])
    return dp  # 正确答案本应该是 result[-1] 但是为了方便理解可以详细看看 dp 是什么样的

print(function(4))

定义数组:dp为第n个斐波那契数的集合

动规方程:我们可以从题意中得出斐波那契数列满足

f(x) = f(x - 1) + f(x - 2) 

并以此推断出动规方程为:

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

设法将其写入到代码里面即可完成

数组初始化:我们已知的是斐波那契数列最初两项为 [0 , 1] 我们如同上述步骤一样写入到代码里面即可完成初始化,这也与我们的数组定义相符合,我们往 dp 数组内存入的是第n个斐波那契数,正好 [0, 1] 为最初的斐波那契数

第N个泰波纳契数列

泰波那契序列 Tn 定义如下: 

T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2

给你整数 n,请返回第 n 个泰波那契数 Tn 的值。

示例 1:

输入:n = 4
输出:4
解释:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4

示例 2:

输入:n = 25
输出:1389537
def tybonacci(num):
    if num == 0:
        return 0
    if num == 1 or num == 2:
        return 1
    else:
        return tybonacci(num - 3) + tybonacci(num - 2) + tybonacci(num - 1)

print(tybonacci(4))

思路与斐波那契数列一模一样,只不过方程改变了而已

定义数组:dp为第n个泰波那契数的集合

动规方程:我们可以从题意中得出斐波那契数列满足

f(x) = f(x - 1) + f(x - 2) +f(x - 3)

并以此推断出动规方程为:

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

设法将其写入到代码里面即可完成

数组初始化:我们已知的是斐波那契数列最初三项为 [0 , 1,1] 我们如同上述步骤一样写入到代码里面即可完成初始化,这也与我们的数组定义相符合,我们往 dp 数组内存入的是第n个泰波那契数,正好 [0 , 1,1] 为最初的泰波那契数

三步问题

三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。

示例1:

 输入:n = 3 
 输出:4
 说明: 有四种走法
def function(n):
    dp = []
    dp.append(1)
    dp.append(2)
    dp.append(4)
    if n == 1:
        return dp[0]
    if n == 2:
        return dp[1]
    if n == 3:
        return dp[2]
    for i in range(3, n):
        dp.append(dp[i - 1] + dp[i - 2] + dp[i - 3])
    return dp[-1]


print(function(61))

特别说明:新手第一次接触此类题目的时候可能会想到穷举所有的可能性,回溯最后的答案,但其实没有必要这样,我们所要处理的问题是得出有几种方法,而不是得知道得出的方法是什么样的,仅需知道方案的个数即可 

定义数组:dp为爬上第n阶台阶可以用用到的方法数量

动规方程:我们先从中找到规律,例如:

爬到一节台阶的方案:1 ..........直接走一阶,除此以外没方法

爬到二节台阶的方案:2

(1:先走一阶,然后再走一阶)

(2:直接走两阶)

爬到三节台阶的方案:4

(1:先走一阶,然后再走一阶,最后再走一阶)

(2:先走两阶,然后再走一阶)

(3:先走一阶,然后再走两阶)

(4:直接走三阶)

.....................................................等等等等

我们就可以推断出,我们在原有方案的个数上加上可跨越阶梯数量范围以内进行相加即可,例如:

爬上四阶的方法:从前一阶爬上来 or 从前二阶爬上来 or 从前三阶爬上来

所以爬上四阶的方案数 = 爬上一阶的方案数 + 爬上二阶的方案数 + 爬上三阶的方案数

并以此推断出动规方程为:

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

设法将其写入到代码里面即可完成

数组初始化:我们已知的是爬上前三节楼梯的方案数为 [1 , 2,4] 在此基础上推到即可

注:MOD为取模运算防止发生溢出,与本题无关

爬楼梯

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

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

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
def function(n):
    result = [1, 2]
    if n == 1:
        return result[0]
    if n == 2:
        return result[1]
    for i in range(2, n):
        result.append(result[i - 1] + result[i - 2])
    return result

print(function(5))

 

定义数组:dp为爬上第n阶台阶可以用用到的方法数量

动规方程:我们先从中找到规律,例如:

爬到一节台阶的方案:1 ..........直接走一阶,除此以外没方法

爬到二节台阶的方案:2

(1:先走一阶,然后再走一阶)

(2:直接走两阶)

爬到三节台阶的方案:3

(1:先走一阶,然后再走一阶,最后再走一阶)

(2:先走两阶,然后再走一阶)

(3:先走一阶,然后再走两阶)

.....................................................等等等等

我们就可以推断出,我们在原有方案的个数上加上可跨越阶梯数量范围以内进行相加即可,例如:

爬上四阶的方法:从前一阶爬上来 or 从前二阶爬上来

所以爬上四阶的方案数 = 爬上一阶的方案数 + 爬上二阶的方案数

并以此推断出动规方程为:

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

设法将其写入到代码里面即可完成

数组初始化:我们已知的是爬上前三节楼梯的方案数为 [1 , 2] 在此基础上推到即可

爬楼梯进阶版

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

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

示例 1:

输入:n = 3
输出:4
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶 
4. 3 
def function(n):
    result = [1, 2]
    for i in range(2, n):
        result.append(sum(result) + 1)
    return result[-1]

print(function(5))

定义数组:dp为爬上第n阶台阶可以用用到的方法数量

动规方程:我们先从中找到规律,例如:

爬到一节台阶的方案:1 ..........直接走一阶,除此以外没方法

爬到二节台阶的方案:2

(1:先走一阶,然后再走一阶)

(2:直接走两阶)

爬到三节台阶的方案:3

(1:先走一阶,然后再走一阶,最后再走一阶)

(2:先走两阶,然后再走一阶)

(3:先走一阶,然后再走两阶)

(4:直接走三阶)

.....................................................等等等等

我们就可以推断出,我们在原有方案的个数上加上可跨越阶梯数量范围以内进行相加即可,例如:

爬上四阶的方法:从第零阶爬上来 or 从前一阶爬上来 or 从前二阶爬上来 or 从前三阶爬上来

所以爬上四阶的方案数 = 爬上一阶的方案数 + 爬上二阶的方案数 +....+ 爬上n - 1阶的方案数  + 1

最后的 +1 为直接从第零阶跨过来的

并以此推断出动规方程为:

dp[i] = sum(dp) + 1

设法将其写入到代码里面即可完成

数组初始化:我们已知的是爬上前三节楼梯的方案数为 [1 , 2,4] 在此基础上推到即可

判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

致谢:

特别感谢 @pbrother 添加此问题并且创建所有测试用例。

示例 1:

输入:s = "abc", t = "ahbgdc"
输出:true

示例 2:

输入:s = "axc", t = "ahbgdc"
输出:false
def function(str_0, str_1):
    index_0 = 0
    index_1 = 0
    while index_0 < len(str_0) and index_1 < len(str_1):
        if str_0[index_0] == str_1[index_1]:
            index_0 += 1
        index_1 += 1
    if index_0 == len(str_0):
        return True
    return False


s = "abc"
t = "ahbgdc"

print(function(s, t))

其实这道题不是很传统的动态规划类型的题,但是思维是类似的,我们要知道,动态规划并不一定要动态规划一个数组,我们可以动态规划任意的东西,她可以是一个数,数组或者是一个链表等等

定义变量:index_0 index_1分别为字符串指向其字母的指针

动规方程:遍历两个字符串,当期字母相同时移动 index_0 并且无论是否满足条件都移动index_1

变量初始化:还没开始遍历,所有都指向第一个位置,即都为零

买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
def function(arr):
    max_num = 0
    min_num = float('inf')
    for i in range(len(arr)):
        if min_num > arr[i]:
            min_num = arr[i]
        elif max_num < arr[i] - min_num:
            max_num = arr[i] - min_num
    print(max_num)

prices = [7,1,5,3,6,4]
function(prices)

定义变量:max_num min_num分别为最大收益和最小支出

动规方程:遍历题干中给定的数组,当存在之前所记录的收益小于当前收益时,更新max_num,对于最小支出min_num进行同类型的操作

变量初始化:将最大收益初始化为零,因为还没开始收益,最小支出初始化为无穷大,因为我们并不知道需要支出多少,先预算为无限

最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
def function(cost):
    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])


nums = [10, 15, 20]
print(function(nums))

定义数组:dp 为从第n阶开始向上爬所要花费的费用

动规方程:遍历题干中给定的数组,我们可以从第i - 1阶楼梯向上爬或者是从第i - 2阶楼梯向上爬,所以我们只需要从第i - 1阶第i - 2阶求最小的那个再加上我们将要花费的费用即可

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

变量初始化dp 数组的定义是从第n阶开始向上爬所要花费的费用,我们刚开始可以从第一节台阶开始,或者从第二节台阶开始,故初始化为 dp = [ cost[0], cost[1] ]

连续天数的最高销售额

某公司每日销售额记于整数数组 sales,请返回所有 连续 一或多天销售额总和的最大值。

要求实现时间复杂度为 O(n) 的算法。

示例 1:

输入:sales = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:[4,-1,2,1] 此连续四天的销售总额最高,为 6。

示例 2:

输入:sales = [5,4,-1,7,8]
输出:23
解释:[5,4,-1,7,8] 此连续五天的销售总额最高,为 23。 
def function(arr):
    dp = [arr[0]]
    for i in range(1, len(arr)):
        dp.append(max(arr[i] + dp[i - 1], arr[i]))
    print(max(dp))

nums = [5,4,-1,7,8]
function(nums)

 定义数组:dp 销售额的和,注意是,而不是单独某一天的销售额

动规方程:遍历题干中给定的数组,若我当前的销售额的和再加上今日份销售大于今日份的销售,那么我就将今日份销售额加入到连续天数的销售额的和内

而如果今日份销售额大于当前销售额的和的话,我就断开,不要之前的销售额的和,反而从今天开始计算新的销售额的和

dp[i] = max(arr[i], dp[i - 1] + arr[i])

变量初始化dp 数组的定义是销售额的和,我们刚开始可以从第一次的销售额为arr[0],此时销售额的和也为arr[0],因为只有一个元素,故初始化为arr[0]

按摩师

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。

注意:本题相对原题稍作改动

示例 1:

输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。

示例 2:

输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。

示例 3:

输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。
def function(arr):
    dp = [arr[0], arr[1]]
    for i in range(2, len(arr)):
        dp.append(max(dp[i - 1], dp[i - 2] + arr[i]))
    return max(dp)


nums = [2,1,4,5,3,1,1,3]
print(function(nums))

定义数组:dp 最大收益的和,注意是,而不是单独某一天的收益

动规方程:遍历题干中给定的数组,其实遇上提类似,只不过方程是相反的

若前天的之前加上当前的收益如果大于昨天的收益,那我昨天就不上班了,反正前天 + 今天我能赚的更多

相反,若昨天的收益大于前天和当前的收益,那我昨天就得去上班而前天和今天就得休息

并且记录下上班的天数,随后如此的动规

注:这里为了更好的解释所以这么讲了,但实际思路并不是如此,博主表达能力欠佳,十分抱歉

dp[i] = max(dp[i - 1], dp[i - 2] + arr[i])

变量初始化dp 数组的定义是最大收益的和,我们呢可以从同意一号预约,也可以同意二号预约,但不能同时同意,故初始化为[arr[0], arr[1]]

总结

动态规划其实算是比较难以理解的内容了,最难的一点是如何抽象的把数据与关系式表达出来,这就需要很多很多的练习来掌握这种思想,多做做题,多看看书,相信你也能取得好成绩!

希望本期文章对你有所帮助,也请你点赞关注留言支持一下博主,博主会第一时间为你解答

Emmmm..........................最后一件事情:
自己动手试试吧,你可以做的更好!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值