進(jìn)一步理解動態(tài)規(guī)劃

理解動態(tài)規(guī)劃、BFS和DFS一文中,只是初步講解了一下動態(tài)規(guī)劃,理解的并不到位,這里再加深理解一下。

本文主要參考什么是動態(tài)規(guī)劃一文。

一、前言

1.1、算法問題的求解過程

類似于機器學(xué)習(xí)的步驟,對同一個問題,可以用不同的模型建模,然后對于確定的模型,可以用不同的算法求解。

一般的算法問題求解步驟,分為兩步:

  • 1、問題建模:
    對于同一個問題,可以有不同的模型。
  • 2、問題求解:
    對于特定的模型,選出一個合適的算法(時間復(fù)雜度和空間復(fù)雜度滿足要求),求解問題。

對應(yīng)到動態(tài)規(guī)劃算法上,具體分為這兩步:

  • 1、問題建模:[最優(yōu)子結(jié)構(gòu)][邊界][狀態(tài)轉(zhuǎn)移方程]
  • 2、用動態(tài)規(guī)劃算法求解問題。

1.2、動態(tài)規(guī)劃的思想

大事化小,小事化了。把一個復(fù)雜的問題分階段進(jìn)行簡化,逐步化簡成簡單的問題。

1.3、動態(tài)規(guī)劃的步驟

1.3.1 問題建模
  • 1、 根據(jù)問題,找到【最優(yōu)子結(jié)構(gòu)】
    把原問題從大化小的第一步,找到比當(dāng)前問題要小一號的最好的結(jié)果,而一般情況下當(dāng)前問題可以由最優(yōu)子結(jié)構(gòu)進(jìn)行表示。
  • 2、確定問題的【邊界】
    根據(jù)上述的最優(yōu)子結(jié)構(gòu),一步一步從大化小,最終可以得到最小的,可以一眼看出答案的最優(yōu)子結(jié)構(gòu),也就是邊界。
  • 3、通過上述兩步,通過分析最優(yōu)子結(jié)構(gòu)與最終問題之間的關(guān)系,我們可以得到【狀態(tài)轉(zhuǎn)移方程】
1.3.2 問題求解的各個方法(從暴力枚舉 逐步優(yōu)化到動歸)
  • 暴力枚舉:
    下面的樓梯問題,國王與金礦問題,還有最少找零硬幣數(shù)問題,都可以通過多層嵌套循環(huán)遍歷所有的可能,將符合條件的個數(shù)統(tǒng)計起來。只是時間復(fù)雜度是指數(shù)級的,所以一般 不推薦。

  • 遞歸:
    1、既然是從大到小,不斷調(diào)用狀態(tài)轉(zhuǎn)移方程,那么就可以用遞歸。
    2、遞歸的時間復(fù)雜度是由階梯數(shù)和最優(yōu)子結(jié)構(gòu)的個數(shù)決定的。不同的問題,用遞歸的話可能效果會大不相同。
    3、在階梯問題,最少找零問題中,遞歸的時間復(fù)雜度和空間復(fù)雜度都比動歸方法的差, 但是在國王與金礦的問題中,遞歸的時間復(fù)雜度和空間復(fù)雜度都比動歸方法好。這是需要注意的。

每一種算法都沒有絕對的好與壞,關(guān)鍵看應(yīng)用場景。、

上面這句話說的很好,不止于遞歸和動歸,一般的算法也是,比如一般的排序算法,在不同的場景中,效果也大不相同。

  • 備忘錄算法:
    1、在階梯數(shù)N比較多的時候,遞歸算法的缺點就顯露出來了:時間復(fù)雜度很高。如果畫出遞歸圖(像二叉樹一樣),會發(fā)現(xiàn)有很多很多重復(fù)的節(jié)點。然而傳統(tǒng)的遞歸算法并不能識別節(jié)點是不是重復(fù)的,只要不到終止條件,它就會一直遞歸下去。
    2、為了避免上述情況,使遞歸算法能夠不重復(fù)遞歸,就把已經(jīng)得到的節(jié)點都存起來,下次再遇到的時候,直接用存起來的結(jié)果就行了。這就是備忘錄算法。
    3、備忘錄算法的時間復(fù)雜度和空間復(fù)雜度都得到了簡化。

  • 正經(jīng)的動歸算法:
    1、上述的備忘錄算法,盡管已經(jīng)不錯了,但是依然還是從最大的問題,遍歷得到所有的最小子問題,空間復(fù)雜度是O(N)。
    2、為了再次縮小空間復(fù)雜度,我們可以自底向上的構(gòu)造遞歸問題,通過分析最優(yōu)子結(jié)構(gòu)與最終問題之間的關(guān)系,我們可以得到【狀態(tài)轉(zhuǎn)移方程】
    然后從最小的問題不斷往上迭代,即使一直到最大的原問題,也是只依賴于前面的幾個最優(yōu)子結(jié)構(gòu)。這樣,空間復(fù)雜度就大大簡化。也就得到了正經(jīng)的動歸算法。

