背包dp模板总结

背包dp是一类很基础的dp,主要形式就是在具有若干限制和价值,从属分组个数等属性的物品,在满足要求的前提下的最优(往往是最大价值)答案,再次对一些主要分类的模板题进行总结。(根据网络资料总结)

1,01背包

模板:洛谷p1048

  就是简单的选或不选,每个物品有体积v,价值w,在不超过最大体积v的情况下求最大价值

  首先问题的分析很简单,递归的解非常简单,就是每个物体分为选或不选向下走,一直走到最后一个物体,如果体积超标,那么是非法解,如果没有超标就记录,最后记录所有解的最优解。

  那么根据递归的思路,最简单的二维dp思路就是i代表只考虑前i个物品,体积不超过j时候的最大价值,因为最终答案就取决于这两个参数,所以我们不断向下走,当来到了第i个物体的时候,我们可以进行选择要或者不要这个物品。

  那么最大价值依赖哪些位置呢,首先肯定依赖i-1,j的位置,因为不选这个位置所以对容量没有任何影响,其次要注意的是我们的要求是不超过j的最大价值,所以要对于所有满足条件的容量j都需要更新,那么代码的递推思路自然就是,首先当前位置等于i-1,j位置的值,就是如果不选的位置,对于i这一层j从零开始到最末尾,只要大于当前物品体积的j位置都要更新为i-1,j,以及(i-1,j-v[i])+w[i],的最大值,因为这些位置选了当前位置的值的时候,从w[i]到v其实都没有超标(假设只考虑这个位置),具体看代码。

