背包九讲(模板题+思路分析+代码)

背包九讲

博客介绍

我所写例题均来自**https://www.acwing.com/problem/**,如果有疑问也可去这个网站听yxc讲解课。

一、 01背包

问题描述:

有N件物品,容量是V的背包,每件物品只能使用一次,根据给出物品的体积与价值,求出使总体积不超过背包容量的最大价值

求解思路:

  1. f[i] [j]为在前i个物品中,背包容量为j时的最优解。因此,我们可以将f[0] [0]赋值为0.
  2. 分析每件物品时,我们都有两种决策方案:选与不选
  3. 当此时背包的容量不可以容纳这件物品时,我们不可以选择此物品,则价值等于有i-个物品时,f[i] [j] = f[i-1] [j]
  4. 当满足此时背包的容量可以容纳这件物品,即 j < v[i].那么此时有两种决策方案:选与不选
  5. 如果不选,则和第3点一样,如果,背包容量由 j —>j-v[i] ,此时价值为则f[i] [j]=f[i-v[i]] [j] +w[i]
  6. 我们是否选择要根据题意所求判断,我们需要最大价值,因此f[i] [j]应取两者最大值
二维代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v[1005],w[1005];
int f[1005][1005];
int main()
{
    cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(v[i]>j)//不选该物品
			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]<<endl;
	return 0;

优化

f[j] 为N件物品,背包容量为j时的最优解。我们从代码中可以发现f[i] [j]的状态与前一个状态有关,我们简化为一维,可以直接去掉一维,我们可以发现这样是错误的。

为什么呢?因为二维时f[i] [j]的状态由f[i-1] [j]得到的,当优化为一维时,只剩一个变量,我们无法去保证用的是i-1轮的状态。在循环时,我们可能会使需要用的上一轮状态被改变,即当前物品被选多次

如何解决这个问题呢?我们可以采取逆序,当前物品不会影响下一个容量。

一维代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005];
int f[1005];
int main()
{
    cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
	}
	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;
	return 0;
}

二、完全背包

简单介绍

这道题与01背包的区别在于:完全背包里的物品可以无限次使用

思路

我们在选择物品时则转化为了

f[i][j]  =max(f[i][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,...f[i-1][j-k*v]+k*w);

最初我想添加一层循环来得到f[i] [j]的最大值,听了yxc的课发现没必要

f[i][j]  =max(f[i][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,...f[i-1][j-k*v]+k*w);
f[i][j-v]=max(       ,f[i-1][j-v]+w,f[i-1][j-2v]+2w,...f[i-1][j-k*v]+k*w);

得到递推关系
f[i][j]=max[f[i][j],f[i][j-v[i]]+w[i]];

这与01背包类似,不同点在于01背包用的是i-1层的状态,而完全背包则使用的是i层的状态,也正因如此,当优化为一维时可以正序,无需倒序,以下是优化前后的代码

二维代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005];
int f[1005][1005];
int main()
{
    cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			f[i][j]=f[i-1][j];
			if(j>=v[i])
			f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
		}
	}
	cout<<f[n][m]<<endl;
	return 0;
}
优化代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005];
int f[1005];
int main()
{
    cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
	}
	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]<<endl;
	return 0;
}

三、多重背包

简单介绍:

这个题与01背包的区别在于每件物品有固定的数量

思路:

我们思路与完全背包类似,在选取物品时我们需要多加一层循环

f[i][j]  =max(f[i][j],f[i-1][j-k*v]+k*w);

时间复杂度O(n * v * s)

二维代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005],s[1005];
int f[1005][1005];
int main()
{
    cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i]>>s[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			for(int k=0;k<=s[i];k++) 
			{
				if(j>=k*v[i])
				{
					f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
				}
			}
		}
	}
	cout<<f[n][m]<<endl;
	return 0;
}
一维代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005],s[1005];
int f[1005];
int main()
{
    cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i]>>s[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=v[i];j--)
		{
			for(int k=0;k<=s[i];k++) 
			{
				if(j>=k*v[i])
				{
					f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
				}
			}
		}
	}
	cout<<f[m]<<endl;
	return 0;
}

优化1(二进制优化):

  1. 我们对于这个问题需要考虑每组商品需要选几个,我们可以使用二进制进行优化。

  2. 例如8,我们可以使用1,2,4,8组合出0-8所有的情况,但是他也会组合出超出8的情况,因此我们应进行限制。

  3. 我们将所有组合出来的体积价值放入容器中,此时问题又转化为01背包,我们进行求解即可