下面通過幾個例題,來具體了解動歸問題。

二、例題

例1:Climbing Stairs

leetcode原題:你正在爬一個有n個臺階的樓梯,每次只能上 1個 或者 2個臺階,那么到達(dá)頂端共有多少種不同的方法?

1.1、 建立模型

  • 最終問題F(N):
    假設(shè)從0到達(dá)第N個臺階的方法共有F(N)個。
  • 最優(yōu)子結(jié)構(gòu)F(N-1),F(xiàn)(N-2):
    到達(dá)N個臺階,有兩種可能,第一種可能是從第 N-1 個臺階上1個臺階到達(dá)終點,第二種可能是從第 N-2 個臺階上2個臺階到達(dá)終點。
  • 最優(yōu)子結(jié)構(gòu)與最終問題之間的關(guān)系:
    按照上述表達(dá),那么可以歸納出F(N) = F(N-1) + F(N-2) (n>=3)

結(jié)束條件為F(1) = 1,F(2) = 2

1.2、 問題求解

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)

回想前面所說,遞歸的時間復(fù)雜度是由階梯數(shù)和最優(yōu)子結(jié)構(gòu)的個數(shù)決定的。這里的階梯數(shù)是 N ,最優(yōu)子結(jié)構(gòu)個數(shù)是 2 。如果想象成一個二叉樹,那么就可以認(rèn)為是一個高度為N-1,節(jié)點個數(shù)接近 2 的 N-1 次方的樹,因此此方法的時間復(fù)雜度可以近似的看作是O(2N) 。

1.2.2、 解法2:備忘錄算法

參考什么是動態(tài)規(guī)劃中遞歸的圖,發(fā)現(xiàn)有很多相同的參數(shù)被重復(fù)計算,重復(fù)的太多了。

所以這里我們想到了把重復(fù)的參數(shù)存儲起來,下次遞歸遇到時就直接返回該參數(shù)的結(jié)果,也就是備忘錄算法了,這里需要用到一個哈希表,解決方法就是對類用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 個結(jié)果,時間復(fù)雜度和空間復(fù)雜度都是O(N)。程序性能得到了明顯優(yōu)化。

1.2.3、 解法3:動態(tài)規(guī)劃

之前都是自頂向下的求解,考慮一下自底向上的求解過程。從F(1)和F(2)邊界條件求,可知F(3) = F(1)+F(2)。不斷向上,可知F(N)只依賴于前兩個狀態(tài)F(N-1)和F(N-2)。于是我們只需要保留前兩個狀態(tài),就可以求得F(N)。相比于備忘錄算法,我們再一次簡化了空間復(fù)雜度。

這就是動態(tài)規(guī)劃了。(具體的細(xì)節(jié)看漫畫比較好理解。)

具體代碼實現(xiàn)中,可以令F(N-2)=a,F(xiàn)(N-1)=b,則temp等于a+b,然后把a向前挪一步等于b,b向前挪一步等于temp。那么下一次迭代時,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ù)目找零錢的一個例子。

問題描述:
假設(shè)你是一家自動售貨機制造商的程序員。你的公司正設(shè)法在每一筆交易 找零時都能提供最少數(shù)目的硬幣以便工作能更加簡單。已知硬幣有四種(1美分,5美分,10美分,25美分)。假設(shè)一個顧客投了1美元來購買37美分的物品 ,你用來找零的硬幣的最小數(shù)量是多少?
(這個問題用貪心算法也能解,具體細(xì)節(jié)看參考文獻(xiàn))

2.1、 建立模型

