一、背包问题
“背包”是程序设计和信息学竞赛中的一类重要问题。背包问题种类繁多,其中最简单的就是“0-1背包”。青和优化策略。背包问题的求解涉及计算机算法的灵活应用:
1.背包问题
2.完全背包问题
3.多重背包问题
4.混合三种背包问题
5.二维费用的背包问题
6.分组的背包问题
二、例题讲解-0-1背包问题
【问题描述】
有N件物品和一个容量为V的背包。放入第i件物品耗费的空间是Ci,得到的价值是Wi。求解在不超过容量的前提下,将哪些物品装入背包可使价值总和最大。
【输入格式】
第1行两个正整数,分别表示N和V,中间用一个空格隔开。
第2行N个正整数,表示Ci,中间用一个空格隔开。
第3行N个正整数,表示 Wi,中间用一个空格隔开,
其中:1≤N<100,1≤V≤1e5,1≤Ci<1000,1≤Wi≤1000。
【输出格式】
一行一个正整数,表示最大的价值总和.
【输入样例】
4 20
8 9 5 2
5 6 7 3
【输出样例】
16
【问题分析】
- 定义状态F[i][v],表示前i件物品放入一个容量为v的背包可以获得的最大价值,则状态转移方程为F[i][v] = max{F[i-1][v], F[i-1, v-Ci] + Wi}
可以这样理解:
依次考虑每一件物品,对于第i件物要么不放、要么放,对应计算机中的0或1,所以是“0-1背包”,那么就,可以将一个规模为i的问是
转化为一个只和前i-1件物品相关的子问题(规模为i-1)。如果不放第i件物品,问题就转化为“前i-1件物品放入容量为v的背包”。如果不放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-Ci的背包”。此时能获得的最大价值就是F[i-1,v-Ci]再加上通过放入第i件物品获得的价值。
状态:F[i,v]表示用体积为v的背包装前i个物品能获得的最大价值。
考虑第i种物品装或不装进行状态转移:
1.装:f[i-1,v-C[i]]+W[i](必须满足v≥C[i])
2.不装:f[i - 1, v]
两种情况取较大值。
条件转移方程为:
答案为f[N,V],时间复杂度为O(NV)
我们发现,其中第i行的值只与第i-1行有关系。很自然的想到用滚动数组来优化空间复杂度到O(2*N);根据这个思路写成代码如下:
#include<bits/stdc++.h>
using namespace std;
int N, V;
int F[101][10000];
int C[101], W[101];
int main(){
cin >> N >> V; //物品数量N和背包容量V
for(int i = 1; i <= N; i++){
cin >> C[i]; //第i件商品空间
}
for(int i = 1; i <= N; i++){
cin >> W[i]; //第i件商品价值
}
for(int i = 1; i <= N; i++){ //F[i][v]表示前i件物品,总重量不超过v的最优价值
for(int v = 0; v <= V; v++){
if(v >= C[i]){
F[i][v] = max(F[i - 1][v], F[i - 1][v - C[i]] + W[i]);
}else{
F[i][v] = F[i - 1][v];
}
}
}
cout << F[N][V]; //F[N][V]为最优解
return 0;
}
我们再仔细分析第一个程序,发现F[i, v]是由F[i-1, v]和F[i-1, v-Ci]两个子问题逆推而来,能否保证在求F[i, v]时(也即在第二个程序的第i次主循环中推F[v]时)能够取用F[i-1, v]和F[i-1, v-Ci]的值呢?事实上,这要求我们在每次主循环中以v = V..0的递减顺序计算F[v],这样就能保证在推F[v]时F[v-Ci]保存的是状态F[i-1, v-Ci]的值。
程序中的 F[v] = max{F[v], F[v-Ci]+Wi} 就对应于原来的转移方程,因为现在的 F[v-Ci] 就相当于原来的 F[i-1, v-Ci]。
以上代码还可以做一个“常数优化”,将第二重循环的下限 Ci 改为:
max{V-Σi=1^N Wi, Ci}
由此可以得到01背包的公式:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-w[i]]+c[i]};
代码如下:
#include<bits/stdc++.h>
using namespace std;
int N, V, c[110], w[110], F[1000000]; //F[V]当v容量时获得的最大价值
int main(){
cin >> N >> V;
for(int i = 1; i <= N; i++){
cin >> c[i];
}
for(int i = 1; i <= N; i++){
cin >> w[i];
}
//01背包算法
for(int i = 1; i <= N; i++){
for(int v = V; v >= c[i]; v--){
F[v] = max(F[v], F[v - c[i]] + w[i]);
}
}
cout << F[V];
return 0;
}
三、例题讲解-01背包+输出方案
题目描述
一个旅行者有一个最多能装 M 公斤的背包,现在有 ��n 件物品,它们的重量分别1,2,…,W1,W2,…,Wn,它们的价值分别为1,2,…,C1,C2,…,Cn,求旅行者能获得最大总价值。
输入
第一行:两个整数M (背包容量,M≤200)和N (物品数量N≤30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
输出
两行,第一行一个数,表示最大总价值。第二行为具体方案,以空格分开。
样例输入1
8 4
2 3
3 4
4 5
5 6
样例输出1
10
3 5
【分析】
这道题和上一道题基本一样。
按照样例来分析一下:
// 输出方案
void findWhat(int i, int v) {
if (i > 0) {
if (f[i][v] == f[i - 1][v]) { // 没有放
item[i] = 0;
findWhat(i - 1, v);
} else if (v - w[i] >= 0 && f[i][v] == f[i - 1][v - w[i]] + c[i]) { // 放了
item[i] = 1;
findWhat(i - 1, v - w[i]);
}
}
}
所以总的代码如下:
#include<bits/stdc++.h>
using namespace std;
#define M 10000
int N,V,C[M],W[M];
int f[101][M]; //f[i][j]第i见物品v重量时的最大价值
bool item[M];
// 输出方案
void findWhat(int i, int v) {
if (i > 0) {
if (f[i][v] == f[i - 1][v]) { // 没有放
item[i] = 0;
findWhat(i - 1, v);
} else if (v - C[i] >= 0 && f[i][v] == f[i - 1][v - C[i]] + W[i]) { // 放了
item[i] = 1;
findWhat(i - 1, v - C[i]);
}
}
}
int main(){
cin >> V >> N;
for(int i = 1; i <= N; i++){
cin >> C[i];
cin >> W[i];
}
for(int i = 1; i <=N; i++){
for(int v = 0; v <= V; v++){ //01背包
if(v >= C[i]){
f[i][v] = max(f[i - 1][v], f[i - 1][v - C[i]] + W[i]);
}else{
f[i][v] = f[i - 1][v];
}
}
}
cout << f[N][V] << endl;
findWhat(N, V); //找出具体方案
for(int i = 1; i <= N; i++){
if(item[i]){
cout << C[i] << ' ';
}
}
return 0;
}
四、完全背包问题
题目描述
设有 n 种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为 M,今从 n 种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于 M,而价值的和为最大。
输入
第一行:两个整数,M (背包容量M≤200)和 N (物品数量N≤30);
第2..N+1 行:每行二个整数 Wi、Ci,表示每个物品的重量和价值。
输出
仅一行,一个数,表示最大总价值。
样例输入1
10 4
2 1
3 3
4 5
7 9
样例输出1
max=12
【完全背包问题题目分析】
基本思路:
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略己并非取或不取两种,而是有取0件、取1件、取2件...等很多种。
如果咱们按照解01背包时的思路,用f[i][v]表示前i种物品恰好放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略出状态转移方程,像这样
。
将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。
这个算法使用一维数组,先看伪代码:
你会发现,这个伪代码与01背包问题的伪代码只有v的循环次序不同而已。 为什么这样一改就可行呢?
首先想想为什么01背包问题中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-w[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无己经选入第i件物品的子结果f[i-1][v-w[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已经选入第i种物品的子结果f[i][v-w[i]],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理。
这个算法也可以以另外的思路得出,例如:基本思路中的状态转移方程可以等价的变形称为这种形式:
(求解方案数)
将这个方程用一维数组实现,便得到了上面的伪代码。
【题目解法一】
设f[i][y]表示前i件物品,总重量不超过v的最优价值,
则
F[n][m]即为最优解。
#include<cstdio>
#include<bits/stdc++.h>
using namespace std;
const int maxm = 201, maxn = 31;
int m, n;
int w[maxn], c[maxn];
int f[maxn][maxm];
int main(){
cin >> m >> n; //背包容量m和物品数量n
for(int i = 1; i <= n; i++){
cin >> w[i] >> c[i]; //每个物品的重量和价值
}
for(int i = 1; i <= n; i++){ //f[i][v]表示前i件物品,总重量不超过v的最优价值
for(int v = 1; v <= m; v++){
if(v < w[i]){
f[i][v] = f[i - 1][v];
}else{
f[i][v] = max(f[i - 1][v], f[i][v - w[i]] + c[i]);
}
}
}
printf("max=%d", f[n][m]); //f[n][m]为最优解
return 0;
}
【方法二】
本问题的数学模型如下:
求f[v]表示重量不超过v公斤的最大价值,即
#include<bits/stdc++.h>
using namespace std;
const int maxm = 2001, maxn = 31;
int n, m, v, i;
int c[maxn], w[maxn], f[maxm];
int main(){
cin >> m >> n; //背包容量m和物品数量N
for(int i = 1; i <= n; i++){
cin >> w[i] >> c[i];
}
for(int i = 1; i <= n; i++){
for(v = w[i]; v <= m; v++){ //设f[v]表示重量不超过v公斤的最大价值
if(f[v - w[i]] + c[i] > f[v]){
f[v] = f[v - w[i]] + c[i];
}
}
}
printf("max=%d\n", f[m]); //f[m]为最优解
return 0;
}