优化代码1
#include<bits/stdc++.h> 
using namespace std;
#define N 2010
int f[N];
struct Good
{
	int v,w;
};
vector<Good>good;
int main()
{
	int n,m;
    cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int v,w,s;
		cin>>v>>w>>s;
		for(int k=1;k<=s;k*=2)
		{
			s-=k;
			good.push_back({k*v,w*k});
		} 
		if(s>0)
		good.push_back({s*v,s*w});
	}
	for(auto t:good)
	{
		for(int j=m;j>=t.v;j--)
		{
			f[j]=max(f[j],f[j-t.v]+t.w);
		}
	}
	cout<<f[m]<<endl;
	return 0;
}

优化2(单调队列优化):

  1. 我们可以根据余数来优化,把余数相同的归为一类,余数不同无法相互干涉

    dp[0],  dp[v],   dp[2*v],...dp[k*v]
    dp[1],  dp[v+1], dp[2*v+1],...dp[k*v+1]
    dp[2],  dp[v+2], dp[2*v+2],...dp[k*v+2]
    ...
    dp[j],  dp[v+j], dp[2*v+j],...dp[k*v+j]
    
  2. 因此我们需要得到(dp[j], dp[v+j], dp[2 * v+j], … dp[k * v+j])中的最大值。我们可以将其通过单调队列来求取最大值

  3. dp[j]    =     dp[j]
    dp[j+v]  = max(dp[j] +  w,  dp[j+v])
    dp[j+2v] = max(dp[j] + 2w,  dp[j+v] +  w, dp[j+2v])
    dp[j+3v] = max(dp[j] + 3w,  dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
    

    由于每次dp[j]所加数都会改变,因此我们可以做修改:

    dp[j]    =     dp[j]
    dp[j+v]  = max(dp[j], dp[j+v] - w) + w
    dp[j+2v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w) + 2w
    dp[j+3v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w, dp[j+3v] - 3w) + 3w
    

    此时每次入队列的都为

    dp[j+k*v]-k*w
    
  4. 单调队列问题中 维持单调队列长度与单调性

优化代码2
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 20010
int f[N],q[N],g[N];
signed main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int v,w,s;
		cin>>v>>w>>s;
		memcpy(g,f,sizeof(f));
		for(int j=0;j<v;j++)
		{
			int hh=0,tt=-1;
			for(int k=j;k<=m;k+=v)
			{
				if(hh<=tt&&q[hh]<k-s*v) //维持单调队列长度
				hh++;
				if(hh<=tt)
				f[k]=max(f[k],g[q[hh]]+(k-q[hh])/v*w);
				while(hh<=tt&&g[q[tt]]-(q[tt]-j)/v*w<=g[k]-(k-j)/v*w)//维持单调性
				tt--;
				q[++tt]=k;
			}
		}
	}
	cout<<f[m]<<endl;
	return 0;
}

四、混合背包

简单介绍:

混合背包,顾名思义,将多种类型混合在一起。每次输入的s来区别它的类型,-1为01背包,0为完全背包,>0则为多重背包。

思路:

  1. 多重背包我们经过二进制优化,放进容器中可以转化为01背包,此时背包里只剩01背包与完全背包

  2. 在一维中01背包和完全背包的区别在于状态转移方程一个倒序,一个正序,因此我们需要分开写。

代码
#include <bits/stdc++.h>

using namespace std;

const int N = 20010;
int f[N];

struct Good
{
	int kind;
	int v,w;
}; 
vector<Good>good;
int main() {
	int n,m;
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        int v, w, s;
        cin >> v >> w >> s;
        if(s==-1) 
        good.push_back({-1,v,w});
        else if(s==0)
        good.push_back({0,v,w});
        else
        {
        	for(int k=1;k<=s;k*=2)
        	{
        		s-=k;
        		good.push_back({-1,v*k,w*k});
			}
			if(s>0) good.push_back({-1,v*s,w*s});
		}
        }
        for(auto t:good)
        {
        	if(t.kind==-1)
        	{
        		for(int i=m;i>=t.v;i--)
        		{
        			f[i]=max(f[i],f[i-t.v]+t.w);
				}
			}
			else
			{
				for(int i=t.v;i<=m;i++)
					f[i]=max(f[i],f[i-t.v]+t.w);				  
			}
		}
    
    cout << f[m] << endl;
    return 0;
}

