(轉)python自頂向下設計步驟_python實現自頂向下,自底向上


原文:https://blog.csdn.net/weixin_39765209/article/details/110771081

常用的算法設計思想主要有動態規划、貪婪法、隨機化算法、回溯法等等,這些思想有重疊的部分,當面對一個問題的時候,從這幾個思路入手往往都能得到一個還不錯的答案。

本來想把動態規划單獨拿出來寫三篇文章呢,后來發現自己學疏才淺,實在是只能講一些皮毛,更深入的東西嘗試構思了幾次,也沒有什么進展,打算每種設計思想就寫一篇吧。

動態規划(Dynamic Programming)是一種非常有用的用來解決復雜問題的算法,它通過把復雜問題分解為簡單的子問題的方式來獲得最優解。

一、自頂向下和自底向上

總體上來說,我們可以把動態規划的解法分為自頂向下和自底向上兩種方式。

一個問題如果可以使用動態規划來解決,那么它必須具有“最優子結構”,簡單來說就是,如果該問題可以被分解為多個子問題,並且這些子問題有最優解,那這個問題才可以使用動態規划。

自頂向下(Top-Down)

自頂向下的方式其實就是使用遞歸來求解子問題,最終解只需要調用遞歸式,子問題逐步往下層遞歸的求解。我們可以使用緩存把每次求解出來的子問題緩存起來,下次調用的時候就不必再遞歸計算了。

舉例著名的斐波那契數列的計算:

#!/usr/bin/env python

# coding:utf-8

def fib(number):

if number == 0 or number == 1:

return 1

else:

return fib(number - 1) + fib(number - 2)

if __name__ == '__main__':

print fib(35)

有一點開發經驗的人就能看出,fib(number-1)和fib(number-2)會導致我們產生大量的重復計算,以上程序執行了14s才出結果,現在,我們把每次計算出來的結果保存下來,下一次需要計算的時候直接取緩存,看看結果:

#!/usr/bin/env python

# coding:utf-8

cache = {}

def fib(number):

if number in cache:

return cache[number]

if number == 0 or number == 1:

return 1

else:

cache[number] = fib(number - 1) + fib(number - 2)

return cache[number]

if __name__ == '__main__':

print fib(35)

耗費時間為 0m0.053s 效果提升非常明顯。

自底向上(Bottom-Up)

自底向上是另一種求解動態規划問題的方法,它不使用遞歸式,而是直接使用循環來計算所有可能的結果,往上層逐漸累加子問題的解。

我們在求解子問題的最優解的同時,也相當於是在求解整個問題的最優解。其中最難的部分是找到求解最終問題的遞歸關系式,或者說狀態轉移方程。

這里舉一個01背包問題的例子:

你現在想買一大堆算法書,需要很多錢,所以你打算去搶一個商店,這個商店一共有n個商品。問題在於,你只能最多拿 W kg 的東西。wi和vi分別表示第i個商品的重量和價值。我們的目標就是在能拿的下的情況下,獲得最大價值,求解哪些物品可以放進背包。對於每一個商品你有兩個選擇:拿或者不拿。

首先我們要做的就是要找到“子問題”是什么,我們發現,每次背包新裝進一個物品,就可以把剩余的承重能力作為一個新的背包來求解,一直遞推到承重為0的背包問題:

作為一個聰明的賊,你用m[i,w]表示偷到商品的總價值,其中i表示一共多少個商品,w表示總重量,所以求解m[i,w]就是我們的子問題,那么你看到某一個商品i的時候,如何決定是不是要裝進背包,有以下幾點考慮:

該物品的重量大於背包的總重量,不考慮,換下一個商品;

該商品的重量小於背包的總重量,那么我們嘗試把它裝進去,如果裝不下就把其他東西換出來,看看裝進去后的總價值是不是更高了,否則還是按照之前的裝法;

極端情況,所有的物品都裝不下或者背包的承重能力為0,那么總價值都是0;

由以上的分析,我們可以得出m[i,w]的狀態轉移方程為:

有了狀態轉移方程,那么寫起代碼來就非常簡單了,首先看一下自頂向下的遞歸方式,比較容易理解:

#!/usr/bin/env python

# coding:utf-8

cache = {}

items = range(0,9)

weights = [10,1,5,9,10,7,3,12,5]

values = [10,20,30,15,40,6,9,12,18]

# 最大承重能力

W = 4

def m(i,w):

if str(i)+','+str(w) in cache:

return cache[str(i)+','+str(w)]

result = 0

# 特殊情況

if i == 0 or w == 0:

return 0

# w < w[i]

if w < weights[i]:

result = m(i-1,w)

# w >= w[i]

if w >= weights[i]:

# 把第i個物品放入背包后的總價值

take_it = m(i-1,w - weights[i]) + values[i]

# 不把第i個物品放入背包的總價值

ignore_it = m(i-1,w)

# 哪個策略總價值高用哪個

result = max(take_it,ignore_it)

if take_it > ignore_it:

print 'take ',i

else:

print 'did not take',i

cache[str(i)+','+str(w)] = result

return result

if __name__ == '__main__':

# 背包把所有東西都能裝進去做假設開始

print m(len(items)-1,W)

改造成非遞歸,即循環的方式,從底向上求解:

#!/usr/bin/env python

# coding:utf-8

cache = {}

items = range(1,9)

