揹包問題小結

#include <iostream>
#include <cstring>
using namespace std;
int main()
{
    int i,j,n,m,v,c[100100],w[100100],cnt[50001];
    cin>>n;
    while(n--)
    {
        cin>>m>>v;
        for(i=1;i<=m;i++)
            cin>>c[i]>>w[i];
            memset(cnt,-100,sizeof(cnt));
            cnt[0]=0;
        for(i=1;i<=m;i++)
            for(j=c[i];j<=v;j++)
            cnt[j]=max(cnt[j],cnt[j-c[i]]+w[i]);
        if(cnt[v]<=0)
            cout<<"NO"<<endl;
        else
            cout<<cnt[v]<<endl;
    }
    return 0;
}

//這個是徹底揹包的,南陽oj 311,所謂徹底揹包,就是揹包所裝的物品能夠無限取,一開始我找不出01和徹底代碼之間的差異,我看了下核心語句裏面,若是是徹底揹包的話,第一個for循環是1~m,遍歷m種物品,而第二個for是肯定好一種物品後,在c[i]~v的範圍內不斷增長,直到揹包裝不下,那cnt[j]就是在揹包裝下物品後的價值,由於每次j循環都是創建在i循環的基礎上的,因此每次增長物品都是在前面物品的基礎上肯定的。而01揹包問題,核心語句與徹底揹包變化不大,一樣第一個for循環是遍歷物品種類,可是每一個物品都是隻有一個,因此第二個for我是這樣理解的,在j=v,即j初始爲揹包總容量的狀況下,每次遞減,cnt[j]和cnt[j-1]分別表示當前狀態和前一次狀態的總價值,由於物品只有一件,因此只能判斷放與不放,若是不放,那麼也就是求cnt[j-1]的最大價值,若是放,要加上剛剛計算進去的我w[i],此時揹包所能容納的就只有j-c[i],因此總價值就是cnt[j-c[i]]+w[i]。。。ios

#include <iostream>
#include <cstring>
#define N 1000
using namespace std;
int main()
{
    int i,j,n,v,cnt[N],c[N],w[N];
    while(cin>>n>>v&&n&&v)
    {
        memset(cnt,0,sizeof(cnt));
        for(i=1;i<=n;i++)
           //{
               cin>>c[i]>>w[i];
        for(i=1;i<=n;i++)
            for(j=v;j>=1;j--)
               cnt[j]=max(cnt[j],cnt[j-c[i]]+w[i]);
           //}
            cout<<cnt[v]<<endl;
    }
    return 0;
}//這個是01揹包問題的代碼

 

 

貼一些大神的總結吧:算法

 

P01: 01揹包問題
題目編程

有N件物品和一個容量爲V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使價值總和最大。數組

基本思路學習

這是最基礎的揹包問題,特色是:每種物品僅有一件,能夠選擇放或不放。優化

用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量爲v的揹包能夠得到的最大價值。則其狀態轉移方程即是:spa

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}翻譯

這個方程很是重要,基本上全部跟揹包相關的問題的方程都是由它衍生出來的。因此有必要將它詳細解釋一下:「將前i件物品放入容量爲v的揹包中」這個子問題,若只考慮第i件物品的策略(放或不放),那麼就能夠轉化爲一個只牽扯前i-1件物品的問題。若是不放第i件物品,那麼問題就轉化爲「前i-1件物品放入容量爲v的揹包中」,價值爲f[i-1][v];若是放第i件物品,那麼問題就轉化爲「前i-1件物品放入剩下的容量爲v-c[i]的揹包中」,此時能得到的最大價值就是f[i-1][v-c[i]]再加上經過放入第i件物品得到的價值w[i]。設計

優化空間複雜度code

以上方法的時間和空間複雜度均爲O(N*V),其中時間複雜度基本已經不能再優化了,但空間複雜度卻能夠優化到O(V)。

先考慮上面講的基本思路如何實現,確定是有一個主循環i=1..N,每次算出來二維數組f[i][0..V]的全部值。那麼,若是隻用一個數組f[0..V],能不能保證第i次循環結束後f[v]中表示的就是咱們定義的狀態f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]兩個子問題遞推而來,可否保證在推f[i][v]時(也即在第i次主循環中推f[v]時)可以獲得f[i-1][v]和f[i-1][v-c[i]]的值呢?事實上,這要求在每次主循環中咱們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-c[i]]保存的是狀態f[i-1][v-c[i]]的值。僞代碼以下:

for i=1..N

    for v=V..0

        f[v]=max{f[v],f[v-c[i]]+w[i]};

其中的f[v]=max{f[v],f[v-c[i]]}一句恰就至關於咱們的轉移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},由於如今的f[v-c[i]]就至關於原來的f[i-1][v-c[i]]。若是將v的循環順序從上面的逆序改爲順序的話,那麼則成了f[i][v]由f[i][v-c[i]]推知,與本題意不符,但它倒是另外一個重要的揹包問題P02最簡捷的解決方案,故學習只用一維數組解01揹包問題是十分必要的。

事實上,使用一維數組解01揹包的程序在後面會被屢次用到,因此這裏抽象出一個處理一件01揹包中的物品過程,之後的代碼中直接調用不加說明。

過程ZeroOnePack,表示處理一件01揹包中的物品,兩個參數cost、weight分別代表這件物品的費用和價值。

procedure ZeroOnePack(cost,weight)

    for v=V..cost

        f[v]=max{ f[v],f[v-cost]+weight }

注意這個過程裏的處理與前面給出的僞代碼有所不一樣。前面的示例程序寫成v=V..0是爲了在程序中體現每一個狀態都按照方程求解了,避免沒必要要的思惟複雜度。而這裏既然已經抽象成看做黑箱的過程了,就能夠加入優化。費用爲cost的物品不會影響狀態f[0..cost-1],這是顯然的。

有了這個過程之後,01揹包問題的僞代碼就能夠這樣寫:

for i=1..N

    ZeroOnePack(c[i],w[i]);

初始化的細節問題

咱們看到的求最優解的揹包問題題目中,事實上有兩種不太相同的問法。有的題目要求「剛好裝滿揹包」時的最優解,有的題目則並無要求必須把揹包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不一樣。

若是是第一種問法,要求剛好裝滿揹包,那麼在初始化時除了f[0]爲0其它f[1..V]均設爲-∞,這樣就能夠保證最終獲得的f[N]是一種剛好裝滿揹包的最優解。

若是並無要求必須把揹包裝滿,而是隻但願價格儘可能大,初始化時應該將f[0..V]所有設爲0。

爲何呢?能夠這樣理解:初始化的f數組事實上就是在沒有任何物品能夠放入揹包時的合法狀態。若是要求揹包剛好裝滿,那麼此時只有容量爲0的揹包可能被價值爲0的nothing「剛好裝滿」,其它容量的揹包均沒有合法的解,屬於未定義的狀態,它們的值就都應該是-∞了。若是揹包並不是必須被裝滿,那麼任何容量的揹包都有一個合法解「什麼都不裝」,這個解的價值爲0,因此初始時狀態的值也就所有爲0了。

這個小技巧徹底能夠推廣到其它類型的揹包問題,後面也就再也不對進行狀態轉移以前的初始化進行講解。

小結

01揹包問題是最基本的揹包問題,它包含了揹包問題中設計狀態、方程的最基本思想,另外,別的類型的揹包問題每每也能夠轉換成01揹包問題求解。故必定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最後怎樣優化的空間複雜度。

P02: 徹底揹包問題

題目

有N種物品和一個容量爲V的揹包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使這些物品的費用總和不超過揹包容量,且價值總和最大。

基本思路

這個問題很是相似於01揹包問題,所不一樣的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並不是取或不取兩種,而是有取0件、取1件、取2件……等不少種。若是仍然按照解01揹包時的思路,令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值。仍然能夠按照每種物品不一樣的策略寫出狀態轉移方程,像這樣:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}

這跟01揹包問題同樣有O(N*V)個狀態須要求解,但求解每一個狀態的時間已經不是常數了,求解狀態f[i][v]的時間是O(v/c[i]),總的複雜度是超過O(VN)的。

將01揹包問題的基本思路加以改進,獲得了這樣一個清晰的方法。這說明01揹包問題的方程的確是很重要,能夠推及其它類型的揹包問題。但咱們仍是試圖改進這個複雜度。

一個簡單有效的優化

徹底揹包問題有一個很簡單有效的優化,是這樣的:若兩件物品i、j知足c[i]<=c[j]且w[i]>=w[j],則將物品j去掉,不用考慮。這個優化的正確性顯然:任何狀況下均可將價值小費用高得j換成物美價廉的i,獲得至少不會更差的方案。對於隨機生成的數據,這個方法每每會大大減小物品的件數,從而加快速度。然而這個並不能改善最壞狀況的複雜度,由於有可能特別設計的數據能夠一件物品也去不掉。

