【01背包问题】
【题目描述】
给定n个物品,每个物体有个体体积
和一个价值
。现有一个容量为V的背包,请问如何选择物品装入背包,使得获得的总价值最大?
【思路】
通过讨论每个物品放与不放,连接前 i -1 个物品的状态和前 i 个物品状态之间的关系,最终结果就是两种选择下,收益的更大值。
我们维护一个二维状态 f [ i , j ], 来表示前 i 个物品,放到体积为 j 的背包里。
可以得到:f [ i , j ] = max( f [ i − 1, j ] , f [ i −1, j − v [ i ] ] + p [ i ] )
对于01背包问题的更详细解释,可以参考以下blog:
【0-1背包问题动态规划的四要素】
(1)状态:一个二维状态 f [ i , j ], 来表示前 i 个物品,放到体积为 j 的背包里
(2)转移方程:
f[i][j]=f[i-1][j];//表示装不下第i个物品 f[i][j]=max(f[i-1][j],f[i-1,j-v[i]]+p[i]);
(3)初始状态:
f[0][j]=0;//表示一个物品都没放,价值为0
(4)转移方向:保证 i 从小到大增大,等式右边的状态比等式左边先算出来。
完整代码如下:
#include<iostream> #include<algorithm> #define N 1002 using namespace std; int n, V, v[N], p[N],f[N][N]; int main() { cin >> n >> V; for (int i = 1; i <= n; i++) cin >> v[i] >> p[i]; for (int i = 1; i <= n; i++) { for (int j = 1; j <= V; j++) { if (j < v[i]) f[i][j] = f[i - 1][j]; else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + p[i]); } } cout << f[n][V]<<endl; return 0; }
【复杂度分析】
空间复杂度:O(nV)使用了二维数组 f [ n ][ V ]
时间复杂度:O(nV)双层for循环
【缺点分析】
因为这个算法与物品个数,背包容量有关,假如物品个数很多,物品体积也非常大的时候,空间复杂度会急剧增加。
【算法优化1——滚动数组优化】
【基本思想】
在动态规划中,有时候内存空间会比较紧张,所以我们需要一些技巧来优化内存开销,下面提出一种优化方式为“滚动数组优化”,其基本思想类似于“踩石头过河”。
而在此题中,当我们在计算第 i 行时,只需保留第 i -1 行,可以把前 i - 2 行的内存空间释放掉,那么也就是说每一次计算只需要两行的数据。那么我们可以只利用 f [ 2 ][ V ]来记录数组的状态。奇数行填入状态f [ 1 ][ j ]中,偶数行填入状态f [ 0 ][ j ]中。
代码修改如下:
int f[2][V]; for(int i=1;i<=n;i++) for(int j=1;j<=V;j++) if(j<v[i]) f[i&1][j]=f[(i-1)&1][j]; else f[i&1][j]=max(f[(i-1)&1][j],f[(i-1)&1][j-v[i]]+p[i]); cout<<f[n&1][V]<<endl;
【注意】
(1)奇数的二进制表示的最低位为“1”,偶数的最低位为“0”,可以利用 i & 1 来取 i 的奇偶性:
i & 1 = 1 ( i 为奇数)
i & 1 = 0( i 为偶数)
(2)利用 i & 1 来取 i 的奇偶性,为什么不用 i % 2 呢?因为位运算的优先级最低,但是运算速度却最高,用 i & 1来判断奇偶性比用 i % 2 要高4倍,当循环的次数非常大时,位运算是非常有效率的。
【复杂度分析】
空间复杂度降为O(2V)
【算法优化2——优化到一维数组】
【基本思路】
//j<V时 if (j < v[i]) f[i][j] = f[i - 1][j]; else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + p[i]);
根据上面代码可以看出来,当 j < v [ i ]时,f [ i ][ j ] = f [ i - 1 ][ j ],那么如果我们将 j 从大到小进行枚举,当 j 从 V 变化到 v[ i ]的过程中,一直记录的是:
f [ i ][ j ] = max(f [ i - 1 ][ j ], f [ i - 1 ][ j - v [ i ] ] + p [ i ] )
可以用这个图来进行理解(摘取自上文提到的blog):
当 j < v [ i ] 时,一直都有f [ i ][ j ] = f [ i - 1 ][ j ],那么如果映射到一维数组的话,相当于没有变化。
所以我们维护一个一维数组f [ j ],当 j < v [ i ]时,f [ j ]记录的就是f [ i - 1, j ],当 j > v [ i ]时,f [ j ]记录的就是f [ i ,j ]。
采用代码如下:
int f[N];//维护一个一维数组 for(int i=1;i<=n;i++) for(int j=V;j>=v[i];j--)//j从V开始枚举到v[i],1~v[i]的状态都是一样的,f[i,j]=f[i-1,j] //可以用v[i]的状态来代表v[i]之前的状态 f[j]=max(f[j],f[j-v[i]]+p[i]); cout<<f[V]<<endl;
【复杂度分析】
空间复杂度降为O(V)
【完全背包问题】
【题目描述】
有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大, 输出最大价值。
【思路】(以下思路摘录于下述blog)(1条消息) 01背包问题,完全背包,多重背包详解(C++代码实现)_完全背包问题代码_BabyCrys的博客-CSDN博客
https://blog.csdn.net/BabyCrys/article/details/104747308
完全背包和01背包的区别在于:01背包每种物品只有一件,而完全背包每种物品有无限件,即为每件物品可以选择无数次。
▲01背包问题中要按照v=V…0的逆序来循环。
这是因为要保证第 i 次循环中的状态 f [i][v] 是由状态 f [i-1][v-c[i]] 递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第 i 件物品的子结果 f [i-1][v-c[i]]。▲完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第 i 种物品的子结果 f [i][v-c[i]],所以就可以并且必须采用
v=0…V的顺序循环。
【代码实现如下】
#include<iostream>
#include<algorithm>
using namespace std;
int f[1001];
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++){
int v,m;
cin>>v>>m; //这里没有像01背包那样,利用数组去存储每件物品的体积和价值
//因为这里采用的是顺序遍历,可以在得到第i种物品的体积的时候,直接进行顺序循环
for(int j=v;j<=V;j++){
f[j]=max(f[j],f[j-v]+m);}
cout<<f[V]<<endl;
return 0;
}
【多重背包问题】
【题目描述】
有
种物品和一个容量是
的背包。
第
种物品最多有
件,每件体积是
,价值是
。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大,输出最大价值。
【思路】
【基本思路】
最优解中可以包含 0,1,2,…,s 个 第
个物品。
状态转移矩阵为:f [ i ][ v ] = max { f [ i-1 ][ v ] , f [ i-1 ][ v- k * c[ i ] ]+k*w[ i ] }
(k=0,1,2,…,s)
空间复杂度为:O( V*∑s[i] )上述思路的方法是将第 i 个物品拆分为1,2, …,s,对应的质量与价值也乘以相应的倍数,每个不同倍数的物品 i 就是一个全新的物品,然后就转换为了01背包问题。
【二进制思想】
参考二进制数的表示方法,将每件物品的数量都可以用1,2,4,8,… 的
组合来表示。一个正整数n,可以被分解成1,2,4,…,
,n-
+1的形式。其中,k是满足n-
+1>0的最大整数。
例如13,则0~13范围内的所有数都可以用1,2,4,6,四个数来表示,其中6=13-(1+2+4)。这样13就由之前拆分为13个物品,简化为拆分4个物品。
空间复杂度:O( V*∑log n[i] )
【基本思路——暴力代码实现】
#include<iostream> #include<algorithm> using namespace std; int f[101]; int main(){ int N,V; cin>>N>>V; for(int i=1;i<=N;i++){ int v,w,s; cin>>v>>w>>s; for(int j=V;j>=v;j--){ for(int k=1;k<=s&&k*v<=j;k++){ f[j]=max(f[j],f[j-k*v]+k*w); } } } cout<<f[V]<<endl; }
【二进制算法代码实现】
#include<iostream> #include<algorithm> using namespace std; int f[101]; int v[10005],w[10005]; int cnt=0; int main(){ int N,V; cin>>N>>V; for(int i=1;i<=N;i++){ int vi,wi,si; cin>>vi>>wi>>si; for(int k=1;k<=si;k*=2){ cnt++; v[cnt]=vi*k; w[cnt]=wi*k; si-=k; } if(si>0){ cnt++; v[cnt]=vi*si; w[cnt]=wi*si; } } for(int i=1;i<=cnt;i++){ for(int j=V;j>=v[i];j--){ f[j]=max(f[j],f[j-v[i]]+w[i]); } } cout<<f[V]<<endl; }