就以動歸作為解題的算法來建立模型吧。

  • 邊界:當(dāng)需要找零的面額正好等于上述的四種整額硬幣時,返回1即可
  • 最優(yōu)子結(jié)構(gòu):回想找到最優(yōu)子結(jié)構(gòu)的方法,就是往后退一步,能夠得到的最好的結(jié)果。這里有四個選擇,1 + mincoins(63-1),1 + mincoins(63-5),1 + mincoins(63-10) 或者 1 + mincoins(63-25),這四個選擇可以認(rèn)為是63的最優(yōu)子結(jié)構(gòu)。
  • 狀態(tài)轉(zhuǎn)移方程:按照 上述的最優(yōu)子結(jié)構(gòu),mincoins(63)也就等于上述四個最優(yōu)子結(jié)構(gòu)的最小值。于是,方程可以表示為:

2.2、 問題求解

模型已經(jīng)得到,接下來就運用算法進(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)

但是,對于每一個大于25的數(shù)目,都有四個最優(yōu)子結(jié)構(gòu),然后對于每個最優(yōu)子結(jié)構(gòu),還有大量相同重復(fù)的參數(shù)(具體細(xì)節(jié)看參考)。所以這個解法并不合適。

2.2.2、解法2,動態(tài)規(guī)劃