#include<bits/stdc++.h>
#include<string>
using namespace std;
void solve(){
	int t,m;
	cin>>t>>m;
	vector<int>v(m+1);//时间
	vector<int>w(m+1);//价值
	for(int i=1;i<=m;i++){
		cin>>v[i]>>w[i];
	}
	vector<vector<int>> dp(m+1,vector<int>(t+1));
	for(int i=1;i<=m;i++){
		for(int j=0;j<=t;j++){
			dp[i][j]=dp[i-1][j];
			if(j>=v[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
		}
	}
	cout<<dp[m][t];
}
int main()
{
	 solve();
return 0;
}

2,依赖背包

洛谷p1064

  解释起来就是,其余不变,唯一改变的是背包的选择与否不再是简单的01,而是存在依赖,也就是对于一些作为附属的物品而言,想要选他们就必须选他们的主人,主人则不存在依赖(当然可能有的题目存在多层依赖,我们先就最简单的讨论),最后的问题仍然是不超过最大容积的最大价值。

  其实很多背包问题都可以通过转化转化为01的背包问题,比如这道题,当你来到一个主人地位的物品的时候,你的选择无非:都不选,只选主人,选主人加上某个附属,或主人加上某几个附属,一般这种题目附属的个数不会太多,对于这道题就是最多两个附属,那么选择的情况无非是(用01表达主附附)0 0 0;1 0 0;1 1 0;1 0 1;1 1 1,可以看作有这样几个物体,但是他们不能同时选择罢了,所以我们就需要在来到主人内部的时候就把情况穷举最后得出最优答案,再接着走下去即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){
	int n,m;
	cin>>n>>m;
	int x;
	vector<int> v(m+1);//价格(体积)
	vector<int> p(m+1);//重要度(价值)
	vector<vector<int>> q(m+1,vector<int>(3));//从属
	vector<vector<int>> dp(m+1,vector<int>(n+1));
	for(int i=1;i<=m;i++){
		cin>>v[i]>>p[i]>>x;
		p[i]*=v[i];
		if(x!=0){
			q[i][0]=-1;//代表此位置是附属,在之后的循环里不再展开
		if(q[x][1]==0){
			q[x][1]=i;
		}else{
			q[x][2]=i;
		}
		}
	}
	int pre=0;
	for(int i=1;i<=m;i++){
		if(q[i][0]==0){
			for(int j=0;j<=n;j++){
			 dp[i][j]=dp[pre][j];
			 int x=q[i][1],y=q[i][2];
			 if(j>=v[i]){
			 	dp[i][j]=max(dp[i][j],dp[pre][j-v[i]]+p[i]);
			 }	
			 if(j>=v[i]+v[x]&&x!=0){
			 	dp[i][j]=max(dp[i][j],dp[pre][j-v[i]-v[x]]+p[i]+p[x]);
			 }
			 if(y!=0&&j>=v[i]+v[y]){
			 	dp[i][j]=max(dp[i][j],dp[pre][j-v[i]-v[y]]+p[i]+p[y]);
			 }
			 if(x!=0&&y!=0&&j>=v[i]+v[x]+v[y]){
			 	dp[i][j]=max(dp[i][j],dp[pre][j-v[i]-v[x]-v[y]]+p[i]+p[x]+p[y]);
			 }
			}
			pre=i;//pre用来记录上一个主人出现的位置,因为到达附属的时候不会展开
		}
	}
	cout<<dp[pre][n];
}
int main()
{
	 solve();
	
	return 0;
}

  这段代码有一些地方很值得讨论,首先我们要对作为附属的物品进行标记,因为这个地方我们不再进行展开,只在作为主人的位置进行展开,同样的,每当我们走到一个主人的位置,对于每个合理的j(也就是不超过当前位置体积)我们都要讨论出这个位置的最优答案,也就是在双层循环的内部每次都讨论当前是否有更优的答案。

  还有一些特殊的就是,本位置依赖的不再是上一个位置,因为上一个位置其实不一定是主人,所以我们还需要维护一个变量代表上一个主人,也就是在每一次展开的结尾更新这个变量。详情看代码注释。

  另外看这道题的空间优化版本,要空间优化我们就要注意几个点,首先他所依赖的位置是否足够去表示,本位置新的答案很明显只依赖于上一次更新的对应几个位置,所以不需要pre变量,另外需要注意的是,我们要从右往左更新,也就是遍历的顺序必须保证不能被已经更新的数值污染,注意到每个位置依赖的都是自己左上方的值,所以我们从右往左更新,看代码。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){//空间压缩版本
	int n,m;
	cin>>n>>m;
	int x;
	vector<int> v(m+1);//价格(体积)
	vector<int> p(m+1);//重要度(价值)
	vector<vector<int>> q(m+1,vector<int>(3));//从属
	vector<int> dp((n+1));
	for(int i=1;i<=m;i++){
		cin>>v[i]>>p[i]>>x;
		p[i]*=v[i];
		if(x!=0){
			q[i][0]=-1;//代表此位置是附属,在之后的循环里不再展开
		if(q[x][1]==0){
			q[x][1]=i;
		}else{
			q[x][2]=i;
		}
		}
	}
	for(int i=1;i<=m;i++){
		if(q[i][0]==0){
			for(int j=n;j>=0;j--){
			 int x=q[i][1],y=q[i][2];
			 if(j>=v[i]){
			 	dp[j]=max(dp[j],dp[j-v[i]]+p[i]);
			 }	
			 if(j>=v[i]+v[x]&&x!=0){
			 	dp[j]=max(dp[j],dp[j-v[i]-v[x]]+p[i]+p[x]);
			 }
			 if(y!=0&&j>=v[i]+v[y]){
			 	dp[j]=max(dp[j],dp[j-v[i]-v[y]]+p[i]+p[y]);
			 }
			 if(x!=0&&y!=0&&j>=v[i]+v[x]+v[y]){
			 	dp[j]=max(dp[j],dp[j-v[i]-v[x]-v[y]]+p[i]+p[x]+p[y]);
			 }
			}
			}
	}
	cout<<dp[n];
}
int main()
{
	 solve();
	
	return 0;
}

3,分组背包

洛谷p1757

  分组背包的区别就是把物品分为几个组,而每个组中只能选择一个物品,或者一个都不选,其实大差不差,改变的地方在于,我们遍历的不再是物品而是每个组,然后在每个组的位置对所有可能性展开,找到最优再继续往下走。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){//空间压缩版本
	int m,n;
	cin>>m>>n;
	vector<int> a(n+1);
	vector<int> b(n+1);
	vector<vector<int>> c(n+1,vector<int>(n+1));
	vector<vector<int>> dp(n+1,vector<int>(m+1));
	for(int i=1,x;i<=n;i++){
		cin>>a[i]>>b[i]>>x;
		c[x][++c[x][0]]=i;
	} 
	int size=0;
	for(int i=1;i<=n;i++){
		if(c[i][0]!=0){
			size++;
			for(int j=0;j<=m;j++){
				dp[i][j]=dp[i-1][j];
				for(int k=1;k<=c[i][0];k++){
					if(j>=a[c[i][k]]){
						dp[i][j]=max(dp[i][j],dp[i-1][j-a[c[i][k]]]+b[c[i][k]]);
					}
				}
			}
		}else break;
	}
	cout<<dp[size][m];
}
int main()
{
	 solve();
	
	return 0;
}

空间优化的code就不再赘述。

4,完全背包

