1. 程式人生 > >01揹包問題詳解

01揹包問題詳解

  首先非常感謝劉汝佳的小白書、HDU劉春英老師的ACM程式設計揹包演算法課件以及dd_engi的揹包九講和眾多大神的部落格,看這麼多資料最後花了整整一天時間才算大致弄懂了揹包問題的思路(=_=搞這麼久我確實有點笨=_=)。OK,接下來就大致講一下我所理解的揹包問題,對這些問題做一下梳理總結,便於以後的複習查閱,如果可以幫到你那就更好了~~

  揹包問題是動態規劃(dp)的一種,但是它的解法相對來說比較特別,所以我們把它單獨列出來學習和分析,但是它的基本思想還是dp。關於概念就不多說了,大家可以參考我以上所提到的資料,上面對概念的講解都是比較獨到和權威的。

  而01揹包是最典型最基礎的揹包問題,它的基本模型為:

    有一個容量為v的揹包,現在有n件價值為value[i],體積為volume[i]的物品(i為第i件),問怎麼樣放才可以在物品體積之和不超過v的前提之下使得物品的總價值最大,求出這個最大值。

  這裡我們的條件是每件物品只有一件,揹包可以不裝滿。其它的情況我們在後面會有討論,這裡的容量、價值和體積只是我們具體化的概念,遇到問題時可能不一定是這樣子,但是隻要滿足這樣的關係我們就稱之為01揹包問題並且用01揹包問題的解法來解決問題。

01揹包問題的貪心想法

  然後我們開始對問題進行分析,我們先按照貪心演算法的思路來分析一下,如果我們先放價值大的物品(以價值從大到小排序),那麼可能這個物品的價值大,但同時佔用的空間也很大,那結果顯然不對,如果我們先放體積小的,那麼可能它價值也小結果顯然也不對。那我們按照價值和體積的比率(傳說中的價效比)

排序呢?乍一想好像挺有道理的,那麼我們具體分析一下:

  我們按照價效比排序之後肯定是把價效比高的先放進去,如果我們遍歷到了某一個物品,雖然它的價效比很高但同時它的體積也很大,剩下的的空間不足以容下它,OK那我們跳過它好了找下一個,遇見盛不下的就跳過找下一個,直至最後,但是,這個時候我們就要進行討論了,我們這樣子“貪出來”的方案是否就是最優解呢?現在有一種情況:當我們遍歷所有物品以後,揹包還沒有裝滿怎麼辦(這肯定是正常的,因為之前我們跳過了很多放不下的),這個時候顯然之前已經跳過的也不能再裝了(因為本來就是盛不下它們才跳過的嘛),那剩下的空間就這樣剩著麼?稍微思考一下就知道這是不合理的,現在假設我們遍歷到最後的兩件物品,剩餘空間還剩下6,第一件物品價值是3,佔空間為1,價效比為3,第二件物品價值為6,佔空間為6,價效比為1,按照我們的策略,先遍歷價效比高的,OK,我們把第一件物品放進去了,價值多了3,剩下空間只剩5了,然後遍歷到了第二件物品,空間不夠了,不放了,遍歷結束!那麼很顯然問題就出來了,我們選擇第二件價效比比較低的物品所獲取的總價值顯然比選擇第一件的高,而這種問題是普遍存在於所有遍歷中的,我們為了解決這種矛盾只能回溯,然而回溯怎麼回,反正我是不會。由此可以證明,01揹包問題是不能單純用貪心演算法來解決的。(注:相信你也看出來了,我舉的反例只是個別的,只是為了證明01揹包問題不能全部使用貪心演算法來解決,其實部分滿足特定條件的01揹包問題是可以用貪心演算法來解決的,但是這裡不深入討論了,以後有機會在貪心演算法裡面寫一篇部落格來討論好了。)

