背包九講學習筆記

时间:2022-06-25 16:56:41

參考資料: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]数组

”*