0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

動態(tài)規(guī)劃詳細指南(上)

jf_78858299 ? 來源:labuladong ? 作者:labuladong ? 2023-04-19 10:25 ? 次閱讀

算法技巧就那幾個套路,如果你心里有數(shù),就會輕松很多,本文就來扒一扒動態(tài)規(guī)劃的褲子,形成一套解決這類問題的思維框架。廢話不多說了,上干貨。

動態(tài)規(guī)劃問題的一般形式就是求最值 。動態(tài)規(guī)劃其實是運籌學(xué)的一種最優(yōu)化方法,只不過在計算機問題上應(yīng)用比較多,比如說讓你求最長遞增子序列呀,最小編輯距離呀等等。

既然是要求最值,核心問題是什么呢? 求解動態(tài)規(guī)劃的核心問題是窮舉 。因為要求最值,肯定要把所有可行的答案窮舉出來,然后在其中找最值唄。

動態(tài)規(guī)劃就這么簡單,就是窮舉就完事了?我看到的動態(tài)規(guī)劃問題都很難?。?/p>

首先,動態(tài)規(guī)劃的窮舉有點特別,因為這類問題 存在「重疊子問題」 ,如果暴力窮舉的話效率會極其低下,所以需要「備忘錄」或者「DP table」來優(yōu)化窮舉過程,避免不必要的計算。

而且,動態(tài)規(guī)劃問題一定會 具備「最優(yōu)子結(jié)構(gòu)」 ,才能通過子問題的最值得到原問題的最值。

另外,雖然動態(tài)規(guī)劃的核心思想就是窮舉求最值,但是問題可以千變?nèi)f化,窮舉所有可行解其實并不是一件容易的事,只有列出 正確的「狀態(tài)轉(zhuǎn)移方程 才能正確地窮舉。

以上提到的重疊子問題、最優(yōu)子結(jié)構(gòu)、狀態(tài)轉(zhuǎn)移方程就是動態(tài)規(guī)劃三要素。具體什么意思等會會舉例詳解,但是在實際的算法問題中, 寫出狀態(tài)轉(zhuǎn)移方程是最困難的 ,這也就是為什么很多朋友覺得動態(tài)規(guī)劃問題困難的原因,我來提供我研究出來的一個思維框架,輔助你思考狀態(tài)轉(zhuǎn)移方程:

明確「狀態(tài)」 -> 定義 dp 數(shù)組/函數(shù)的含義 -> 明確「選擇」-> 明確 base case。

下面通過斐波那契數(shù)列問題和湊零錢問題來詳解動態(tài)規(guī)劃的基本原理。前者主要是讓你明白什么是重疊子問題(斐波那契數(shù)列嚴(yán)格來說不是動態(tài)規(guī)劃問題),后者主要集中于如何列出狀態(tài)轉(zhuǎn)移方程。

請讀者不要嫌棄這個例子簡單, 只有簡單的例子才能讓你把精力充分集中在算法背后的通用思想和技巧上,而不會被那些隱晦的細節(jié)問題搞的莫名其妙 。想要困難的例子,歷史文章里有的是。

一、斐波那契數(shù)列

1、暴力遞歸

斐波那契數(shù)列的數(shù)學(xué)形式就是遞歸的,寫成代碼就是這樣:

int fib(int N) {
    if (N == 1 || N == 2) return 1;
    return fib(N - 1) + fib(N - 2);
}

這個不用多說了,學(xué)校老師講遞歸的時候似乎都是拿這個舉例。我們也知道這樣寫代碼雖然簡潔易懂,但是十分低效,低效在哪里?假設(shè) n = 20,請畫出遞歸樹。

PS:但凡遇到需要遞歸的問題,最好都畫出遞歸樹,這對你分析算法的復(fù)雜度,尋找算法低效的原因都有巨大幫助。

圖片

這個遞歸樹怎么理解?就是說想要計算原問題f(20),我就得先計算出子問題f(19)f(18),然后要計算f(19),我就要先算出子問題f(18)f(17),以此類推。最后遇到f(1)或者f(2)的時候,結(jié)果已知,就能直接返回結(jié)果,遞歸樹不再向下生長了。

遞歸算法的時間復(fù)雜度怎么計算?子問題個數(shù)乘以解決一個子問題需要的時間。

子問題個數(shù),即遞歸樹中節(jié)點的總數(shù)。顯然二叉樹節(jié)點總數(shù)為指數(shù)級別,所以子問題個數(shù)為 O(2^n)。

