title: 背包问题 - 01背包 、完全背包 、多重背包、还有混合背包
tag: [算法, C++, 背包]
date: 2021/04/08

主要讲三个背包:背包,完全背包,多重背包。
主要讲四个背包:
背包,完全背包,多重背包,混合背包。

01 背包

01背包的意思是选一个或者不选。
朴素版:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1111;
int v[N];		//v[i]表示i个物品的价值
int w[N];		//w[i]表示第i个物品的重量
int f[N][N];	//f[i][j]表示在容量为j的情况下,选i个物品的最大值
int n, m;
int main(){
	cin >> n >> m;
	for(int i = 1;i <= n ;i++)cin >> v[i] >> w[i];
	//数据输入
	for(int i = 1;i <= n;i++)
		for(int j = 0;j <= m;j++){
		    f[i][j] = f[i - 1][j];		//如果不选的话,前面的状态要推到后面去。
	        if(j>=v[i])
		    f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
		}
			//f[i][j]的最大值,是前一个状态推出来,选第i个物品或者不选
			//不选的话,f[i][j] = f[i - 1][j]
			//选的话,f[i][j] = f[i-1][j - w[i]] + v[i],前一个状态(i-1) + 第i件物品
			//容量至少需要w[i],加上以后会减少w[i]的容量,所以需要减去这部分的重量,再加上v[i]的价值。
	cout << f[n][m]<<endl; 
	return 0;
}

优化:
这里我们可以发现,其实每次最多只用到了f [ i ]的前一个状态f[ i -1 ],那么其实有些是不需要的额,我们可以用滚动数组解决。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1111;
int v[N], w[N], f[N];
int n, m;
int main(){
	cin >> n >> m;
	for(int i = 1;i <= n ;i++)cin >> v[i] >> w[i];
	//数据输入
	for(int i = 1;i <= n;i++)
		for(int j = m;j >= v[i];j--)
			f[j] = max(f[j], f[j - v[i]] + w[i]);
	cout << f[m]<<endl; 
	return 0;
}

完全背包:

完全背包是一个物品可以选很多个,没有上限

朴素版:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1111;
int v[N], w[N], f[N][N];
int n, m;
int main(){
	cin >> n >> m;
	for(int i = 1;i <= n ;i++)cin >> v[i] >> w[i];
	//数据输入
	for(int i = 1;i <= n;i++)
		for(int j = 0;j <= m;j++)
			for(int k = 0;k * v[i] <= j;k++)//一个物品选k个,开始枚举,到装不下为止
				f[i][j] = max(f[i][j], f[i-1][j - v[i] * k] + w[i] * k);
	
	cout<<f[n][m]<<endl;
	return 0;
}

朴素版的代码很容易超时,也是因为这个,我不敢保证这里的代码是否正确。因为我之前验证代码的时候就报了Time Limit Exceeded。

优化一下:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N],w[N];
int f[N][N],n,m;
int main(){
    cin >> n >> m;
    for(int i = 1;i <= n; i++)cin >> v[i] >> w[i];
	
	for(int i = 1;i <= n; i++)
        for(int j = 0;j <= m; j++)
		{
		    f[i][j] = f[i - 1][j];
		    if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
		}
		cout<<f[n][m]<<endl;
	return 0;
}

优化x2:

#include <iostream>
#include <algorithm>
using  namespace std;
const int N = 1010;
int v[N],w[N];
int f[N],n,m;

int main(){
    cin >> n >> m;
    for(int i = 1;i <= n; i++)cin >> v[i] >> w[i];

    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]);

    cout<<f[m]<<endl;

    return 0;
}

多重背包:

多重背包就是每个物品有 s[i] 个的上限限制的完全背包。

所以朴素的写法:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 111;
int v[N],w[N],s[N],n,m,f[N][N];
int main(){

    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i]>>s[i];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for(int k=0;k<=s[i] && k * v[i] <= j ;k++)
                f[i][j] = max(f[i][j],f[i-1][j - k*v[i]] + k*w[i]);
    cout<<f[n][m]<<endl;
    
    return 0;
}

这种写法也容易报超时,这个代码在数据量小的时候可以过。

优化一下:
思路是把s[i]件物品拆成k个,用一个2进制数来表示。
比如1023件。就拆成1 ,2 ,4,8,16, 32, 64, 128, 256, 512。或者说,我们在枚举的时候不是一个一个枚举。而是跳着跳着枚举
通过这种方式,我们可以表示出0~1023的所有数,也就是把1023拆成了10个小物品。每个物品做一个01 背包。

#include <iostream>
#include <algorithm>
using namespace std;
const int M = 25000; //M = 2000 * log2000
int n,m,cnt;
int V[M],W[M],f[M];
int a,b,s;

int main(){
    
    cin >> n >> m;
    
    for(int i = 1;i <= n;i++){
        cin >> a >> b >> s;
        int k = 1;
        while(k <= s){
            cnt++;              //物品是1序,所以先cnt++
            V[cnt] = k * a;
            W[cnt] = k * b;
            s -= k;
            k *= 2;
                                //按照之前的设想拆开来
        }
        if(s > 0){
                                //如果还有剩的,直接当成一个物品打包加上去就行
            cnt++;
            V[cnt] = s * a;
            W[cnt] = s * b;
           
        }
    }
    //开始 01 背包
    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]);    
            
    cout << f[m] << endl;
    
    
    return 0;
}

混合背包

混合背包是说,物品有 只能选一次的,无数次的,s次的
就是上面三种背包混合在一起了

思路是这样:

如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是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])

混合背包思路参考的这篇博客