1. 程式人生 > >揹包九講學習筆記

揹包九講學習筆記

參考資料:https://www.cnblogs.com/jbelial/articles/2116074.html     揹包九講

     https://www.cnblogs.com/-guz/p/9866118.html  

感謝HMR姐姐的部分程式碼滋磁和講解以及rqy的講解以及筮安小哥哥指出錯誤qwq

(以下加引號的簡體字都是從上面兩個部落格裡搬運過來的(後面不標記'*'的是揹包九講的內容,否則就是出自參考資料的第二個連結),加引號的繁體字是兩位神仙的講解)

所以粗略一算發現基本沒有我自己的思想emmm

 以下所有問題中,都有N件物品,揹包體積為V,第i件物品費用c[i],價值w[i]

一·01揹包

問題:求不超過揹包體積的物品的最大價值

“這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。 

用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值。則其狀態轉移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。

意思是,在列舉到第i件物品時,如果不選當前物品,那麼當前價值就是選(i-1)件物品、體積為v的價值;如果選,那麼當前價值就是選(i-1)件物品、體積為(v-c[i])時的價值加上當前價值。

所以程式碼:

for(int i=1;i<=n;i++)
	for(int j=W;j>=w[i];j--)
		f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);		
printf("%d",f[n][W])  

優化顯然,f[i][ ]都是由f[i-1][ ]遞推而來的,所以優化掉第一維

於是狀態轉移方程是:f[v]=max{f[v],f[v-c[i]]}  

程式碼:

for(int i=1;i<=n;i++)
	for(int j=W;j>=w[i];j--) f[j]=max(f[j],f[j-w[i]]+v[i]);

“01揹包倒序的原因:它不能訪問到已經被當前元素訪問過的元素,否則就會放多個”

二·完全揹包

問題:每種物品有無限件可用,求不超過揹包體積的物品的最大價值

如果把它當成01揹包來做,那麼狀態轉移方程:f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<= v} 顯然

如果資料大了就會超時,所以這樣是沒有出路的 

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

優化於是我們很自然地想到了01揹包的優化:去掉第一維

感覺自己講不太清楚所以先看程式碼吧

for(int i=1;i<=n;++i)
	for(int j=v[i];j<=m;++j)
		f[j]=max(f[j],f[j-v[i]]+w[i]);    

顯然這裡的第二重迴圈和01揹包的第二重迴圈只是順序不同。

“原因:正序走的時候,用來更新f[j]的f[j-v[i]]很有可能已經更新過,而且可能不只更新過一次。假設這些更新都是成功的,也就是賦上值了,那麼,每次賦值就等於往揹包裡放一個當前物品;幾次成功賦值,就是放了幾件。但是當前成功賦值並不代表最後的揹包裡一定會有這件物品,因為以後的更新可能把這個物品覆蓋。”

三·多重揹包

問題:第i種物品有n[i]件可用,求不超過揹包體積的物品的最大價值

多重揹包長得很像完全揹包,所以狀態轉移方程:f[i][v]=max{f[i-1][v-k*c[i]]+ k*w[i]|0<=k<=n[i]} 顯然這樣也會超時

二進位制優化

原理:我們可以用 1,2,4,8...2^n表示出1 到 2^(n+1)1的所有數.”*

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

所以我們把n[i]件物品拆分成log n[i]件物品,就“不需要顯式地去求放多少件物品了,因為跑01揹包的時候它會自己湊起來。”超神奇的是不是!

for(int i=1;i<=n;i++){
	scanf("%d%d%d",&ww,&vv,&cc);
	for(int j=1; ;j<<=1){
		if(j<=cc) w[++cnt]=ww*j,v[cnt]=vv*j,cc-=j;
		else{
			w[++cnt]=ww*cc,v[cnt]=vv*cc;
			break;
		}
	}
}
for(int i=1;i<=cnt;i++)
	for(int j=m;j>=v[i];j--)
		f[j]=max(f[j],f[j-v[i]]+w[i]);

單調佇列優化:我不會

題目:洛谷P2347砝碼稱重 

四·混合三種揹包問題

相信你看到這個題目就知道是什麼意思了。

在第一重迴圈下面判斷一下是哪種物品,再根據物品種類確定第二重迴圈的列舉順序就好了qwq

說的這麼簡單是因為懶得寫程式碼

五·二維費用的揹包問題

問題:每件物品有兩種體積,已知兩種體積的最大值,求不超過揹包體積的物品的最大價值

“費用加了一維,只需狀態也加一維即可。”

