DP 动态规划初识
前面的 HMM 中参数求解, 都会用到动态规划, 全是各种概率公式, 是有一些抽象, 今天决定举个一波简单的栗子, 帮助理解DP 把一个大问题,不断划分为更小的子问题来求解的这种方式, 就是动态规划. 这是最为直观和通俗的理解.
DP vs 递归
我之前也是经常把 DP 和递归弄混淆. 递归, 其实就是, 函数调用自身. 在某种程度上来说, 递归和DP有其相似性. 我的理解是, DP 降低了 递归 的时间复杂度, 具体说, DP解决了递归的重复子问题计算(overlap) 问题. 为了说明其问题, 从 B站找了几段视频来帮助理解.
case: 斐波那契数列
已知: 1, 1, 2, 3, 5, 8, 13, 21, 34 ......
求第 n 个数的值.
其实这就是一个非常典型的, 用递归来表述的一类问题, 递归专注两个因素: 退出条件, 递推关系
"""
主题:
动态规划 之 斐波那契数列
问题特点:
类似递归, 有很多的重复子问题 overlap
描述:
f(1) = 1
f(2) = 1
f(3) = f(2) + f(3)
f(4) = f(3) + f(2)
...
f(n) = f(n-1) + f(n-1)
"""
def rec_opt(n):
"""递归-求解斐波那契数列"""
if n <= 2:
return 1
return rec_opt(n - 1) + rec_opt(n - 2)
非常好理解, 但这种写法会带来什么问题呢? 就是时间复杂度高, 是 \(O(2^n)\), 比如要计算 f(5):
f(5) = f(4) + f(3) 然后分为两叉
左边: f(4) = f(3) + f(2)
f(3) = f(2) + f(1)
右边: f(3) = f(2) + f(1)
咋眼一看, 兄弟, 有没有发现, 左边在计算 f(4) 的时候, 计算了 f(3), f(2), f(1); 而右边在计算 f(3) 的时候, 又在重复计算 f(2), f(1), 随着规模越大, 重复计算的量会呈指数级增长, 这个就叫做 overlap. 这就是递归, 在这个时候, 会极大第增加时间复杂度.
其实我们如何这样想, 既然在计算 f(3) 的时候, 会用到, f(1), f(2); f(4) 的时候, 会用到 f(3) + f(2) ... 那, 我们为什么不会想到, 把这些计算好的历史数据给存起来, 直接用呢?
即, 可以用一个 list 将历史数据给存起来, 要用的时候, 直接索引就ok了, 复杂度是: \(O(n)\) 这样利用历史数据来推测后面的方法, 就是动态规划.
- 退出条件
- 初始值
- 状态转移方程
def dp_opt(n):
"""DP 求解斐波那契数列"""
# 用来存历史数据的列表
lst = [0 for _ in range(n)]
# 退出条件
if n < 2:
return 1
# 初始化数列的第 0,1 项
lst[0], lst[1] = 1, 1
for i in range(2, n):
# 状态转移方程
lst[i] = lst[i - 1] + lst[i - 2]
return lst[n - 1]
正好来测试一波时间对比
def calc_func_time(func, n):
print(f"test 规模为{n}的函数{func}运行时间为:")
start_time = time.time()
func(n)
end_time = time.time()
print("time used", end_time - start_time, "s.")
if __name__ == '__main__':
# 递归就是不容易使用装饰器写计时器来哦
n = 40 # 规模
calc_func_time(dp_opt, n)
calc_func_time(rec_opt, n)
C:\Python\Python36\python.exe E:/Pytest/base_case/DP-斐波那契数列.py
test 规模为40的函数<function dp_opt at 0x00000271927B7840>运行时间为:
time used 0.0 s.
test 规模为40的函数<function rec_opt at 0x0000027190902E18>运行时间为:
time used 39.237488985061646 s.
可以看到, 递归的复杂度真的太可怕了, 而DP非常淡定,
当规模超过1000, 递归, 我的电脑都不动了...而可以测试一波 DP, 更大的.
if __name__ == '__main__':
# 递归就是不容易使用装饰器写计时器来哦
n = 10000 # 1万规模
calc_func_time(dp_opt, n)
test 规模为100000的函数<function dp_opt at 0x0000026675A67840>运行时间为:
time used 0.5376913547515869 s.
DP 果然牛逼! 毕竟复杂度为 \(O(n)\) 嘛
DP 入门级案例
举个简单的栗子来熟悉求解流程即可, 稍微复杂的, 我自己个还在研究, 不敢妄言.....
全篇想说明一个核心技巧: 选 or 不选.
case1: 爬 n阶 台阶
事实是这样的: 小陈同学有一双令人无比羡慕的大长腿, 现在呢, 要去爬一个有 n 阶的台阶, 每次要么向上走1步, 要么向上走3步, 请问, 小陈同学从底部爬到台阶顶端, 一共有多要种可能的方案?
分析:
首先定义一个函数 f(n) 表示爬到第 n 阶级时有 最多的方案数 则
f(1) = 1 最多1种方案, 向上1步
f(2) = 1 最多1中方案, 走1步, 再走1步
f(3) = 2 最多2种方案, 走1步重复3次; 或直接走3步
f(4) = f(1) + f(3) 最多3种方案, 1步重复4次; 先1步,再3步; 先3步, 再1步
...
其实就是 f(n), 在要到达 n 前有2个选择: 要么从选择 f(n-1); 要么选择f(n-3) , 两种情况之和, 就是总方案数
f(n) = f(n-1) + f(n-3)
def dp_craw(n):
# 1. 用list来存储历史数据,可理解为最优方案的函数值
opt = [0 for _ in range(n+1)]
# 2. 写退出条件
if n <= 2:
return 1
elif n == 3:
return 2
# 3. 初始化历史数据
opt[0] = 1
opt[1] = 1
opt[2] = 2
# 4. 状态转移方程
for i in range(2, n):
opt[i] = opt[i-1] + opt[i - 3]
return opt[n-1]
if __name__ == '__main__':
print(dp_craw(50))
C:Python36\python.exe E:/Pytest/base_case/DP爬台阶.py
83316385
当然这这样写还是有重复项的, 复杂度并非 \(O(n)\) 嗯, 不想改了, 主要是阐明这种思路就好.
case2: 给定一堆数字求最大值 (带约束)
"""
描述:
arr = [1, 2, 4, 1, 7, 8, 3]
下标: 0, 1, 2, 3, 4, 5, 6
需求:
目标: 从数组(列表) 中选择任意个元素, 求出其之和最大的值
约束: 相邻元素不能选, 如选了8, 则不能选 7 和 3
思路:
定义一个最优方案函数 opt(n) 表示前n个中最好的选择方案
比如本例, opt(1) = 1, opt(2)=1
tips: 选和不选
抽象: 对于下标i, 有选和不选
if 选择当前值arr[i]:
值 = opt(i-2) + arr[i] 不能选相邻元素, 从前i-2个中选最好方案
else:
值 = opt(i-1) 不选,则中前i-1 个中选择最好方案
最后比较取最大, 即: max( (opt(i-1) + arr[i]), opt(i) )
递归出口:
opt[0] = arr[0] 前下标0个, 即第一位, 只有一种最优方案,就是选第一个值arr[0]
opt[1] = max( arr[0], arr[1]) 前两个,则从1,2中选最大
"""
def rec_opt(arr, i):
"""递归"""
if i == 0:
return arr[0]
elif i == 1:
return max(arr[0], arr[1])
else:
select_i = rec_opt(arr, i - 2) + arr[i]
no_select_i = rec_opt(arr, i - 1)
return max(select_i, no_select_i)
# 递归是产生很多的重叠子问题, overlap 之前演示过与DP的对比,复杂度高很多
def dp_opt(arr):
"""DP实现"""
n = len(arr)
# 用一个list来存储前 i 个最优方案的值, opt 比 lst 要更形象些
opt = [0 for _ in range(n)]
# 初始化前0, 前1的小标下的最优方案值
opt[0] = arr[0]
opt[1] = max(arr[0], arr[1])
# 从第3个元素起, 后面项将前面的项作为其子问题
for i in range(2, n):
# 选择下标为i的值和不选, 比较取最大, 前面的值都有存opt
select_i = opt[i - 2] + arr[i]
no_select_i = opt[i - 1]
opt[i] = max(select_i, no_select_i)
return opt[n - 1]
if __name__ == '__main__':
opt = [1, 2, 4, 1, 7, 8, 3]
opt1 = [4, 1, 1, 9, 1]
print(rec_opt(opt, 6))
print(dp_opt(opt1))
case3: 数字之和为定值
"""
描述:
arr = [3, 34, 4, 12, 5, 12]
S = 9
需求:
从 arr 中选择数字, 使其值和等于定值 S=9, 如果可以返回 True, 否则 False
思路:
跟之前一样的思路: 对于每个元素, 有两种选择,选 or 不选
定义一个subset(arr, i, s)
if 选择arr[i]:
A = subset(arr, i, s-arr[i])
else:
B = subset(arr, i-1, s)
A or B 是True 则True
退出条件:
if sub_set(arr[i], s) 中 s=0 则 return True
if sub_set(arr[0], s) 中 arr[0] != s return False
还约定, arr的每个元素都是正整数.即当 arr[i] > s, 只考虑不选, 即: sub_set(arr[i-1], s)
"""
def rec_subset(arr, i, s):
if s == 0:
return True
elif i == 0:
return arr[0] == s
elif arr[i] > s:
return rec_subset(arr, i - 1, s)
else:
# arr[i] < s, 有选和不选两种情况
select_i = rec_subset(arr, i - 1, s - arr[i])
no_select_i = rec_subset(arr, i - 1, s)
# 二者其中一个满足条件即可
return select_i or no_select_i
import numpy as np
# 用2维数组来记录,行代表arr[i], 列代表s=1, s=2...
def dp_subset(arr, s):
"""DP求解"""
subset = np.zeros((len(arr), s + 1), dtype=bool)
subset[:, 0] = True
subset[0, :] = False
subset[0, arr[0]] = True
for i in range(1, len(arr)):
for s in range(1, s + 1):
if arr[i] > s:
subset[i, s] = subset[i - 1, s]
else:
select_i = subset[i - 1, s - arr[i]]
no_select_i = subset[i - 1, s]
subset[i, s] = select_i or no_select_i
row, col = subset.shape
return subset[row - 1, col - 1]
if __name__ == '__main__':
lst = [3, 34, 4, 12, 5, 12]
print(rec_subset(lst, len(lst) - 1, 9))
print(dp_subset(lst, 9))
True
True
入门就到这吧, 后面再研究一波复杂的DP. 感觉还是非常锻炼思维能力的. 嗯, 咋说呢, 我感觉对我有点难, 尤其是多维数组, 图, 树这一块的抽象... 用我同事的话说, 就是我, 数论可以, 但空间不行...这感觉, 老天爷不赏饭呀....
DP 动态规划初识的更多相关文章
- Day 5 笔记 dp动态规划
Day 5 笔记 dp动态规划 一.动态规划的基本思路 就是用一些子状态来算出全局状态. 特点: 无后效性--狗熊掰棒子,所以滚动什么的最好了 可以分解性--每个大的状态可以分解成较小的步骤完成 dp ...
- (转)dp动态规划分类详解
dp动态规划分类详解 转自:http://blog.csdn.NET/cc_again/article/details/25866971 动态规划一直是ACM竞赛中的重点,同时又是难点,因为该算法时间 ...
- 【模板整合计划】DP动态规划
[模板整合计划]DP动态规划 一:[背包] 1.[01背包] 采药 \([P1048]\) #include<algorithm> #include<cstdio> int T ...
- DP动态规划学习笔记——高级篇上
说了要肝的怎么能咕咕咕呢? 不了解DP或者想从基础开始学习DP的请移步上一篇博客:DP动态规划学习笔记 这一篇博客我们将分为上中下三篇(这样就不用咕咕咕了...),上篇是较难一些树形DP,中篇则是数位 ...
- 树形DP——动态规划与数据结构的结合,在树上做DP
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是算法与数据结构的第15篇,也是动态规划系列的第4篇. 之前的几篇文章当中一直在聊背包问题,不知道大家有没有觉得有些腻味了.虽然经典的文 ...
- 初识DP动态规划
一.多阶段决策过程的最优化问题 在现实生活中,有类活 动的过程,由于 它的特殊性,可将过程分成若干个互相阶段.在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果.当阶段决策的选取不是任意确 ...
- [原]POJ1141 Brackets Sequence (dp动态规划,递归)
本文出自:http://blog.csdn.net/svitter 原题:http://poj.org/problem?id=1141 题意:输出添加括号最少,并且使其匹配的串. 题解: dp [ i ...
- DP动态规划练习
先来看一下经典的背包问题吧 http://www.cnblogs.com/Kalix/p/7617856.html 01背包问题 https://www.cnblogs.com/Kalix/p/76 ...
- 5. Longest Palindromic Substring(最长回文子串 manacher 算法/ DP动态规划)
Given a string s, find the longest palindromic substring in s. You may assume that the maximum lengt ...
- 摆花 (DP动态规划)
2012_p3 摆花 (flower.cpp/c/pas) 时间限制: 1 Sec 内存限制: 128 MB提交: 17 解决: 10[提交][状态][讨论版][命题人:外部导入] 题目描述 3. ...
随机推荐
- LINUX 服务器安装nginx redis jdk等步聚
1.安装指令步聚 sudo yum update 更新linux系统 yum install -y nginx 安装nginx systemctl enable nginx 设置开机启动nginx s ...
- 【Blender】杂项笔记
[Blender]杂项笔记 空间坐标系 Blender 中的轴向: Y 轴向前(前视图看向的方向就是前方,其默认向 Y 轴看) Z 轴向上 保持轴向导出到 Unity 时(包括直接保存.导出 FBX ...
- Flink学习(十八) 状态管理与状态编程
Flink中的状态 由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态:可以认为状态就是一个本地变量,可以被任务的业务逻辑访问:Flink会进行状态管理,包括状态一致性,故障处理以及 ...
- PHP中&&与and、||与or的区别
https://blog.csdn.net/asty9000/article/details/80652064 在PHP中,&&与and都表示逻辑与,||与or都表示逻辑或,并且它们都 ...
- Git pull(拉取),push(上传)命令整理(详细)
转自:https://www.cnblogs.com/wbl001/p/11495110.html (文档较长,请大家耐心阅读,很有帮助) git比较本地仓库和远程仓库的差异 更新本地的远程分支 gi ...
- python包管理工具pip使用手册
pip是什么? pip 是 Python 标准库管理器,也就是一个工具让你安装不同的类库来使用. 当你要安装某些类库时,都会使用 pip,那 pip 除了安装类库之外,还能做什么呢? 首先,我们进入 ...
- Mac 安装php Swoole扩展出现 Enable openssl support, require openssl library 或者fatal error: 'openssl/ssl.h' file not found
Mac 安装php Swoole扩展时出现 Enable openssl support, require openssl library 或者fatal error: 'openssl/ssl.h' ...
- HTML5
转
贴个图:
- C#多线程编程(二)线程池与TPL
一.直接使用线程的问题 每次都要创建Thread对象,并向操作系统申请创建一个线程,这是需要耗费CPU时间和内存资源的. 无法直接获取线程函数返回值 无法直接捕捉线程函数内发生的异常 使用线程池可以解 ...
- Tengine-rpm 基于Tengine 3.1深度定制优化
Tengine RPM Tengine是亚洲最大的电子商务网站淘宝网推出的高性能的HTTP和反向代理web服务器.它基于 Nginx HTTP 服务器,拥有许多高级功能.事实证明,Tengine 在淘 ...