五、二维背包的费用问题

简单介绍:

与01背包类似,不过问题转化为二维了。在01背包中限制条件只有体积,而在这个题中又加了重量这一限制条件。

思路:

01背包状态转移方程为

f[i][j]=max(f[i-1][j],f[i-1][ j-v[i] ]+w);
f[i][j]表示在前i个物品中,当背容量为j时的最优解

多了重量这一限制条件,那么我们就多加一维:

f[i][j][k]=max(f[i-1][j][k],f[i-1][ j-v[i] ][ k-m[i] ]+w[i]);
f[i][j][k]表示在前i个物品中,当背包容量为j,可承受重量为k时的最优解

我们依然可以类似01背包操作,将其压缩为二维,当优化为二维时,不包含变量i,我们无法去保证用的是i-1轮的状态。在循环时,我们可能会使需要用的上一轮状态被改变,即当前物品被选多次。我们可以采取逆序解决这一问题,使得当前物品不会影响下一个容量

f[j][k]=max(f[j][k],f[ j-v[i] ][ k-m[i] ]+w[i]);
代码:
#include <bits/stdc++.h>

using namespace std;
int t[1010],z[1010],jz[1010];
int f[1010][110];
int main() {
	int n,v,m;
	cin>>n>>v>>m;
	for(int i=0;i<n;i++)
	{
		cin>>t[i]>>z[i]>>jz[i];
	}
	for(int i=0;i<n;i++)
	{
		for(int j=v;j>=t[i];j--)
		{
			for(int k=m;k>=z[i];k--)
			{
				f[j][k]=max(f[j][k],f[j-t[i]][k-z[i]]+jz[i]);
			}
		 } 
	}
	cout<<f[v][m]<<endl;
    return 0;
}

六、分组背包

简单介绍:

这道题是n组物品,每组物品有若干个,在同一组只能选择一个,体积不超过背包容量,求最大价值

思路:

  1. 若一组中有5件物品,则有六种决策:不选和选择任意这五个物品中的任意一个
  2. 我们加一层循环,在这个循环中找到这个组内的最佳决策
  3. 此时这道题又一次转换为01背包,求解即可
代码
#include <bits/stdc++.h>
using namespace std;
#define N 110
int v[N][N],w[N][N],p[N];
int f[N][N]; 
int main() {
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>p[i];
		for(int j=1;j<=p[i];j++)
		{
			cin>>v[i][j]>>w[i][j];
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			f[i][j]=f[i-1][j];
			for(int k=1;k<=p[i];k++)
			{
				if(j>=v[i][k])
				f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
			} 
		}
	}
	cout<<f[n][m]<<endl;
    return 0;
}

优化:

我们依然可以类似于01背包的优化,将其压缩为一维,记得倒序,原因在01背包中解释过了

优化代码:
#include <bits/stdc++.h>
using namespace std;
#define N 110
int v[N][N],w[N][N],p[N];
int f[N]; 
int main() {
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>p[i];
		for(int j=1;j<=p[i];j++)
		{
			cin>>v[i][j]>>w[i][j];
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=0;j--)
		{
			for(int k=1;k<=p[i];k++)
			{
				if(j>=v[i][k])
				f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
			} 
		}
	}
	cout<<f[m]<<endl;
    return 0;
}

七、有依赖的背包问题

简单介绍:

物品间有依赖关系,如果选取一个物品则必须选取它的父节点,求最大价值

思路:

1.我们在遍历到根节点x时,考虑选取这个节点,此时我们可以赋值:

for(int i=v[x];i<=m;i++) 
f[x][i]=w[x];

2.由于背包选取了x,因此j一定大于v[x],我们枚举j时,j的范围时j—v[x] (倒着枚举)

3.此时在加一层循环,用来枚举这个子树可分得的空间,范围为[0, j-v[x] ]

代码
#include<bits/stdc++.h>
using namespace std;
int f[110][110];//f[x][v]表达选择以x为子树的物品,在容量不超过v时所获得的最大价值
vector<int> g[110];
int v[110],w[110];
int n,m,root;