weights = [10,1,5,9,10,7,3,12,5]

values = [10,20,30,15,40,6,9,12,18]

# 最大承重能力

W = 4

def knapsack():

for w in range(W+1):

cache[get_key(0,w)] = 0

for i in items:

cache[get_key(i,0)] = 0

for w in range(W+1):

if w >= weights[i]:

if cache[get_key(i-1,w-weights[i])] + values[i] > cache[get_key(i-1,w)]:

cache[get_key(i,w)] = values[i] + cache[get_key(i-1,w-weights[i])]

else:

cache[get_key(i,w)] = cache[get_key(i-1,w)]

else:

cache[get_key(i,w)] = cache[get_key(i-1,w)]

return cache[get_key(8,W)]

def get_key(i,w):

return str(i)+','+str(w)

if __name__ == '__main__':

# 背包把所有東西都能裝進去做假設開始

print knapsack()

從這里可以看出,其實很多動態規划問題都可以使用循環替代遞歸求解,他們的區別在於,循環方式會窮舉出所有可能用到的數據,而遞歸只需要計算那些對最終解有幫助的子問題的解,但是遞歸本身是很耗費性能的,所以具體實踐中怎么用要看具體問題具體分析。

最長公共子序列(LCS)

解決了01背包問題之后,我們對“子問題”和“狀態轉移方程”有了一點點理解,現在趁熱打鐵,來試試解決LCS問題:

字符串一“ABCDABCD”和字符串二”BDCFG”的公共子序列(不是公共子串,不需要連續)是BDC,現在給出兩個確定長度的字符串X和Y,求他們的最大公共子序列的長度。

首先,我們還是找最優子結構,即把問題分解為子問題,X和Y的最大公共子序列可以分解為X的子串Xi和Y的子串Yj的最大公共子序列問題。

其次,我們需要考慮Xi和Yj的最大公共子序列C[i,j]需要符合什么條件:

如果兩個串的長度都為0,則公共子序列的長度也為0;

如果兩個串的長度都大於0且最后面一位的字符相同,則公共子序列的長度是C[i−1,j−1]的長度加一;

如果兩個子串的長度都大於0,且最后面一位的字符不同,則最大公共子序列的長度是C[i−1,j]和C[i,j−1]的最大值;

最后,根據條件獲得狀態轉移函數:

由此轉移函數,很容易寫出遞歸代碼:

#!/usr/bin/env python

# coding:utf-8

cache = {}

# 為了下面表示方便更容易理解,數組從1開始編號

# 即當i,j為0的時候,公共子序列為0,屬於極端情況

A = [0,'A','B','C','B','D','A','B','E','F']

B = [0,'B','D','C','A','B','A','F']

def C(i,j):

if get_key(i,j) in cache:

return cache[get_key(i,j)]

result = 0

if i > 0 and j > 0:

if A[i] == B[j]:

result = C(i-1,j-1)+1

else:

result = max(C(i,j-1),C(i-1,j))

cache[get_key(i,j)] = result

return result

def get_key(i,j):

return str(i)+','+str(j)

if __name__ == '__main__':

print C(len(A)-1,len(B)-1)

上面程序的輸出結果為5,我們也可以像背包問題一樣,把上面代碼改造成自底向上的求解方式,這里就省略了。

但是實際應用中,我們可能更需要求最大公共子序列的序列,而不只是序列的長度,所以我們下面額外考慮一下如何輸出這個結果。

其實輸出LCS字符串也是使用動態規划的方法,我們假設LCS[i,j]表示長度為i的字符串和長度為j的字符串的最大公共子序列,那么我們有以下狀態轉移函數:

其中C[i,j]是我們之前求得的最大子序列長度的緩存,根據上面的狀態轉移函數寫出遞歸代碼並不麻煩:

#!/usr/bin/python

# coding:utf-8

"""Dynamic Programming"""

CACHE = {}

# 為了下面表示方便,數組從1開始編號

# 即當i,j為0的時候,公共子序列為0,屬於極端情況

A = [0, 'A', 'B', 'C', 'B', 'D', 'A', 'B', 'E', 'F']

B = [0, 'B', 'D', 'C', 'A', 'B', 'A', 'F']

def lcs_length(i, j):

"""Calculate max sequence length"""

if get_key(i, j) in CACHE:

return CACHE[get_key(i, j)]

result = 0

if i > 0 and j > 0:

if A[i] == B[j]:

result = lcs_length(i-1, j-1)+1

else:

result = max(lcs_length(i, j-1), lcs_length(i-1, j))

CACHE[get_key(i, j)] = result

return result

def lcs(i, j):

"""backtrack lcs"""

if i == 0 or j == 0 :

return ""

if A[i] == B[j]:

return lcs(i-1, j-1) + A[i]

else:

if CACHE[get_key(i-1, j)] > CACHE[get_key(i, j-1)]:

return lcs(i-1, j)

else:

return lcs(i, j-1)

def get_key(i, j):

"""build cache keys"""

return str(i) + ',' + str(j)

if __name__ == '__main__':

print lcs_length(len(A)-1, len(B)-1)

print lcs(len(A)-1, len(B)-1)

本小節就暫時到這里了,其實我們很容易能體會到,動態規划的核心就是找到那個狀態轉移方程,所以遇到問題的時候,首先想一想其有沒有最優子結構,很可能幫助我們省下大把的思考時間。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM