背包九讲_模板整理

背包九讲模板整理

01背包

例题:
P1048 [NOIP2005 普及组] 采药
P1734 最大约数和
P1060 [NOIP2006 普及组] 开心的金明

一维数组,逆序循环(避免一件物品重复取)

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

完全背包

P1616 疯狂的采药

一位数组,正序转换

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

多重背包

加了限制条件的完全背包
规定了每件物品的最多数量

优化前

按照01背包的方式处理

为啥不按照完全背包的方式,正序循环呢?
01背包为了避免物品重复\多拿,所以才采用倒序循环
完全背包的单个物品数量是无限的,所以正序循环不会影响
而多重背包的数量是有限的,如果正序循环可能会超出物品数量,所以倒序

时间复杂度是O(n*m)级别

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

二进制优化

优化后
时间复杂度O(n*log)级别

二进制优化
for (int i = 1; i <= n; i++) {
	    int num = min(p[i], m / v[i]); // V/c[i]是最多能放多少个进去,优化上界
	    for (int k = 1; num > 0; k <<= 1) { //这里的k就相当于上面例子中的1,2,4,6
	        if (k > num) k = num;
	        num -= k;
	        for (int j = m; j >= v[i] * k; j--) // 01背包
	            f[j] = max(f[j], f[j - v[i] * k] + w[i] * k);
	    }
	}
	cout<<f[m];

单调队列优化

使用二进制优化,能够达到O(n*log)级别
使用单调队列可以达到O(n)
使用单调队列维护区间最值问题

数组实现单调队列

#include<bits/stdc++.h>
using namespace std;
const int N=2000010;
int n,m;
int f[N],g[N];
int q[N];
int main(){
	cin>>n>>m;
	int v,w,s;
	for(int i=1;i<=n;i++){
		cin>>v>>w>>s;
		memcpy(g,f,sizeof(f));
		for(int j=0;j<v;j++){
		//因为背包容量可以为0,所以从h=0,t=-1开始
			int h=0,t=-1;
			for(int k=j;k<=m;k+=v){
				if(h<=t&&k-s*v>q[h])
					h++;
				while(h<=t&& g[k]>=g[q[t]]+(k-q[t])/v*w)
					t--;
				if(h<=t)
					f[k]=max(g[k],g[q[h]]+(k-q[h])/v*w);
				q[++t]=k;
			}
		}
	}
	cout<<f[m]<<endl;
}

双端队列实现单调队列

效率不如第一种方式
推荐使用上面的数组实现单调队列

for(int i=1;i<=n;i++){
		cin>>v>>w>>s;
		for(int j=0;j<v;j++){
			memcpy(g,f,sizeof(f));
			q.clear();
			for(int k=j;k<=m;k+=v)
			{
				if(!q.empty()&&k-s*v>q.front())
					q.pop_front();
				while(!q.empty()&&g[k]>=g[q.back()]+(k-q.back())/v*w)
					q.pop_back();
				if(!q.empty())
					f[k]=max(g[k],g[q.front()]+(k-q.front())/v*w);
				q.push_back(k); 
			}
		}
	}
	cout<<f[m];

混合背包

以上三种背包的混合版

for(int i=1;i<=n;i++){
		if(p[i]==-1){//01背包 
			for(int j=m;j>=v[i];j--){
				dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
			}
		}
		else if(p[i]==0){//完全背包 
			for(int j=v[i];j<=m;j++){
				dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
			}
		}
		else{//多重背包二进制优化 
			int num=min(p[i],m/v[i]) ;
			for(int k=1;num>0;k<<=1){
				if(k>num)
					k=num;
				num-=k;
				for(int j=m;j>=k*v[i];j--){
					dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
				}
			}
		}
	}

二维费用背包

将一件物品放入背包需要付出两种代价

P1507 NASA的食物计划

//01背包问题
	for (int i = 1; i <= n; i++)
    	for (int j = m; j >= v[i]; j--)
        	for (int k = p; k >= c[i]; k--)
            	f[j][k] = max(f[j][k], f[j - v[i]][k - c[i]] + w[i]);
//完全背包问题,只需要正序即可
	for (int i = 1; i <= n; i++)
    	for (int j = v[i]; j  <= m; j++)
        	for (int k = c[i]; k <= p; k++)
            	f[j][k] = max(f[j][k], f[j - v[i]][k - c[i]] + w[i]);

分组背包

例题:
分组背包问题
P1757 通天之分组背包

分组背包的时间复杂度难以优化,因为每一组的物品不同,难以分类
但是还是可以用滚动数组来优化空间
使用一维数组,必须要确定体积去循环物品,因为每组只能选一个物品

#include<bits/stdc++.h>
using namespace std;
int n,m;
int v[10005],w[10005];
int f[10005];
int main(){
    cin>>n>>m;
    int s;
    for(int i=1;i<=n;i++){
        cin>>s;
        for(int j=1;j<=s;j++)
            cin>>v[j]>>w[j];
        for(int j=m;j>=0;j--){//固定体积
            for(int k=1;k<=s;k++){//枚举物品
                if(j>=v[k])
                    f[j]=max(f[j],f[j-v[k]]+w[k]);
            }
        }
    }
    cout<<f[m];
}

依赖背包

典型的树形DP
洛谷P1352.没有上司的舞会

  1. 递归写法