這個優化能夠簡單的O(N^2)地實現,通常均可以承受。另外,針對揹包問題而言,比較不錯的一種方法是:首先將費用大於V的物品去掉,而後使用相似計數排序的作法,計算出費用相同的物品中價值最高的是哪一個,能夠O(V+N)地完成這個優化。這個不過重要的過程就不給出僞代碼了,但願你能獨立思考寫出僞代碼或程序。

轉化爲01揹包問題求解

既然01揹包問題是最基本的揹包問題,那麼咱們能夠考慮把徹底揹包問題轉化爲01揹包問題來解。最簡單的想法是,考慮到第i種物品最多選V/c[i]件,因而能夠把第i種物品轉化爲V/c[i]件費用及價值均不變的物品,而後求解這個01揹包問題。這樣徹底沒有改進基本思路的時間複雜度,但這畢竟給了咱們將徹底揹包問題轉化爲01揹包問題的思路:將一種物品拆成多件物品。

更高效的轉化方法是:把第i種物品拆成費用爲c[i]*2^k、價值爲w[i]*2^k的若干件物品,其中k知足c[i]*2^k<=V。這是二進制的思想,由於無論最優策略選幾件第i種物品,總能夠表示成若干個2^k件物品的和。這樣把每種物品拆成O(log(V/c[i]))件物品,是一個很大的改進。

但咱們有更優的O(VN)的算法。

O(VN)的算法

這個算法使用一維數組,先看僞代碼:

for i=1..N

    for v=0..V

        f[v]=max{f[v],f[v-cost]+weight}

你會發現,這個僞代碼與P01的僞代碼只有v的循環次序不一樣而已。爲何這樣一改就可行呢?首先想一想爲何P01中要按照v=V..0的逆序來循環。這是由於要保證第i次循環中的狀態f[i][v]是由狀態f[i-1][v-c[i]]遞推而來。換句話說,這正是爲了保證每件物品只選一次,保證在考慮「選入第i件物品」這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[i-1][v-c[i]]。而如今徹底揹包的特色恰是每種物品可選無限件,因此在考慮「加選一件第i種物品」這種策略時,卻正須要一個可能已選入第i種物品的子結果f[i][v-c[i]],因此就能夠而且必須採用v=0..V的順序循環。這就是這個簡單的程序爲什麼成立的道理。

這個算法也能夠以另外的思路得出。例如,基本思路中的狀態轉移方程能夠等價地變造成這種形式:

f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}

將這個方程用一維數組實現,便獲得了上面的僞代碼。

最後抽象出處理一件徹底揹包類物品的過程僞代碼,之後會用到:

procedure CompletePack(cost,weight)

    for v=cost..V

        f[v]=max{  f[v],f[v-c[i]]+w[i]  }

總結

徹底揹包問題也是一個至關基礎的揹包問題,它有兩個狀態轉移方程,分別在「基本思路」以及「O(VN)的算法「的小節中給出。但願你可以對這兩個狀態轉移方程都仔細地體會,不只記住,也要弄明白它們是怎麼得出來的,最好可以本身想一種獲得這些方程的方法。事實上,對每一道動態規劃題目都思考其方程的意義以及如何得來,是加深對動態規劃的理解、提升動態規劃功力的好方法。

P03: 多重揹包問題

題目

有N種物品和一個容量爲V的揹包。第i種物品最多有n[i]件可用,每件費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使這些物品的費用總和不超過揹包容量,且價值總和最大。

基本算法

這題目和徹底揹包問題很相似。基本的方程只需將徹底揹包問題的方程略微一改便可,由於對於第i種物品有n[i]+1種策略:取0件,取1件……取n[i]件。令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值,則有狀態轉移方程:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}

複雜度是O(V*Σn[i])。

轉化爲01揹包問題

另外一種好想好寫的基本方法是轉化爲01揹包求解:把第i種物品換成n[i]件01揹包中的物品,則獲得了物品數爲Σn[i]的01揹包問題,直接求解,複雜度仍然是O(V*Σn[i])。

可是咱們指望將它轉化爲01揹包問題以後可以像徹底揹包同樣下降複雜度。仍然考慮二進制的思想,咱們考慮把第i種物品換成若干件物品,使得原問題中第i種物品可取的每種策略——取0..n[i]件——均能等價於取若干件代換之後的物品。另外,取超過n[i]件的策略必不能出現。