洛谷p1616

  完全背包的区别就在于每一种物品不再是01而是可以无限的选择,当然仍然存在最大体积的限制,最后求最大价值。

  那么如何去展开呢,其实就是每走到一个位置就去尝试他的所有可能并返回最优的一个,对于完全背包问题,表面上看物品是无穷的,但是其实由于最大体积的限制是存在一个最大个数的,所以我们就一直尝试到超出体积即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){//空间压缩版本
	int time,m;
	cin>>time>>m;
	vector<int> t(m+1);
	vector<int> v(m+1);
	vector<vector<int>> dp(m+1,vector<int>(time+1));
	for(int i=1;i<=m;i++) cin>>t[i]>>v[i];
	for(int i=1;i<=m;i++){
		for(int j=0;j<=time;j++){
			dp[i][j]=dp[i-1][j];//不要的情况
			int k=time/t[i];
			for(int w=1;w<=k;w++){
				if(j>=t[i]*w){
					dp[i][j]=max(dp[i][j],dp[i-1][j-t[i]*w]+v[i]*w);
				}
			}
		}
	}
	cout<<dp[m][time];
}
int main()
{
	 solve();
	
	return 0;
}

  但是提交会超时,因为我们有三层循环,在内部对本物品的可以的任何尝试个数都进行尝试,导致复杂度太大,最终导致超时

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){//空间压缩版本
	int time,m;
	cin>>time>>m;
	vector<int> t(m+1);
	vector<int> v(m+1);
	vector<ll> dp(time+1);
	for(int i=1;i<=m;i++) cin>>t[i]>>v[i];
	for(int i=1;i<=m;i++){
		for(int j=t[i];j<=time;j++){
			dp[j]=max(dp[j],dp[j-t[i]]+v[i]);
			}
		}
	cout<<dp[time];
}
int main()
{
	 solve();
	
	return 0;
}

  所以接下来我们介绍最终的完全背包的代码,我们先回看01背包的版本,当前位置依赖i-1,j以及i-1,j-v[i]的值,意为在前i-1个选完的情况下进行当前状态的递推,反过来看完全背包,当前位置其实不止依赖于前i-1个物品的选择,而是依赖于前i个,因为每个物品都是随便选,所以我们来到一个位置的时候要对于包括本位置的所有内存情况下的最优情况。

  如果觉得难以理解可以这样去想:来到一个位置i,j我们要抉择的不是上一个物品选择i-1,j-v[i]抉择本位置是否选择,而是考虑上一次选择,也就是来到前i个位置(可能第i个物品已经选过若干次),某个最大容量的时候,我决定是不是接着选下去,详情看代码(直接看空间压缩后的版本)。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){//空间压缩版本
	int time,m;
	cin>>time>>m;
	vector<int> t(m+1);
	vector<int> v(m+1);
	vector<ll> dp(time+1);
	for(int i=1;i<=m;i++) cin>>t[i]>>v[i];
	for(int i=1;i<=m;i++){
		for(int j=t[i];j<=time;j++){
			dp[j]=max(dp[j],dp[j-t[i]]+v[i]);
			}
		}
	cout<<dp[time];
}
int main()
{
	 solve();
	
	return 0;
}

5,多重背包

洛谷p1776

  区别在于每个物品的属性多了一个物品的数量,也就是物品的选择有一个上限,最粗暴的方式当然就是来到一个物品的时候就展开全部可能性找到最优的再进行返回,但是多重背包有一些优化的方法值得学习,先从最基础的暴力方法开始。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){//空间压缩版本
	int n,W;
	cin>>n>>W;
	vector<int> v(n+1);//价值
	vector<int> w(n+1);//重量
	vector<int> m(n+1);//数量
	vector<vector<int>> dp(n+1,vector<int>(W+1));
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>m[i];
	for(int i=1;i<=n;i++){
		for(int j=0;j<=W;j++){
			dp[i][j]=dp[i-1][j];
			for(int k=1;k<=m[i];k++){
				if(j>=k*w[i]){
					dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
				}
			}
		}
	}
	cout<<dp[n][W];
}
int main()
{
	 solve();
	
	return 0;
}

  这是最暴力的版本,思路上面已经说过,但是提交过后就会超时,想来也肯定是算上枚举复杂度太高,所以我们要想到优化枚举的方式。

  这种优化枚举的方式就是二进制枚举,从而将多重背包转化为01背包,对于每个有若干个的物品,把他合并为2^1,2^2,2^3…直到剩余的个数不满足下一个二的幂次,就把他合并为一个物品,如此就大大的减少了枚举的代价,以50个为例我们只需要1,2,4,8,16,19这几种物品即可,具体看代码。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){//空间压缩版本
	int n,W;
	cin>>n>>W;
	vector<int> v(100005);//价值
	vector<int> w(100005);//重量
	int size=0;
	for(int i=1;i<=n;i++){
		int value,wei,cnt;
		cin>>value>>wei>>cnt;
		for(int k=1;k<=cnt;k*=2){
			v[++size]=value*k;
			w[size]=wei*k;
			cnt-=k;
		}
		if(cnt>0){
			v[++size]=value*cnt;
			w[size]=wei*cnt;
		}
	}
	vector<int> dp(W+1);
	for(int i=1;i<=size;i++){
		for(int j=W;j>=0;j--){
				if(j>=w[i]){
					dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
				}
			}
		}
	cout<<dp[W];
}
int main()
{
	 solve();
	
	return 0;
}

  在这里直接进行了空间压缩,空间压缩的技巧没有什么创新,与上方一样,把数量拆分成若干个二进制的物品这段代码要学习,首先从1开始一个一个加入直到剩余的不够,最后也别忘了把剩下的不足二的下一个幂次的部分加入,剩余就是01背包即可,学习!

  此外,多重背包还有单调队列优化技巧,这里先欠着,后续背包题目大总结或者单调栈/队列文章里会提到。