解決一個子問題的時間,在本算法中,沒有循環(huán),只有 f(n - 1) + f(n - 2) 一個加法操作,時間為 O(1)。

所以,這個算法的時間復(fù)雜度為 O(2^n),指數(shù)級別,爆炸。

觀察遞歸樹,很明顯發(fā)現(xiàn)了算法低效的原因:存在大量重復(fù)計算,比如f(18)被計算了兩次,而且你可以看到,以f(18)為根的這個遞歸樹體量巨大,多算一遍,會耗費巨大的時間。更何況,還不止f(18)這一個節(jié)點被重復(fù)計算,所以這個算法及其低效。

這就是動態(tài)規(guī)劃問題的第一個性質(zhì): 重疊子問題 。下面,我們想辦法解決這個問題。

2、帶備忘錄的遞歸解法

明確了問題,其實就已經(jīng)把問題解決了一半。即然耗時的原因是重復(fù)計算,那么我們可以造一個「備忘錄」,每次算出某個子問題的答案后別急著返回,先記到「備忘錄」里再返回;每次遇到一個子問題先去「備忘錄」里查一查,如果發(fā)現(xiàn)之前已經(jīng)解決過這個問題了,直接把答案拿出來用,不要再耗時去計算了。

一般使用一個數(shù)組充當(dāng)這個「備忘錄」,當(dāng)然你也可以使用哈希表(字典),思想都是一樣的。

int fib(int N) {
    if (N < 1) return 0;
    // 備忘錄全初始化為 0
    vector<int> memo(N + 1, 0);
    // 初始化最簡情況
    return helper(memo, N);
}

int helper(vector<int>& memo, int n) {
    // base case 
    if (n == 1 || n == 2) return 1;
    // 已經(jīng)計算過
    if (memo[n] != 0) return memo[n];
    memo[n] = helper(memo, n - 1) + 
                helper(memo, n - 2);
    return memo[n];
}

現(xiàn)在,畫出遞歸樹,你就知道「備忘錄」到底做了什么:

圖片

實際上,帶「備忘錄」的遞歸算法,把一棵存在巨量冗余的遞歸樹通過「剪枝」,改造成了一幅不存在冗余的遞歸圖,極大減少了子問題(即遞歸圖中節(jié)點)的個數(shù)。

圖片

遞歸算法的時間復(fù)雜度怎么算? 子問題個數(shù)乘以解決一個子問題需要的時間。

子問題個數(shù),即圖中節(jié)點的總數(shù),由于本算法不存在冗余計算,子問題就是f(1),f(2),f(3)f(20),數(shù)量和輸入規(guī)模 n = 20 成正比,所以子問題個數(shù)為 O(n)。

解決一個子問題的時間,同上,沒有什么循環(huán),時間為 O(1)。

所以,本算法的時間復(fù)雜度是 O(n)。比起暴力算法,是降維打擊。

至此,帶備忘錄的遞歸解法的效率已經(jīng)和迭代的動態(tài)規(guī)劃一樣了。實際上,這種解法和迭代的動態(tài)規(guī)劃思想已經(jīng)差不多,只不過這種方法叫做「自頂向下」,動態(tài)規(guī)劃叫做「自底向上」。

啥叫「自頂向下」? 注意我們剛才畫的遞歸樹(或者說圖),是從上向下延伸,都是從一個規(guī)模較大的原問題比如說f(20),向下逐漸分解規(guī)模,直到f(1)f(2)觸底,然后逐層返回答案,這就叫「自頂向下」。

啥叫「自底向上」? 反過來,我們直接從最底下,最簡單,問題規(guī)模最小的f(1)f(2)開始往上推,直到推到我們想要的答案f(20),這就是動態(tài)規(guī)劃的思路,這也是為什么動態(tài)規(guī)劃一般都脫離了遞歸,而是由循環(huán)迭代完成計算。

3、dp 數(shù)組的迭代解法

有了上一步「備忘錄」的啟發(fā),我們可以把這個「備忘錄」獨立出來成為一張表,就叫做 DP table 吧,在這張表上完成「自底向上」的推算豈不美哉!

int fib(int N) {
    vector<int> dp(N + 1, 0);
    // base case
    dp[1] = dp[2] = 1;
    for (int i = 3; i <= N; i++)
        dp[i] = dp[i - 1] + dp[i - 2];
    return dp[N];
}