狀態轉移方程

  那麼貪心貪不出來,我們就只能採用dp來做了,那我們為什麼要用這種演算法?這種演算法為什麼正確?實現過程是什麼?作為一隻問題寶寶,雖然大神和老師的講解中都是預設我們已經會了很多東西直接上狀態轉移方程(然而我並不會=_=),可是我覺得這背後的過程還是有必要深究一下的。

  回到問題本身,我們要解決的問題是怎麼樣選擇物品可以使我們最終獲得的總價值最大,我們這裡用到的是dp,dp的思想和貪心的類似,都是求出每一個子問題的最優解,然後推及整體的最優解,但是兩者不同之處在於,貪心的每一次遞推都建立在上一個子問題的解就是確定的最優解的基礎之上,如果接下來一步影響到了之前子問題的最優解,那麼貪心演算法就無法解決問題了,(這就是上面我所舉出貪心演算法無法解決01揹包問題的原因),而dp不同,它在由區域性推及整體的時候每一次都會更新子問題的最優解.(我猜這也是為什麼叫動態規劃不叫靜態規劃的原因/2333)這也是我們可以用dp來解決01揹包的原因。

  它的基本實現過程就是分解為每一件物品而言,我要麼取你,要麼不取你(感覺有歧義的樣子),然後我們只需要每次問一下,我是取了你好(獲取的總價值高)呢?還是不取你好呢?把最後的結果存起來,再問下一個,直到問到最後一個,那麼最後一步所得到的值自然就是我們所想要得到的最大值啦。思路是很顯然的,正確性也不言而喻。

  那既然思路沒有問題,那我們就開始對程式進一步求精,我們怎麼來比較取了好還是不取好呢?這個時候就要開始思考了,我用什麼來儲存這兩種狀態的值?自然而然我們想到了可以用陣列啊,用陣列分別把取了它之後的總價值和不取它的總價值存起來,然後取較大者就好了。這個時候問題就又來了,還有容量呢?你不考慮容量的話肯定是每次都取啊,肯定是取了總價值大啊?所以顯然我們的陣列也是要考慮到容量的,那我們怎麼樣在考慮容量的前提下判斷哪種好呢?我們想到了用二維陣列,容量同時也作為下標然後不就有辦法比較了。這個時候方程已經呼之欲出了,建議你在這個時候畫一張表格,自己就可以把狀態轉移方程列出來了。已經囉嗦太多了我就不繼續囉嗦了。

dp[i][k]=max(dp[i-1][k],dp[i-1][v-volume[i]]+value[i])

  其中的i表示第i件物品,k揹包容量(不是總容量,而是每一個子問題的“總容量”),dp[i][j]就是用來儲存答案和狀態更新的陣列,dp[i-1][k]就表示我不取你的最大價值,dp[i-1][v-value[i]]+value[i]則表示我取了你的最大價值。

  在最核心的狀態轉移列出來之後,思想已經很清楚了,接下來就只是一些純粹程式碼方面的實現和優化了,其中巧妙之處不再細說,自行感受。現在就以一道題為例看一下01揹包問題的基本實現。

HDU-2602 Bone Collector

題目連結:http://acm.hdu.edu.cn/showproblem.php?pid=2602

題目:

Bone Collector

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 43836    Accepted Submission(s): 18287


Problem Description Many years ago , in Teddy’s hometown there was a man who was called “Bone Collector”. This man like to collect varies of bones , such as dog’s , cow’s , also he went to the grave …
The bone collector had a big bag with a volume of V ,and along his trip of collecting there are a lot of bones , obviously , different bone has different value and different volume, now given the each bone’s value along his trip , can you calculate out the maximum of the total value the bone collector can get ?


Input The first line contain a integer T , the number of cases.
Followed by T cases , each case three lines , the first line contain two integer N , V, (N <= 1000 , V <= 1000 )representing the number of bones and the volume of his bag. And the second line contain N integers representing the value of each bone. The third line contain N integers representing the volume of each bone.
Output One integer per line representing the maximum of the total value (this number will be less than 231).
Sample Input 1 5 10 1 2 3 4 5 5 4 3 2 1
Sample Output 14   題目就是直接用揹包,然後上程式碼:
#include<iostream>
#include<algorithm>
#include<math.h>
#define maxn 1005
using namespace std;
int dp[maxn][maxn],value[maxn],volume[maxn];
int    main(){
    int t;
    cin>>t;
    while(t--){
        for(int i=0;i<maxn;i++){
            volume[i]=0;value[i]=0;
            for(int j=0;j<maxn;j++)
                dp[i][j]=0;
        }
        int n,v;
        cin>>n>>v;
        for(int i=1;i<=n;i++)
          cin>>value[i];
        for(int i=1;i<=n;i++)
          cin>>volume[i];
        for(int i=n;i>=1;i--){                           //對個數進行迴圈
            for(int j=0;j<=v;j++){                       //對空間進行迴圈
                dp[i][j]=(i==n?0:dp[i+1][j]);            //巧妙地完成賦初值和避免遞迴影響的操作(亮了)
                if(j>=volume[i])
                    dp[i][j]=max(dp[i][j],dp[i+1][j-volume[i]]+value[i]); //狀態轉移方程
            }
        }
        cout<<dp[1][v]<<endl;
    }
    return 0;
}
   優化為一維陣列   以上是用二維陣列來解決這道問題,但是01揹包的解法其實是可以用一維陣列來儲存的,這樣子可以優化空間,那為什麼可以用一維陣列呢?因為有一種神奇的東西叫做滾動陣列
  什麼是滾動陣列大家手中資料應該都有介紹,不再介紹,這裡可以用滾動陣列的前提是,我們在使用二維陣列時,每次狀態更新我們都只用到了上一行的資料,可以這樣理解,我們用二維陣列是一個矩陣,第i個是第i行,我們除了對第一行全部賦初值之外,其它每一行的值都是通過上一行的值來推出來的,
