日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【动态规划】不信看完你还不懂动态规划

發(fā)布時間:2025/3/20 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【动态规划】不信看完你还不懂动态规划 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

1.什么是動態(tài)規(guī)劃?

維基百科:動態(tài)規(guī)劃(Dynamic programming,簡稱DP)是一種通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。

使用場景:動態(tài)規(guī)劃常常適用于有重疊子問題和最優(yōu)子結構性質的問題。

dynamic programming is a m·· ethod for solving a complex problem by breaking it down into a collection of simpler subproblems.

簡單來說,動態(tài)規(guī)劃其實就是,給定一個問題,我們把它拆成一個個子問題,直到子問題可以直接解決,然后把子問題答案保存起來,以減少重復計算。再根據(jù)子問題答案反推,得出原問題解的一種方法。

動態(tài)規(guī)劃最核心的思想,就在于拆分子問題,記住過往,減少重復計算。

一般這些子問題很相似,可以通過函數(shù)關系式遞推出來。動態(tài)規(guī)劃就致力于解決每個子問題一次,減少重復計算,比如`斐波那契數(shù)列`,就可以看做入門級的經(jīng)典動態(tài)規(guī)劃問題。

2.動態(tài)規(guī)劃3個核心點

  • 確定邊界--退出條件
  • 確定最優(yōu)子結構--拆分子問題
  • 狀態(tài)轉移方程--子問題合并方式,即子問題和原問題關系,將子問題結果合并得出最終答案

對于這個3點,后面會做一一解釋,請看我道來。

3.從「錢」講起

一個算法星球的央行發(fā)行了奇葩幣,幣值分別為1、5、11,要湊夠15元最少要幾個貨幣?

它的問題其實是「給定一組面額的硬幣,我們用現(xiàn)有的幣值湊出n最少需要多少個幣」。

我們要湊夠這個 n,只要 n 不為0,那么總會有處在最后一個的硬幣,這個硬幣恰好湊成了 n,比如我們用 {11,1,1,1,1} 來湊15,前面我們拿出 {11,1,1,1},最后我們拿出 {1} 正好湊成 15。

?如果用 {5,5,5} 來湊15,最后一個硬幣就是5,我們按照這個思路捋一捋,:

  • 那么假設最后一個硬幣為11的話,那么剩下4,這個時候問題又變成了,我們湊出 n-11 最少需要多少個幣,此時n=4,我們只能取出4個面值為1的幣
  • 如果假設最后一個硬幣為 5 的話,這個時候問題又變成了,我們用現(xiàn)有的幣值湊出 n-5 最少需要多少個幣

大家發(fā)現(xiàn)了沒有,我們的問題提可以不斷被分解為「我們用現(xiàn)有的幣值湊出 n 最少需要多少個幣」,比如我們用 f(n) 函數(shù)代表 「湊出 n 最少需要多少個幣」.

把「原有的大問題逐漸分解成類似的但是規(guī)模更小的子問題」這就是最優(yōu)子結構,我們可以通過自底向上的方式遞歸地從子問題的最優(yōu)解逐步構造出整個問題的最優(yōu)解。

這個時候我們分別假設 1、5、11 三種面值的幣分別為最后一個硬幣的情況:

  • 最后一枚硬幣的面額為 11: min = f(4) + 1
  • 最后一枚硬幣的面額為 5: min = f(10) + 1
  • 最后一枚硬幣的面額為 1: min = f(14) + 1

這個時候大家發(fā)現(xiàn)問題所在了嗎?最少找零 min 與 f(4)、f(10)、f(14) 三個函數(shù)解中的最小值是有關的,畢竟后面的「+1」是大家都有的。

假設湊的硬幣總額為 n,那么 f(4) = f(n-11)、f(10) = f(n-5)、f(14) = f(n-1),我們得出以下公式:

f(n) = min{f(n-1), f(n-5), f(n-11)} + 1?

我們再具體到上面公式中 f(n-1) 湊夠它的最小硬幣數(shù)量是多少,是不是又變成下面這個公式:

f(n-1) = min{f(n-1-1), f(n-1-5), f(n-1-11)} + 1

以此類推...

