斐波那契數列:Fn = Fn-1 + Fn-2python
Fibonacci:除第一個和第二個數外,任意一個數可由前兩個數相加獲得。算法
使用遞歸和非遞歸的方法來求解斐波那契數列的第n項。app
def fibnacci(n): """ 遞歸版本——斐波那契函數 :param n: :return: """ if n == 1 or n == 2: return 1 else: return fibnacci(n-1) + fibnacci(n-2) # 動態規劃(DP)的思想 = 最優子結構 = 遞推式 def fibnacci_no_recurision(n): """ 非遞歸版本——斐波那契 :param n: :return: """ 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(100)) # 須要計算好久 print(fibnacci_no_recurision(100)) # 354224848179261915075
經過上例能夠發現遞歸版原本計算斐波那契速度比非遞歸慢不少不少。函數
這是因爲相同的問題算了不少遍,致使速度很慢。spa
爲何遞歸很慢:子問題的重複計算 f(5) = f(4)+f(3) f(4) = f(3)+f(2) f(3) = f(2)+f(1) f(2) = 1 f(1) = 1
而非遞歸版本就用到了 動態規劃(DP)的思想 = 最優子結構 = 遞推式 + 重複子問題3d
某公司出售鋼條,出售價格與鋼條長度之間的關係以下表:blog
問題:現有一段長度爲n的鋼條和上面的價格表,求切割鋼條方案,使得總收益最大。遞歸
答:長度爲n的鋼條有n-1個能夠切割的位置。每一個能夠切割的位置,都有切和不切兩種選擇。所以切割方案有2n-1個。ci
但若是過b\d這樣的狀況看作是一種方案,這就比較難了,在組合數學裏的叫整數分割問題。字符串
好比鋼條長度是4,總體賣的話價格是9,但若是切成2+2,則能夠賣出10的價格;若是鋼條長度是9,總體賣出價格是24,分拆爲6+3,6最高可賣17,3最高可賣8,所以9能夠賣出25的價格。
設長度爲n的鋼條切割後最優收益值爲rn,能夠得出遞推式:
rn = max(pn, r1 + rn-1, r2 + rn-2, ..., rn-1 + r1)
能夠將求解規模爲n的原問題,劃分爲規模更小的子問題:完成一次切割後,能夠將產生的兩段鋼條當作兩個獨立的鋼條切割問題。
組合兩個子問題的最優解,並在全部可能的兩段切割方案中選取組合收益最大的,構成原問題的最優解。
鋼條切割知足最優子結構:問題的最優解由相關子問題的最優解組合而成,這些子問題能夠獨立求解。
鋼條切割問題還存在更簡單的遞歸求解方法:
import time def cal_time(func): def wrapper(*args, **kwargs): t1 = time.time() result = func(*args, **kwargs) t2 = time.time() print("%s running time: %s secs." % (func.__name__, t2 - t1)) return result return wrapper # 鋼條長度對應的價格 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_recurision_1(p, n): """ 鋼條切割——遞歸版(兩邊切割) :param p: 鋼條價格 :param n: 鋼條長度 :return: """ 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 @cal_time def c1(p, n): return cut_rod_recurision_1(p, n) def cut_rod_recurision_2(p, n): """ 鋼條切割——遞歸版(只一邊切割) :param p:鋼條價格 :param n:鋼條長度 :return: """ if n == 0: return 0 else: res = 0 for i in range(1, n+1): # 從1到n,即1~n+i res = max(res, p[i] + cut_rod_recurision_2(p, n-i)) return res @cal_time def c2(p, n): return cut_rod_recurision_2(p, n) print(cut_rod_recurision_1(p, 9)) # 25 print(c1(p, 10)) print(c2(p, 10)) # 因爲每次都少遞歸一次,效率高了不少 """ c1 running time: 0.013891935348510742 secs. 27 c2 running time: 0.0005898475646972656 secs. 27 """
自頂向下遞歸實現的效率這麼差,緣由分析:
僅分析n=4的狀況就發現有大量相同子問題重複求解。
遞歸算法因爲重複求解相同子問題,效率極低。時間複雜度:O(2n)
動態規劃的思想:
@cal_time def cut_rod_dp(p, n): """ 鋼條切割——動態規劃 :param p:鋼條價格 :param n:鋼條長度 :return: """ r = [0] # 長度爲0時,最優收益爲0 for i in range(1, n+1): # 計算出r1,r2,...,rn的最優收益 res = 0 # ri最小值默認是0 for j in range(1, i+1): # ri的i種方案 res = max(res, p[j] + r[i-j]) r.append(res) # 添加最優值 return r[n] print(cut_rod_dp(p, 10)) print(cut_rod_dp(p, 20)) """ cut_rod_dp running time: 2.7894973754882812e-05 secs. 27 cut_rod_dp running time: 7.104873657226562e-05 secs. 56 """
時間複雜度:O(n2),這是因爲每一個都是直接去取以前存好的值,而不是把值再算一遍,以下圖所示:
如何修改動態規劃算法,使其不只輸出最優解,還輸出最優切割方案?
針對每一個子問題,保存切割一次時左邊切下的長度。
上表中,長度爲i,r[i]是對應長度時最優收益,s[i]是左邊切下的長度。
# 鋼條長度對應的價格 # p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30] def cut_rod_extent(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 r, s = cut_rod_extent(p, 10) print(s) # 輸出左邊切下的長度 """ [0, 1, 2, 3, 2, 2, 6, 1, 2, 3, 10] """ def cut_rod_solution(p, n): """重構解——輸出結果""" r, s = cut_rod_extent(p, n) ans = [] # 保存最後切成的樣子 while n > 0: ans.append(s[n]) # 保存左邊切下的長度 n -= s[n] # 剩下的長度 return ans print(cut_rod_solution(p, 9)) # [3, 6]
若是使用以前的鋼條長度價格表:p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]
print(cut_rod_dp(p, 20)) # 56 print(cut_rod_solution(p, 20)) # [2, 6, 6, 6] r, s = cut_rod_extent(p, 20) print(s) # [0, 1, 2, 3, 2, 2, 6, 1, 2, 3, 2, 2, 6, 1, 2, 3, 2, 2, 6, 1, 2]
什麼問題可使用動態規劃方法?
(1)最優子結構
(2)重疊子問題
一個序列的子序列是在該序列中刪去若干元素後獲得的序列。例如:「ABCD」和「BDF」都是「ABCDEFG」的子序列。
最長公共子序列(Longest Common Subsequence,簡寫LCS)問題:給定兩個序列X和Y,求X和Y長度最大的公共子序列。例如:X=「ABBCBDE」, Y="DBBCDB", LCS(X,Y)="BBCD"
應用場景:字符串類似度比對、基因比對。
令X=<x1,x2,...,xm>和Y=<y1,y2,...,yn>爲兩個序列,Z=<z1,z2,...,zk>爲X和Y的任意LCS。
1.若是xm=yn,則zk=xm=yn且Zk-1是Xm-1和Yn-1的一個LCS。
2.若是xm≠yn,那麼zk≠xm意味着Z是Xm-1和Y的一個LCS。
3.若是xm≠yn,那麼zk≠yn意味着Z是X和Yn-1的一個LCS。
c[i,j]表示Xi和Yj的LCS長度。
例如:要求a="ABCBDAB"與b="BDCABA"的LCS:
因爲最後一位"B"≠"A",所以LCS(a,b)應該來源於LCS(a[:-1],b)與LCS(a,b[:-1])中的一個。意思就是最後一位不同,要不去除a的最後一位,要不去除b的最後一位。
def lcs_length(x, y): # x,y是列表或字符串 # 查看x,y的長度 m = len(x) n = len(y) # 用二維列表生成式生成一個m+1行,n+1列的二維列表 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 # 若i,j>0,且xi==yi else: c[i][j] = max(c[i-1][j], c[i][j-1]) # 若i,j>0,且xi!=yi for _ in c: # 將列表c逐行打印 print(_) return c[m][n] # print(lcs_length("ABCBDAB", "BDCABA")) # 4 """ [0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 1, 1, 1] [0, 1, 1, 1, 1, 2, 2] [0, 1, 1, 2, 2, 2, 2] [0, 1, 1, 2, 2, 3, 3] [0, 1, 2, 2, 2, 3, 3] [0, 1, 2, 2, 3, 3, 4] [0, 1, 2, 2, 3, 4, 4] """ 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 c, b = lcs("ABCBDAB", "BDCABA") for _ in b: print(_) """ [0, 0, 0, 0, 0, 0, 0] [0, 2, 2, 2, 1, 3, 1] [0, 1, 3, 3, 2, 1, 3] [0, 2, 2, 1, 3, 2, 2] [0, 1, 2, 2, 2, 1, 3] [0, 2, 1, 2, 2, 2, 2] [0, 2, 2, 2, 1, 2, 1] [0, 1, 2, 2, 2, 1, 2] """ def lcs_trackback(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: # ==3,來自左方=》不匹配 j -= 1 print(res) return "".join(reversed(res)) print(lcs_trackback("ABCBDAB", "BDCABA")) # BCBA