1.从斐波那契数列看动态规划
递归方法解决斐波那契存在的问题:
子问题重复计算
非递归的解法就是一种动态规划(DP)的思想:
1.最优子结构/递推式
2.重复子问题的结果记录下来,避免重复计算
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author: Xiang Hai
# wechat: xiaoyou42952
"""
从斐波那契数列看动态规划
递归方法解决斐波那契存在的问题:
子问题重复计算
非递归的解法就是一种动态规划(DP)的思想:
1.最优子结构/递推式
2.重复子问题的结果记录下来,避免重复计算
"""
def fibnacci(n):
"""递归方法"""
if n == 1 or n == 2:
return 1
else:
return fibnacci(n-1) + fibnacci(n-2)
print(fibnacci(10))
def fibnacci_no_recur(n):
"""非递归"""
f = [0, 1, 1]
if n > 2:
for i in range(n-2):
num = f[-1] + f[-2]
f.append(num)
return f[n]
print(fibnacci_no_recur(100))
2.钢条切割问题
某公司出售钢条,不同长度价格不同
问题:现有一段长度为n的钢条和价格表,求切割钢条方案,使得总收益最大?
思考,长度为n的钢条有多少种切割方案?
2^(n-1): 有n-1个可以切割的位置,可以切也可以不切,即是2的n-1次方
设长度为n的钢条切割后最优收益值为rn,可以得出递推式:
rn = max(pn, r1 + r(n-1), r2 + r(n-2), r3 + r(n-3), …, r(n-1) + r1)
第一个参数pn表示不切割
其他n-1个参数分别表示另外n-1种不同切割方案,对方案i=1,2,…,n-1
将钢条切割为长度为i和n-i两段
方案i的收益ri为其长度的最优收益
考察所有的i,选择其中收益最大的方案
最优子结构:
可以将求解规模为n的原问题,划分为规模更小的子问题:完成一次切割
后,可以将产生的两段钢条看成两个独立的钢条切割问题
组合两个子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解
钢条切割刚好满足最优子结构:问题的最优解由相关子问题的最优解组合成,这些问题可以独立求解
钢条切割问题还存在更简单的递归求解方法(cut_rod_recursion_2):
从钢条左边切割下长度为i的一段,只对右边剩下的一段继续切割,左边的不再切割
递推式简化为 rn = max(pi + r(n-i), …) i取值从1到n
不做切割的方案就可以描述为: 左边一段长度为n,收益为pn,剩余一段长度为0, 收益为r0=0
动态规划解法: (时间复杂度 O(n^2))
1.每个子问题只求解一次,保存求解结果
2.之后需要该子问题,只要查询保存结果
自底向上求解
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author: Xiang Hai
# wechat: xiaoyou42952
"""
某公司出售钢条,不同长度价格不同
问题:现有一段长度为n的钢条和价格表,求切割钢条方案,使得总收益最大?
思考,长度为n的钢条有多少种切割方案?
2^(n-1): 有n-1个可以切割的位置,可以切也可以不切,即是2的n-1次方
设长度为n的钢条切割后最优收益值为rn,可以得出递推式:
rn = max(pn, r1 + r(n-1), r2 + r(n-2), r3 + r(n-3), ..., r(n-1) + r1)
第一个参数pn表示不切割
其他n-1个参数分别表示另外n-1种不同切割方案,对方案i=1,2,...,n-1
将钢条切割为长度为i和n-i两段
方案i的收益ri为其长度的最优收益
考察所有的i,选择其中收益最大的方案
最优子结构:
可以将求解规模为n的原问题,划分为规模更小的子问题:完成一次切割
后,可以将产生的两段钢条看成两个独立的钢条切割问题
组合两个子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解
钢条切割刚好满足最优子结构:问题的最优解由相关子问题的最优解组合成,这些问题可以独立求解
钢条切割问题还存在更简单的递归求解方法(cut_rod_recursion_2):
从钢条左边切割下长度为i的一段,只对右边剩下的一段继续切割,左边的不再切割
递推式简化为 rn = max(pi + r(n-i), ...) i取值从1到n
不做切割的方案就可以描述为: 左边一段长度为n,收益为pn,剩余一段长度为0, 收益为r0=0
"""
p = [0,1,5,8,9,10,17,17,20,21,23,24,26,27,27,28,30,33,36,39,40]
def cut_rod_recursion(p, n):
"""递归,自顶向下实现,不推荐,有大量重复计算"""
if n == 0:
return 0
else:
res = p[n]
for i in range(1, n):
res = max(res, cut_rod_recursion(p, i) + cut_rod_recursion(p, n-i))
return res
print(cut_rod_recursion(p,10))
def cut_rod_recursion_2(p, n):
"""还是递归,自顶向下实现,但左边不切割,依然有大量重复计算"""
if n == 0:
return 0
else:
res = 0
for i in range(1, n+1):
res = max(res, p[i]+cut_rod_recursion(p, n-i))
return res
print(cut_rod_recursion_2(p,10))
"""
动态规划解法: (时间复杂度 O(n^2))
1.每个子问题只求解一次,保存求解结果
2.之后需要该子问题,只要查询保存结果
自底向上求解
"""
def cut_rod_dp(p, n):
r = [0]
for i in range(1, n+1):
res = 0
for j in range(1, i+1):
res = max(res, p[j] + r[i-j])
r.append(res)
return r[n]
print(cut_rod_dp(p, 20))
"""
如何修改动态规划算法,使其不仅输出最优解,还输出最优解的切割方案?
对每个子问题,保存切割一次时左边切下的长度
"""
def cur_rod_extend(p, n):
r = [0]
s = [0]
for i in range(1, n+1):
res_r = 0 # 价格最大值
res_s = 0 # 对应的左边不切割长度
for j in range(1, i+1):
if p[j] + r[i-j] > res_r:
res_r = p[j] + r[i-j]
res_s = j
r.append(res_r)
s.append(res_s)
return r[n],s
def cut_rod_solution(p, n):
r, s = cur_rod_extend(p, n)
ans = []
while n > 0:
ans.append(s[n])
n -= s[n]
return r, ans
print(cut_rod_solution(p, 20))
3.最长公共子序列
最长公共子序列(LCS)
暴力穷举法的时间复杂度:O(2^(m+n))
最长公共子序列是否有最优子结构?
有
例如:要求a = “ABCBDAB” 与 b=“BDCABA"的LCS:
由于最后一位"B” != “A”
所以LCS(a,b)是LCS(a[:-1],b)与LCS(a,b[:-1])中更长的LCS
递推式:
c[i,j] = 0 if i = 0 or j = 0
c[i,j] = c[i-1,j-1]+1 if i,j>0 and xi = yi
c[i,j] = max(c[i,j-1],c[i-1,j]) if i,j>0 and xi != yi
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author: Xiang Hai
# wechat: xiaoyou42952
"""
最长公共子序列(LCS)
暴力穷举法的时间复杂度:O(2^(m+n))
最长公共子序列是否有最优子结构?
有
例如:要求a = "ABCBDAB" 与 b="BDCABA"的LCS:
由于最后一位"B" != "A"
所以LCS(a,b)是LCS(a[:-1],b)与LCS(a,b[:-1])中更长的LCS
递推式:
c[i,j] = 0 if i = 0 or j = 0
c[i-1,j-1]+1 if i,j>0 and xi = yi
max(c[i,j-1],c[i-1,j]) if i,j>0 and xi != yi
"""
def lcs_length(x, y):
m = len(x)
n = len(y)
c = [[0 for _ in range(n+1)] for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if x[i-1] == y[j-1]:
c[i][j] = c[i-1][j-1] + 1
else:
c[i][j] = max(c[i-1][j], c[i][j-1])
return c[m][n]
def lcs(x, y):
m = len(x)
n = len(y)
c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
b = [[0 for _ in range(n + 1)] for _ in range(m + 1)] # 1 左上方 2 上方 3 左方
for i in range(1, m+1):
for j in range(1, n+1):
if x[i-1] == y[j-1]:
c[i][j] = c[i-1][j-1] + 1
b[i][j] = 1
elif c[i-1][j] > c[i][j-1]:
c[i][j] = c[i-1][j]
b[i][j] = 2
else:
c[i][j] = c[i][j-1]
b[i][j] = 3
return c[m][n], b
def lcs_traceback(x, y):
c, b = lcs(x, y)
i = len(x)
j = len(y)
res = []
while i > 0 and j > 0:
if b[i][j] == 1:
res.append(x[i-1])
i -= 1
j -= 1
elif b[i][j] == 2:
i -= 1
elif b[i][j] == 3:
j -= 1
return "".join(reversed(res))
print(lcs_traceback("ABCBDAB", "BDCABA"))