“有時,“二維費用”的條件是以這樣一種隱含的方式給出的:最多隻能取M件物品。這事實上相當於每件物品多了一種“件數”的費用,每個物品的件數費用均為1,可以付出的最大件數費用為M。換句話說,設f[v][m]表示付出費用v、最多選m件時可得到的最大價值,則根據物品的型別(01、完全、多重)用不同的方法迴圈更新,最後在f[0..V][0..M]範圍內尋找答案。 

另外,如果要求“恰取M件物品”,則在f[0..V][M]範圍內尋找答案。 

依舊懶得寫程式碼

事實上,當發現由熟悉的動態規劃題目變形得來的題目時,在原來的狀態中加一緯以滿足新的限制是一種比較通用的方法。

六·分組揹包

問題:物品被劃分為若干組,每組中物品互相衝突,只能選擇一個,求不超過揹包體積的物品的最大價值

每組可以選某一件或不選,所以狀態轉移方程:f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i屬於第k組}

for(int i=1;i<=mx;++i)//最大組數 
	for(int j=m;j>=1;--j)//最大體積 
		for(int k=1;k<=t[i][0];++k)//每組物品個數 
			if(j>=w[t[i][k]]) f[j]=max(f[j],f[j-w[t[i][k]]]+c[t[i][k]]);

“這類問題是01揹包的演變,需要注意的位置就是我們列舉體積要在列舉第i組的物品之前

(因為每組只能選一個!)”*  

這一段程式碼是LuoguP1757通天之分組揹包的啦qwq

七·有依賴的揹包問題

是由毒瘤的NOIp2006金明的預算方案開始的。“遵從該題的提法,將不依賴於別的物品的物品稱為“主件”,依賴於某主件的物品稱為“附件”。由這個問題的簡化條件可知所有的物品由若干主件和依賴於每個主件的一個附件集合組成。 

根據題意,對於每個主件,我們都有4種選擇:1個主件+0個、1個(2種)、2個附件。“所以,我們可以對主件i的“附件集合”先進行一次01揹包,得到費用依次為0..V-c[i]所有這些值時相應的最大價值f'[0..V-c[i]]。那麼這個主件及它的附件集合相當於V-c[i]+1個物品的物品組,其中費用為c[i]+k的物品的價值為f'[k]+w[i]。也就是說原來指數級的策略中有很多策略都是冗餘的,通過一次01揹包後,將主件i轉化為 V-c[i]+1個物品的物品組,就可以直接應用P06/*分組揹包*/的演算法解決問題了。 

這是顧z的程式碼

for(int i=1;i<=n;i++){//列舉主件.
    memset(g,0,sizeof g);//做01揹包要初始化.
    for(now=belong[i]){//列舉第i件物品的附件. 
        for(int j=V-1;j>=c[now];j--){//因為要先選擇主件才能選擇附件,所以我們從V-1開始. 
        
            g[j]=max(g[j],g[j-1]+w[now]);
        }
    }
    g[V]=g[V-1]+w[i];
    for(int j=V;j>=0;j--)
        for(int k=1;k<=V;k++){//此時相當於"打包" .. 
            if(j-k>=0)
                f[j]=max(f[j],f[j-k]+w[i]+g[k-1]);
        }
}
printf("%d",f[V]); 

“更一般的問題 
更一般的問題是:依賴關係以圖論中“森林”的形式給出(森林即多叉樹的集合),也就是說,主件的附件仍然可以具有自己的附件集合,限制只是每個物品最多隻依賴於一個物品(只有一個主件)且不出現迴圈依賴。 

解決這個問題仍然可以用將每個主件及其附件集合轉化為物品組的方式。唯一不同的是,由於附件可能還有附件,就不能將每個附件都看作一個一般的01 揹包中的物品了。若這個附件也有附件集合,則它必定要被先轉化為物品組,然後用分組的揹包問題解出主件及其附件集合所對應的附件組中各個費用的附件所對應的價值。 

事實上,這是一種樹形DP,其特點是每個父節點都需要對它的各個兒子的屬性進行一次DP以求得自己的相關屬性。這已經觸及到了“泛化物品”的思想。看完P08後,你會發現這個“依賴關係樹”每一個子樹都等價於一件泛化物品,求某節點為根的子樹對應的泛化物品相當於求其所有兒子的對應的泛化物品之和。 ”

感覺dd大牛講得很清楚啊qwq我就不過多解釋了 因為這觸及到我的知識盲區了QAQ

  

八·泛化物品 //都是搬運的qwq

定義 
考慮這樣一種物品,它並沒有固定的費用和價值,而是它的價值隨著你分配給它的費用而變化。這就是泛化物品的概念。 

更嚴格的定義之。在揹包容量為V的揹包問題中,泛化物品是一個定義域為0..V中的整數的函式h,當分配給它的費用為v時,能得到的價值就是h(v)。 