方法是:將第i種物品分紅若干件物品,其中每件物品有一個係數,這件物品的費用和價值均是原來的費用和價值乘以這個係數。使這些係數分別爲1,2,4,...,2^(k-1),n[i]-2^k+1,且k是知足n[i]-2^k+1>0的最大整數。例如,若是n[i]爲13,就將這種物品分紅係數分別爲1,2,4,6的四件物品。

分紅的這幾件物品的係數和爲n[i],代表不可能取多於n[i]件的第i種物品。另外這種方法也能保證對於0..n[i]間的每個整數,都可以用若干個係數的和表示,這個證實能夠分0..2^k-1和2^k..n[i]兩段來分別討論得出,並不難,但願你本身思考嘗試一下。

這樣就將第i種物品分紅了O(log n[i])種物品,將原問題轉化爲了複雜度爲O(V*Σlog n[i])的01揹包問題,是很大的改進。

下面給出O(log amount)時間處理一件多重揹包中物品的過程,其中amount表示物品的數量:

procedure MultiplePack(cost,weight,amount)

    if cost*amount>=V

        CompletePack(cost,weight)

        return

    integer k=1

    while k<num

        ZeroOnePack(k*cost,k*weight)

        amount=amount-k

        k=k*2

    ZeroOnePack(amount*cost,amount*weight)

但願你仔細體會這個僞代碼,若是不太理解的話,不妨翻譯成程序代碼之後,單步執行幾回,或者頭腦加紙筆模擬一下,也許就會慢慢理解了。

O(VN)的算法

多重揹包問題一樣有O(VN)的算法。這個算法基於基本算法的狀態轉移方程,但應用單調隊列的方法使每一個狀態的值能夠以均攤O(1)的時間求解。因爲用單調隊列優化的DP已超出了NOIP的範圍,故本文再也不展開講解。

小結

這裏咱們看到了將一個算法的複雜度由O(V*Σn[i])改進到O(V*Σlog n[i])的過程,還知道了存在應用超出NOIP範圍的知識的O(VN)算法。但願你特別注意「拆分物品」的思想和方法,本身證實一下它的正確性,並將完整的程序代碼寫出來。

P04: 混合三種揹包問題

問題

若是將P0一、P0二、P03混合起來。也就是說,有的物品只能夠取一次(01揹包),有的物品能夠取無限次(徹底揹包),有的物品能夠取的次數有一個上限(多重揹包)。應該怎麼求解呢?

01揹包與徹底揹包的混合

考慮到在P01和P02中給出的僞代碼只有一處不一樣,故若是隻有兩類物品:一類物品只能取一次,另外一類物品能夠取無限次,那麼只需在對每一個物品應用轉移方程時,根據物品的類別選用順序或逆序的循環便可,複雜度是O(VN)。僞代碼以下:

for i=1..N

    if 第i件物品是01揹包

        for v=V..0

            f[v]=max{f[v],f[v-c[i]]+w[i]};

    else if 第i件物品是徹底揹包

        for v=0..V

            f[v]=max{f[v],f[v-c[i]]+w[i]};

再加上多重揹包

若是再加上有的物品最多能夠取有限次,那麼原則上也能夠給出O(VN)的解法:遇到多重揹包類型的物品用單調隊列解便可。但若是不考慮超過NOIP範圍的算法的話,用P03中將每一個這類物品分紅O(log n[i])個01揹包的物品的方法也已經很優了。

固然,更清晰的寫法是調用咱們前面給出的三個相關過程。

for i=1..N

    if 第i件物品是01揹包

        ZeroOnePack(c[i],w[i])

    else if 第i件物品是徹底揹包

        CompletePack(c[i],w[i])

    else if 第i件物品是多重揹包

        MultiplePack(c[i],w[i],n[i])

在最初寫出這三個過程的時候,可能徹底沒有想到它們會在這裏混合應用。我想這體現了編程中抽象的威力。若是你一直就是以這種「抽象出過程」的方式寫每一類揹包問題的,也很是清楚它們的實現中細微的不一樣,那麼在遇到混合三種揹包問題的題目時,必定能很快想到上面簡潔的解法,對嗎?

小結

有人說,困難的題目都是由簡單的題目疊加而來的。這句話是否公理暫且存之不論,但它在本講中已經獲得了充分的體現。原本01揹包、徹底揹包、多重揹包都不是什麼難題,但將它們簡單地組合起來之後就獲得了這樣一道必定能嚇倒很多人的題目。但只要基礎紮實,領會三種基本揹包問題的思想,就能夠作到把困難的題目拆分紅簡單的題目來解決。

相關文章
相關標籤/搜索