dp[i][j]=max(dp[i][j],dp[i+1][j-volume[i]]+value[i])
  這個方程中第一個dp[i][j]其實還是dp[i+1][j]的值,看上面的那一行程式碼就知道了,我們會發現這兩個數都是dp[i+1][j]這一行的,那我們何必還要用二維陣列呢?趕緊滾起來用一維的吧,用一維陣列就要注意了,因為我們每次更新資料所需要用到是上一行的資料,所以在使用之前我們不能對其更新,這就決定了我們第二重迴圈只能逆序進行。OK,滾動之後本題的程式碼奉上:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<math.h>
#define maxn 1005
using namespace std;
int dp[maxn],value[maxn],volume[maxn];
int    main(){
    int t;
    cin>>t;
    while(t--){
        memset(dp,0,sizeof(dp));
        int n,v;
        cin>>n>>v;
        for(int i=1;i<=n;i++)
          cin>>value[i];
        for(int i=1;i<=n;i++)
          cin>>volume[i];
        for(int i=1;i<=n;i++){
            for(int j=v;j>=0;j--){
                if(j>=volume[i])
                    dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);
            }
        }
        cout<<dp[v]<<endl;
    }
    return 0;
}
  可以很明顯看到雖然時間上沒什麼變化,但是在空間上優化了很多。 特殊情況 如果題目有要求必須全部裝滿,揹包容量不能有剩餘的時候,只要在賦初值的時候i=0,其餘的值全都賦一個 -10的X次方,反正就是很小很小,其它什麼都不用變,最後求出的就是我們所需要的答案了,如果輸出的是我們所賦的最小值的話說明沒有符和條件的方案。   原因如果以上都搞懂的話也不難想到,從第一行賦完初值之後,每一行只有在"容量"等於0的基礎上更新,那麼最後剩下的只有一種剩餘容量為0的情況才會被更新到最後,其餘情況的到最後依舊是相當於負無窮。所以只需要改初值就可以了。   注意:01揹包是揹包問題的基礎,其餘的揹包均可以轉化為01揹包來解決,所以徹底理解01揹包是特別有必要的。

相關推薦

01揹包

01揹包 給定一個容量為c的揹包,有n個物品,第i個質量為wi,價值為vi,求揹包的最大價值 由於每種物品只有1個,因此每個物品只有01兩種狀態,即拿和不拿 用V【i,j】表示在面對第i個物品且揹包容量

揹包問題——“01揹包及實現(包含揹包中具體物品的求解)

-----Edit by ZhuSenlin HDU          01揹包是在M件物品取出若干件放在空間為W的揹包裡,每件物品的體積為C1,C2,…,Cn,與之相對應的價值為W1,W2,…,Wn.求解將那些物品裝入揹包可使總價值最大。         動態規劃(DP)

揹包問題——“完全揹包及實現(包含揹包具體物品的求解)

原文地址:http://blog.csdn.net/wumuzi520/article/details/7014830   完全揹包是在N種物品中選取若干件(同一種物品可多次選取)放在空間為V的揹包裡,每種物品的體積為C1,C2,…,Cn,與之相對應的價值為W1

揹包問題(0-1揹包、完全揹包、多重揹包)

揹包問題 一個揹包總容量為V, 現在有N個物品, 第i個物品容量為weight[i], 價值為value[i], 現在往揹包裡面裝東西, 怎樣裝才能使揹包內物品總價值最大.主要分為3類: 1. 0-1揹包, 每個物品只能取0個,或者1個. 2. 完全揹

揹包問題01揹包、完全揹包、多重揹包

參考連結: 揹包問題是動態規劃演算法的一個典型例項,首先介紹動態規劃演算法: 動態規劃: 基本思想: 動態規劃演算法通常用於求解具有某種最優性質的問題。在這類問題中, 可能會有很多可行解。沒一個解都對應於一個值,我們希望找到具有最優值的解。胎動規

