在理解動(dòng)態(tài)規(guī)劃局骤、BFS和DFS一文中攀圈,只是初步講解了一下動(dòng)態(tài)規(guī)劃,理解的并不到位峦甩,這里再加深理解一下赘来。
本文主要參考什么是動(dòng)態(tài)規(guī)劃一文现喳。
一、前言
1.1犬辰、算法問(wèn)題的求解過(guò)程
類似于機(jī)器學(xué)習(xí)的步驟嗦篱,對(duì)同一個(gè)問(wèn)題,可以用不同的模型建模幌缝,然后對(duì)于確定的模型灸促,可以用不同的算法求解。
一般的算法問(wèn)題求解步驟涵卵,分為兩步:
- 1浴栽、問(wèn)題建模:
對(duì)于同一個(gè)問(wèn)題,可以有不同的模型轿偎。 - 2典鸡、問(wèn)題求解:
對(duì)于特定的模型,選出一個(gè)合適的算法(時(shí)間復(fù)雜度和空間復(fù)雜度滿足要求)坏晦,求解問(wèn)題萝玷。
對(duì)應(yīng)到動(dòng)態(tài)規(guī)劃算法上,具體分為這兩步:
- 1英遭、問(wèn)題建模:[最優(yōu)子結(jié)構(gòu)]间护,[邊界],[狀態(tài)轉(zhuǎn)移方程]挖诸。
- 2汁尺、用動(dòng)態(tài)規(guī)劃算法求解問(wèn)題。
1.2多律、動(dòng)態(tài)規(guī)劃的思想
大事化小痴突,小事化了。把一個(gè)復(fù)雜的問(wèn)題分階段進(jìn)行簡(jiǎn)化狼荞,逐步化簡(jiǎn)成簡(jiǎn)單的問(wèn)題辽装。
1.3、動(dòng)態(tài)規(guī)劃的步驟
1.3.1 問(wèn)題建模
- 1相味、 根據(jù)問(wèn)題拾积,找到【最優(yōu)子結(jié)構(gòu)】。
把原問(wèn)題從大化小的第一步丰涉,找到比當(dāng)前問(wèn)題要小一號(hào)的最好的結(jié)果拓巧,而一般情況下當(dāng)前問(wèn)題可以由最優(yōu)子結(jié)構(gòu)進(jìn)行表示。 - 2一死、確定問(wèn)題的【邊界】肛度。
根據(jù)上述的最優(yōu)子結(jié)構(gòu),一步一步從大化小投慈,最終可以得到最小的承耿,可以一眼看出答案的最優(yōu)子結(jié)構(gòu)冠骄,也就是邊界。 - 3加袋、通過(guò)上述兩步凛辣,通過(guò)分析最優(yōu)子結(jié)構(gòu)與最終問(wèn)題之間的關(guān)系,我們可以得到【狀態(tài)轉(zhuǎn)移方程】锁荔。
1.3.2 問(wèn)題求解的各個(gè)方法(從暴力枚舉 逐步優(yōu)化到動(dòng)歸)
暴力枚舉:
下面的樓梯問(wèn)題蟀给,國(guó)王與金礦問(wèn)題蝙砌,還有最少找零硬幣數(shù)問(wèn)題阳堕,都可以通過(guò)多層嵌套循環(huán)遍歷所有的可能,將符合條件的個(gè)數(shù)統(tǒng)計(jì)起來(lái)择克。只是時(shí)間復(fù)雜度是指數(shù)級(jí)的恬总,所以一般 不推薦。遞歸:
1肚邢、既然是從大到小壹堰,不斷調(diào)用狀態(tài)轉(zhuǎn)移方程,那么就可以用遞歸骡湖。
2贱纠、遞歸的時(shí)間復(fù)雜度是由階梯數(shù)和最優(yōu)子結(jié)構(gòu)的個(gè)數(shù)決定的。不同的問(wèn)題响蕴,用遞歸的話可能效果會(huì)大不相同谆焊。
3、在階梯問(wèn)題浦夷,最少找零問(wèn)題中辖试,遞歸的時(shí)間復(fù)雜度和空間復(fù)雜度都比動(dòng)歸方法的差, 但是在國(guó)王與金礦的問(wèn)題中劈狐,遞歸的時(shí)間復(fù)雜度和空間復(fù)雜度都比動(dòng)歸方法好罐孝。這是需要注意的。
每一種算法都沒(méi)有絕對(duì)的好與壞肥缔,關(guān)鍵看應(yīng)用場(chǎng)景莲兢。、
上面這句話說(shuō)的很好续膳,不止于遞歸和動(dòng)歸改艇,一般的算法也是,比如一般的排序算法姑宽,在不同的場(chǎng)景中遣耍,效果也大不相同。
備忘錄算法:
1炮车、在階梯數(shù)N比較多的時(shí)候舵变,遞歸算法的缺點(diǎn)就顯露出來(lái)了:時(shí)間復(fù)雜度很高酣溃。如果畫出遞歸圖(像二叉樹一樣),會(huì)發(fā)現(xiàn)有很多很多重復(fù)的節(jié)點(diǎn)纪隙。然而傳統(tǒng)的遞歸算法并不能識(shí)別節(jié)點(diǎn)是不是重復(fù)的赊豌,只要不到終止條件,它就會(huì)一直遞歸下去绵咱。
2碘饼、為了避免上述情況,使遞歸算法能夠不重復(fù)遞歸悲伶,就把已經(jīng)得到的節(jié)點(diǎn)都存起來(lái)艾恼,下次再遇到的時(shí)候,直接用存起來(lái)的結(jié)果就行了麸锉。這就是備忘錄算法钠绍。
3、備忘錄算法的時(shí)間復(fù)雜度和空間復(fù)雜度都得到了簡(jiǎn)化花沉。正經(jīng)的動(dòng)歸算法:
1柳爽、上述的備忘錄算法,盡管已經(jīng)不錯(cuò)了碱屁,但是依然還是從最大的問(wèn)題磷脯,遍歷得到所有的最小子問(wèn)題,空間復(fù)雜度是O(N)娩脾。
2赵誓、為了再次縮小空間復(fù)雜度,我們可以自底向上的構(gòu)造遞歸問(wèn)題晦雨,通過(guò)分析最優(yōu)子結(jié)構(gòu)與最終問(wèn)題之間的關(guān)系架曹,我們可以得到【狀態(tài)轉(zhuǎn)移方程】。
然后從最小的問(wèn)題不斷往上迭代闹瞧,即使一直到最大的原問(wèn)題绑雄,也是只依賴于前面的幾個(gè)最優(yōu)子結(jié)構(gòu)。這樣奥邮,空間復(fù)雜度就大大簡(jiǎn)化万牺。也就得到了正經(jīng)的動(dòng)歸算法。
下面通過(guò)幾個(gè)例題洽腺,來(lái)具體了解動(dòng)歸問(wèn)題脚粟。
二、例題
例1:Climbing Stairs
leetcode原題:你正在爬一個(gè)有n個(gè)臺(tái)階的樓梯蘸朋,每次只能上 1個(gè) 或者 2個(gè)臺(tái)階核无,那么到達(dá)頂端共有多少種不同的方法?
1.1藕坯、 建立模型:
-
最終問(wèn)題F(N):
假設(shè)從0到達(dá)第N個(gè)臺(tái)階的方法共有F(N)個(gè)团南。 -
最優(yōu)子結(jié)構(gòu)F(N-1)噪沙,F(xiàn)(N-2):
到達(dá)N個(gè)臺(tái)階,有兩種可能吐根,第一種可能是從第 N-1 個(gè)臺(tái)階上1個(gè)臺(tái)階到達(dá)終點(diǎn)正歼,第二種可能是從第 N-2 個(gè)臺(tái)階上2個(gè)臺(tái)階到達(dá)終點(diǎn)。 -
最優(yōu)子結(jié)構(gòu)與最終問(wèn)題之間的關(guān)系:
按照上述表達(dá)拷橘,那么可以歸納出F(N) = F(N-1) + F(N-2) (n>=3)
結(jié)束條件為F(1) = 1,F(2) = 2
1.2局义、 問(wèn)題求解:
1.2.1、 解法1:遞歸
先用比較容易理解的遞歸求解(結(jié)束條件已知冗疮,遞歸公式已知萄唇,可以直接寫代碼了)
class Solution:
def climbStairs(self, n):
"""
:type n: int
:rtype: int
"""
if n == 1:
return 1
elif n == 2:
return 2
else:
return self.climbStairs(n-1) + self.climbStairs(n-2)
回想前面所說(shuō),遞歸的時(shí)間復(fù)雜度是由階梯數(shù)和最優(yōu)子結(jié)構(gòu)的個(gè)數(shù)決定的赌厅。這里的階梯數(shù)是 N 穷绵,最優(yōu)子結(jié)構(gòu)個(gè)數(shù)是 2 。如果想象成一個(gè)二叉樹特愿,那么就可以認(rèn)為是一個(gè)高度為N-1,節(jié)點(diǎn)個(gè)數(shù)接近 2 的 N-1 次方的樹勾缭,因此此方法的時(shí)間復(fù)雜度可以近似的看作是O(2N) 揍障。
1.2.2、 解法2:備忘錄算法
參考什么是動(dòng)態(tài)規(guī)劃中遞歸的圖俩由,發(fā)現(xiàn)有很多相同的參數(shù)被重復(fù)計(jì)算毒嫡,重復(fù)的太多了。
所以這里我們想到了把重復(fù)的參數(shù)存儲(chǔ)起來(lái)幻梯,下次遞歸遇到時(shí)就直接返回該參數(shù)的結(jié)果兜畸,也就是備忘錄算法了,這里需要用到一個(gè)哈希表碘梢,解決方法就是對(duì)類用init進(jìn)行初始化咬摇。
class Solution:
def __init__(self):
self.map = {}
def climbStairs(self, n):
"""
:type n: int
:rtype: int
"""
if n == 1:
return 1
if n == 2:
return 2
if n in self.map:
return self.map[n]
else:
value = self.climbStairs(n-1) + self.climbStairs(n-2)
self.map[n] = value
return value
這里哈希表里存了 N-2 個(gè)結(jié)果,時(shí)間復(fù)雜度和空間復(fù)雜度都是O(N)煞躬。程序性能得到了明顯優(yōu)化肛鹏。
1.2.3、 解法3:動(dòng)態(tài)規(guī)劃
之前都是自頂向下的求解恩沛,考慮一下自底向上的求解過(guò)程在扰。從F(1)和F(2)邊界條件求,可知F(3) = F(1)+F(2)雷客。不斷向上芒珠,可知F(N)只依賴于前兩個(gè)狀態(tài)F(N-1)和F(N-2)。于是我們只需要保留前兩個(gè)狀態(tài)搅裙,就可以求得F(N)皱卓。相比于備忘錄算法总放,我們?cè)僖淮魏?jiǎn)化了空間復(fù)雜度。
這就是動(dòng)態(tài)規(guī)劃了好爬。(具體的細(xì)節(jié)看漫畫比較好理解局雄。)
具體代碼實(shí)現(xiàn)中,可以令F(N-2)=a存炮,F(xiàn)(N-1)=b炬搭,則temp等于a+b,然后把a(bǔ)向前挪一步等于b穆桂,b向前挪一步等于temp宫盔。那么下一次迭代時(shí),temp就依然等于a+b享完。
代碼如下:
class Solution:
def climbStairs(self, n):
"""
:type n: int
:rtype: int
"""
if n == 1:
return 1
if n == 2:
return 2
a = 1
b = 2
for i in range(3,n+1):
temp = a + b
a = b
b = temp
return temp
例2: Making change using the fewest coins.
參考Dynamic Programming中灼芭,用最少的硬幣數(shù)目找零錢的一個(gè)例子。
問(wèn)題描述:
假設(shè)你是一家自動(dòng)售貨機(jī)制造商的程序員般又。你的公司正設(shè)法在每一筆交易 找零時(shí)都能提供最少數(shù)目的硬幣以便工作能更加簡(jiǎn)單彼绷。已知硬幣有四種(1美分,5美分茴迁,10美分寄悯,25美分)。假設(shè)一個(gè)顧客投了1美元來(lái)購(gòu)買37美分的物品 堕义,你用來(lái)找零的硬幣的最小數(shù)量是多少猜旬?
(這個(gè)問(wèn)題用貪心算法也能解,具體細(xì)節(jié)看參考文獻(xiàn))
2.1倦卖、 建立模型:
就以動(dòng)歸作為解題的算法來(lái)建立模型吧洒擦。
- 邊界:當(dāng)需要找零的面額正好等于上述的四種整額硬幣時(shí),返回1即可
- 最優(yōu)子結(jié)構(gòu):回想找到最優(yōu)子結(jié)構(gòu)的方法怕膛,就是往后退一步熟嫩,能夠得到的最好的結(jié)果。這里有四個(gè)選擇嘉竟,1 + mincoins(63-1)邦危,1 + mincoins(63-5),1 + mincoins(63-10) 或者 1 + mincoins(63-25)舍扰,這四個(gè)選擇可以認(rèn)為是63的最優(yōu)子結(jié)構(gòu)倦蚪。
- 狀態(tài)轉(zhuǎn)移方程:按照 上述的最優(yōu)子結(jié)構(gòu),mincoins(63)也就等于上述四個(gè)最優(yōu)子結(jié)構(gòu)的最小值边苹。于是陵且,方程可以表示為:
2.2、 問(wèn)題求解:
模型已經(jīng)得到,接下來(lái)就運(yùn)用算法進(jìn)行求解慕购。
這里依然可以按照例1的解法聊疲,由模型,很自然的想到用遞歸求解沪悲。
2.2.1获洲、解法1,遞歸
邊界條件已知殿如,模型已知贡珊,可以直接寫代碼了。
def recMC(coinValueList,change):
minCoins = change
if change in coinValueList:
return 1
else:
for i in [c for c in coinValueList if c <= change]:
numCoins = 1 + recMC(coinValueList,change-i)
if numCoins < minCoins:
minCoins = numCoins
return minCoins
print(recMC([1,5,10,25],63)
但是涉馁,對(duì)于每一個(gè)大于25的數(shù)目门岔,都有四個(gè)最優(yōu)子結(jié)構(gòu),然后對(duì)于每個(gè)最優(yōu)子結(jié)構(gòu)烤送,還有大量相同重復(fù)的參數(shù)(具體細(xì)節(jié)看參考)寒随。所以這個(gè)解法并不合適。
2.2.2帮坚、解法2妻往,動(dòng)態(tài)規(guī)劃
首先要有自底向上的思想,從change等于1時(shí)叶沛,開始往上迭代蒲讯,參考最優(yōu)子結(jié)構(gòu),記錄下來(lái)最少硬幣數(shù)灰署。一直迭代到63。
1==>1
2==>min(2-1) + 1 = 2
3==>min(3-1) + 1 = 3
4==>min(4-1) + 1 = 4
5==>min(min(5-1) + 1 = 5局嘁, min(5-5) + 1 = 1)= 1
6==>min(min(6-1) + 1 = 2溉箕, min(6-5) + 1 = 2)= 2
7==>min(min(7-1) + 1 = 3, min(7-5) + 1 = 3)= 3
由此可以推下去悦昵,每一個(gè)change對(duì)應(yīng)的最少硬幣數(shù)肴茄,都可以由前面的若干個(gè)最優(yōu)子結(jié)構(gòu)(有幾個(gè)最優(yōu)子結(jié)構(gòu),由change是多少?zèng)Q定但指,change大于5就有兩個(gè)子結(jié)構(gòu)寡痰,大于10就有三個(gè)。棋凳。)得到拦坠。這樣一直迭代到63,那么就可以得到63的最少硬幣數(shù)剩岳。
因此贞滨,需要一個(gè)循環(huán)來(lái)從頭到尾遍歷。
需要一定需要一個(gè)map來(lái)記錄部分結(jié)果拍棕。
每一個(gè)change晓铆,我們可以根據(jù)上面的式子遍歷最優(yōu)子結(jié)構(gòu)勺良,并將每個(gè)子結(jié)構(gòu)的結(jié)果都添加到一個(gè)list中,在遍歷完最有子結(jié)構(gòu)以后骄噪,選擇最小的那一個(gè)尚困,添加到map中去。
求解一個(gè)新的 i 的最優(yōu)解的過(guò)程是很方便的链蕊,從最優(yōu)子結(jié)構(gòu)中挑選最小的值然后加1即可事甜。
最優(yōu)子結(jié)構(gòu)的值,可以用minCoin[i-j]得到示弓。其中j為有效硬幣面額讳侨。
實(shí)現(xiàn)代碼:
def dpMakeChange(coinValueList,change):
minCoins = { }
for cents in range(change+1):
#cents小于等于1時(shí),coinCount會(huì)為空奏属,沒(méi)法執(zhí)行min跨跨。
#因此這里先填上
if cents <= 1:
minCoins[cents] = cents
continue
#遍歷cents的每個(gè)最優(yōu)子結(jié)構(gòu)并且添加到list中,等待篩選
coinCount = [ ]
for j in coinValueList:
if cents >= j:
coinCount.append(minCoins[cents - j] + 1)
minCoins[cents] = min(coinCount)
return minCoins[change]
result = dpMakeChange([1,5,10,25],63)
print(result)
當(dāng)然這個(gè)函數(shù)是有瑕疵的囱皿,因?yàn)檫@個(gè)函數(shù)只告訴我們最少的硬幣數(shù)勇婴,并不能告訴我們應(yīng)該找零的面額。所以我們可以擴(kuò)展一下函數(shù)嘱腥,跟蹤記錄我們使用的硬幣即可耕渴。具體細(xì)節(jié)可以看參考。
例3: 國(guó)王與金礦問(wèn)題
只講一下大致的思路齿兔。
問(wèn)題中需要注意的地方:
- 國(guó)王與金礦的問(wèn)題中橱脸,因?yàn)槊總€(gè)金礦需要的人不同,所含金礦數(shù)量也不同分苇。為了簡(jiǎn)化問(wèn)題添诉,這里第 i 個(gè)金礦所含的金礦數(shù)量和所需要的工人都是 特定不變的。
- 在實(shí)現(xiàn)自底向上的遞推時(shí)医寿,因?yàn)閱?wèn)題的參數(shù)有兩個(gè)栏赴,那么存在兩個(gè)輸入維度。為此靖秩,可以畫一個(gè)表格來(lái)做分析须眷。
- 在實(shí)現(xiàn)自底向上的遞推時(shí),為了比較快的找到規(guī)律沟突,最好把從邊界不斷地往上迭代花颗,結(jié)合最優(yōu)子結(jié)構(gòu)和存儲(chǔ)的結(jié)果,慢慢的找到規(guī)律事扭。
3.1捎稚、問(wèn)題建模
這里著重講解一下最后一點(diǎn),也就是動(dòng)態(tài)規(guī)劃最重要的地方妈经。
最優(yōu)子結(jié)構(gòu):對(duì)于5個(gè)金礦乔询,10個(gè)工人的情況耘沼,往后退一步存在兩種情況功戚。(第五個(gè)金礦的金礦數(shù)量為350丁存,所需工人為3人)
- 情況1:國(guó)王選擇不挖第五個(gè)金礦蛾绎,那么此時(shí)最大化的金礦數(shù)量就是在有4個(gè)金礦群发,10個(gè)工人的情況下镊屎,能夠挖到的最多金礦數(shù)量宰睡。
- 情況2:國(guó)王選擇挖第五個(gè)金礦蒲凶,那么此時(shí)用3個(gè)工人挖得350的金礦數(shù)量是已知的,還剩4個(gè)金礦與7個(gè)工人拆内。
那么最優(yōu)解相當(dāng)于在4個(gè)金礦與7個(gè)工人的情況下能夠挖得的最多金礦數(shù)量 + 350旋圆。
最優(yōu)子結(jié)構(gòu)與最終問(wèn)題之間的關(guān)系:5個(gè)金礦10個(gè)工人的最優(yōu)選擇,就是上述兩個(gè)最優(yōu)子結(jié)構(gòu)的最大值麸恍。
于是我們可以得到狀態(tài)轉(zhuǎn)移方程:
最重要的狀態(tài)轉(zhuǎn)移方程已經(jīng)得到灵巧,至于剩下的邊界條件,現(xiàn)實(shí)中會(huì)遇到的各種特殊情況抹沪,這里就不贅述了刻肄。細(xì)節(jié)參考漫畫。
3.2融欧、問(wèn)題求解
3.2.1 解法1敏弃、遞歸
程序 :把狀態(tài)轉(zhuǎn)移方程翻譯成遞歸程序,遞歸的結(jié)束條件就是方程式中的邊界即可噪馏。
復(fù)雜度:因?yàn)槊總€(gè)狀態(tài)有兩個(gè)最優(yōu)子結(jié)構(gòu)麦到,所以遞歸的執(zhí)行流程類似于一個(gè)高度為N的二叉樹。所以方法的時(shí)間復(fù)雜度是O(2N)欠肾。
3.2.2 解法2隅要、備忘錄算法
程序:在簡(jiǎn)單遞歸的基礎(chǔ)上,增加一個(gè)HashMap備忘錄董济,用來(lái)存儲(chǔ)中間的結(jié)果,HashMap的Key是一個(gè)包含金礦數(shù)N和工人數(shù)W的對(duì)象要门,Value是最優(yōu)選擇獲得的黃金數(shù)虏肾。
復(fù)雜度:時(shí)間復(fù)雜度和空間復(fù)雜度相同,都等于被網(wǎng)絡(luò)中不同Key的數(shù)量欢搜。
3.2.3 解法3封豪、動(dòng)態(tài)規(guī)劃
為了實(shí)現(xiàn)自底向上的迭代,對(duì)于參數(shù)有兩個(gè)的問(wèn)題炒瘟,我們可以先畫要一個(gè)表格來(lái)做分析吹埠。根據(jù)狀態(tài)轉(zhuǎn)移方程,我們可以方便的畫出表格。注意缘琅,一定是要根據(jù)狀態(tài)轉(zhuǎn)移方程來(lái)求的粘都。
由于我們?cè)谇蠼饷總€(gè)格子的數(shù)值時(shí),結(jié)合狀態(tài)轉(zhuǎn)移方程刷袍,發(fā)現(xiàn)除了第一行以外翩隧,每一個(gè)格子都可以由前一行的格子中的一個(gè)或者兩個(gè)格子推導(dǎo)而來(lái)。
從整體上來(lái)說(shuō)呻纹,每一行的值都可以由前一行來(lái)求得堆生。
于是,我們?cè)趯懘a的時(shí)候雷酪,也可以像畫表格一樣淑仆,從左至右,從上到下一個(gè)一個(gè)的推出最終結(jié)果哥力。反映到程序上就是:
for i in range(金礦數(shù)):
for j in range(工人數(shù)目):
狀態(tài)轉(zhuǎn)移方程
另外蔗怠,由上可知,我們并不需要存儲(chǔ)整個(gè)表格省骂,只需要存儲(chǔ)前一行的結(jié)果即可推出新的一行蟀淮。
代碼這里就不寫了。
注意:
- 這里動(dòng)態(tài)規(guī)劃的時(shí)間復(fù)雜度是O(n*w)钞澳,空間復(fù)雜度是O(w)怠惶。在n=5,w=1000是轧粟,顯然要計(jì)算5000次策治,開辟1000單位的空間。
- 但是如果用簡(jiǎn)單遞歸算法的話兰吟,時(shí)間復(fù)雜度是O(2N)通惫,需要計(jì)算32次 ,開辟5單位(遞歸深度)的空間混蔼。
- 這是由于動(dòng)態(tài)規(guī)劃方法的時(shí)間和空間都和w成正比履腋,而簡(jiǎn)單遞歸卻和w無(wú)關(guān),所以當(dāng)工人數(shù)量很多的時(shí)候惭嚣,動(dòng)態(tài)規(guī)劃反而不如遞歸遵湖。
所以說(shuō),每一種算法沒(méi)有絕對(duì)的好與壞晚吞,關(guān)鍵要看應(yīng)用場(chǎng)景延旧。
總結(jié):
個(gè)人覺(jué)得, 動(dòng)態(tài)規(guī)劃算法最重要的有兩點(diǎn)
- 建模:一定要找對(duì)最優(yōu)子結(jié)構(gòu)槽地,然后分析最優(yōu)子結(jié)構(gòu)與最終問(wèn)題的關(guān)系迁沫,從而得到狀態(tài)轉(zhuǎn)移方程芦瘾。
- 問(wèn)題求解:先手動(dòng)的自底向上的,運(yùn)用狀態(tài)轉(zhuǎn)移方程迭代一下集畅,一直到最終問(wèn)題近弟,從而確定程序的主體部分。
至于牡整,模型中的邊界問(wèn)題藐吮,特殊情況等,就是需要多敲代碼來(lái)慢慢考慮的了逃贝。