從八卦的遞歸到動(dòng)態(tài)規(guī)劃

題外話:

好久沒(méi)寫(xiě)博客了,主要覺(jué)得自己寫(xiě)的不怎么樣,肚子沒(méi)墨水了。今天又出來(lái)榨干肚子里的最后一滴墨水。

進(jìn)入正題:

前面寫(xiě)過(guò)兩篇?jiǎng)討B(tài)規(guī)劃的題目,今天又遇到動(dòng)態(tài)規(guī)劃了,想到了一個(gè)更容易理解的方法,我們用湊硬幣這道題目來(lái)加深理解。網(wǎng)絡(luò)上這個(gè)題目已經(jīng)被寫(xiě)爛了,大多數(shù)文章都是復(fù)制粘貼的。本文原創(chuàng)。開(kāi)始講解之前讀者需要了解遞歸。本文從遞歸解法講起直到推出動(dòng)態(tài)規(guī)劃方法。

問(wèn)題描述:

(湊硬幣問(wèn)題)給你 k 種面值的硬幣,面值分別為 c1, c2 ... ck ,每種硬幣的數(shù)量無(wú)限,
再給一個(gè)總金額 amount ,問(wèn)你最少需要幾枚硬幣湊出這個(gè)金額,如果不可能湊出,算法返回-1
這里我們k = 3 , c1 = 1、c2 = 3、c3 = 5.

1. 遞歸解法:

形象的理解遞歸
比如f是一個(gè)八卦的人,有一天a和b談戀愛(ài)了,只告訴了c。 f想知道a和b的關(guān)系, 就去問(wèn)e, e說(shuō)他自己不知道可以幫忙去問(wèn)d,d說(shuō)他也不知道可以幫忙去問(wèn)c(遞歸調(diào)用過(guò)程),c呢就告訴了d,d又告訴了e,e告訴了f(回溯過(guò)程,回溯過(guò)程可以看作答案一步步傳到f耳朵里的過(guò)程)。
用遞歸的思想來(lái)思考該問(wèn)題大致如下
我們知道amount=0和amount<0基本情況(c知道a和b的關(guān)系),求amount=11的答案(f想知道a和b的關(guān)系),ans(11)=ans(11-5)+1 or ans(11)= ans(11-3)+1 or ans(11)= ans(11-1)+1 (f去問(wèn)了他認(rèn)識(shí)的人e。如果f認(rèn)識(shí)d或者直接認(rèn)識(shí)c他可以更快的知道答案)。遞歸的回溯過(guò)程程序會(huì)自動(dòng)完成。

def colCoins_rec(coins, amount):
    """
    遞歸解法
    :param coins: 硬幣面值
    :param amount: 總金額
    :return: 湊齊總金額的最少硬幣數(shù)量
    """
    # 基本情況(c知道的情況)
    if amount == 0:
        return 0
    if amount < 0:
        return -1
    
    res = float("inf")
    # 有三種面值的硬幣可以選擇(f認(rèn)識(shí)的人,因?yàn)閒只認(rèn)識(shí)e所以只有一個(gè)選擇)
    for coin in coins:
        #選擇(f選擇問(wèn)誰(shuí),交給誰(shuí)來(lái)獲取答案)
        subproblem = colCoins_rec(coins, amount-coin)
        # 子問(wèn)題無(wú)解,(f問(wèn)到了錯(cuò)誤地人g,g是個(gè)孤兒誰(shuí)都不認(rèn)識(shí),走到了死胡同,f要問(wèn)下一個(gè)人)
        if subproblem < 0: continue
        # 選擇最優(yōu)的答案(如果f認(rèn)識(shí)c,那么他很快可以獲取答案,就可以選擇這條信息鏈)
        res = min(res, 1+colCoins_rec(coins, amount-coin))
    return res

遞歸方法存在什么問(wèn)題呢?很明顯的一個(gè)是使用了函數(shù)調(diào)用棧,棧在計(jì)算機(jī)中是一個(gè)相對(duì)很有限的資源。還有一個(gè)是存在重疊子問(wèn)題,導(dǎo)致了時(shí)間復(fù)雜度大大增加。LeetCode上這個(gè)方法一般是無(wú)法通過(guò)的。重疊子問(wèn)題是什么呢?

                    h                  存在直接連線的表示互相認(rèn)識(shí)
                 /     \               如果h想從c那里獲得c知道的答案那么他要問(wèn)認(rèn)識(shí)的f,g同樣
                f       g               f,g要問(wèn)他們認(rèn)識(shí)的人。
              /  \    /  \            觀察發(fā)現(xiàn)f,e,g被問(wèn)了多次!這是導(dǎo)致算法復(fù)雜度高的主要原因
            g    e   e   f
          /  \   ........
        e     f
      /  \
    c    d