01揹包的四種解法:動態規劃,貪心法,回溯法,優先佇列式分支限界法(C語言編寫)

最近剛完成了演算法課程設計,題目是用多種解法解決01揹包問題,經過一番探索,終於成功的用四種方法完成了本次實驗,下面記錄分享一下成果: 首先解釋下什麼是01揹包問題:給定一組共n個物品,每種物品都有自己的重量wi, i=1~n和價值vi, i=1~n,在限定的總重量(揹包的

01揹包問題(轉載)

這個學期開的演算法設計與分析課程老師說是研究生才要學的課,但是我們大二就要學! 雖然有難度,但還是要學滴。 上機課題目有一道0-1揹包的問題,上課的時候由於沒有聽課。。所以只有自己再啃書本了。 程式碼雖然不長,但是還是。。很有。。技術含量的。 本人文筆不是很好,所以就 不多說啦!直接上菜! 問題描述: 給定

01揹包問題

  首先非常感謝劉汝佳的小白書、HDU劉春英老師的ACM程式設計揹包演算法課件以及dd_engi的揹包九講和眾多大神的部落格,看這麼多資料最後花了整整一天時間才算大致弄懂了揹包問題的思路(=_=搞這麼久我確實有點笨=_=)。OK,接下來就大致講一下我所理解的揹包問題,對這些

01,完全,多重揹包

揹包問題泛指以下這一種問題: 給定一組有固定價值和固定重量的物品,以及一個已知最大承重量的揹包,求在不超過揹包最大承重量的前提下,能放進揹包裡面的物品的最大總價值。 這一類問題是典型的使用動態規劃解決的問題,我們可以把揹包問題分成3種不同的子問題:0-1揹包問題、完全揹包和多重揹包問題。下面對這三種問題分別進

01揹包問題吐血

揹包問題我真是學一次忘一次,很多dp問題也是由這個衍生而來,今天終於痛下決心寫個部落格供自己日後參考 問題描述: 有N件物品和一個容量為V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使價值總和最大。 基本思路 : 這是最基礎的揹包問題,特點是

動態規劃之01背包

題解 for 可見 round 往裏面 原創 ble -a eight 先看問題: 有N件物品和一個容量為V的背包。(每種物品均只有一件)第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入背包可使價值總和最大。 通過閱讀問題,因為背包就是要往裏面放東西,所以一件

01-struts2配置

調試 dev efault nbsp config patch 錯誤 public include 1 struts.xml配置詳解 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts P

01-spring配置

eth height 直接 arr work ssp creat exp context 1 bean元素 <!--將User對象交給spring容器進行管理 --> <!-- Bean元素:使用該元素描述需要spring容器管理的對象

Java虛擬機01----初識JVM

日誌 可變 lar 反射 開始 rac ibm java語言 lan 主要內容如下: JVM的概念 JVM發展歷史 JVM種類 Java語言規範 JVM規範 一、JVM的概念: JVM:   Java Virtual Machine,意為Java虛擬機。 虛擬機:   

FFMPEG進階系列01-ffplay命令

概述 ffplay是一個基於FFMPEG庫和SDL庫開發的多媒體播放器。它的主要目的是是用來測試FFMPEG的各種API,比如codec/format/filter等等庫。 掌握ffplay的設計邏輯,對於播放器開發人員提升經驗非常有幫助。嗶哩嗶哩的ijkplayer就是基於ffplay做的二次開

【iOS】第01講 UIView/UIViewController/UIApplication

一、UIView詳解 Command+Alt+Enter -> 顯示ViewController 按住Ctrl直接把UIView拖到ViewController  1.1 UIView的常見屬性  @property(nonatomic,reado

利用動態規劃演算法01揹包問題->二維陣列傳參->cpp記憶體管理->堆和棧的區別->常見的記憶體錯誤及其對策->指標和陣列的區別->32位系統是4G

1、利用動態規劃演算法解01揹包問題 https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html 兩層for迴圈,依次考察當前石塊是否能放入揹包。如果能,則考察放入該石塊是否會得到當前揹包尺寸的最優解。 // 01 knap

keras之ImageDataGenerator引數及用法例項-01

keras圖片生成器ImageDataGenerator keras.preprocessing.image.ImageDataGenerator(featurewise_center=False,     samplewise_center=False,   &nbs

01、Synchronized

1. synchronized簡介 在學習知識前,我們先來看一個現象: public class SynchronizedDemo implements Runnable { private static int count = 0; public static void