背包九讲问题

    背包问题是一个经典的动态规划模型。熟练掌握背包问题,对于动态规划的理解与应用有较大的提升。

    笔者也是在读了dd engi的《背包九讲V1.1》后受到启发。本文的算法思路与解题技巧基本上属于搬运,可以算是笔者的读后笔记与个人总结。同时本文提供了较多篇幅的模板与源代码供读者借鉴参考、学习交流,相比于《背包九讲V1.1》,笔者在分组背包、多重背包的O(VN)算法单调队列实现等问题均给出了模板。


 01背包问题

问题描述:有N件物品和容量为V的背包,放入第i件物品耗费费用是Ci,得到价值是Wi,求将哪些物品装入可使价值总和最大。

由题意可得,状态转移方程为:F[i,v]=max{F[i−1,v],F[i−1,v−Ci]+Wi}

   进一步分析不难得到,可以将O(VN)的空间复杂度缩小至O(V)。以下是01背包模板。

void ZeroOnePack(int cost,int weight)
{
	for(int i=V;i>=cost;i--)
		dp[i]=MAX(dp[i-cost]+weight,dp[i]);
}
for(int i=1;i<=N;i++)
	ZeroOnePack(c[i],w[i]);

 

 完全背包问题

    问题描述:有N种物品和一个容量为V的背包,每种物品都有无限件可用。放入第i种物品的费用是Ci,价值是Wi。求将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大。

     与01背包问题类似,只不过每一件物品都能使用1件、2件……直至⌊V/Ci⌋件。我们可以按照01背包问题的思路来写状态转移方程,其时间复杂度为O(VN):F[i,v]=max{F[i−1,v−kCi]+kWi|0≤kCi≤v}

     我们可以做一个简单优化,若两件物品i、j满足Ci≤Cj且Wi≥Wj,则将可以不用考虑物品j。得到的时间复杂度为O(N^2)。

     二进制优化法。可以将物体拆分成费用为2^kCi、价值为2^kWi的若干件物品,其中k取遍满足2^kCi≤V的非负整数,得到的时间复杂度为O(log⌊V/Ci⌋)。

     下面将介绍O(VN)的算法模板。

void CompletePack(int cost,int weight)
{
	for(int i=cost;i<=V;i++)
		dp[i]=MAX(dp[i-cost]+weight,dp[i]);
}
for(int i=1;i<=N;i++)
	Complete(c[i],w[i]);

 

 多重背包问题

        问题描述:有N种物品和一个容量为V的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

        和01背包、完全背包类似,状态转移方程:F[i,v]=max{F[i−1,v−k∗Ci]+k∗Wi|0≤k≤Mi} ,复杂度是O(VΣMi)。

        当VN>10^7左右时会超时,下面介绍一种二进制优化方法,能将时间复杂度缩小为O(VΣlogMi)。

void MutiplePack(int cost,int weight,int amount)
{
	if(cost*amount>V)//可用数量装入时超过背包容量,即可视为完全背包
	{
		Complete(cost,weight);
		return;
	}
	int k=1;
	while(k<amount)
	{	
		ZeroOnePack(k*cost,k*weight);//将物品拆分成多个01背包
		amount-=k;
		k*=2;
	}
	ZeroOnePack(cost*amount,weighr*amount);
}
for(int i=1;i<=N;i++)
	MutiplePack(c[i],w[i],m[i]);


        使用二进制优化虽然可以较大地提高时间效率,但是对于更高要求的数据规模仍然乏力。我们可以使用单调队列优化,使多重背包达到O(VN)的时间复杂度。