畫個圖就很好理解了,而且你發(fā)現(xiàn)這個 DP table 特別像之前那個「剪枝」后的結(jié)果,只是反過來算而已。實際上,帶備忘錄的遞歸解法中的「備忘錄」,最終完成后就是這個 DP table,所以說這兩種解法其實是差不多的,大部分情況下,效率也基本相同。

這里,引出「狀態(tài)轉(zhuǎn)移方程」這個名詞,實際上就是描述問題結(jié)構(gòu)的數(shù)學(xué)形式:

圖片

為啥叫「狀態(tài)轉(zhuǎn)移方程」?為了聽起來高端。你把 f(n) 想做一個狀態(tài) n,這個狀態(tài) n 是由狀態(tài) n - 1 和狀態(tài) n - 2 相加轉(zhuǎn)移而來,這就叫狀態(tài)轉(zhuǎn)移,僅此而已。

你會發(fā)現(xiàn),上面的幾種解法中的所有操作,例如 return f(n - 1) + f(n - 2),dp[i] = dp[i - 1] + dp[i - 2],以及對備忘錄或 DP table 的初始化操作,都是圍繞這個方程式的不同表現(xiàn)形式??梢娏谐觥笭顟B(tài)轉(zhuǎn)移方程」的重要性,它是解決問題的核心。很容易發(fā)現(xiàn),其實狀態(tài)轉(zhuǎn)移方程直接代表著暴力解法。

千萬不要看不起暴力解,動態(tài)規(guī)劃問題最困難的就是寫出狀態(tài)轉(zhuǎn)移方程 ,即這個暴力解。優(yōu)化方法無非是用備忘錄或者 DP table,再無奧妙可言。

這個例子的最后,講一個細節(jié)優(yōu)化。細心的讀者會發(fā)現(xiàn),根據(jù)斐波那契數(shù)列的狀態(tài)轉(zhuǎn)移方程,當(dāng)前狀態(tài)只和之前的兩個狀態(tài)有關(guān),其實并不需要那么長的一個 DP table 來存儲所有的狀態(tài),只要想辦法存儲之前的兩個狀態(tài)就行了。所以,可以進一步優(yōu)化,把空間復(fù)雜度降為 O(1):

int fib(int n) {
    if (n == 2 || n == 1) 
        return 1;
    int prev = 1, curr = 1;
    for (int i = 3; i <= n; i++) {
        int sum = prev + curr;
        prev = curr;
        curr = sum;
    }
    return curr;
}

有人會問,動態(tài)規(guī)劃的另一個重要特性「最優(yōu)子結(jié)構(gòu)」,怎么沒有涉及?下面會涉及。斐波那契數(shù)列的例子嚴(yán)格來說不算動態(tài)規(guī)劃,因為沒有涉及求最值,以上旨在演示算法設(shè)計螺旋上升的過程。

下面,看第二個例子,湊零錢問題。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 計算機
    +關(guān)注

    關(guān)注

    19

    文章

    7360

    瀏覽量

    87632
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4277

    瀏覽量

    62323
  • 動態(tài)規(guī)劃
    +關(guān)注

    關(guān)注

    0

    文章

    17

    瀏覽量

    8902
