背包问题 (大全详细)

背包问题

01背包

描述:
[背包九讲]
csdn
csdn
csdn
题目解读:
有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其它F[1…V]均设为−∞,这样就可以保证最终得到的F[V]是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0…V]全部设为0。
如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为-∞了。如果背包并非
必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
基础版:
用 dp[i][j] 表示前 i 件物品放入容量为 j 的空间时的最大价值。将第 i 件物品放入容量为 j 的空间,只有放与不放两种选择:
(1) 放不下(不放),则当前最大价值为 i-1 件物品放入容量为 j 的空间时的最大价值。即:dp[i][j]=dp[i-1][j];
(2) 放的下有两种情况,取最大价值
 ①不放,当前最大价值为 i-1件物品放入容量为 j 的空间时的最大价值,即 dp[i][j]=dp[i-1][j];
 ②放,肯定就要腾出w[i]的空间(因为当前枚举的空间容量固定为 j ),则腾出后的空间为 j-w[i] ,所以此时最大价值为: i-1 件物品放入 j-w[i] 的空间的最大价值加上当前物品的价值。即dp[i][j]=dp[i-1][j-w[i]]+v[i]。
所以放得下的综合式子为:dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])
注: 这里提到 i-1 件物品放入背包的最大价值,并不意味着 i-1 件物品都被放进去了,只是说已经对 i-1 件物品做出了最大价值的选择,实际可能放了可能没放。当我们将 n 件物品放入[0,m]的空间全部做出最优选择,答案也就出来了——dp[n][m]。

for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			if(j<w[i])//装不下
				dp[i][j]=dp[i-1][j];
			else//装得下,两种情况取较大的
				dp[i][j]=Max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); 
		}
	}

回溯:
csdn

int i=n,j=m;
	while(i>0)
	{
		//选择了当前物品(由状态转移方程可知)
		if(dp[i][j]!=dp[i-1][j])
		{
			vis[i]=1;//标记
			j-=w[i];//空间减少
		}
		i--;
	}
	printf("选择的物品为:\n");
	for(int i=1;i<=n;i++)
		if(vis[i])
			printf("%d号\t重量为%d\t价值为%d\n",i,w[i],v[i]);

空间优化
空间优化
核心是基于一维数组做反向迭代,这样的话j<w[i]的部分直接不用复制了,且由于是反向的线性更新也不会发生冲突(即不会同一个物品被放多次)。理由很简单,如果我们第二层循环从前往后遍历,因为后面会用到前面的数据,而前面的数据可能是放过该物品后的,这样就有可能导致放入该物品多次。
反向迭代,初始状态
dp[0]=0 dp[1]=0 dp[2]=0 dp[3]=0
dp[3]=max(dp[3],dp[3-1]+2)=2
dp[2]=max(dp[2],dp[2-1]+2)=2
dp[1]=max(dp[1],dp[1-1]+2)=2
dp[0]=0

for (int i = 1; i <= n; i++){
      for (int j = m; j >= w[i]; j--){
           dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
      }
}

函数典例:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=1e3+5;
int dp[maxn];
int n,m;//n件物品,m的容量
int v[maxn],w[maxn],k[maxn];//w是物品费用,v是物品价值,k是物品个数 
void init(){
    memset(dp, 0, sizeof dp);
    memset(v, 0, sizeof v);
    memset(w, 0, sizeof w);
}
void zeroonepack(int w,int v){//容量,价值   
	for(int i=m;i>=w;i--)//最大容量,
		dp[i]=max(dp[i],dp[i-w]+v);
}
int main(){
    while(cin>>n>>m){
        init();
        for (int i = 1; i <= n;i++){
            cin >> v[i];
        }
        for (int i = 1; i <= n;i++){
            cin >> w[i];
        }
        for (int i = 1; i <= n;i++){
            zeroonepack(w[i], v[i]);
        }
        cout << dp[m] << endl;
    }
    return 0;
}

完全背包

描述:
csdn
普通版:

for(i=1;i<=n;i++){
	for(j=0;j<=v;j++){
		dp[i][j]=dp[i-1][j];
		//对应上面的初始化状态(2)
		for(k=0;k*v[i]<=j;k++)
			dp[i][j]=max(dp[i][j],dp[i][j-k*v[i]]+k*w[i]);
	}
}

时间优化:

for(i=1;i<=n;i++){
	for(j=0;j<=v;j++){
		dp[i][j]=dp[i-1][j];
		//对应上面的初始化状态(2)
		if(j>=v[i])
			dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
		else 
			dp[i][j]=dp[i-1][j];
	}
}

最终