//MAX_V 背包的体积上限值
void MutiplePack(int cost, int weight, int amount)
{
	if(amount==0||cost==0) return;
	if(amount==1)                      //01背包
	{               
		for(int i=V;i>=cost;i--)
      			if(dp[i]< f[i - v] + w) f[i] = f[i - v] + w;
    		return;
  	}
  	if(amount*cost>=V-cost+1)          //完全背包(amount>= V/cost)
	{   
		for(int i=cost;i<=V;i++)
      			if(dp[i]<dp[i-cost]+weight) 
				dp[i]=dp[i-cost]+weight;
    		return;    
  	}
	int va[MAX_V],vb[MAX_V];           //va/vb: 主/辅助队列
  	for(int j=0;j<cost;j++)
	{   
    		int *pb=va,*pe=va-1;       //pb/pe分别指向队列首/末元素
    		int *qb=vb,*qe=vb-1;       //qb/qe分别指向辅助队列首/末元素  
           for(int k=j,i=0;k<=V;k+=v,++i)
          {
      			if(pe==pb+n)	   //若队列大小达到指定值,第一个元素X出队
			{       
        			if(*pb==*qb) ++qb;   
        			++pb;      //若辅助队列第一个元素等于X,该元素也出队。 
     			}
      			int tt=dp[k]-i*w;
      			*++pe=tt;          //元素X进队
      			while(qe>=qb&&*qe<tt) --qe;
      				*++qe=tt   //元素X也存放入辅助队列        
      			dp[k]=*qb+i*w;    
	}
	}
}

 混合三种背包问题

//p[]为判断第i件物品时何种背包问题。设p[]=1为01背包,p[]=2为完全背包,p[]=3为多重背包
for(i=1;i<=N;i++)
{
	if(p[i]==1)
		ZeroOnePack(c[i],w[i]);
	if(p[i]==2)
		CompletePack(c[i],w[i]);
	if(p[i]==3)
		MutiplePack(c[i],w[i],m[i]);
}


 二维费用背包问题

      问题描述:对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种费用。对于每种费用都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设第i件物品所需的两种费用分别为Ci和Di。两种费用可付出的最大值(也即两种背包容量)分别为V和U,物品的价值为Wi。

      由题意易得,只要将费用加上一维,就能得到状态转移方程,其中u和v为两种不同的费用:

F[i,v,u]=max{F[i−1,v,u],F[i−1,v−Ci,u−Di]+Wi}

 

 分组背包问题

        问题描述:有N件物品和一个容量为V的背包。第i件物品的费用是Ci,价值是Wi。这些物品被划分为K组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

      设F[k,v]为前k组取得的最大价值,策略是可以选择第k组中的某一件物品或者不选。可得状态转移方程为:

F[k,v]=max{F[k−1,v],F[k−1,v−Ci]+Wi|itemi∈groupk} 

      我们可以优化空间复杂度,用一维数组给出模板。

//为每件物品新增参数g[i],表示第i件物品所在的组别
void GroupPack(int group)//group表示组别
{
	for(int i=V;i>=0;i--)
	{
		int j=1;
		while(j++<=N&&g[j]==group)	//对所有属于第k组的物品枚举
			dp[i]=MAX(dp[i-c[j]]+w[j],dp[i]);
	}
}
for(int i=1;i<=k;i++)//调用每个组
	GroupPack(i);
        需要注意的是,for(i=V;i>=0;i--)这层循环必须写在while(j)循环的外围,这样才能保证每一组内的物品最多只有一个会被添加到背包中。


有依赖背包问题

        这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。 

        这里给出一道例题,是NOIP上的经典题目《金明的预算》。说实话,笔者在发稿时对于有依赖的背包问题仍不求甚解,《金明的预算方案》此题笔者虽然AC了,但是并没有用“标准”的做法。笔者是将主件和附件的关系表示为:主件、主件+一个附件、主件+两个附件。也就是说一个(主)物品可以拆分成三个物品,其花费与价值另算。

对于部分有依赖背包问题都可以采用笔者这种方法,但是由于附件数量的增加,其物品拆分量也是指数级增涨,当增多附件数较大时,不宜采用。

        就一般来说,有依赖的背包问题大部分可以用树形DP来做。以下给出了树形DP的解法,采用了泛化背包的解法,复杂度为O(VN)。