這真是似曾相識,這不就是遞歸嗎?是的,我們可以通過遞歸來求出最少找零問題的解。

4.遞歸解法

public static int coinChange(int n) {if (n <= 0) return 0;int min = Integer.MAX_VALUE;if (n >= 1) {min = Math.min(coinChange(n - 1) + 1, min);}if (n >= 5) {min = Math.min(coinChange(n - 5) + 1, min);}if (n >= 11) {min = Math.min(coinChange(n - 11) + 1, min);}return min; }
  • 當n=0的時候,直接返回0,增加程序魯棒性
  • 我們先設最少找零 min 為 「無限大」,方便之后Math.min 求最小值
  • 當最后一個硬幣為1的時候,我們遞歸 min = Math.min(f(n-1) + 1, min),求此種情況下的最小找零
  • 當最后一個硬幣為5的時候,我們遞歸 min = Math.min(f(n-5) + 1, min),求此種情況下的最小找零
  • 當最后一個硬幣為11的時候,我們遞歸 min = Math.min(f(n-11) + 1, min),求此種情況下的最小找零

遞歸的弊端

我們看似已經(jīng)把問題解決了,但是別著急,我們繼續(xù)測試,當n越來越大時,執(zhí)行時間幾何數(shù)增長,而且最后還出現(xiàn)棧溢出的情況。所以為什么會造成如此長的執(zhí)行耗時?歸根到底是遞歸算法的低效導致的。

我們?nèi)绻嬎鉬(70)就需要分別計算最后一個幣為1、5、11三種面值時的不同情況,而這三種不同情況作為子問題又可以被分解為三種情況,依次類推...這樣的算法復雜度有 O(3?),這是極為低效的。

我們再仔細看圖:

我們用紅色標出來的都是相同的計算函數(shù),比如有兩個f(64)、f(58)、f(54),這些都是重復的,這些只是我們整個計算體系下的冰山一角,我們還有非常多的重復計算沒辦法在圖中展示出來。

可見我們重復計算了非常多的無效函數(shù),浪費了算力。

我們不妨再舉一個簡單的例子,比如我們要計算 「1 + 1 + 1 + 1 + 1 + 1 + 1 + 1」的和。

我們開始數(shù)數(shù)...,直到我們數(shù)出上面計算的和為 8,那么,我們再在上述 「1 + 1 + 1 + 1 + 1 + 1 + 1 + 1」 后面 「+ 1」,那么和是多少?

這個時候你肯定數(shù)都不會數(shù),脫口而出「9」。

為什么我們在后面的計算這么快?是因為我們已經(jīng)在大腦中記住了之前的結果 「8」,我們只需要計算「8 + 1」即可,這避免了我們重復去計算前面的已經(jīng)計算過的內(nèi)容。

我們用的遞歸像什么?像繼續(xù)數(shù)「1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1」來計算出「9」,這是非常耗時的。

我們假設用 m 種面值的硬幣湊 n 最少需要多少硬幣,在上述問題下遞歸的時間復雜度是驚人的O(n?),指數(shù)級的時間復雜度可以說是最差的時間復雜度之一了。我們已經(jīng)發(fā)現(xiàn)問題所在了,大量的重復計算導致時間復雜度奇高,我們必須想辦法解決這個問題。

5.備忘錄與遞歸

既然已經(jīng)知道存在大量冗余計算了,那么我們可不可以建立一個備忘錄,把計算過的答案記錄在備忘錄中,再有我們需要答案的時候,我們?nèi)渫浿胁檎?如果能查找到就直接返回答案,這樣就避免了重復計算,這就是算法中典型的空間換時間的思維。

有了思路后,其實代碼實現(xiàn)非常簡單,我們只需要建立一個緩存?zhèn)渫?在函數(shù)內(nèi)部校驗校驗是否存在在結果,如果存在返回即可。