for(i=1;i<=n;i++)
	for(j=v[i];j<=v;j++)
		dp[j]=max(dp[j],dp[j-v[i]]+w[i]);

函数模板:
完全背包求最小值
Cash Machine

//b 完全背包求最小
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string.h>
using namespace std;
const int INF=0x3f3f3f3f;
const int MAX=5e4+5;
int dp[MAX];
int n, m;
int e, f;
int v[MAX],w[MAX];
void completepack(int w,int v){   
	for(int i=w;i<=m;i++) 
		dp[i]=min(dp[i],dp[i-w]+v);
}
int main(){
    int T;cin>>T;
    while(T--){
        memset(dp, INF, sizeof dp);
        dp[0] = 0;
        cin >> e >> f;
        m = f - e;
        cin>>n;
        for(int i=1;i<=n;i++)
            cin>>v[i]>>w[i];
        for (int i = 1; i <= n;i++){
            completepack(w[i], v[i]);
        }
        if(dp[m]!=INF)
            printf("The minimum amount of money in the piggy-bank is %d.\n",dp[m]);
        else printf("This is impossible.\n"); 
    }
	return 0;
} 

多重背包

描述:
dp[i][j]=max(dp[i−1][j−c[i]∗k]+w[i]∗k)
1)不放:如果 “第 i 种物品不放入容量为 j 的背包”,那么问题转化成求 “前 i − 1 种物品放入容量为 j 的背包” 的问题;由于不放,所以最大价值就等于 “前 i − 1 种物品放入容量为j的背包” 的最大价值,对应状态转移方程中 k = 0 的情况, 即 d p [ i − 1 ] [ j ]
2)放 k 个:如果 “第 i 种物品放入容量为 j的背包”,那么问题转化成求 “前 i − 1 种物品放入容量为 j − c [ i ] ∗ k 的背包” 的问题;那么此时最大价值就等于 “前 i − 1 种物品放入容量为 j − c [ i ] ∗ k 的背包” 的最大价值 加上放入 k 个第 i 种物品的价值,即 d p [ i − 1 ] [ j − c [ i ] ∗ k ] + w [ i ] ∗ k ;
枚举所有满足条件的 k就是我们所求的 “前 i 种物品恰好放入容量为 j 的背包” 的最大价值了。
优化解法 (二进制优化)
1.如果限制的数量物品容量>=当前最大容量
直接考虑成完全背包问题来处理,把物品数量当成无限数量来考虑。
2.如果限制的数量
物品容量<当前最大容量
将单种物品二进制分解为概念上的多个不同物品然后做01背包
总结一下就是,从1开始分解,直到不够分出一个2的指数为止,补上剩余的数就是分解的结果。
当 x [ i ] = 14 x[i] = 14x[i]=14 时,可以拆分成 1,2,4,7 个物品,
那么当我们要取 13 个这类物品的时候,相当于选择 2、4、7,容量分别为
c[i]*2, c[i]4, c[i] 7,价值分别为 w[i]*2, w[i]4, w[i] 7
当 x[i]=14 时,可以拆分成 1,2,4,7 个物品
普通版

for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++)
			for(int l=0;l*w[i]<=j&&l<=k[i];l++)
                dp[i][j]=max(dp[i][j],dp[i-1][j-l*w[i]]+l*v[i]);
    cout<<dp[n][m]<<endl;

二进制优化

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string.h>

using namespace std;
const int inf=0x3f3f3f3f;
const int MAX=1e5+5;
int dp[MAX];
int n,m;//n是物品种数,m是背包最大容量
int v[MAX],w[MAX],k[MAX];// w是物品费用,v是物品价值,k是物品个数 
int main()
{
	cin>>n>>m;
    for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i]>>k[i];
	for(int i=1;i<=n;i++){
		if(k[i]*w[i]>=m){//化为完全背包
			for(int j=w[i];j<=m;j++)
				dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
		else{
			int tk=k[i];//存值分解
			int k=1;
			while(k<=tk){//二进制化为01背包
				for(int j=m;j>=k*w[i];j--)
					dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
				tk-=k;
				k<<=1;//2进制
			}
			if(tk)//没有完全被二进制减完
				for(int j=m;j>=tk*w[i];j--)
					dp[j]=max(dp[j],dp[j-tk*w[i]]+tk*v[i]);
		}
	}
	cout<<dp[m]<<endl; //能获得的最大价值 
	return 0;
}