6,混合背包

  其实就是多种背包的组合使用,比如多重加上完全,又或者分组加上多重,没有固定的某种模板,而是思路上的开阔,本文只是用于复习的模板总结,所以先不考虑混合背包问题的题目。

7,多维费用背包

leetcode.474,879

  区别在于常见的背包问题限制条件只有一个,而多维费用背包指的是有多个限制的条件(往往是两个限制)的情况下求最大总价值。

  其实思路也很好想,就是用一个三维的动态规划去做,分别代表,考虑前i个物品,不超过j的某种限制,不超过k的某种限制,然后在不超过两种限制的情况下更新最优.

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
      vector<vector<int>> dp(m+1,vector<int>(n+1));
      int len=strs.size();
      for(int i=0;i<len;i++){
        int z=0,o=0;
        int lenn=strs[i].size();
        for(int j=0;j<lenn;j++){
            if(strs[i][j]=='0'){
                z++;
            }else o++;
        }
        for(int j=m;j>=z;j--){
            for(int k=n;k>=o;k--){
               if(j>=z&&k>=o){
                   dp[j][k]=max(dp[j][k],dp[j-z][k-o]+1);
               }
              }
            }
        }
      return dp[m][n];
    }
};

  这道题直接放出代码,思路很粗暴,两个限制就是1和零的数量,每个小字符串的0,1的数量就是限制属性,问最长的答案就当作每个物品(字符串)价值是1即可。很简单的更新思路,不多赘述。

879:

集团里有 n 名员工,他们可以完成各种各样的工作创造利润。

第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。

工作的任何至少产生 minProfit 利润的子集称为 盈利计划 。并且工作的成员总数最多为 n 。

有多少种计划可以选择?因为答案很大,所以 返回结果模 10^9 + 7 的值

  这道题跟第一题的大体思路相同,但是区别就是要求不再是不能超过某个值,而是不能低于某个值,那么我们在更新第三维的时候就需要注意,如果最终的数值是负数,我们无需再开一段内存,直接置为零即可,也就是说每个位置的值的含义就是大于等于当前位置的值方案数,因为有的值是本来为负却当作零推上来的,虽然最终递推到了minProfit处,但是意义就是大于等于这个最小利益的方案数。

  对于初始化这个问题,最开始应该就有方案数,才能逐渐累加到最后,所以一开始就应该有,在一个都没选的时候,没有利益也没有人数,所以不超过任何人数,大于等于0利润的方案数是1即可(具体看代码)。

  还有就是区别于一般的背包问题,本题问的是方案数,所以我们需要的不是更新最大值,而是把每种选择都加在一起,即选本位置和不选本位置两种情况,然后我直接放出空间压缩版本,空间压缩很常规,画图注意一下更新顺序注意不要污染数据即可。

class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
         int mod=1000000007;
         int p=profit.size();
         vector<vector<int>> dp(200,vector<int>(200));
         for(int i=0;i<=n;i++){
            dp[i][0]=1;
         }
         for(int i=1;i<=p;i++){
            int a=group[i-1],b=profit[i-1];
            for(int j=n;j>=a;j--){
                for(int k=minProfit;k>=0;k--){
                    dp[j][k]=(dp[j][k]+dp[j-a][max(0,k-b)])%mod;
                }
            }
         }
         return dp[n][minProfit];
    }
};

  至此,常规的常见背包dp模板总结结束,后续还会有一些思路新颖的背包问题(或类背包问题)总结,仅供个人复习。

  • 26
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值