public class Cions {public static void main(String[] args) {int a = coinChange(70);System.out.println(a);}private static HashMap<Integer,Integer> cache = new HashMap<>();public static int coinChange(int amount) {return makeChange(amount);}public static int makeChange(int amount) {if (amount <= 0) return 0;// 校驗是否已經(jīng)在備忘錄中存在結果,如果存在返回即可if(cache.get(amount) != null) return cache.get(amount);int min = Integer.MAX_VALUE;if (amount >= 1) {min = Math.min(makeChange(amount-1) + 1, min);}if (amount >= 5) {min = Math.min(makeChange(amount-5) + 1, min);}if (amount >= 11) {min = Math.min(makeChange(amount-11) + 1, min);}cache.put(amount, min);return min;} }

實際上利用備忘錄來解決遞歸重復計算的問題叫做「記憶化搜索」。

這個方法本質上跟回溯法的「剪枝」是一個目的,就是把上圖中存在重復的節(jié)點全部剔除,只保留一個節(jié)點即可,當然上圖沒辦法把所有節(jié)點全部展示出來,如果剔除全部重復節(jié)點最后只會留下線性的節(jié)點形式:

?帶備忘錄的遞歸算法時間復雜度只有O(n),已經(jīng)跟動態(tài)規(guī)劃的時間復雜度相差不大了。

那么這不就可以了嗎?為什么還要搞動態(tài)規(guī)劃?還記得我們上面提到遞歸的另一大問題嗎?

爆棧!編程語言棧的深度是有限的,即使我們進行了剪枝,在五位數(shù)以上的情況下就會再次產(chǎn)生爆棧的情況,這導致遞歸根本無法完成大規(guī)模的計算任務。

這是遞歸的計算形式?jīng)Q定的,我們這里的遞歸是「自頂向下」的計算思路,即從 f(70) f(69)...f(1) 逐步分解。「自頂向下」的思路在另一種算法思想中非常常見,那就是分治算法。

這個思路在這里并不完全適用,我們需要一種「自底向上」的思路來解決問題。「自底向上」就是 f(1) ... f(70) f(69)通過小規(guī)模問題遞推來解決大規(guī)模問題,動態(tài)規(guī)劃通常是用迭代取代遞歸來解決問題。

除此之外,遞歸+備忘錄的另一個缺陷就是再沒有優(yōu)化空間了,因為在最壞的情況下,遞歸的最大深度是 n。因此,我們需要系統(tǒng)遞歸堆棧使用 O(n) 的空間,這是遞歸形式?jīng)Q定的,而換成迭代之后我們根本不需要如此多的的儲存空間,我們可以繼續(xù)往下看。

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

還記得上面我們利用備忘錄緩存之后各個節(jié)點的形式是什么樣的嗎,我們把它這個「備忘錄」作為一張表,這張表就叫做 DP table,如下:

?注意: 上圖中 f[n] 代表湊夠 n 最少需要多少幣的函數(shù),方塊內(nèi)的數(shù)字代表函數(shù)的結果

我們不妨在上圖中找找規(guī)律?

我們觀察f[1]: f[1] = min(f[0], f[-5], f[-11]) + 1

由于f[-5] 這種負數(shù)是不存在的,我們都設為正無窮大,那么f[1] = 1。

再看看f[5]: f[1] = min(f[4], f[0], f[-6]) + 1,這實際是在求f[4] = 4、f[0] = 0、f[-6]=Infinity中最小的值即0,最后加上1,即1,那么f[5] = 1。

狀態(tài)轉移方程

發(fā)現(xiàn)了嗎?我們?nèi)魏我粋€節(jié)點都可以通過之前的節(jié)點來推導出來,根本無需再做重復計算,這個相關的方程是:

f[n] = min(f[n-1], f[n-5], f[n-11]) + 1

還記得我們提到的動態(tài)規(guī)劃有更大的優(yōu)化空間嗎?遞歸+備忘錄由于遞歸深度的原因需要 O(n) 的空間復雜度,但是基于迭代的動態(tài)規(guī)劃只需要常數(shù)級別的復雜度。

看下圖,比如我們求解 f(70),只需要前面三個解,即 f(59) f(69) f(65) 套用公式即可求得,那么 f(0)f(1) ... f(58) 根本就沒有用了,我們可以不再儲存它們占用額外空間,這就留下了我們優(yōu)化的空間。