#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 210;
struct edge {
    int v, c;
    edge* next;
} *V[MAXN], ES[MAXN * 2];
int EC, dp[MAXN][MAXN], val[MAXN];
bool vis[MAXN];
void addedge(int u, int v)
{
    ES[++EC].next = V[u];
    V[u] = ES + EC; V[u]->v = v;
}
void initdata(int n)
{
    EC = 0;
    memset(V, 0, sizeof(V));
    memset(vis, false, sizeof(vis));
    for (int u = 1; u <= n; ++u)
    {
        int v;
        scanf("%d %d", &v, &val[u]);
        addedge(v, u);
    }
}
void treedp(int u, int vol)
{
    vis[u] = true;
    for (edge* e = V[u]; e; e = e->next)
    {
        if (vis[e->v])
            continue;
        for (int i = vol; i >= 0; --i)
            dp[e->v][i] = dp[u][i];
        treedp(e->v, vol - 1);
        for (int i = vol; i >= 1; --i)
            dp[u][i] = max(dp[u][i], dp[e->v][i-1] + val[e->v]);
    }
}
int main()
{
    int n, m;
    while (scanf("%d %d", &n, &m) && n && m)
    {
        initdata(n);
        memset(dp[0], 0, sizeof(dp[0]));
        treedp(0, m);
        printf("%d\n", dp[0][m]);
    }
    return 0;
}

 泛化物品

        泛化物品定义: 考虑这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分配给它的费用而变化。更严格定义,在背包容量为V的背包问题中,泛化物品是一个定义域为0..V中的整数的函数h,当分配给它的费用为v时,能得到的价值就是h(v)。这个定义有一点点抽象,另一种理解是一个泛化物品就是一个数组h[0..V],给它费用v,可得到价值h[V]。
        一个费用c价值w的物品,如果它是01背包中的物品,那么把它看成泛化物品,它就是除了h(c)=w其它函数值都为0的函数。如果它是完全背包中的物品,那么它可以看成这样一个函数,仅当v被c整除时有h(v)=v/c×w,其它函数值均为0。如果它是多重背包中重复次数最多为n的物品,那么它对应的泛化物品的函数有h(v)=v/c×w仅当v被c整除且v/c<=n,其它情况函数值均为0。
一个物品组可以看作一个泛化物品h。对于一个0..V中的v,若物品组中不存在费用为v的的物品,则h(v)=0,否则h(v)为所有费用为v的物品的最大价值。如果面对两个泛化物品h和l,要用给定的费用从这两个泛化物品中得到最大的价值,怎么求呢?事实上,对于一个给定的费用v,只需枚举将这个费用如何分配给两个泛化物品就可以了。同样的,对于0..V的每一个整数v,可以求得费用v分配到h和l中的最大价值dp(v)。也即 dp(v)=max{h(k)+l(v−k)}   0<=k<=v
可以看到,dp也是一个由泛化物品h和l决定的定义域为0..V的函数,也就是说,dp是一个由泛化物品h和l决定的泛化物品。由此可以定义泛化物品的和:h、l都是泛化物品,若泛化物品dp满足以上关系式,则称dp是h与l的和。这个运算的时间复杂度取决于背包的容量,是Θ(V2)。
        泛化物品的定义表明:在一个背包问题中,若将两个泛化物品代以它们的和,不影响问题的答案。事实上,对于其中的物品都是泛化物品的背包问题,求它的答案的过程也就是求所有这些泛化物品之和的过程。设此和为s,则答案就是s[0..V]中的最大值。


背包问题问法变式

初始化

       如果题目要求恰好装满背包,那么在初始化时除了F[0]为0,其它F[1..V]均设为−∞,这样可以保证得到的F[V]是一种恰好装满背包的最优解。如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0..V]全部设为0。这个细节可以推广到其他背包问题。

求方案总数

      对于一个给定了背包容量、物品费用、物品间相互关系(分组、依赖等)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数。
解法一般只需将状态转移方程中的max改成sum即可。若每件物品均是完全背包中的物品,转移方程为:f[i][v]=sum{f[i−1][v],f[i][v−c[i]]},初始条件f[0][0]=1。
      事实上,这样做可行的原因在于状态转移方程已经考察了所有可能的背包组成方案。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值