動態規劃:關於01背包問題,你該了解這些!(滾動數組)

2021-02-15 代碼隨想錄

通知:我已經將刷題指南全部整理到了Github :https://github.com/youngyangyang04/leetcode-master,方便大家在電腦上閱讀,這個倉庫每天都會更新,大家快去給一個star支持一下吧!

昨天動態規劃:關於01背包問題,你該了解這些!中是用二維dp數組來講解01背包。

今天我們就來說一說滾動數組,其實在前面的題目中我們已經用到過滾動數組了,就是把二維dp降為一維dp,一些錄友當時還表示比較困惑。

那麼我們通過01背包,來徹底講一講滾動數組!

接下來還是用如下這個例子來進行講解

背包最大重量為4。

物品為:

問背包能背的物品最大價值是多少?

一維dp數組(滾動數組)

對於背包問題其實狀態都是可以壓縮的。

在使用二維數組的時候,遞推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其實可以發現如果把dp[i - 1]那一層拷貝到dp[i]上,表達式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

於其把dp[i - 1]這一層拷貝到dp[i]上,不如只用一個一維數組了,只用dp[j](一維數組,也可以理解是一個滾動數組)。

這就是滾動數組的由來,需要滿足的條件是上一層可以重複利用,直接拷貝到當前層。

讀到這裡估計大家都忘了 dp[i][j]裡的i和j表達的是什麼了,i是物品,j是背包容量。

dp[i][j] 表示從下標為[0-i]的物品裡任意取,放進容量為j的背包,價值總和最大是多少

一定要時刻記住這裡i和j的含義,要不然很容易看懵了。

動規五部曲分析如下:

在一維dp數組中,dp[j]表示:容量為j的背包,所背的物品價值可以最大為dp[j]。

dp[j]為 容量為j的背包所背的最大價值,那麼如何推導dp[j]呢?

dp[j]可以通過dp[j - weight[j]]推導出來,dp[j - weight[i]]表示容量為j - weight[i]的背包所背的最大價值。

dp[j - weight[i]] + value[i] 表示 容量為 j - 物品i重量 的背包 加上 物品i的價值。(也就是容量為j的背包,放入物品i了之後的價值即:dp[j])

此時dp[j]有兩個選擇,一個是取自己dp[j],一個是取dp[j - weight[i]] + value[i],指定是取最大的,畢竟是求最大價值,

所以遞歸公式為:

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

可以看出相對於二維dp數組的寫法,就是把dp[i][j]中i的維度去掉了。

關於初始化,一定要和dp數組的定義吻合,否則到遞推公式的時候就會越來越亂

dp[j]表示:容量為j的背包,所背的物品價值可以最大為dp[j],那麼dp[0]就應該是0,因為背包容量為0所背的物品的最大價值就是0。

那麼dp數組除了下標0的位置,初始為0,其他下標應該初始化多少呢?

看一下遞歸公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

dp數組在推導的時候一定是取價值最大的數,如果題目給的價值都是正整數那麼非0下標都初始化為0就可以了,如果題目給的價值有負數,那麼非0下標就要初始化為負無窮。

這樣才能讓dp數組在遞歸公式的過程中取的最大的價值,而不是被初始值覆蓋了

那麼我假設物品價值都是大於0的,所以dp數組初始化的時候,都初始為0就可以了。

代碼如下:

for(int i = 0; i < weight.size(); i++) { // 遍歷物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍歷背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

這裡大家發現和二維dp的寫法中,遍歷背包的順序是不一樣的!

二維dp遍歷的時候,背包容量是從小到大,而一維dp遍歷的時候,背包是從大到小。

為什麼呢?

倒敘遍歷是為了保證物品i只被放入一次!,在動態規劃:關於01背包問題,你該了解這些!中講解二維dp數組初始化dp[0][j]時候已經講解到過一次。

舉一個例子:物品0的重量weight[0] = 1,價值value[0] = 15

如果正序遍歷

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此時dp[2]就已經是30了,意味著物品0,被放入了兩次,所以不能正序遍歷。

為什麼倒敘遍歷,就可以保證物品只放入一次呢?

倒敘就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15  (dp數組已經都初始化為0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以從後往前循環,每次取得狀態不會和之前取得狀態重合,這樣每種物品就只取一次了。

那麼問題又來了,為什麼二維dp數組歷的時候不用倒敘呢?

因為對於二維dp,dp[i][j]都是通過上一層即dp[i - 1][j]計算而來,本層的dp[i][j]並不會被覆蓋!

(如何這裡讀不懂,大家就要動手試一試了,空想還是不靠譜的,實踐出真知!)

再來看看兩個嵌套for循環的順序,代碼中是先遍歷物品嵌套遍歷背包容量,那可不可以先遍歷背包容量嵌套遍歷物品呢?

不可以!

因為一維dp的寫法,背包容量一定是要倒序遍歷(原因上面已經講了),如果遍歷背包容量放在上一層,那麼每個dp[j]就只會放入一個物品,即:背包裡只放入了一個物品。

(這裡如果讀不懂,就在回想一下dp[j]的定義,或者就把兩個for循環順序顛倒一下試試!)

所以一維dp數組的背包在遍歷順序上和二維其實是有很大差異的!,這一點大家一定要注意。

一維dp,費用用物品0,物品1,物品2 來遍歷背包,最終得到結果如下:

動態規劃-背包問題9一維dp01背包完整C++測試代碼
void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍歷物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍歷背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

int main() {
    test_1_wei_bag_problem();
}

可以看出,一維dp 的01背包,要比二維簡潔的多!初始化 和 遍歷順序相對簡單了。

所以我傾向於使用一維dp數組的寫法,比較直觀簡潔,而且空間複雜度還降了一個數量級!

在後面背包問題的講解中,我都直接使用一維dp數組來進行推導

總結

以上的講解可以開發一道面試題目(畢竟力扣上沒原題)。

就是本文中的題目,要求先實現一個純二維的01背包,如果寫出來了,然後再問為什麼兩個for循環的嵌套順序這麼寫?反過來寫行不行?再講一講初始化的邏輯。

然後要求實現一個一維數組的01背包,最後再問,一維數組的01背包,兩個for循環的順序反過來寫行不行?為什麼?

注意以上問題都是在候選人把代碼寫出來的情況下才問的。

就是純01背包的題目,都不用考01背包應用類的題目就可以看出候選人對算法的理解程度了。

相信大家讀完這篇文章,應該對以上問題都有了答案!

此時01背包理論基礎就講完了,我用了兩篇文章把01背包的dp數組定義、遞推公式、初始化、遍歷順序從二維數組到一維數組統統深度剖析了一遍,沒有放過任何難點。

大家可以發現其實信息量還是挺大的。

如果把動態規劃:關於01背包問題,你該了解這些!和本篇的內容都理解了,後面我們在做01背包的題目,就會發現非常簡單了。

不用再憑感覺或者記憶去寫背包,而是有自己的思考,了解其本質,代碼的方方面面都在自己的掌控之中。

即使代碼沒有通過,也會有自己的邏輯去debug,這樣就思維清晰了。

接下來就要開始用這兩天的理論基礎去做力扣上的背包面試題目了,錄友們握緊扶手,我們要上高速啦!

就醬,學算法,認準「代碼隨想錄」,值得你推薦給身邊每一位朋友同學們!

相關焦點

  • 乾貨總結 | 動態規劃十問十答
    今天給大家總結動態規劃十問十答,快速幫你掃盲動態規劃。答:動態規劃是一種通過「大而化小」的思路解決問題的算法。區別於一些固定形式的算法,如二分法,寬度優先搜索法,動態規劃沒有實際的步驟來規定第一步做什麼第二步做什麼。
  • PHP 0-1背包問題
    0-1背包問題背景我們有n種物品,物品j的重量為wj,價格為pj。我們假定所有物品的重量和價格都是非負的。背包所能承受的最大重量為W。如果限定每種物品只能選擇0個或1個,則問題稱為0-1背包問題。背包問題遞歸算法假如背包的容量是C=6,物品的體積為w,對應的物品的價值為v。我們從最後一個物品開始,如果它的體積比背包剩餘容量小,它面臨著兩種選擇:1,放進背包;2,不放進背包。
  • LeetCode 例題精講 | 16 最大子數組和:子數組類問題的動態規劃技巧
    Maximum Subarray Sum 最大子數組和(Easy)LeetCode 718. Maximum Length of Repeated Subarray 最長公共子數組(Medium)在前面的文章中,我們分別講解了一維和二維動態規劃問題的解題步驟與基本思路。不過,僅僅掌握基本步驟是不夠的。
  • 在VBA中如何使用動態數組,以及利用動態數組去除重複值的方法
    大家好,我們今日繼續講解VBA數組與字典解決方案第22講:在VBA中如何使用動態數組,以及利用動態數組去除重複值的方法。如果文本中含有大量的重複值,此時,如果我們要剔除重複值,該怎麼辦?用VBA的方法該如何做到呢?我在這講和下一講中將解答這個問題,並提供給讀者一個可以測試的實例。今日先講這個內容要用到的知識點。
  • C語言編程技巧:以實例跟我學動態數組的創建及使用方法
    問題提出在C語言編程中,對於普通數組的定義,如定義一個包含10個int類型元素的一維數組a,我們採用下面的方式:int a[10];這種方式定義的數組是靜態數組,其特點是定義方便,無需管理其內存的佔用情況,但其缺點是一旦定義後,其數組的長度就固定了,而不能動態的改變其大小
  • 5張圖帶你了解Numpy包所有的數組操作
    圖1 Numpy對數組操作的函數功能組成在工作或者學習中,我們經常要變化數組的類型。有時我們需要實現數組的選擇或者轉置。Numpy包可以實現按照指定的軸進行移動或者同指定軸進行交換來實現。其中moveaxis()函數同rollaxis()類似可以將軸進行滾動而其他的軸保持不變。transpose()函數和swapexes()函數類似都可以實現兩個軸的交換,而前者可以認為是後者的一種情況。
  • VBA中動態數組的創建及利用
    這些內容大多是我的經驗的記錄,來源於我多年的經驗。今日分享的是NO.244,內容是:VBA過程代碼244:VBA中動態數組的定義及創建VBA過程代碼244:VBA中動態數組的定義及創建Sub Mynz()Dim arr() As Stringerow = [c65536].End(3).Row '最後一個非空單元格行號j = 1 '數組索引號
  • 詳解連續子數組的最大累乘之動態規劃解法
    這個題目,當然可以用窮舉所有子數組的方法,找出最大值,時間複雜度妥妥地為O(n^2),這顯然不是我們想要的。如何用DP降低到O(n)才是我們的目標,這才是算法的魅力所在,接下來,總結DP求解的思維過程。
  • 動態規劃:一樣的套路,再求一次完全平方數
    你需要讓組成和的完全平方數的個數最少。給你一個整數 n ,返回和為 n 的完全平方數的 最少數量 。完全平方數 是一個整數,其值等於另一個整數的平方;換句話說,其值等於一個整數自乘的積。例如,1、4、9 和 16 都是完全平方數,而 3 和 11 不是。
  • 你真的了解JS中的數組嗎?——數組API的總結
    在JS中,數組是一個非常重要的知識點,不管是在面試還是在日常工作中,都非常需要;而該文章,不去深究數據的定義方法等,而只是總結相應的API並簡單的介紹相應方法的應用;如下圖所示,是我本篇文章介紹的相應的數組方法。
  • 你面試穩了!通關LeetCode刷題完整攻略,省時又高效
    因為這能幫你解決一大堆問題。這些問題從插入區間到優化區間合併都有。怎麼識別啥時候用合併區間模式呀?這些問題一般設計到排序好的數組,而且數值一般滿足於一定的區間如果問題讓你需要在排好序/翻轉過的數組中,尋找丟失的/重複的/最小的元素經典題目:Cyclic Sort (easy)Find the
  • 了解什麼是數組,如何應用數組,只需1分鐘就可以秒變數組大神!
    Hi,大家好,有很多的小夥伴在私信提問能不能說說什麼是Excel數組,因為不了解什麼是數組,因此對數組公式感覺非常神秘和陌生。由於大部分人都對數組公式很陌生,我一直都在思考如何和大家介紹這個,讓所有人都可以學會的入門資料,鑑於此情此景,本文應景而生,希望本文對你的Excel的水平提高有幫助。同時希望大家交流有錯漏的請給予斧正。
  • 在Delphi XE7下如何建立動態數組呢?
    在Delphi XE7中Object Pascal有了一個很有意義的新功能,Object Pascal提供了初始化動態數組,並改進了它的工作與操作模式。那麼新的Delphi XE7中, Object Pascal語言可以讓我們做那些新的業務呢?大致來說有以下幾點:1.
  • Python語言中使用array模塊實現動態數組的操作
    背景對於動態數組諸如創建、插入、刪除、查詢大小等操作,在C/C++語言中,可以使用標準庫中的vector類實現,而在python語言中,也同樣提供了內置的array模塊實現類似的功能。動態數組的創建創建方式為:array.array(typecode[, initializer]),第1個參數typecode定義了數組元素的類型,第2個可選參數給出了數組中的初始值。如下面的代碼創建了一個int型的包含3個元素的數組x,其初始值為分別為1、2、3。其索引方式同列表類似,下標從0開始,如x[1]代表取數組x中的第2個元素。
  • 基於一維數組動態處理矩陣運算
    [代碼]基於一維數組動態處理矩陣運算     跳至 [1] [全屏預覽]
  • Filter函數和ReDim語句講解,以及VBA中利用動態數組排重的方法一
    大家好,我們今日繼續講解VBA代碼解決方案的第61講內容:在VBA中如何使用動態數組,以及利用動態數組去除重複值的方法。在上一講中我們講了使用數組函數將單元格中的文本進行分隔後寫入到工作表中的方法,那麼問題來了,如果文本中含有大量的重複值,在寫入時也會將重複值寫入到工作表中,此時,如果我們要剔除重複值,該怎麼辦?用VBA的方法該如何做到呢?我在這講和下一講中將解答這個問題,並提供給讀者一個可以測試的實例。今日先講這個內容要用到的知識點。
  • LeetCode刷題第三周【數組(簡單)】
    數組是目前Leetcode上題量最多的一個模塊了。刷題前我們來了解一下什麼是數組:數組是在程序設計中,為了處理方便, 把具有相同類型的若干元素按有序的形式組織起來的一種形式。首先,數組會利用 索引 來記錄每個元素在數組中的位置,且在大多數程式語言中,索引是從 0 算起的。我們可以根據數組中的索引,快速訪問數組中的元素。事實上,這裡的索引其實就是內存地址。
  • [代碼全屏查看]-基於一維數組動態處理矩陣運算
    [代碼] 基於一維數組動態處理矩陣運算 跳至 [1] [2] [3]
  • Java基礎篇——數組詳解
    Java語言提供了數組(array)的數據結構,可以解決這個問題。數組的概念一個數組是相同數據類型的元素按一定順序排列的集合。使用數組可以將同一類型的數據存儲在連續的內存位置。數組中各元素的類型相同,通過下標的方式來訪問數組中的元素,下標從0開始。