函数模板
多重背包

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int v[100010];
int num[100101];
int dp[501010];
int n, m;
void zeroonepack(int w,int v){    //01背包 
	for(int i=n;i>=w;i--) 
		dp[i]=max(dp[i],dp[i-w]+v);
}
void completepack(int w,int v){   //完全背包问题 
	for(int i=w;i<=n;i++) 
		dp[i]=max(dp[i],dp[i-w]+v);
}
void multiplepack(int w,int v,int k){
	if(w*k>=n) 
		completepack(w,v);
	else{
		for(int t=1;t<=k;k-=t,t<<=1) 
			zeroonepack(t*w,t*v);
		if(k) zeroonepack(k*w,k*v);
	}
}
int main(){
    while(cin>>n>>m){
        memset(dp,0,sizeof(dp));
        memset(v,0,sizeof(v));
        memset(num,0,sizeof(num)); 
        for(int i=1;i<=m;i++){
            cin >> num[i] >> v[i];
        }
        for(int i=1;i<=m;i++){//重量等于钱的金额大小
            multiplepack(v[i],v[i],num[i]);
        }
        cout << dp[n] << endl;
    }
}

背包模板

//背包模板
void zeroonepack(int w,int v){//容量,价值   
	for(int i=m;i>=w;i--)//最大容量,
		dp[i]=max(dp[i],dp[i-w]+v);
}
 
void completepack(int w,int v){   
	for(int i=w;i<=m;i++) 
		dp[i]=max(dp[i],dp[i-w]+v);
}
 
void multiplepack(int w,int v,int k){
	if(w*k>=m) 
		completepack(w,v);
	else{
		for(int t=1;t<=k;k-=t,t<<=1) 
			zeroonepack(t*w,t*v);
		if(k) zeroonepack(k*w,k*v);
	}
}

分组背包

题意:
思路:典型的分组背包,分组背包最重要的地方就是保证每组里面的物品不同时取,这个可以通过改变枚举物品顺序做到,分三重枚举,第一重枚举第几组,第二重枚举背包容量,要倒着来,第三重枚举该组里面
的物品
AC代码:

#include<iostream>
#include<cstring>
#define IOS std::ios::sync_with_stdio(false);std::cin.tie(0);
using namespace std;
const int maxn = 105;
int n, m;
int dp[maxn], a[maxn][maxn];
void solve() {
    while (cin >> n >> m && n && m) {
        memset(dp, 0, sizeof dp);
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                cin >> a[i][j];
            }
        }
        for (int i = 1; i <= n; i++) {
            for (int j = m; j > 0; j--) {
                for (int k = 1; k <= j; k++) {
                    dp[j] = max(dp[j], dp[j - k] + a[i][k]);
                }
            }
        }
        cout << dp[m] << endl;
    }
}
int main() {
    IOS;
    solve();
    return 0;
}

混合背包

题意:
思路:这是一道混合背包问题。题目给出了多种科目,每种科目的选择规则不同,有些科目至少要选一项,有些科目最多选一项,有些科目任意选。
  这道题需要深入理解状态转移才能够明白。数组dp[i][j],表示第i组,时间剩余为j时的过科率。每得
到一中科目就进行一次DP,所以dp[i]为第i种科目的结果。下面对三种情况进行讨论。
  第一类,至少选一项,即必须要选,那么在开始时,对于这一种的dp的初值,应该全部赋为负无穷,这样才能保证不会出现都不选的情况。状态转移方程为dp[i][k]=max{ dp[i][k],dp[i-1][kcost[j]]+val[k],dp[i][k-cost[j]]+val[j] }。dp[i][k]是不选择当前作业;dp[i-1][k-cost[j]]+val[k]是选择当前作业,但是是第一次在本科目中选,由于开始将该组dp赋为了负无穷,所以第一次取时,必须由上一种的结果推知,这样才能保证得到全局最优解;dp[i][k-cost[j]]+val[j]表示选择当前作业,并且不是第一次
选。
  第二类,最多选一项,即要么不选,一旦选,只能是第一次选。所以状态转移方程为dp[i][k]=max{dp[i][k],dp[i-1][k-cost[j]]+val[k]}。由于要保证得到全局最优解,并且直选一次,所以在该组DP开始以前,应该将上一组的DP结果先复制到这一组的dp[i]数组里,因为这一组的数据是在上一组数据的基础上
进行更新的。
  第三类,任意选,即不论选不选,选几次都可以,显然状态转移方程为dp[i][k]=max{ dp[i][k],dp[i1][k-cost[j]]+val[k],dp[i][k-cost[j]]+val[j] }。同样要保证为得到全局最优解,先复制上一组解。
AC代码:

#include<iostream>
#include<cstring>
#define ios ios::sync_with_stdio(false)
#define iotie cin.tie(NULL),cout.tie(NULL)
using namespace std;
const int maxn = 105;
int n, T;
int dp[maxn][maxn], v[maxn], w[maxn];
int solve() {
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= n; i++) {
        int m, s; cin >> m >> s;
        for (int j = 1; j <= m; j++) cin >> w[j] >> v[j];
        if (s == 0) {
            for (int k = 0; k <= T; k++)
                dp[i][k] = -0x3f3f3f3f;
            for (int j = 1; j <= m; j++) {
                for (int k = T; k >= w[j]; k--) {
                    dp[i][k] = max(dp[i][k], dp[i][k - w[j]] + v[j]);
                    dp[i][k] = max(dp[i][k], dp[i - 1][k - w[j]] + v[j]);
                }
            }
        }
        else if (s == 1) {
            for (int k = 0; k <= T; k++)
                dp[i][k] = dp[i - 1][k];
            for (int j = 1; j <= m; j++) {
                for (int k = T; k >= w[j]; k--) {
                    dp[i][k] = max(dp[i][k], dp[i - 1][k - w[j]] + v[j]);
                }
            }
        }
        else if (s == 2) {
            for (int k = 0; k <= T; k++)
                dp[i][k] = dp[i - 1][k];
            for (int j = 1; j <= m; j++) {
                for (int k = T; k >= w[j]; k--) {
                    dp[i][k] = max(dp[i][k], dp[i][k - w[j]] + v[j]);
                    dp[i][k] = max(dp[i][k], dp[i - 1][k - w[j]] + v[j]);
                }
            }
        }
    }
    return dp[n][T];
}
int main() {
    ios, iotie;
    while (cin >> n >> T) {
        int ans = solve();
        cout << max(ans, -1) << endl;
    }
    return 0;
}

依赖背包

题意
思路:依赖背包模板题,属于01背包的变式。
每一个物品最多只有两个附件,那么我们在对主件进行背包的时候,决策就不再是两个了,而是五个。
还记得01背包的决策是什么吗?
1.不选,然后去考虑下一个
2.选,背包容量减掉那个重量,总值加上那个价值。
这个题的决策是五个,分别是:
1.不选,然后去考虑下一个
2.选且只选这个主件
3.选这个主件,并且选附件1
4.选这个主件,并且选附件2
5.选这个主件,并且选附件1和附件2.
这样,状态转移方程就是四个。
不选附件的①:f[j] = max(f[j],f[j-main_item_w[i]]+main_item_c[i]);
选附件1的②:f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] ] + main_item_c[i] +
annex_item_c[i][1]);
选附件2的③:f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][2] ] + main_item_c[i] +
annex_item_c[i][2]);
选附件1和附件2的④:f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] - annex_item_w[i][2] ]

  • main_item_c[i] + annex_item_c[i][1] + annex_item_c[i][2]);
    AC代码:
#include<iostream>
#include<cstring>
#define ios ios::sync_with_stdio(false)
#define iotie cin.tie(NULL),cout.tie(NULL)
#define mt(a, b) memset(a, b, sizeof a)
using namespace std;
const int maxn = 32005;
struct Node {
    int q, v, w;
    int cv[2], cw[2];
}obj[65];
int n, m, dp[maxn];
void solve() {
    mt(dp, 0), mt(obj, 0);
    for (int i = 1; i <= m; i++) {
        int v, p, q; cin >> v >> p >> q;
        if (!q) {
            obj[i].v = v * p;
            obj[i].w = v;
        }
        else {
            int dex = obj[q].q++;
            obj[q].cv[dex] = v * p, obj[q].cw[dex] = v;
        }
    }
    for (int i = 1; i <= m; i++) {
        for (int j = n; j >= obj[i].w; j--) {
            int temp = j - obj[i].w;
            dp[j] = max(dp[j], dp[temp] + obj[i].v);
            if (obj[i].q >= 1 && temp >= obj[i].cw[0]) {
                dp[j] = max(dp[j], dp[temp - obj[i].cw[0]] + obj[i].v +obj[i].cv[0]);
            }
            if (obj[i].q >= 2) {
                if (temp >= obj[i].cw[1]) {
                    dp[j] = max(dp[j], dp[temp - obj[i].cw[1]] + obj[i].v +obj[i].cv[1]);
                }
                if (temp >= obj[i].cw[0] + obj[i].cw[1]) {
                    dp[j] = max(dp[j], dp[temp - obj[i].cw[0] - obj[i].cw[1]] +obj[i].v + obj[i].cv[0] + obj[i].cv[1]);
                }
            }
        }
    }
    cout << dp[n] << endl;
    return;
}
int main() {
    ios, iotie;
    while (cin >> n >> m)solve();
    return 0;
}
  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值