int dfs(int x,int m)
{
    for(int i=v[x];i<=m;i++) f[x][i]=w[x];//选取点x
    for(int i=0;i<g[x].size();i++)
    {
        int y=g[x][i];
        dfs(y,m-v[x]);
        for(int j=m;j>=v[x];j--)//j的范围为v[x]~m, 小于v[x]无法选择以x为子树的物品
        {
            for(int k=0;k<=j-v[x];k++)//分给子树y的空间不能大于j-v[x],不然都无法选根物品x
            {
                f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
            }
        }
    }
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int fa;
        cin>>v[i]>>w[i]>>fa;
        if(fa==-1)
            root=i;
        else
            g[fa].push_back(i);
    }
    dfs(root,m);
    cout<<f[root][m];
    return 0;
}

八、 背包问题求方案数

简单介绍:

题目条件大致与01背包类似,但所求不同,这道题让求出最优方案选择数,结果对1e9+7取模

思路:

if(f[i][j]==f[i-1][j]&&f[i][j]==f[i-1][j-v]+w);
则
g[i][j]=g[i-1][j]+g[i-1][j-v];
if(f[i][j]==f[i-1][j]&&f[i][j]!=f[i-1][j-v]+w)
g[i][j]=g[i-1][j]
if(f[i][j]!!=f[i-1][j]&&f[i][j]==f[i-1][j-v]+w)
g[i][j]=g[i-1][j-v]
  1. 根据以上三种判断可以得出结果,由于当物品为0,背包容量为0时,什么都不选也为1种方案,因此g[0] [0]=1.
  2. 最优方案为f[n] [m],因此我们将所有与其相等的方案数相加得到的结果对1e9+7取模即为所求
代码
#include<bits/stdc++.h>
using namespace std;
#define N 1010
#define mod 1000000007

int f[N][N],v[N],w[N],g[N][N]; 
int n,m;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;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][j],f[i-1][j-v[i]]+w[i]);
		}
	}
	//cout<<f[n][m]<<endl;
	g[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			if(f[i][j]==f[i-1][j])
			g[i][j]=(g[i][j]+g[i-1][j])%mod;
			if(j>=v[i]&&f[i][j]==f[i-1][j-v[i]]+w[i])
			g[i][j]=(g[i][j]+g[i-1][j-v[i]])%mod;
		 } 
	}
	int res=0;
	for (int j = 0; j <= m; ++ j)
    {
        if (f[n][j] == f[n][m])
        {
            res = (res + g[n][j]) % mod;
        }
    }
    cout << res << endl;
	return 0;
} 
优化代码:

我们依然可以将其压缩为一维

#include<bits/stdc++.h>
using namespace std;
#define N 1010
#define mod 1000000007
int v[N],w[N],f[N],g[N];
int main()
{
	int n,m;
	cin>>n>>m;
	g[0]=1;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i];
	}
	int t;
	int c;
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=v[i];j--)
		{
			t=max(f[j],f[j-v[i]]+w[i]),c=0;
			if(t==f[j]) c=(c+g[j])%mod;
			if(t==f[j-v[i]]+w[i]) c=(c+g[j-v[i]])%mod; 
			f[j]=t;
			g[j]=c;
		}
	}
	int res=0;
	for(int j=0;j<=m;j++)
	{
		if(f[j]==f[m])
		res=(res+g[j])%mod;
	}
	cout<<res<<endl;
	return 0;
}

九、背包问题求具体方案

简单介绍:

与01背包所给条件类似,其所求为输出最优解中所选编号的序列,并且该编号序列的字典序最小

思路:

  1. 题目要得到的答案为字典序最小,我们需要采取由结果反推

  2. 为什么使用反推可以达到预期效果?在dp过程中状态是不断变化的,如果在一个问题中编号2,3同时可以使问题达到最优解,在后续递推过程中会记录编号3,和我们想得到的预期结果不同,因此选择从结果反推

  3. if(f[i][j]==f[i+1][j])
    

    则说明不选第i个物品为最优解

    if(f[i][j]==f[i+1][j-v[i]]+w[i])
    

    则说明选第i个物品为最优解

    if(f[i][j]==f[i+1][j]&&f[i][j]==f[i+1][j-v[i]]+w[i])
    

    则说明选不选第i个物品都可以得到最优解,但由于我们结果要求字典序最小,因此我们需要选择该物品。

  4. 汇总可得当f[i][j]==f[i+1] [j-v[i]]+w[i]时,必选该物品

代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1010
int v[N],w[N],f[N][N];
int main()
{
	int n,m;
	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][j],f[i+1][j-v[i]]+w[i]);
		}
	}
	int i=1;
	int j=m;
	while(i<=n)
	{
		if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
		{
			cout<<i<<" ";
			j-=v[i];
			i++;
		}
		else
		i++;
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值