這個定義有一點點抽象,另一種理解是一個泛化物品就是一個數組h[0..V],給它費用v,可得到價值h[V]。 

一個費用為c價值為w的物品,如果它是01揹包中的物品,那麼把它看成泛化物品,它就是除了h(c)=w其它函式值都為0的一個函式。如果它是完全揹包中的物品,那麼它可以看成這樣一個函式,僅當v被c整除時有h(v)=v/c*w,其它函式值均為0。如果它是多重揹包中重複次數最多為n的物品,那麼它對應的泛化物品的函式有h(v)=v/c*w僅當v被c整除且v/c<=n,其它情況函式值均為0。 

一個物品組可以看作一個泛化物品h。對於一個0..V中的v,若物品組中不存在費用為v的的物品,則h(v)=0,否則h(v)為所有費用為v的物品的最大價值。P07/*有依賴的揹包問題*/中每個主件及其附件集合等價於一個物品組,自然也可看作一個泛化物品。 

泛化物品的和 
如果面對兩個泛化物品h和l,要用給定的費用從這兩個泛化物品中得到最大的價值,怎麼求呢?事實上,對於一個給定的費用v,只需列舉將這個費用如何分配給兩個泛化物品就可以了。同樣的,對於0..V的每一個整數v,可以求得費用v分配到h和l中的最大價值f(v)。也即f(v)=max{h(k) +l(v-k)|0<=k<=v}。可以看到,f也是一個由泛化物品h和l決定的定義域為0..V的函式,也就是說,f是一個由泛化物品h和 l決定的泛化物品。 

由此可以定義泛化物品的和:h、l都是泛化物品,若泛化物品f滿足f(v)=max{h(k)+l(v-k)|0<=k<=v},則稱f是h與l的和,即f=h+l。這個運算的時間複雜度是O(V^2)。 

泛化物品的定義表明:在一個揹包問題中,若將兩個泛化物品代以它們的和,不影響問題的答案。事實上,對於其中的物品都是泛化物品的揹包問題,求它的答案的過程也就是求所有這些泛化物品之和的過程。設此和為s,則答案就是s[0..V]中的最大值。 

揹包問題的泛化物品 
一個揹包問題中,可能會給出很多條件,包括每種物品的費用、價值等屬性,物品之間的分組、依賴等關係等。但肯定能將問題對應於某個泛化物品。也就是說,給定了所有條件以後,就可以對每個非負整數v求得:若揹包容量為v,將物品裝入揹包可得到的最大價值是多少,這可以認為是定義在非負整數集上的一件泛化物品。這個泛化物品——或者說問題所對應的一個定義域為非負整數的函式——包含了關於問題本身的高度濃縮的資訊。一般而言,求得這個泛化物品的一個子域(例如0..V)的值之後,就可以根據這個函式的取值得到揹包問題的最終答案。 

綜上所述,一般而言,求解揹包問題,即求解這個問題所對應的一個函式,即該問題的泛化物品。而求解某個泛化物品的一種方法就是將它表示為若干泛化物品的和然後求之。 

 

九·揹包問題問法的變化  

1·輸出方案

“一般而言,揹包問題是要求一個最優值,如果要求輸出這個最優值的方案,可以參照一般動態規劃問題輸出方案的方法:記錄下每個狀態的最優值是由狀態轉移方程的哪一項推出來的,換句話說,記錄下它是由哪一個策略推出來的。便可根據這條策略找到上一個狀態,從上一個狀態接著向前推即可。 

還是以01揹包為例,方程為f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。再用一個數組g[i] [v],設g[i][v]=0表示推出f[i][v]的值時是採用了方程的前一項(也即f[i][v]=f[i-1][v]),g[i][v]表示採用了方程的後一項。注意這兩項分別表示了兩種策略:未選第i個物品及選了第i個物品。

g陣列在跑01揹包的同時維護就好啦qwq

“另外,採用方程的前一項或後一項也可以在輸出方案的過程中根據f[i][v]的值實時地求出來,也即不須紀錄g陣列,將上述程式碼中的g[i] [v]==0改成f[i][v]==f[i-1][v],g[i][v]==1改成f[i][v]==f[i-1][v-c[i]]+w[i]也可。 

2·輸出字典序最小的輸出方案

“這裡“字典序最小”的意思是1..N號物品的選擇方案排列出來以後字典序最小。以輸出01揹包最小字典序的方案為例。 

一般而言,求一個字典序最小的最優方案,只需要在轉移時注意策略。首先,子問題的定義要略改一些。我們注意到,如果存在一個選了物品1的最優方案,那麼答案一定包含物品1,原問題轉化為一個揹包容量為v-c[1],物品為2..N的子問題。反之,如果答案不包含物品1,則轉化成揹包容量仍為V,物品為2..N的子問題。不管答案怎樣,子問題的物品都是以i..N而非前所述的1..i的形式來定義的,所以狀態的定義和轉移方程都需要改一下。但也許更簡易的方法是先把物品逆序排列一下,以下按物品已被逆序排列來敘述。 