首先要有自底向上的思想,從change等于1時,開始往上迭代,參考最優(yōu)子結(jié)構(gòu),記錄下來最少硬幣數(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

由此可以推下去,每一個change對應(yīng)的最少硬幣數(shù),都可以由前面的若干個最優(yōu)子結(jié)構(gòu)(有幾個最優(yōu)子結(jié)構(gòu),由change是多少決定,change大于5就有兩個子結(jié)構(gòu),大于10就有三個。。)得到。這樣一直迭代到63,那么就可以得到63的最少硬幣數(shù)。

因此,需要一個循環(huán)來從頭到尾遍歷。
需要一定需要一個map來記錄部分結(jié)果。
每一個change,我們可以根據(jù)上面的式子遍歷最優(yōu)子結(jié)構(gòu),并將每個子結(jié)構(gòu)的結(jié)果都添加到一個list中,在遍歷完最有子結(jié)構(gòu)以后,選擇最小的那一個,添加到map中去。

求解一個新的 i 的最優(yōu)解的過程是很方便的,從最優(yōu)子結(jié)構(gòu)中挑選最小的值然后加1即可。
最優(yōu)子結(jié)構(gòu)的值,可以用minCoin[i-j]得到。其中j為有效硬幣面額。

實現(xiàn)代碼:

def dpMakeChange(coinValueList,change):
    minCoins = { }

    for cents in range(change+1):
        #cents小于等于1時,coinCount會為空,沒法執(zhí)行min。
        #因此這里先填上
        if cents <= 1:
            minCoins[cents] = cents
            continue
        #遍歷cents的每個最優(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)然這個函數(shù)是有瑕疵的,因為這個函數(shù)只告訴我們最少的硬幣數(shù),并不能告訴我們應(yīng)該找零的面額。所以我們可以擴展一下函數(shù),跟蹤記錄我們使用的硬幣即可。具體細(xì)節(jié)可以看參考。

例3: 國王與金礦問題

只講一下大致的思路。
問題中需要注意的地方:

  • 國王與金礦的問題中,因為每個金礦需要的人不同,所含金礦數(shù)量也不同。為了簡化問題,這里第 i 個金礦所含的金礦數(shù)量和所需要的工人都是 特定不變的。
  • 在實現(xiàn)自底向上的遞推時,因為問題的參數(shù)有兩個,那么存在兩個輸入維度。為此,可以畫一個表格來做分析。
  • 在實現(xiàn)自底向上的遞推時,為了比較快的找到規(guī)律,最好把從邊界不斷地往上迭代,結(jié)合最優(yōu)子結(jié)構(gòu)和存儲的結(jié)果,慢慢的找到規(guī)律。

3.1、問題建模

這里著重講解一下最后一點,也就是動態(tài)規(guī)劃最重要的地方。

最優(yōu)子結(jié)構(gòu):對于5個金礦,10個工人的情況,往后退一步存在兩種情況。(第五個金礦的金礦數(shù)量為350,所需工人為3人)

  • 情況1:國王選擇不挖第五個金礦,那么此時最大化的金礦數(shù)量就是在有4個金礦,10個工人的情況下,能夠挖到的最多金礦數(shù)量。
  • 情況2:國王選擇挖第五個金礦,那么此時用3個工人挖得350的金礦數(shù)量是已知的,還剩4個金礦與7個工人。
    那么最優(yōu)解相當(dāng)于在4個金礦與7個工人的情況下能夠挖得的最多金礦數(shù)量 + 350。

最優(yōu)子結(jié)構(gòu)與最終問題之間的關(guān)系:5個金礦10個工人的最優(yōu)選擇,就是上述兩個最優(yōu)子結(jié)構(gòu)的最大值。

于是我們可以得到狀態(tài)轉(zhuǎn)移方程:

最重要的狀態(tài)轉(zhuǎn)移方程已經(jīng)得到,至于剩下的邊界條件,現(xiàn)實中會遇到的各種特殊情況,這里就不贅述了。細(xì)節(jié)參考漫畫。

3.2、問題求解

3.2.1 解法1、遞歸

程序 :把狀態(tài)轉(zhuǎn)移方程翻譯成遞歸程序,遞歸的結(jié)束條件就是方程式中的邊界即可。
復(fù)雜度:因為每個狀態(tài)有兩個最優(yōu)子結(jié)構(gòu),所以遞歸的執(zhí)行流程類似于一個高度為N的二叉樹。所以方法的時間復(fù)雜度是O(2N)。

3.2.2 解法2、備忘錄算法

程序:在簡單遞歸的基礎(chǔ)上,增加一個HashMap備忘錄,用來存儲中間的結(jié)果,HashMap的Key是一個包含金礦數(shù)N和工人數(shù)W的對象,Value是最優(yōu)選擇獲得的黃金數(shù)。
復(fù)雜度:時間復(fù)雜度和空間復(fù)雜度相同,都等于被網(wǎng)絡(luò)中不同Key的數(shù)量。

3.2.3 解法3、動態(tài)規(guī)劃

為了實現(xiàn)自底向上的迭代,對于參數(shù)有兩個的問題,我們可以先畫要一個表格來做分析。根據(jù)狀態(tài)轉(zhuǎn)移方程,我們可以方便的畫出表格。注意,一定是要根據(jù)狀態(tài)轉(zhuǎn)移方程來求的。

由于我們在求解每個格子的數(shù)值時,結(jié)合狀態(tài)轉(zhuǎn)移方程,發(fā)現(xiàn)除了第一行以外,每一個格子都可以由前一行的格子中的一個或者兩個格子推導(dǎo)而來。
從整體上來說,每一行的值都可以由前一行來求得。

于是,我們在寫代碼的時候,也可以像畫表格一樣,從左至右,從上到下一個一個的推出最終結(jié)果。反映到程序上就是:

for i in range(金礦數(shù)):
    for j in range(工人數(shù)目):
         狀態(tài)轉(zhuǎn)移方程

另外,由上可知,我們并不需要存儲整個表格,只需要存儲前一行的結(jié)果即可推出新的一行。

代碼這里就不寫了。

注意:

  • 這里動態(tài)規(guī)劃的時間復(fù)雜度是O(n*w),空間復(fù)雜度是O(w)。在n=5,w=1000是,顯然要計算5000次,開辟1000單位的空間。
  • 但是如果用簡單遞歸算法的話,時間復(fù)雜度是O(2N),需要計算32次 ,開辟5單位(遞歸深度)的空間。
  • 這是由于動態(tài)規(guī)劃方法的時間和空間都和w成正比,而簡單遞歸卻和w無關(guān),所以當(dāng)工人數(shù)量很多的時候,動態(tài)規(guī)劃反而不如遞歸。

所以說,每一種算法沒有絕對的好與壞,關(guān)鍵要看應(yīng)用場景。

總結(jié):

個人覺得, 動態(tài)規(guī)劃算法最重要的有兩點

  • 建模:一定要找對最優(yōu)子結(jié)構(gòu),然后分析最優(yōu)子結(jié)構(gòu)與最終問題的關(guān)系,從而得到狀態(tài)轉(zhuǎn)移方程。
  • 問題求解:先手動的自底向上的,運用狀態(tài)轉(zhuǎn)移方程迭代一下,一直到最終問題,從而確定程序的主體部分。

至于,模型中的邊界問題,特殊情況等,就是需要多敲代碼來慢慢考慮的了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內(nèi)容