如何解決上述存在的重疊子問(wèn)題呢?答案是使用備忘錄方法。

2備忘錄遞歸方法

備忘錄方法是這些人共用一個(gè)列表的,如果誰(shuí)已經(jīng)知道答案就填在列表對(duì)應(yīng)的元素里里,每個(gè)人在問(wèn)之前先訪問(wèn)列表,如果得到大難就不用再繼續(xù)問(wèn)下去了,避免重復(fù)的被問(wèn)。

mem = [-1]*15
def colCoins_rec_mem(coins, amount, mem):
    """
    在遞歸的解法的基礎(chǔ)上加上備忘錄解決重疊子問(wèn)題。
    :param coins: 硬幣面值
    :param amount: 總金額
    :param mem: 備忘錄
    :return: 最少硬幣數(shù)量
    """
    # 基本情況(c知道的答案)
    if amount < 0:
        return -1
    if amount == 0:
        return 0
    # 查看備忘錄,看看是否計(jì)算過(guò)(是否被問(wèn)過(guò))
    if mem[amount] != -1:
        return mem[amount]
    # 選擇(詢問(wèn)的過(guò)程)
    res = float("inf")
    for coin in coins:
        subproblem = colCoins_rec_mem(coins, amount-coin, mem)
        if subproblem < 0: continue
        res = min(res, 1+colCoins_rec_mem(coins, amount-coin, mem))
        # 得到答案后寫(xiě)進(jìn)備忘錄
        mem[amount] = res
    return res

我們可以看到基于備忘錄的遞歸算法,會(huì)在計(jì)算(詢問(wèn))前去查看備忘錄,避免重復(fù)計(jì)算。而且代碼與純遞歸方法也非常的相似。只多了查看備忘錄的過(guò)程和獲得答案后寫(xiě)進(jìn)備忘錄的操作。這個(gè)備忘錄解決了重疊子問(wèn)題,實(shí)際上他還是遞歸方法。我們可以看到其實(shí)備忘錄記錄的就是子問(wèn)題和原問(wèn)題的答案,遞歸方法是自頂向下的以“詢問(wèn)”的方法求得答案,那么可不可以讓c主動(dòng)的向他認(rèn)識(shí)的人擴(kuò)散答案直到讓f知道答案。這個(gè)自底向上的“傳播”就是動(dòng)態(tài)規(guī)劃?。ê脽ヽ這種人啊,就像我很煩動(dòng)態(tài)規(guī)劃題目)話不多說(shuō)看代碼

def colCoins_mem(coins, amount):
    """
    自底向上遞推出答案
    :param coins:  硬幣面值
    :param amount: 總金額
    :return:
    """
    # 構(gòu)造一個(gè)備忘錄
    mem = [0]*(amount+1)  # mem[i]表示湊出金額i需要的最少硬幣的數(shù)量
    # 基本情況(這句代碼可以不要)
    mem[0] = 0
    # 開(kāi)始推導(dǎo)(c開(kāi)始傳播八卦了)
    for i in range(1, amount+1):
        # 從c開(kāi)始向他認(rèn)識(shí)的人傳播消息
        for coin in coins:
            res = i - coin
            if res >= 0:
                mem[i] = mem[res] + 1
            else:
                continue
    return mem[amount]

總結(jié):

我們看看動(dòng)態(tài)規(guī)劃的要素就是:1、基本情況(知道答案的那個(gè)人)2、明確mem里面存儲(chǔ)的是什么我們也可以稱(chēng)作狀態(tài)。3、選擇(每個(gè)狀態(tài)有多少轉(zhuǎn)移方方式)4、明確狀態(tài)轉(zhuǎn)移的細(xì)節(jié)(狀態(tài)轉(zhuǎn)移方程)。該文章一氣呵成有什么不懂的留言,有空改一下。動(dòng)態(tài)規(guī)劃就到這里了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。