背包问题----分组背包(超详细讲解)

分组背包


1.定义
分组背包,通俗的讲就是,给你N组物品,然后每一组你至多选择一个物品(也可以不选),每个物品都有自己的体积和价值,现在给你一个容里为M的背包,让你用这个背包装物品,使得物品价值总和最大.

2.讲解
其实就类似于01背包,对于一个物品有两种决策选或不选,但是分组背包是在01背包的基础上对物品进行了分组,并且每一组只能最多选择一个物品,所以我们不妨用01背包的思想去思考分组背包.

分析:我们设f[i][j]为当前考虑到了第i组物品,剩余容里为j的背包能装物品的最大价值,那么很容易想到我们需要去枚举第i组物品,考虑选哪一个物品时最优的(或者不选),状态转移方程就是i f ( j > = v [ i ] [ k ] ) f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i − 1 ] [ j − v [ i ] [ k ] ] + w [ i ] [ k ] ) if(j>=v[i][k]) f[i][j] = max(f[i][j],f[i-1][j-v[i][k]]+w[i][k])if(j>=v[i][k])f[i][j]=max(f[i][j],f[i−1][j−v[i][k]]+w[i][k]),v[i][k]和w[i][k]分别表示第i组物品中第k个物品的体积和价值

代码:

for(int i=1;i<=n;i++)
	 for(int j=0;j<=m;j++)
	  for(int k=1;k<=s[i];k++)//s[i]表示第i组物品的个数
	   if(j>=v[i][k])//剩余的背包容量j大于第i组的第k个物品的体积 
	   {
	   	  f[i][j] = max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
	   }

这里我们还可以对空间进行优化,我们可以观察到,f[i][…]只会用到f[i-1][…]的值,所以数组的第一维的空间完全可以用滚动数组的方式处理掉,但是如何不影响状态转移呢,我们来看滚掉之后的状态转移方程,f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] [ k ] ] + w [ i ] [ k ] ) f[j] = max(f[j],f[j-v[i][k]]+w[i][k])f[j]=max(f[j],f[j−v[i][k]]+w[i][k]),这里的max里面的f [ j ] 和 f [ j − v [ i ] [ k ] ] f[j]和f[j-v[i][k]]f[j]和f[j−v[i][k]]其实是f [ i − 1 ] [ j ] 和 f [ i − 1 ] [ j − v [ i ] [ k ] ] f[i-1][j]和f[i-1][j-v[i][k]]f[i−1][j]和f[i−1][j−v[i][k]],而不是f [ i ] [ j ] 和 f [ i ] [ j − v [ i ] [ k ] ] f[i][j]和f[i][j-v[i][k]]f[i][j]和f[i][j−v[i][k]],所以我们需要对体积的遍历做一些修改,从大到小循环,如果还是从小到大循环的话,那么这里的f [ j ] 和 f [ j − v [ i ] [ k ] ] f[j]和f[j-v[i][k]]f[j]和f[j−v[i][k]]的含义就有可能是f [ i ] [ j ] 和 f [ i ] [ j − v [ i ] [ k ] ] f[i][j]和f[i][j-v[i][k]]f[i][j]和f[i][j−v[i][k]],而不是我们需要的f [ i − 1 ] [ j ] 和 f [ i − 1 ] [ j − v [ i ] [ k ] ] f[i-1][j]和f[i-1][j-v[i][k]]f[i−1][j]和f[i−1][j−v[i][k]],可以模拟一下就明白了,只靠想的话有点抽象.

3.练习题

题目连接
[CTSC1997] 选课 - 洛谷
这是一道基于分组背包的模板题

AC代码:

#include<bits/stdc++.h>
using namespace std;
const int Maxn = 400;
struct Edge
{
	int v;
	int next;
}edge[Maxn];
int cnt = 0;
int head[Maxn];
int w[Maxn];
int sz[Maxn];
int f[Maxn][Maxn];
void dfs(int u)
{
	f[u][1] = w[u];
//    f[u][0] = 0;
	sz[u] = 1;
	for(int i=head[u];i!=-1;i=edge[i].next)
	{
		int v =edge[i].v;
		dfs(v);
		sz[u]+=sz[v];
		for(int j=sz[u];j>=0;j--)
		 for(int k = 0;k<=sz[v];k++)
		 {
		 	/*j-k>=1,因为选儿子的话必须选父亲要给父亲留位置*/
		 	if(j>k) f[u][j] = max(f[u][j],f[u][j-k]+f[v][k]);
		 }
	}
	return ;
}
void build(int u,int v)
{
	edge[++cnt].v = v;
	edge[cnt].next = head[u];
	head[u] = cnt;
	return ;
}
int main()
{
	memset(f,~0x7f,sizeof(f));
	memset(head,-1,sizeof(head));
	
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++) f[i][0] = 0;
	for(int i=1;i<=n;i++)
	{
		int u;
		cin>>u>>w[i];
		build(u,i);
	}
	w[0] = 0;
	dfs(0);
	cout<<f[0][m+1]<<'\n';
	return 0;
}

依赖背包


1.定义
什么是依赖背包,顾名思义就是具有依赖属性,这种背包常见于树形结构上面,例如:一棵树有N个节点,每一个节点放有一个物品,这些物品有自己的体积和价值,但是如果你要选择v好节点的物品,那么必须先选择v的父亲节点上的物品(所谓的依赖关系),现在你有容里为M的背包,问你选择物品的最大权值和是多少.

2.讲解
我们这里设dp[u][j]表示以u为根节点,背包剩余容里为j能够选择的物品的最大权值和,那么可想而知dp[u][j]这个值一定是由子节点更新来的,状态转移方程如下,用到了滚动数组优化

/*这里i最小为v[u]因为你要选子节点的话,u这个节点必选,给u留空间*/
for(int i=m;i>=v[u];i--) 
 for(int k=0;k<=i-v[u];i++)
  f[u][i] = max(f[u][i],f[u][i-k]+f[s][k]);//s是u的子节点 

那么很显然答案就是f[root][m],root是我们的根节点

3.练习题

题目链接
有线电视网 - 洛谷

这道题就是分组背包+依赖背包的结合体型,一道很经典的树形dp

AC代码:

#include<bits/stdc++.h>
using namespace std;
const int Maxn = 3010;
struct Edge
{
	int v;
	int next;
	int w;
}edge[10000];
int head[Maxn];
int val[Maxn];
int f[Maxn][Maxn];//f[u][j]以u为根节点,选择j个用户赚取的最大价值 
int cnt = 0;
void build(int u,int v,int w)
{
	edge[++cnt].v = v;
	edge[cnt].w = w;
	edge[cnt].next = head[u];
	head[u] = cnt;
	return ;
}
int n,m;
int dfs(int u)
{
	/*说明是用户*/
	if(u>(n-m))
	{
		f[u][1] = val[u];
		return 1;//用户个数 
	}
	int sum = 0,now;
	for(int i=head[u];i!=-1;i=edge[i].next)
	{
		int v = edge[i].v;
		now = dfs(v);//以v为根节点的子树有多少个用户 
		sum+=now;
		for(int j=sum;j>=0;j--)
		 for(int k=1;k<=now;k++)
		 {
		 	if(j>=k)
		 	f[u][j] = max(f[u][j],f[u][j-k]+f[v][k]-edge[i].w);//edge[i].w是u到v的花费 
		 }
	}
	return sum;
	
}
int main()
{
	memset(f,~0x7f,sizeof(f));
 	memset(head,-1,sizeof(head));
 	cin>>n>>m;
	for(int i=1;i<=n;i++) f[i][0] = 0;//选0个用户赚不到钱 
	
	for(int i=1;i<=(n-m);i++)
	{
		int k;
		cin>>k;
		for(int j=1;j<=k;j++)
		{
			int A,C;
			cin>>A>>C;
			build(i,A,C);
		}
	}
	
	for(int i=n-m+1;i<=n;i++) cin>>val[i];
	dfs(1);
	for(int i=m;i>=0;i--)
	{
		if(f[1][i]>=0)
		{
			cout<<i<<'\n';
			break;
		} 
	}
	return 0;
}

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值