数据结构和算法进阶笔记--14动态规划

动态规划

例子:斐波那契数列: F n = F n − 1 + F n − 2 F_n = F_{n-1} + F_{n-2} Fn=Fn1+Fn2
使用递归和非递归的方法来求解斐波那契数列的第n项。
F 1 = 1 , F 2 = 1 F_1 = 1, F_2 = 1 F1=1,F2=1

# 子问题的重复计算
# f(5) = f(4) + f(3)
# f(4) = f(3) + f(2)
# f(3) = f(2) + f(1)	# f(5)中的f(3)
# f(3) = f(2) + f(1)	# f(4)中的f(3)
# 其中 f(3) 被计算了两次,递归就会重复计算
def fibnacci(n):
	if n == 1 or n == 2:
		return 1
	else:
		return fibnacci(n-1) + fibnacci(n-2)

# 动态规划(Dynamic Program, DP)思想 = 递推式 + 重复子问题
def fibnacci_no_recurision(n):
	f = [0, 1, 1]
	if n > 2:
		for i in range(n-2):
			num = f[-1] + f[-2]
			f.append(f)
	return f[n]

钢条切割问题

某公司出售钢条,出售价格与钢条长度之间的关系如下表:
钢条切割
问:现有一段长度为n的钢条和上面的价格表,求切割钢条方案,使得总收益最大,则如何切割。

长度为4的钢条的所有切割方案如下:(c方案最优)
切割方案
思考:长度为n的钢条的不同切割方式有几种?
2 n 2^n 2n,长度为n的钢条有n-1个切割点,则且刚方案有 C n − 1 0 + C n − 1 1 + . . . + C n − 1 n − 2 + C n − 1 n − 1 = 2 n C_{n-1}^0 + C_{n-1}^1 + ... +C_{n-1}^{n-2} + C_{n-1}^{n-1} = 2^n Cn10+Cn11+...+Cn1n2+Cn1n1=2n, 因此穷举法没办法计算。

下图为长度为n的最优切割价格表,i为长度,r[i]为长度为i的最优价格。
切割结果
递推式:
设长度为n的钢条,切割后最优收益为 r n r_n rn,可以得出递推式:

  • r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , . . . , r n − 1 + r 1 ) r_n = max(p_n, r_1 + r_{n-1}, r_2+r_{n-2},...,r_{n-1}+r_1) rn=max(pn,r1+rn1,r2+rn2,...,rn1+r1)
    第一个参数 p n p_n pn表示不切割
    其他n-1个参数分别表示另外n-1种不同切割方案,对方案i=1,2,…,n-1
  • 将钢条切割为长度为i和n-i两段
  • 方案i的收益为切割两段的最优收益之和

考察所有的i,选择其中收益最大的方案

最优子结构
可以将求解规模为n的原问题,划分为规模更小的子问题:完成一次切割后可以将产生的两段钢条看成两个独立的钢条切割问题。

组合两个子问题的最优解,并在所以可能的两段切割方案中选取组合收益最大的,构成原问题的最优解。

钢条切割满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解。


钢条切割问题还存在更简单的递归求解方法:

  • 从钢条左边切割下单独为i的一端,支队右边剩下的一端继续进行切割,左边不再切割
  • 递推式简化为 r n = m a x 1 ≤ i ≤ n ( p i + r n − i ) r_n = \underset{1\le i \le n}{max}(p_i + r_{n-i}) rn=1inmax(pi+rni)
  • 不做切割的方案就可以描述为:左边一段长度为n,收益为 p n p_n pn,剩余一段长度为0,收益为 r 0 = 0 r_0 = 0 r0=0

自顶向下实现

p = [0,1,5,8,9,10,17,17,20,24,30] # p为价格表,其中下标i为钢条长度,对应的p[i] 为i长度钢条的价格
def cut_rod_recurision_1(p, n):
# 原递推式子
	if n == 0:
		return 0
	else:
		res = p[n]
		for i in range(1, n):
			res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n-i))
	return res

def cut_rod_recurision_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_recurision_2(p, n-i))
		return res

自顶

动态规划解法

递归算法由于重复求解相同子问题,效率很低
动态规划思想:

  • 每个子问题只求解一次,保存求解结果
  • 之后需要此问题时,只需要查找保存的结果

自底向上的算法

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]

时间复杂度:O( n 2 n^2 n2)
自底向上

输出最优切割方法–重构解

重构解
s[i]保存为最优方案中左侧保留的长度(不切的部分)

def cut_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 = cut_rod_extend(p, n)
	ans = []
	while n > 0:
		ans.append(s[n])
		n -= s[n]
	return ans

什么问题可以用动态规划:
1. 最优子结构

  • 原问题的最优解中涉及多少个子问题
  • 在确定最优解使用哪些子问题时,需要考虑多少种选择

2. 重叠子问题

最长公共子序列

一个序列的子序列是在该序列中删去若干元素后得到的序列

  • 例:“ABCD”和“BDF”都是“ABCDEFG”的子序列

最长公共子序列(LCS)问题:给定两个序列X和Y,求X和Y长度最大的公共子序列。

  • 例:X = “ABBCBDE” Y = “DBBCDB” LCS(X, Y) = “BBCD”

应用场景:字符串相似度比对
思考:暴力穷举法的时间复杂度?
思考:是否有最优子序列
最长公共序列
例如:要求a=“ABCBDAB”, b=“BDCABA”。求这两个的LCS

  • 由于最后一位“B” ≠ \neq =“A”:
    • 因此LCS(a,b)应该来源于LCS(a[:-1], b)与 LCS(a,b[:-1])中更大的那一个

递推图

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]: 	# i 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]: 	# i 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
		else: 	# 来自左方
			j -= 1
	return "".join(reversed(res))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值