?上面的方程就是動態(tài)轉移方程,而解決動態(tài)規(guī)劃題目的鑰匙也正是這個動態(tài)轉移方程。

當然,如果你只推導出了動態(tài)轉移方程基本上可以把動態(tài)規(guī)劃題做出來了,但是往往很多人卻做不對,這是為什么?這就得考慮邊界問題。

邊界問題

部分的邊界問題其實我們在上面的部分已經(jīng)給出解決方案了,針對這個找零問題我們有以下邊界問題。

處理f[n]中n為負數(shù)的問題:

凡是n為負數(shù)的情況,一律將f[n]視為正無窮大。?

因為正常情況下我們是不會有下角標為負數(shù)的數(shù)組的,所以其實 n 為負數(shù)的 f[n] 根本就不存在

又因為我們要求最少找零,為了排除這種不存在的情況,也便于我們計算

我們直接將其視為正無窮大,可以最大程度方便我們的動態(tài)轉移方程的實現(xiàn)。

處理f[n]中n為0的問題

n=0 的情況屬于動態(tài)轉移方程的初始條件

初始條件也就是動態(tài)轉移方程無法處理的特殊情況

比如我們?nèi)绻麤]有這個初始條件,我們的方程是這樣的: f[0] = min(f[-1], f[-5], f[-11]) + 1

最小的也是正無窮大,這是特殊情況無法處理,因此我們只能人肉設置初始條件。

處理好邊界問題我們就可以得到完整的動態(tài)轉移方程了:

f[0] = 0 (n=0) f[n] = min(f[n-1], f[n-5], f[n-11]) + 1 (n>0)

那么我們再回到這個找零問題中,這次我們假設給出不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數(shù)來計算可以湊成總金額所需的最少的硬幣個數(shù)。如果沒有任何一種硬幣組合能組成總金額,返回?-1。

比如輸入: coins = [1, 2, 5], amount = 11 輸出: 3 解釋: 11 = 5 + 5 + 1 復制代碼

其實上面的找零問題就是我們一直處理的找零問題的通用化,我們的面額是定死的,即1、5、11,這次是不定的,而是給了一個數(shù)組 coins 包含了相關的面值。

我們重新理一下思路:

  • 確定最優(yōu)子結構: 最優(yōu)子結構即原問題的解由子問題的最優(yōu)解構成,我們假設最少需要k個硬幣湊足總面額n,那么f(n) = min{f(n-c?)}, c? 即是硬幣的面額。
  • 處理邊界問題: 依然是老套路,當n為負數(shù)的時候,值為正無窮大,當n=0時,值也為0.
  • 得出動態(tài)轉移方程:?f[0] = 0 (n=0) f[n] = min(f[n-c?]) + 1 (n>0)?

我們根據(jù)上面的推導,得出以下代碼:

