导师问我最近在家待得是不是有点“怂”了?害,今天,就盘一下一直弄不清楚的“动态规划”吧。一点不怂,好吧!?
动态规划解法在程序员笔试中会经常被cue到,它为什么这么火热呢?可能是因为“妙”吧。其短小精悍、化繁为简的特点,能用于优化某些原本暴力求解的算法。同时,也因为其灵活多变与不易理解,让我之前望而生“怂”,今天我去B站看相关视频学习的时候,视频下面显示同时在看的人数居然有30,可见其火热。
总之,盘,就完事了。
在B站学习的今日,搜索动态规划可以一下找出很多的视频,此处就不一一赘述其特点,想说一些精髓的。动态规划(dynamic programming,DP)是运筹学的一种,当时学管理统计学的时候有所接触,主要是处理:
1)求最值。如,小人走格子问某两点的最远距离;
2)求计数。如,下面的爬楼梯问题中问有多少种走法;
3)求能否存在。如,取硬币问题问最少多少枚硬币可以凑出所需值;
等等【参考:https://www.bilibili.com/video/BV1xb411e7ww】,那么在遇到这三种问题的时候,就可以大胆常识一下DP算法能否求解了。
弄懂了可以解决什么问题之后,就要开始缕逻辑了。简而言之,我们把要处理的问题的最后状态(t时刻),用前一时刻(t-1时刻)表示,写出来的公式,我们称之为“转移方程”,那么任意时刻的值,都可以用前一时刻来表示。最后,该问题,被转化为一个从小到大按顺序遍历的问题,同时,我们在遍历的过程中会用数组结构(python中的list)记录每个值下的最优解,并在后面的计算中直接使用这些最优解,大大的节省时间,我们称之为“备忘录方法”。
以取硬币为例,我们有面值为[ 21, 33, 52 ]的三种硬币,想用最少的硬币数凑一个数值为X的总额,那么在取X时,有转移方程:f(X) = min{ f(X-21) + 1, f(X-33) + 1, f(X-52) + 1},我们只需要从备忘录中找到f(X-21) 、f(X-33) 、f(X-52) 这三个值即可求解。
综上,处理动态规划问题的思路就是:确定问题最终状态->写出转移方程->实现它
例题网址(力扣-初级算法-动态规划):https://leetcode-cn.com/explore/interview/card/top-interview-questions-easy/23/dynamic-programming/
2.1 爬楼梯问题
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?给定 n 是一个正整数。
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
class Solution(object):
def climbStairs(self, n):
"""
:type n: int
:rtype: int
"""
# 转移方程:
# f[x] = f[x-1] + f[x-2]
"""
有木有很像斐波那契数列,第三个数是前两个数的和
"""
steps = [1, 2] # 两种走法
res = [0]*(n+1) # 初始化备忘录列表
res[0] = 1 # 因为第一步没有办法通过计算获得,所以需要我们来初始化
if n == 0:
return 1 # 如果为0,直接返回
for i in range(1, n+1): # 因为0已经处理过了,所以可以直接从1开始,i表示在走第几节台阶
# 因为我们记录了第0节台阶,所以整个数组要往后延一个,记n+1
for j in [s for s in steps if s <= i]: # 在1阶时,只循环小于1节的台阶数,j表示可以走的节数
if res[i-j]>0: # 判断前面的两种走法是否有数值
res[i] += res[i-j] # 加上前面的步数
return res[n] # 返回最后一个,即结果
2.2 买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。注意:你不能在买入股票前卖出股票。
输入: [7,1,5,3,6,4] 输出: 5 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
class Solution(object):
def maxProfit(self, prices):
"""
:type prices: List[int]
:rtype: int
"""
minprice = float("inf")
maxprofit = 0
for price in prices:
maxprofit = max(price - minprice, maxprofit)
minprice = min(price, minprice)
return maxprofit
2.3 最大子序和
给定一个整数数组
nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。输入: [-2,1,-3,4,-1,2,1,-5,4], 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
# f[x] = max( nums[x] + f[x-1], nums[x])
res = [-float("inf")] * len(nums)
if not nums:
return 0
if len(nums) == 1:
return nums[0]
maxsum = -float("inf")
res = -float("inf")
for num in nums:
maxsum = max(num+maxsum, num)
res = max(res, maxsum)
return res
2.4 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
输入: [1,2,3,1] 输出: 4 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
# f[x] = num + f[x-2]
res = [0] * (len(nums)+1)
if not nums:
return 0
if len(nums) == 1:
return nums[0]
res[1] = nums[0] # 预设
for i in range(2, len(nums)+1):
res[i] = max(res[i-1], res[i-2] + nums[i-1])
return res[-1]
一开始我总把动态规划和递归弄混,因为在递归中我们也能使用备忘录方法减少递归的重复次数,现在理清楚了动态规划是从0到n记录备忘录,递归则是从n到0记录备忘录,其次,动态规划是直接使用前面的最优解,递归则是自己调用自己算最优解。
做完最大的感受是有种纸上谈兵的错觉,这么好用的算法,难道只能在专门的算法题里才能有所作为了吗?
维特比算法
维特比算法实际是用动态规划解隐马尔科夫模型预测问题,即用动态规划求解概率最大路径(最优路径),这时一条零对应着一个状态序列。【李航. 统计学习方法(第二版)[M]. 北京: 清华大学出版社, 2019: 208-212.】
一开始听维特比贼害怕,现在看了看就那样吧,就是一个动态规划的过程,不过就是除了记录概率变量以外,多记录了一个节点编号而已。