經典算法——動態規劃入門實例
神馬是動態規劃
專業定義:
動態規劃的本質,是對問題狀態的定義和狀態轉移方程的定義。通過拆分問題,定義問題狀態和狀態之間的關係,使得問題能夠以遞推(或者說分治)的方式去解決。
看完之後或許你一個字也沒記住,這都是什麼鬼?
關鍵點
ok,讓我們摒棄這些專業的條條框框,直奔主題。其實動態規劃的核心就是拆解子問題——把一個大的問題拆解成逐步逐步的小問題,而這些小問題都可以直接由之前更小的問題得到。那麼怎麼拆解這些小問題呢?
靠的就是狀態的定義和狀態轉移方程的定義。
當然,這樣還是很抽象的。不如舉個栗子:
求解最長公共子串
神馬是最長公共子串?
假定現在有兩個字符串,最長公共子串就是它們之間公有的連續的最長子串,就像下圖這樣
橙色部分就是最長的公有子串,記住這裏一定要連續。那麼問題來了?怎麼求出任意兩個字符串的最長公有子串呢?看似很簡單,不過細思又感覺有點點複雜的感覺。
暴力求解
簡單粗暴的方法——先求出str1的所有子串,然後在求出str2的所有子串,然後再一個個比較,找到相同且最大的那個不就好了。ok,我們先來看看要多少次:
str1的可能情況:
n+(n-1)+(n-1)+.......+2+1 = ((n+1)*n)/2
同理str2也是一樣:
m+(m-1)+(m-1)+.......+2+1 = ((m+1)*m)/2
然後在通過兩個for循環一個一個比較就搞定了嘛;
for i in range(((n+1)*n)/2): for i in range(((m + 1) * m) / 2): dosomthing()
ok,這樣不就搞定了,so easy,不過感覺有點細思極恐,這也太暴力了,萬一字符串很大,這得算多久呀!心疼計算機3s。正所謂暴力膜法不可取,所以這時候我們的動態規劃就登場啦。
最開始我們說過,用動態規劃算法時最重要的就是要拆解子問題,那麼我們如何將這個問題拆解爲更小的子問題呢?根據動態規劃的思想,我們首先要分析出如何根據已有的結果算出我們需要的結果,舉慄來說:
假如我們已經求好了截至到str1[i-1]與str2[j-1]處兩個字符串的最大公有子串,是否可以幫助我們求出截止到str1[i]與str[j]處兩個字符串的最大公有子串呢?思考一下:
爲了方便,我們將截止到(這裏的截止到要包含str1[i]與str2[j])str[i]與str[j]處的最大字符串長度記作
lcs(i,j)
,假如:str1[i] == str2[j],那麼我們直接在lcs(i-1,j-1)後面加1不就是lcs(i,j)了嘛,如果str1[i] !=str2[j],最大公有子串到這裏就結束了,所以經過這兩個點沒有最大公有子串,直接記作0就好了。用數學公式就是:
f(m,n)=0 str1[m] != str2[n] ; f(m,n)=f(m-1,n-1) + 1 str[m]==str2[n];
轉化爲代碼就是:
if str1[i]==str2[j]: lcs[i][j] = lcs[i-1][j-1] + 1 else: lcs[i][j] = 0
用一張圖片描述整體流程就是:
ok,既然弄清楚了計算的流程,就可以寫出代碼,完整代碼如下:
def lcs(mes1, mes2): if len(mes1) > len(mes2): mes1, mes2 = mes2, mes1 data = [[0 for i in range(len(mes2) + 1)] for i in range(len(mes1) + 1)] mes1 = "$" + mes1 mes2 = "$" + mes2 max_lenth = 0 end = 0 for i in range(1, len(mes1)): for j in range(1, len(mes2)): if mes1[i] == mes2[j]: data[i][j] = data[i - 1][j - 1] + 1 if data[i][j] > max_lenth: max_lenth = data[i][j] end = i if max != 0: result = mes1[end - max_lenth + 1:end + 1] print(result) if __name__ == '__main__': while True: try: mes1 = input() mes2 = input() lcs(mes1, mes2) except: break
這裏申請的二維數組比兩個字符串都要大1,原因爲我們呢將i,j爲0的邊都賦值爲了0(當然,這裏爲了方便直接把數組都初始化了爲0),有點像爲我們的動態規劃棋盤撒了半圈水雷一樣。參考上面的流程圖。這是一道經典的動態規劃題目,在華爲的筆試題裏面曾經出現過,上面代碼就是之前根據題目寫的,已經通過了測試。原題如下:
求解最大公有子序列
神馬是最大公有子序列
乍一看,和最大公共子串就兩字之差,其實這兩個差的還是挺遠的。還是上面那兩個字符串:
橙色部分就是他們的最大公有子序列。和子串最大的區別就是這裏的子串可以不用連續了,所以叫最大子序列了。這種算法有什麼用呢?
比如:有次毛概老師讓你寫一篇5000字的論文,可是要交的前一天晚上你纔想起來怎麼辦?當然是網上隨便找一篇,但是你又怕老師發現是抄襲的,所以就會在文章開頭改點東西,文章中間改點,文章結尾改點,當你以爲萬事大吉的時候,如果老師剛好學過這個算法,那麼可以通過對比最大子序列找到文章的相似度了,然後就。。。。
暴力求解
。。。。。。。。。。。。這個子集比之前的高多了,有興趣朋友可以計算試試。。。。。。。
其實仔細想想,這個和最大子串其實是一樣一樣的,就是遞推公式有一丟丟不同,希望大家稍微思考下,應該很快就寫出來了。完整代碼如下,建議對比之前代碼就會發現差別了:
def lcs(mes1, mes2): if len(mes1) > len(mes2): mes1, mes2 = mes2, mes1 data = [[0 for i in range(len(mes2) + 1)] for i in range(len(mes1) + 1)] mes1 = "$" + mes1 mes2 = "$" + mes2 max_lenth = 0 end = 0 for i in range(1, len(mes1)): for j in range(1, len(mes2)): if mes1[i] == mes2[j]: data[i][j] = data[i - 1][j - 1] + 1 else: data[i][j] = max(data[i - 1][j], data[i][j - 1]) i = len(mes1) - 1 j = len(mes2) - 1 str = "" while i != 0 and j != 0: if mes1[i] == mes2[j]: str += mes1[i] i -= 1 j -= 1 else: if data[i][j - 1] > data[i - 1][j]: j -= 1 else: i -= 1 print(str[::-1]) if __name__ == '__main__': lcs("abcdacgw", "acdacvgwc")
其實就是拷貝的前面代碼,改了幾行代碼和輸出而已。
總結
- 算法是一門很理論通過也很實踐的學問,建議先搞清流程,再寫代碼加深理解。本人算法菜鳥,如果錯誤,歡迎一起探討。共同學習進步。