public int coinChange(int[] coins, int amount) {// 初始化備忘錄,用amount+1填滿備忘錄,amount+1 表示該值不可以用硬幣湊出來int[] dp = new int[amount + 1];Arrays.fill(dp,amount+1);// 設置初始條件為 0dp[0]=0;for (int coin : coins) {for (int i = coin; i <= amount; i++) {// 根據(jù)動態(tài)轉移方程求出最小值if(coin <= i) {dp[i]=Math.min(dp[i],dp[i-coin]+1);}}}// 如果 `dp[amount] === amount+1`說明沒有最優(yōu)解返回-1,否則返回最優(yōu)解return dp[amount] == amount+1 ? -1 : dp[amount]; }

7.小結

我們總結一下學習歷程:

  • 經(jīng)過分析,我們用遞歸的方式解決了最少找零問題
  • 但是經(jīng)過算法復雜度分析和實際測試,我們發(fā)現(xiàn)遞歸的方法效率奇低,我們必須用一種方法來解決當前問題
  • 我們用備忘錄+遞歸的形式解決了時間復雜度問題,但是自頂向下的思路導致我們無法擺脫爆棧的陰霾,我們需要一種「自底向上」的全新思路
  • 我們通過動態(tài)轉移方程以迭代的方式高效地解出了此題
  • 其實動態(tài)規(guī)劃本質上就是被一再優(yōu)化過的暴力破解,我們通過動態(tài)規(guī)劃減少了大量的重疊子問題,此后我們講到的所有動態(tài)規(guī)劃題目的解題過程,都可以從暴力破解一步步優(yōu)化到動態(tài)規(guī)劃。

    可能你會問面試題這么多,到底哪一道應該用動態(tài)規(guī)劃?如何判斷?

    其實最準確的辦法就是看題目中的給定的問題,這個問題能不能被分解為子問題,再根據(jù)子問題的解是否可以得出原問題的解。

    當然上面的方法雖然準確,但是需要一定的經(jīng)驗積累,我們可以用一個雖然不那么準確,但是足夠簡單粗暴的辦法,如果題目滿足以下條件之一,那么它大概率是動態(tài)規(guī)劃題目:

    • 求最大值,最小值
    • 判斷方案是否可行
    • 統(tǒng)計方案個數(shù)

    參考文檔:

    看一遍就理解:動態(tài)規(guī)劃詳解

    一文搞懂動態(tài)規(guī)劃

    總結

    以上是生活随笔為你收集整理的【动态规划】不信看完你还不懂动态规划的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 精品女同一区 | 骑骑上司妻电影 | 欧美熟妇精品一区二区蜜桃视频 | 亚洲成年人影院 | 亚洲高清视频在线播放 | 91视频合集| 久久久久中文字幕 | 日韩视频在线观看二区 | 欧美日韩专区 | av在线观看地址 | 国产第4页 | 潮喷失禁大喷水aⅴ无码 | 丰满岳妇乱一区二区三区 | 无码人妻丰满熟妇啪啪欧美 | 91成人免费看 | 3p在线视频| 亚洲鲁鲁 | 狠狠爱夜夜 | 美女网站在线免费观看 | 欧美专区日韩专区 | 神马午夜888 | 中文无码日韩欧 | 手机在线一区 | 欧美日韩国产免费一区二区三区 | cao久久| 久久精品人人做人人爽 | av资源新版在线天堂 | 日本乱大交xxxx公交车 | 天天爱天天做天天爽 | 久久9久久| 超碰狠狠 | av中字| 国产精品xxx | 丁香啪啪综合成人亚洲 | 可以看的av网址 | 看一级黄色大片 | 激情视频久久 | 欧美女优在线观看 | 精品人妻一区二区三区四区 | 蜜臀久久精品久久久久 | 亚州中文| 国产激情影院 | 青青草小视频 | 夜夜导航 | 2022精品国偷自产免费观看 | 一级看片免费视频 | 中文字幕综合 | 激情片 | 久章草在线观看 | 少妇脱了内裤让我添 | 日本伦理中文字幕 | 国内精品在线观看视频 | 懂色av一区二区三区蜜臀 | 国产综合久久久久久鬼色 | 久久免费网 | 亚洲无卡视频 | 91秘密入口 | 五月婷婷网 | 欧美在线免费视频 | 国语对白自拍 | 欧美一区中文字幕 | 老熟妇一区二区三区 | 丰满少妇在线观看bd | 日本熟妇成熟毛茸茸 | 国产性猛交xxxⅹ交酡全过程 | 久草资源站 | 伊人爱爱网 | 特黄一区二区 | 国产精品一区麻豆 | 日本人妻一区二区三区 | 牛牛超碰| 国产有码在线 | 玖草在线观看 | 日本熟妇一区二区三区四区 | 97狠狠| 日日操夜夜草 | 午夜网站免费 | 欧美日韩综合在线 | jizz成熟丰满日本少妇 | 秋霞毛片少妇激情免费 | 一区二区国产电影 | 久草久操 | 黄色高清视频 | 男女视频一区二区 | 亚洲在线播放 | 亚洲婷婷在线视频 | 国产成人精品一区二区三区免费 | 亚洲69视频 | 日产精品久久久久久久 | 久久久久久穴 | 国产黄色视 | 欧美日韩色视频 | 欧美性猛交bbbbb精品 | 日日夜夜爽爽 | 国产精品二区三区 | 一二三四av | 理论片在线观看视频 | 欧美成人午夜剧场 | www.一区|