背包问题-C++
第一講 01背包
題目
給定物品個數n,背包容量v,每個物品都有一個體積c和價值w,要求向背包中裝物品使得總價值最高.
基本思路
狀態表示:f(i,j)表示前i個物品試圖裝入一個容量為j的背包的最大價值.?
邊界情況:f(0,j)=0.?
狀態轉移:f(i,j)=max(f(i-1,j),f(i,j-save[i])+value[i]). 即裝或不裝第i個物品?
時間復雜度O(VN) 空間復雜度O(VN)
例1 hdu2602 01背包裸題?
兩個wa點,第一個是多組數據沒有注意到,第二個是存在體積為0的情況,所以j需要從0枚舉起.
?
? ? ? memset(dp,0,sizeof(dp));for(int i=1;i<=n;i++) scanf("%d",&value[i]);for(int i=1;i<=n;i++) scanf("%d",&volume[i]);for(int i=1;i<=n;i++)for(int j=0;j<=v;j++)if(j<volume[i])dp[i][j]=dp[i-1][j];elsedp[i][j]=max(dp[i-1][j],dp[i-1][j-volume[i]]+value[i]);printf("%d\n",dp[n][v] );
優化空間復雜度
注意到每個狀態f(i,j)只與之前的狀態f(i-1,j)和f(i-1,j-save[i])有關.實際上開一個二維數組是不必要的,完全可以開一個一維數組下標為j,然后循環遞推n次.?
即f(j)=max(f(j),f(j-save[i])+value[i]),但是正向循環是錯誤的,因為之前的j-save[i]已經被新狀態所覆蓋,正確的做法只要反轉一下順序,逆向循環就好.
? ? ?
? int n=read(),v=read();for(int i=1;i<=n;i++)value[i]=read();for(int i=1;i<=n;i++)volume[i]=read();memset(dp,0,sizeof(dp));for(int i=1;i<=n;i++)for(int j=v;j>=volume[i];j--)dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);printf("%d\n",dp[v] );
注意此時這個數組的意義是:容量為j的背包最多能裝多少價值的物品.?
遍歷i個物品后,得到結果.
初始化的細節問題(滿包)
01背包滿包問題:給定物品個數n,背包容量v,每個物品都有一個體積c和價值w,請問恰好裝滿背包時最大總價值是多少.?
狀態表示,轉移方程同理:f(j)=max(f(j),f(j-save[i])+value[i]))?
但是初始條件:f(j)=-∞,f(0)=0.意義是0件物品時不可能裝到除0之外的容量,-∞表示無解.?
遞推結束后會有很多狀態都是無解,表示不能達到.
例2 luogu P1164 小A點菜 01背包滿包求方案數?
狀態表示:f(j)表示容量為j的背包裝滿有幾種方案.?
初始條件:f(j)=0,f(0)=1?
狀態轉移:f(j)=f(j)+f(j-save[i])
int n=read(),m=read();for(int i=1;i<=n;i++)save[i]=read();dp[0]=1;for(int i=1;i<=n;i++)for(int j=m;j>=0;j--)dp[j]=dp[j]+dp[j-save[i]];printf("%d\n",dp[m] );
一個常數優化
01背包二重循環的下限可以從Ci優化成max(V ? Σ(i到n)C, Ci)
在這之前我有一個疑惑,為什么下限會是ci,這在未優化空間復雜度時是不成立的.?
但是優化空間復雜度后顯然成立,因為”什么都不做”就是dp[j]=dp[j].?
所以轉移方程就是
下面考慮原文中的優化,這個數值是僅與i有關的.?
看了網上大佬的思路,意思是某些算出來的背包實際上對最終結果是沒有幫助的.?
最多有幫助的就是總體積減去之后每個物品的體積之和.?
比如計算最后一件物品時,V-C(n-1)以下容量的背包是不可能轉移到結果的.?
同理,計算倒數第二件物品時,V-C(n-1)-C(n-2)以下的背包也是不可能轉移到最終結果的.
? ? ?
? int n=read(),v=read(),lim=v;for(int i=1;i<=n;++i) value[i]=read();for(int i=1;i<=n;++i) volume[i]=read(),lim-=volume[i];memset(dp,0,sizeof(dp));for(int i=1;i<=n;++i){int th=max(lim,volume[i]);for(int j=v;j>=th;--j)dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);lim+=volume[i];}printf("%d\n",dp[v] );第二講 完全背包問題
?
問題
完全背包問題:給定一個容量為v的背包和n種物品,每種物品有無限個且有體積Ci和價值Wi.問最多能向背包中裝多少價值的物品?
naive approch
狀態表示:F(i,j)仍表示對于前i種物品,背包容量為j時最多價值為多少??
初始情況:F(0,j)=0?
狀態轉移:f(i,j)=max{f(i-1,j-kCi)+kWi} (0<=k<=j/Ci)?
復雜度O(V∑V/Ci)
優化
優化1:對于Ci<=Cj,Wi>=Wj的情況,扔掉j 需要O(N^2)進行優化?
優化2:基數排序,扔掉Ci>V的i,對0到V每個容量,選擇Wi最大的物品. 需要O(V+N)
轉化
將完全背包轉化為01背包.?
第一種轉化,每種物品對應為V/Ci個物品?
第二種轉化,將第i種物品拆分成Ci*2^k,Wi*2^k的若干件物品,滿足Ci*2^k<=V即可?
每種物品對應log(V/Ci)個物品,優化很大.
O(VN)算法
for(int i=1;i<=n;i++) for(int j=volume[i];j<=v;j++)dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);這個算法與01背包空間優化后的算法,只有j的遍歷順序相反.?
因為01背包倒序遍歷就是為了避免一件物品被計算多次.?
兩層循環可以交換順序,某些情況下帶來常數優化???
第三講 多重背包
?
題目
給n種物品和一個容量為v的背包,每種物品最多有num[i]件可用,每個物品都有一個體積volume[i]和價值value[i],求背包最多能裝多少價值的物品?
基本算法
dp[j]表示容量為j的背包最多能裝多少價值的物品.?
dp[j]=max{dp[j-k*volume[i]]+k*value[i]} 0<=k<=num[i]?
復雜度O(VNnum[i])
如果轉換為01背包,相當于物品數為∑num[i]的01背包問題,復雜度O(V∑num[i])
二進制優化
將Mi件體積為Ci,價值為Wi的物品,可以轉換為ceil(logMi)件物品:?
每件物品的體積和價值都乘以一個系數,系數依次為1,2,4,8,…..,2^(k-1),Mi-(2^k-1).?
其中k滿足是Mi-(2^k-1)>0的最大整數
正確性:?
注意到做01背包時會給出每件物品取或不取的最優情況,也即如果直接分成Mi件物品,跑完之后總會給出最優取幾件的情況.?
而由分成的這些物品和0(不取),可以組合成0到Mi之間的任何一個數字.按01背包跑完后,同樣會給出一摸一樣的最優取幾件的情況.?
兩者是完全等效的.
復雜度?
O(V∑log(Mi))
實現?
對于物品num[i],volume[i],value[i]?
如果num[i]*volume[i]>=V 直接按照完全背包來做這個物品即可?
令k=1;?
當k<num[i],時?
—對物品k*volume[i],k*value[i]做01背包?
—num[i]-=k;?
—k<<=1?
最后,對num[i]*volue[i],num[i]*value[i]做01背包
O(VN)的解法
對于基礎的狀態轉移,使用單調隊列來優化,達到O(VN)的復雜性?
在優化dp的時候再去學習
例題
51nod 1086 背包問題V2 多重背包裸題?
完全如上文所說,有幾個需要注意的點:?
1.要非常熟練完全背包和01背包的基礎代碼寫法.?
2.關于k的循環選取應該有更好的方式
以上是略微改過的代碼,用了一個完全背包兩個01背包.?
有一個疑惑點:如果直接將volu和value左移位,會謎之wa,保存這個問題,先略.
第四講-混合三種背包問題
問題
如果三種背包問題混合起來,即有的物品能選一次,有的物品能選多次,有的物品能選無限次,如何求解?
解法
那當然是分類討論if else.?
其實在完全背包的解法中就有這樣的感覺.對于總體積超過背包體積的,按完全背包算.
實現?
01背包實際上都按多重背包解即可,復雜度不會提高.而對于完全物品,設置它的數量為v/volume[i]*10,保證不會爆范圍就行,這樣走多重背包的時候就會直接走第一個if.
其他
原文中特別注重培養抽象出模型的能力,實際上這個能力也非常重要.在寫多重背包時,調用01背包和完全背包就使用了這樣的能力.
第五講-二維費用的背包問題
問題
給一個容量為V的背包,你的負重最大只有W,然后有n種物品,每種都有若干個(0個,無限,多個),體積為volume[i],重量為weight[i],價值為value[i].問最多能裝多少價值的物品,在不超過體積及負重的情況下?
解法
狀態加一維?
用dp[j][k]表示容量為j負重為k時的最大價值.?
則dp[j][k]=dp[j-volume[i]][k-weight[i]]+value[i].?
根據物品種類的不同,選擇01背包,完全背包,多重背包的轉移方式.
其他
常見的問題是個數限制,這個時候將個數變成第二維是個不錯的選擇.?
除此之外,原文中說二維背包實際上類似于復整數域的一維背包,我的理解是兩個維度是完全正交的,完全不相關地添加.而且用這樣地方法可以實現更高維地背包.
例題 vijos 1334 NASA的食物計劃
二維費用01背包,模板題?
注意01背包和完全背包的遍歷順序不同,不要寫錯了
第六講-分組的背包問題
題目
有n件物品可以被放入一個容量為v的背包中,每件物品體積為volume[i],價值為value[i].此外,這些物品被分成p組,每組中的物品最多只能選一件,求背包中最多可以裝多少價值的物品.
分析
對于每組物品,可以選擇這組物品中某一件,或者這組中一件也不選.?
是不是感覺這個東西有點像,01背包??
如果用dp(k,v)表示前k組物品最多在容量為v的背包中裝多少價值,那么顯然?
dp(k,v)=max(dp(k-1,v),dp(k-1,v-volume[i])+value[i]),其中i遍歷第k個物品集合.
實現
?
for(int k=1;k<=p;k++)? for(int j=v;j>=0;j–) //此處遍歷順序與物品種類有關? for(int i:part[k])? dp[j]=max(dp[j],dp[j-volume[i]]+value[i]).?
注意遍歷方式一定是kji,如果是kij的話就無法保證每組只選一個了.?
先j再i保證了每組內,每個體積只會被一個最優的物品訪問到.
優化
這里可用第二講完全背包中的對應優化?
1.n^2遍歷,將體積大價值小的物品去掉.?
2.計數排序,去掉體積大于v的物品,相同體積的物品只取價值最大的.(限制物品個數大于體積的情況?)
其他
一點自己的看法?
以上得思考都是基于01背包來做的,那么其他呢??
如果每組可以取無限次,每次取的物品可以不一樣:分組就沒有意義了,對所有物品完全背包即可.?
如果每組可以取無限次,每次取的物品必須相同: 這是什么問題?想不出來,先略.
?
第七講-有依賴的背包問題
有依賴的背包問題指的是如果物品j依賴于物品i, 要想選擇物品j必須先選擇了物品i.?
一個簡化的問題是:一件物品只會依賴最多一件物品,且附屬品不會被依賴.
思考
首先想一下我的dp訓練第19題黃金礦工.那道題里實際上一條過原點的直線上的金塊會按順序依賴.當時的解法是將一條線上的所有物品看成一組,然后將組中前方物品的值加在后方物品上.這種方法在解決鏈狀依賴時非常有效.
如果不是鏈狀依賴呢?比如物品2和3都依賴于物品1,這時總的情況實際上有5種:都不選,只選1,選12,選13,選123.物品有多件時情況數會成指數增長,顯然枚舉是不可行的.
簡化后的問題
背包九講原文中給出的方法是處理這一組物品,對每個費用只保留價值最大的情況.相當于做成了一個泛化物品(泛化物品是指某種物品給定一個費用就會返回一個價值).然后使用第六講分組背包中的算法就可以解決.
關鍵的問題是怎么處理?此時一組物品中有1個主件和若干個附件,在選擇主件后,每個附件都是可選可不選的,現在需要得到每個特定費用的最大價值,怎么做?
答案是,01背包. 對所有附件做一次01背包之后,再加上主件的花費和價值,就得到了這個泛化物品的所有屬性.
算法
只空談算法而不寫程序是ACMer大忌!我怎么把這個忘了!?
一個標準的解法是對于每個主件從dp數組copy一個tmp數組,然后讓所有這個主件的附件對tmp數組做01背包.做完之后對每個值加上主件的體積和價值,求一個max.
對每個附件做完01背包之后,tmp表示前i-1組物品和這些附件在每個體積時的最優情況.但是,如果需要tmp里的某個值,得先經過主件之后才能轉移到dp中.?
從dp和tmp得到dp的過程實際上也是一個01背包,只是通常的01背包是針對數組本身選或不選,而本處則是針對兩個數組誰可以轉移到新的dp做出的選擇.?
按最優狀態分析一下,新的dp[j]只可能會由兩個狀態轉移而來,一個是這一組物品任意一個都不選,即舊dp[j],另一個則是選了主件和某些附件,則由tmp轉移.?
這里還是很繞,多回來看一看.
更復雜的問題
復雜問題是指,主件和附件之間的依賴關變成了一棵樹,所有依賴的集合是一個森林.這里假設每個物品只能依賴一件物品且不會循環依賴.?
解法是,從葉子到根依次進行上述的01背包過程,就得到了這整棵樹的泛化模型,森林中所有樹的泛化模型都得到后,使用分組背包的算法即可解決問題.
第八講-泛化物品
泛化物品準確來說,不是一類題目,而是一種思想.泛化物品的定義是
考慮這樣一種物品,它并沒有固定的費用和價值,而是它的價值隨著你分?
配給它的費用而變化。這就是泛化物品的概念。
或者
更嚴格的定義之。在背包容量為V 的背包問題中,泛化物品是一個定義?
域為0 … V 中的整數的函數h,當分配給它的費用為v時,能得到的價值就?
是h(v)。
再或者
這個定義有一點點抽象,另一種理解是一個泛化物品就是一個數組h[0 … V ],?
給它費用v,可得到價值h[v]。
當我看到這里的時候,我真正覺得這個概念的提出者和背包九講的作者崔添翼是一個了不起的人物.他用這樣的概念,簡單準確的描述了到底什么是背包,背包問題到底在干什么.
一個01背包中的物品(體積ci,價值wi),它的泛化物品模型是h(ci)=wi,h(其他)=0.?
一個完全背包中的物品,它的模型是h(ci*k)=wi*k,其中k為正整數且ci*k<=V, h(其他)=0.?
一個多重背包中的物品,則是h(ci*k)=wi*k,其中k<=ni且ci*k<=V, h(其他)=0.?
一個互斥的物品組,h(ci)=wi,i取遍組中物品的編號,ci相同時wi取最小值,h(其他)=0
泛化物品的和
如果給定兩個泛化物品a和b,現在有體積v來裝這兩種物品,要求獲得最大價值.怎么做?
上文中有提到,將泛化物品直接使用分組背包的方法可以求解,注意泛化后每一組中互斥物品個數實際上都是v了,復雜度O(n*v*v),其中n為泛化物品個數.
如果僅僅有兩個物品呢,設用體積j來裝這兩種物品的最大價值為dp[j]?
則dp[j]=max{a(k)+b(v-k)},k取遍0到j,答案就是dp[v]?
新合成的dp數組,實際上,也是一個泛化物品.?
由泛化物品的性質可知,如果將兩個泛化物品這樣合成一個新的物品,新的物品在問題種完全可以取代原有的兩個物品.
背包與泛化物品
背包問題中都會有的dp數組,處理完之后的值實際上也是一個泛化物品,是所有原有物品的和.?
所以,求解背包實際上就是求得各個體積時的最優價值,得到最終的泛化物品.
帶著泛化物品的思想,可以再去回頭看看第六和第七講.
?
第九講-背包問題問法的變化
?
背包問題的一些基礎問法
給定背包的體積限制,求最大價值?
給定背包的體積限制,求最小價值 (max改為min)?
給定背包的體積限制,求最大件數 (價值為1)(或貪心)?
給定背包的體積限制,求最小件數 (貪心)?
給定背包的體積限制,求最多裝滿多少空間(判斷每個體積是否可達)
輸出方案
去掉空間優化,變成二維數組?
記錄每個最優的子問題是由max的哪項推過來的,倒推回去即可得到是否選擇某件物品?
不用顯示記錄,倒推時比較兩項即可.
輸出字典序最小的方案
由于前方物品比后方物品重要,最好將子問題從前i件物品改成后i件物品.?
每次倒推時,如果dp[i-1][j]與dp[i-1][j-volu]+valu并列,選擇后者.?
也可以不改狀態表示,而是將所有物品做一個轉換x=n+1-x?
輸出方案時轉換回來即可
對某個體積求方案數
轉移方程改為sum?
初始條件改為dp[0][0]=1
最優方案總數
多增加一個數組mem[i][j]表示dp[i][j]最優時方案數有幾種?
初始條件mem[0][0]=1?
狀態轉移(在dp轉移后):if(dp[i][j]==dp[i-1][j]) mem[i][j]+=dp[i-1][j]?
if(dp[i][j]==dp[i-1][j-volu]+valu) mem[i][j]+=mem[i-1][j-volu]
求次優解或第k優解
每個dp狀態實際是一個有序數組,代表前k優解?
轉移時將dp[i-1][j]與dp[i-1][j-volu]+val進行一個merge操作,即可得到dp[i][j]?
復雜度O(NVK)
一個正確的狀態轉移方程實際遍歷了所有的解法,只不過忽略了很多非最優解
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
- 上一篇: Charm Bracelet
- 下一篇: C++的cin和cout取消同步