背包问题

背包问题

01背包问题

n个物品,每个物品的体积是vi,价值是wi,背包的容量是j
若每个物品最多只能装一个,且不能超过背包容量,则背包的最大价值是多少?

image-20220423115548094

int n;			//物品数量
int m;			//背包容量
int v[N];		//体积
int w[N];		//价值
//二维朴素版本
int f[N][M];
for(int i=1;i<=n;i++)
    for(int j=1;j<=m;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]]+w[i])
cout<<f[n][m]
//一维版本
int f[M];
for(int i=1;i<=m;i++)
    for(int j=m;j>=v[i];j--)//注意是倒序,否则写后读
        f[j]=max(f[j],f[j-v[i]]+w[i])
cout<<f[m]
    二维压缩成一维,实际上是寻找避开写后读错误的方法:
	由于f[i][j]始终只用上一行的数据f[i-1][...]更新(迭代更新的基础,如果还需用上上行数据则不可压缩)
	且f[i][j]始终用靠左边的数据f[i-1][<=j]更新(决定了只能倒序更新)




	举例说明:
	二维:dp[3][8] = max(dp[2][8], dp[2][3] + w[3])   此时的dp[2][8]和dp[2][3]都是上一轮的状态值
	
	一维:dp[8] = max(dp[8], dp[3] + w[3])      我们要保证dp[8]和dp[3]都是上一轮的状态值
	
	按照逆序的顺序,一维dp数组的更新顺序为:dp[8], dp[7], dp[6], ... , dp[3]
	
	也就是说,在本轮更新的值,不会影响本轮中其他未更新的值!较小的index对应的状态是上一轮的状态值!
	
	如果按照顺序进行更新,dp[3] = max(dp[3], dp[0] + w[0]),对dp[3]的状态进行了更新,那么在更新dp[8]时,用到的dp[3]
	就不是上一轮的状态了,不满足动态规划的要求。
	例子来自链接:https://www.acwing.com/file_system/file/content/whole/index/content/2978/

完全背包问题

每个物品可以取任意

image-20220424094856469

假设背包容量为jj时,最多可装入k个物品i,则有

f(i,j)=maxf(i1,j),f(i1,jvi)+wi,f(i1,j2vi)+2wi,,f(i1,jkvi)+kwi

考虑

f(i,jvi)=maxf(i1,jvi),f(i1,j2vi)+wi,f(i1,j3vi)+2wi,,f(i1,jkvi)+(k1)wi

上式变形得

f(i,jvi)+wi=maxf(i1,jvi)+w,f(i1,j2vi)+2wi,f(i1,j3vi)+3wi,,f(i1,jkvi)+kwi

综上可得

f(i,j)=maxf(i1,j),f(i,jvi)+wi

//二维形式
int f[N][M];    // f[i][j]表示在考虑前i个物品后,背包容量为j条件下的最大价值
for(int i = 1; i <= n; ++i) 
    for(int j = 1; j <= m; ++j)
        if(j < v[i]) f[i][j] = f[i-1][j];   //  当前重量装不进,价值等于前i-1个物品   
        else f[i][j] = max(f[i-1][j], f[i][j-v[i]] + w[i]); // 能装,需判断  
cout << f[n][m];
// ---------------一维形式---------------
int f[M];   // f[j]表示背包容量为j条件下的最大价值
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];           // 注意是m不是n
形式上和01背包差不多,在二维数组表示下,主要差别在:
	在选择第i物品时,用的是f[i][j-v]+w,而不是f[i-1][j-v]+w
	上述条件决定了在每次迭代时,必须正向遍历,而不是反向遍历
在一维数组表示下,主要差别只表现为迭代的顺序(正向或反向)
在一维数组表示下,01背包只能反向是因为它主要用到上一行的数据来更新当前行数据,如果正向遍历,则会修改上一行的数据,出现写后读错误;完全背包只能正向是因为它需要用到当前行的数据更新,如果反向遍历,使用的是上一行的数据,则不符合公式

多重背包问题

第i个物品至多拿si件

f(i,j)=maxf(i1,j),f(i1,jvi)+wi,f(i1,j2vi)+2wi,,f(i1,jsivi)+siwi

f(i,jvi)=maxf(i1,jvi),f(i1,j2vi)+wi,f(i1,j3vi)+2wi,,f(i1,jsivi)+(si1)wi,f(i1,j(si+1)vi)+siwi

变形后得

f(i,jvi)+wi=maxf(i1,jvi)+wi,f(i1,j2vi)+2wi,f(i1,j3vi)+3wi,,f(i1,jsivi)+siwi,f(i1,j(si+1)vi)+(si+1)wi

多了一项

f(i1,j(si+1)vi)+(si+1)wif(i1,j(si+1)vi)+(si+1)wi

因此无法按照完全背包的方式优化

二进制优化

已知1,2,4,,2k可以由系数0和1线性组合出02k+11。考虑更一般的情况,若想线性组合出0SS<2k+2,则猜测可由1,2,4,,2k,C组合出,其中C<2k+1,显然,在C一定存在的情况下,可得到的数的范围为C-S。由于C<2k+1,则C2k+11,故[0,2k+11][C,S][0,2k+11][2k+11,S]=[0,S],即可用1,2,4,,2k,C表示任何<2k+2的数
因此对于有s[i]件的某个物品i,可以打包成logs[i]个物品,每包有1,2,4,⋯,2k,C件物品i,其中k=logs[i]1

// -----------------未优化(完全背包模板)----------------------
int f[N][M];    // f[i][j]表示在考虑前i个物品后,背包容量为j条件下的最大价值
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]);
// -----------------------二进制优化---------------------------
// 读入物品个数时顺便打包,a,b,s 为体积价值数量
int k = 1;      // 当前包裹大小
int cnt=0;
while (k <= s)
{
    cnt ++ ;            // 实际物品种数
    v[cnt] = a * k;
    w[cnt] = b * k;
    s -= k;
    k *= 2;             // 倍增包裹大小
}
if (s > 0)
{
    // 不足的单独放一个,即C
    cnt ++ ;
    v[cnt] = a * s;
    w[cnt] = b * s;
}
n = cnt;        // 更新物品种数

// 转换成01背包问题
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;

用二进制优化后,注意物品种数变成N×logMN×log⁡M,问题转换成01背包问题
时间复杂度为O(nmlogs)

分组背包问题

每组物品中至多拿1个

image-20220424113124536

实际上是带有约束的01背包问题,状态计算为

f(i,j)=maxf(i1,j),f(i1,jv(i,k))+w(i,k)

解法:

int n;              // 物品总数
int m;              // 背包容量
int v[N][S];         // 重量 
int w[N][S];         // 价值
int s[N];           // 各组物品种数

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

// 处理数据
for (int i = 1; i <= n; i ++ )
    for (int j = m; j >= 1; j -- )
        for (int k = 1; k <= s[i]; k ++ )
            if (v[i][k] <= j)
                f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

cout << f[m] << endl;

posted @   Yihoyo  阅读(87)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示