收藏 人收藏

    評論

    相關(guān)推薦

    quartusII 詳細使用指南

    quartusII 詳細使用指南 應(yīng)該有用
    發(fā)表于 04-28 09:24

    動態(tài)規(guī)劃算法。

    動態(tài)規(guī)劃算法資料。
    發(fā)表于 08-30 20:44

    運籌優(yōu)化之動態(tài)規(guī)劃解析

    運籌優(yōu)化(七)--動態(tài)規(guī)劃解析
    發(fā)表于 05-12 09:57

    LCS的動態(tài)規(guī)劃算法

    LCS的動態(tài)規(guī)劃算法(自底向上)
    發(fā)表于 05-25 15:06

    動態(tài)規(guī)劃與貪婪法題的背包問題總結(jié)

    【LeetCode & 劍指offer刷題】動態(tài)規(guī)劃與貪婪法題16:背包問題總結(jié)
    發(fā)表于 06-09 16:44

    企業(yè)節(jié)能規(guī)劃指南

    企業(yè)節(jié)能規(guī)劃指南:節(jié)能:  指加強用能管理,采取技術(shù)可行、經(jīng)濟合理以及環(huán)境和社會可以承受的措施, 減少從能源生產(chǎn)到消費各個環(huán)節(jié)的損失和  浪費,更有效
    發(fā)表于 07-05 18:01 ?11次下載

    TSC動態(tài)補償柜選型指南

    TSC動態(tài)補償柜選型指南 Elspec是在國際領(lǐng)先進行動態(tài)無功補償和濾波的公司,在全球有4個工廠(以色列、葡萄牙、美國),總部位于
    發(fā)表于 03-24 17:27 ?1471次閱讀

    基于動態(tài)規(guī)劃法的電力資源的合理分配

    通過實例在Matlab中展現(xiàn)了基于動態(tài)規(guī)劃法,解決電力資源合理分配的問題,使得現(xiàn)實中電力資源的分配問題得到簡化和程序化。結(jié)果顯示,動態(tài)規(guī)劃法在電力資源的合理分配問題上比較實用
    發(fā)表于 12-07 14:15 ?19次下載

    基于動態(tài)規(guī)劃的梯級泵站優(yōu)化調(diào)度研究_專祥濤

    基于動態(tài)規(guī)劃的梯級泵站優(yōu)化調(diào)度研究_專祥濤
    發(fā)表于 01-21 12:16 ?0次下載

    基于時延Q學(xué)習(xí)的機器人動態(tài)規(guī)劃方法

    機器人動態(tài)規(guī)劃是指在某一個給定的運行空間中,移動機器人通過路徑的動態(tài)規(guī)劃來獲得一條從初始位置到目標(biāo)位置的最優(yōu)路徑。環(huán)境未知的情況下的機器人路徑規(guī)劃
    發(fā)表于 11-28 17:01 ?0次下載
    基于時延Q學(xué)習(xí)的機器人<b class='flag-5'>動態(tài)</b><b class='flag-5'>規(guī)劃</b>方法

    分層學(xué)習(xí)的自適應(yīng)動態(tài)規(guī)劃

    本文基于嬰兒的認(rèn)知發(fā)育模型LOC (Levels of Consciousness)提出了基于分層學(xué)習(xí)的自適應(yīng)動態(tài)規(guī)劃方法以改進學(xué)習(xí)和優(yōu)化。根據(jù)LOC模型中感知的層次性以及工作目標(biāo)的層次定義,為
    發(fā)表于 01-05 15:13 ?0次下載
    分層學(xué)習(xí)的自適應(yīng)<b class='flag-5'>動態(tài)</b><b class='flag-5'>規(guī)劃</b>

    動態(tài)規(guī)劃方法的利用matlab實現(xiàn)及其應(yīng)用的有效工具詳細資料概述

    本文運用 matlab 語言實現(xiàn)了動態(tài)規(guī)劃的逆序算法,根據(jù)狀態(tài)變量的維數(shù),編寫了指標(biāo)函數(shù)最小值的逆序算法遞歸計算程序。兩個實例的應(yīng)用檢驗了該程序的有效性,同時也表明了該算法程序?qū)Ρ姸囝惖湫偷?b class='flag-5'>動態(tài)
    發(fā)表于 06-14 08:00 ?5次下載
    <b class='flag-5'>動態(tài)</b><b class='flag-5'>規(guī)劃</b>方法的利用matlab實現(xiàn)及其應(yīng)用的有效工具<b class='flag-5'>詳細</b>資料概述

    動態(tài)規(guī)劃和遞歸有什么區(qū)別和聯(lián)系

    ? 前言 大家好,我是bigsai,好久不見,甚是想念(天天想念)! 很久前就有小伙伴被動態(tài)規(guī)劃所折磨,確實,很多題動態(tài)規(guī)劃確實太難看出了了,甚至有的題看了題解理解起來都費勁半天。
    的頭像 發(fā)表于 11-16 17:27 ?3145次閱讀

    國賽算法--動態(tài)規(guī)劃詳細資料

    動態(tài)規(guī)劃(dynamic programming)是運籌學(xué)的一個分支,是求解決策過程(decision process)最優(yōu)化的數(shù)學(xué)方法。20 世紀(jì) 50 年代初 R. E. Bellman
    發(fā)表于 11-24 09:57 ?0次下載

    動態(tài)規(guī)劃詳細指南(下)

    動態(tài)規(guī)劃問題的一般形式就是求最值 。動態(tài)規(guī)劃其實是運籌學(xué)的一種最優(yōu)化方法,只不過在計算機問題上應(yīng)用比較多,比如說讓你求最長遞增子序列呀,最小編輯距離呀等等。
    的頭像 發(fā)表于 04-19 10:25 ?476次閱讀
    <b class='flag-5'>動態(tài)</b><b class='flag-5'>規(guī)劃</b><b class='flag-5'>詳細</b><b class='flag-5'>指南</b>(下)