目录
背包问题
题目:
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。所有的背包问题都是先循环物品,再循环体积,再循环决策
1. 01背包
每种物品仅有一件,可以选择放或不放。
记忆方法: 先枚举物品,再从大到小枚举体积
f[i][v]表示前i件物品放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。
详细解释:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f [i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。
tips:1. 当要求容量v是恰好装满时,初始化f[0,0]=0 , f[0,1]=f[0,2]=.......=f[0,n]=-∞(说明:因为前一个0代表什么物品都不能选,而后一个v代表此时背包被占用容量恰好是V,那自然是不可能的,所以用负无穷表示没有方案可以满足要求。)
2. 当不要求恰好装满,初始化f[0,0]=f[0,1]=.......=f[0,n]=0 (因为这里的v表示背包最多能被占多少容量,f【0】【x】的含义是,什么物品都不能选,而背包最多能被放入总共容量为V的物品,此时的最大价值,自然是0)
所以如果用方案一的初始化方法,最大价值是 f【N】【0...V】的最大值,而如果用方案二的初始化方法,最大价值是f【N】【V】
采用空间压缩的代码(且用方案二的初始化方法):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m; // n表示物品总数 ,m表示背包容量
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; //v[i]:第i件物品的体积 w[i]:第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;
}
2.完全背包
每种物品都有无限件可用
记忆方法: 先枚举物品,再从小到大枚举体积
从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品放入一个容量为v的背包的最大价值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<= v}
解释:前i种物品放入一个容量为v的背包的最大价值等于:
- 我完全不用第i件物品, f【i-1】【v】
- 我只用1件第i件物品, f【i-1】【v-c【i】】+w【i】
- 我只用2件第i件物品, f【i-1】【v-2c【i】】+2w【i】
- ......
- 我只用k件第i件物品, f【i-1】【v-kc【i】】+kw【i】(k是总容量为v,最多能放几件第i件物品)
f【i】【v】就等于这么多种情况的最大值,而其实第2种到第5种情况他们之间的最大值,其实就是
f【i】【v-c【i】】+w【i】(假如v小于c【i】,值应该等于0)
所以f【i】【v】=max( f【i-1】【v】,(v-c【i】)>=0? f【i】【v-c【i】】+w【i】: 0)
再进一步,假如采用空间压缩技巧,只要第二个循环枚举容量的时候,从小到大(而不是从大到小),就ok了
(同样,初始化的方法,决定最大值是哪个)
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
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;
}
3. 多重背包
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
记忆方法:先枚举物品,再从大到小枚举体积,最后从1开始枚举第i件物品放几件(需要满足自身限制和体积限制)
3.1 朴素办法:
令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:f[i][v]=max{f[i-1][v-k*c[i]]+ k*w[i]|0<=k<=n[i]}。复杂度是O(V*∑n[i])。 (其中k既要满足 kc【i】<v,又要小于等于物品本身的限制k<n【i】)
tips:这里不能像完全背包一样进行优化,因为你无法知道f【i】【v-c【i】】有没有把第i种物品全给用光了
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[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 = m; j >=v[i] ; j -- ) // 体积需要从大到小枚举
for (int k = 1; k <= s[i] && k * v[i] <= j; k ++ )
f[j] = max(f[j], f[j - k * v[i] ] + k*w[i] );
cout << f[m] << endl;
return 0;
}
3.2 利用二进制进行优化 O(V*n*logn(i))
方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,...,2^(k-1),n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。 分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示。
这样就将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为O(V*∑log n[i])的01背包问题,是很大的改进。
(将第i种物品,用二进制的方法分成若干件物品,这若干件物品选与不选,恰好可以组成0..n[i]间的每一个整数)
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 11010, M = 2010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; i ++ )
{
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while (k <= s)
{
cnt ++ ;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if (s > 0)
{
cnt ++ ;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
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;
}
3.3 单调队列优化
4.混合背包
有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。
解决方法:物品是什么类型的,就用什么方法求解。
具体的就是01背包用01背包的方法求(容量从大到小),完全背包用容量从小到大的方法求,而多重背包用容量从大到小,个数从1到极限求(也可以用二进制方法转为01背包)
代码
//01 背包则直接放入数据容器中
多重背包则化解成 01 背包 放入数据容器中(见多重背包II习题 进行二进制优化)
完全背包也直接放入数据容器中
此刻数据容器vector[HTML_REMOVED] things;中就只有01背包和完全背包 那么就进行遍历处理
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1010;
int n,m;
int f[N];
struct Thing{
int kind;
int v,w;
};
vector<Thing> things;
int main()
{
cin >> n>>m;
for(int i = 0;i<n;i++)
{
int v,w,s;
cin >> v >> w>> s;
if(s < 0)
{
things.push_back({-1,v,w}); // 01背包
}else if(s == 0) things.push_back({0,v,w}); // 完全背包
else{
for(int k = 1;k <= s; k*=2){
s -=k;
things.push_back({-1,v*k,w*k});
}
if(s > 0) things.push_back({-1,v*s,w*s});
}
}
for(auto thing:things)
{
// 01 背包容量从大到小
if(thing.kind < 0){
for(int j = m;j >= thing.v;j--) f[j] = max(f[j],f[j-thing.v]+thing.w);
}
// 完全背包从小到大
else{
for(int j = thing.v;j <= m;j++) f[j] = max(f[j],f[j-thing.v]+thing.w);
}
}
cout << f[m] << endl;
return 0;
}
5.二维费用的背包问题
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。
解决方法:
费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:f [i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}。而如果使用空间压缩,可以只使用两维数组 f【v】【u】:假如是01背包(物品只可以取一次时) 变量v和u采用从大到小的两重循环,当物品是完全背包问题时采用从小到大的两重循环。当物品有如多重背包问题时拆分物品。
物品总个数的限制:
有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后答案就是f[V][M]。
另外,如果要求“恰取M件物品”,则注意初始化方法,
01背包的二维费用代码:
//类似01背包问题用滚动数组,那么这个题目就只用二维数组就行了.
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main(void) {
int N, v, m;//物品种类 背包体积 背包载重
cin >> N >> v >> m;
vector<vector<int>>dp(v + 1, vector<int>(m + 1,0));
for (int i = 1; i <= N; ++i) {
int a,b,c; //物品的体积,质量,价值
cin >> a >> b >> c;
//每个物品只能用一次,从大到小
for (int j = v; j >= a; --j) {
for (int k = m; k >= b; --k) {
dp[j][k] = max(dp[j][k], dp[j - a][k - b] + c);
}
}
}
cout << dp[v][m] << endl;
return 0;
}
6.分组的背包问题
问题:
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
算法
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}。
使用一维数组的伪代码如下:
for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}
另外,显然可以对每组中的物品应用P02中“一个简单有效的优化”。
注:优化
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。
01背包的分组背包代码
// 分组背包问题
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int v[N][N], w[N][N];
int f[N];
int s[N];
int n, m;
int main() {
cin >> n >>m ;
for (int i = 1; i <= n; i++) {
cin >> s[i];
for (int j = 1; j <= s[i]; j++) {
cin >> v[i][j] >> w[i][j];
}
}
// 01背包+每组选一种
/*
j逆序是因为 f[j] = max(f[j], f[j - v[i][k]] + w[i][k])
等价变形是 f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i][k]] + w[i][k])
*/
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) { //体积从大到小
for (int k = 1; k <= s[i]; k++) {
if (j >= v[i][k]) //这里需要判断一下 !!!!!因为我们j是到0
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
cout << f[m] << endl;
return 0;
}
7.01背包问题求最优方案数
有N件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 最优选法的方案数。注意答案可能很大,请输出答案模 10^9+7的结果。
解决方法:
用两个数组 f[i][v]意义同前述,g[i][v]表示这个子问题的最优方案的总数,则在求f[i][v]的同时求g[i][v]的伪代码如下:
for i=1..N
for v=0..V
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
if(f[i][v]==f[i-1][v])
g[i][v]=g[i-1][v]
if(f[i][v]==f[i-1][v-c[i]]+w[i])
g[i][v] += g[i-1][v-c[i]] //因为可能两种方法都能达到最优
tips:这里的初始化方法也有两种
- 所有的f【x】都初始化为0,所有的g【x】都初始化为1(表示啥物品都不能选,最大允许容量分别为0...m的最优方案数,自然都是1,啥都不放)
- f【0】初始为0,其余的f【x】初始为一个很大的负数(可以是INT_MIN),这样f【i】【j】就是恰用了j容量的时候最大获益,其实是为了f【i】【j】只能从f【0】【0】这样一步一步递推过来。 并且g【0】初始化为1,其余的g【x】初始化为0。这时候的g【x】表示容量恰是x的最优方案数
采用方案1初始化,最终答案就是g【m】,而采用方案2初始化最终的答案需要首先遍历f【i】,找到最大的值max_num,然后再遍历一遍f【i】,将等于max_num的f【i】的g【i】都加起来,就是最终的答案。
方案1代码:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N= 1010,MOD=1e9+7;
//int f[N],cnt[N];
int v[N],w[N];
int main(){
int n,m;
cin>>n>>m;
vector<int> f(N,0);
vector<int> cnt(N,1); //初始啥都不能选,最大允许容量为0...m的最优方案数,自然都是1,啥都不放
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){
// 只选i-1的方案要比选了i的方案差
if(f[j]<f[j-v[i]]+w[i])
cnt[j]=cnt[j-v[i]];
//也有可能两种方案最大获利一样,所以要加起来
else if(f[j]==f[j-v[i]]+w[i])
cnt[j] += cnt[j-v[i]];
f[j]=max(f[j],f[j-v[i]]+w[i]);
cnt[j] %= MOD;
}
}
cout<<cnt[m]<<endl;
return 0;
}
方案二代码:
8.背包问题求具体方案
假如要求输出字典序最小的具体方案,那么需要逆序求商品
输出时的判断条件if(f[i][vol]==f[i+1][vol-v[i]]+w[i]);
vol每次输出之后都要减去v[i],所以判断的时候vol-v[i]有可能是负数导致数组越界,所以加一句vol-v[i]>=0的判断。
判断要不要选的逻辑是,当前能选就选(因为是字典序最小)
输出字典序代码:
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N],f[N][N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=n;i>=1;i--) //商品 逆序求
for(int j=0;j<=m;j++)
{
if(j>=v[i]) f[i][j]=max(f[i+1][j],f[i+1][j-v[i]]+w[i]);
else f[i][j]=f[i+1][j];
}
int vol=m;
for(int i=1;i<=n;i++)
{
if(vol<0)
{
break;
}
if(vol-v[i]>=0&&f[i][vol]==f[i+1][vol-v[i]]+w[i])
{
cout<<i<<' ';
vol-=v[i];
}
}
return 0;
}
9.有依赖的背包问题
问题描述:
有 N个物品和一个容量是 V 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如下图所示:
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
f【i】【j】,选节点i,并且所用体积是j,以i为根的子树的最大收益是多少