//树形DP 就是一个dfs的过程,在这个过程中找最大值
#include<bits/stdc++.h>
using namespace std;
const int N = 6005;
//DP数组
//f[i][1]选这个人  
//f[i][0]不选这个人 
int f[N][N] , w[N];
//fa 记录结点有没有父节点
bool fa[N];
vector<int> a[N];//记录编号为i的领导手下的员工的编号
int n;
//深搜下去,在回溯时进行dp
void dfs(int u){
    f[u][1] = w[u];//选u的快乐指数
    for(int i = 0 ;i < a[u].size(); i++){
        int son = a[u][i];//获取u的子节点
        dfs(son);
        f[u][0] += max(f[son][0],f[son][1]);
        f[u][1] += f[son][0];
    }
}
int main(){
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> w[i];
    for(int i = 0 ;i < n - 1 ; i ++){
        int x , y; 
        // 员工---上司
        cin >> x >> y;
        //关于这棵树如何存储这件事
        a[y].push_back(x);
        fa[x] = true;// x有父节点
    }
    int root = 1; 
    //找根节点
    while(fa[root])
        root ++;
    dfs(root);
    cout << max(f[root][0],f[root][1]);
    return 0 ;
}

泛化物品

这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分配给它的费用而变化。这就是泛化物品的概念
更严格的定义之。在背包容量为V VV的背包问题中,泛化物品是一个定义域为0... V 0...V0...V中的整数的函数h hh,当分配给它的费用为v时,能得到的价值就是h ( v ) h(v)h(v)。

背包问题演化

求具体方案

推荐视频教学
董晓算法__求背包具体方案

//在达到最大价值时,输出字典序最小的方案 
/*
假设存在包含第1个物品的最优解,为了确保字典序最小,
我们必然要选择第一个物品
那么 问题就转化成了 2~N这些物品中找到最优解

首先,从后向前遍历物品,让最优解落在f[1][m]中;
然后,从f[1][m]开始搜索字典序最小的路径方案

状态定义: f[i][j]表示从最后一个物品到第i个物品,装入容量为j的背包的最大价值
状态转移: f[i][j]=max(f[i+1][j],f[i+1][j-v[i]]+w[i] 


寻找路径时,正序查找:
如果 f[i][j]=f[i+1][j],表示 不选第i个物品才可以最大价值
如果f[i][j]=f[i+1][j-v[i]]+w[i],必须选第i个物品才能最大价值
如果f[i][j]=f[i+1][j]=f[i+1][j-v[i]]+w[i]
选不选第i个物品都可以达到最大价值,但是为了保证字典序最小
第i个物品也必须选 
!!! 
结论: 如果f[i][j]能通过f[i+1][j-v[i]]+w[i] 转移得到,就选第i个物品 
 
*/
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int f[N][N];
int n,m;
int v[N] , w[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++)
		f[i][j]=f[i+1][j];
		if(j>=v[i])
			f[i][j]=max(f[i+1][j],f[i+1][j-v[i]]+w[i]);
	} 
	//正序寻找
	int j=m;//剩余容量 
	for(int i=1;i<=n;i++){
	//如果当前的价值是由 后面几种 加上第一种 得来的,就选 
		if(j>=v[i] && f[i][j]==f[i+1][j-v[i]]+w[i]){
			cout<<i<<' ';
			j-=v[i];
		}
	} 
	return 0;
} 

求背包方案数

推荐视频教学
董晓算法___求背包方案数量

能达到最大价值的方案数
例如: 01背包求方案数
其他背包只需要在前面的循环条件上做出调整即可

#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int n,m;
//f[i]容量为i时的最大容量,c[i]达到最大值时的方案
int f[N],c[N];
int mod=1e9+7;
int main(){
	cin >> n >> m;
	//背包方案初始化 
	for(int i=0;i<=m;i++)
		c[i]=1;
	int v,w;
	for(int i=1;i<=n;i++){
		cin>>v>>w;
		for(int j=m;j>=v;j--){
			if(f[j-v]+w>f[j]){
				f[j]=f[j-v]+w;
				c[j]=c[j-v];
			}
			else if(f[j-v]+w==f[j])
				c[j]=(c[j]+c[j-v])%mod;
		}
	} 
	cout<<c[m];
}

装满背包求方案数

在装满背包的前提下,.所能达到的最大价值的方案数
只需要初始化一下两个数组

#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int f[N],c[N];
int n,m;
int mm=-1000005;//定义一个最小值 
int main(){
	cin >> n >> m;
	for(int i=1;i<=m;i++)
		f[i]=mm;//将每个背包的价值初始化为最小值
	f[0]=0,c[0]=1;
	int v,w;
	for(int i=1;i<=n;i++){
		cin >> v >> w;
		for(int j=m; j>=v;j--){
			if(f[j-v]+w>f[j]){
				f[j]=f[j-v]+w;
				c[j]=c[j-v];
			}
			else if(f[j-v]+w==f[j])
				c[j]=c[j]+c[j-v];
		}		
		
	} 
	//只有恰好装满背包时, 一定是从c[0]直接或间接转移而来 
	//所以只给 c[0]=1  其他的c[i]=0, 0+0=0,不计入方案数 
	cout << c[m];
	return 0;
}

参考文章: 背包九讲——全篇详细理解与代码实现
作者: 良月澪二

posted @ 2022-11-13 22:27  秋天Code  阅读(74)  评论(0)    收藏  举报