在這種情況下,可以按照前面經典的狀態轉移方程來求值,只是輸出方案的時候要注意:從N到1輸入時,如果f[i][v]==f[i-v]及f[i][v]==f[i-1][f-c[i]]+w[i]同時成立,應該按照後者(即選擇了物品i)來輸出方案。 

3·求方案總數

“對於一個給定了揹包容量、物品費用、物品間相互關係(分組、依賴等)的揹包問題,除了再給定每個物品的價值後求可得到的最大價值外,還可以得到裝滿揹包或將揹包裝至某一指定容量的方案總數。 

對於這類改變問法的問題,一般只需將狀態轉移方程中的max改成sum即可。例如若每件物品均是01揹包中的物品,轉移方程即為f[i][v]=sum{f[i-1][v],f[i-1][v-c[i]]+w[i]},初始條件f[0][0]=1。 

事實上,這樣做可行的原因在於狀態轉移方程已經考察了所有可能的揹包組成方案。 

根據HMR神仙的解釋,這裡的狀態轉移方程是錯誤的,正確的應該是f[0][0]=1, f[i][j]=f[i-1][j-v[i]]+f[i-1][j];或者f[j]+=f[j-c[i]]

“f[i][j]是選擇i個物品,體積為j的方案,所以可以由選擇i-1個物品、體積為j-c[i]的方案數和選擇i-1個物品、體積為j的方案數轉移過來。”第二個就是第一個優化掉第一維的啦qwq

4·最優方案總數

“這裡的最優方案是指物品總價值最大的方案。還是以01揹包為例。 

結合求最大總價值和方案總數兩個問題的思路,最優方案的總數可以這樣求:f[i][v]意義同前述,g[i][v]表示這個子問題的最優方案的總數,則在求f[i][v]的同時求g[i][v]。

for(int i=1;i<=n;++i)
    for(int j=0;j<=m;++j){
        f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+c[i]);
        g[i][j]=0;
        if(f[i][j]==f[i-1][j]) g[i][j]+=g[i-1][j];
        if(f[i][j]==f[i-1][j-w[i]]+c[i]) g[i][j]+=g[i-1][j-w[i]];
    }

//以下都是搬運qwq  

** 求次優解or第k優解 **

此類問題應該是比較難理解.

所以我會盡量去詳細地解釋,qwq.

前置知識

首先根據01揹包的遞推式:(這裡按照一維陣列來講)

(v[i]代表物品i的體積,w[i]代表物品i的價值).

f(j)=max(f(j),f(jv[i])+w[i])

很容易發現f(j)的大小隻會與f(j)、f(jv[i])+w[i]有關

我們f[i][k]代表體積為i的時候,第k優解的值.

則從f[i][1]...f[i][k]一定是一個單調的序列.

f[i][1]即代表體積為i的時候的最優解

解析

很容易發現,我們需要知道的是,能否通過使用某一物品填充其他體積的揹包得到當前體積下的更優解.

我們用體積為7價值為10的物品填充成體積為7的揹包,得到的價值為10.
而我們發現又可以通過一件體積為3價值為12的物品填充一個體積為4價值為6的揹包得到價值為18. 此時我們體積為7的揹包能取得的最優解為18,次優解為10. 我們發現,這個體積為4的揹包還有次優解4(它可能被兩個體積為2的物品填充.) 此時我們可以通過這個次優解繼續更新體積為7的揹包. 最終結果為 18 16 10 

因此我們需要記錄一個變數c1表示體積為j的時候的第c1優解能否被更新.

再去記錄一個變數c2表示體積為j-v[i]的時候的第c2優解.

簡單概括一下

我們可以用v[i]去填充j-v[i]的揹包去得到體積為j的情況,並獲得價值w[i].
同理j-v[i]也可以被其他物品填充而獲得價值
此時,如果我們使用的填充物不同,我們得到的價值就不同.

這是一個刷表的過程(或者叫推表?

為什麼是正確的?

(這裡引用一句話)

一個正確的狀態轉移方程的求解過程遍歷了所有可用的策略,也就覆蓋了問題的所有方案。

做法

考慮到我們的最優解可能變化,變化成次優解.只用一個二維陣列f[i][k]來實現可能會很困難.

所以我們引入了一個新陣列now[]來記錄當前狀態的第幾優解.

now[k]即代表當前體積為i的時候的第k優解.

因此最後我們可以直接將now[]的值賦給f[i]陣列

”*