背包问题的模板

版权声明:https://blog.csdn.net/huashuimu2003 https://blog.csdn.net/huashuimu2003/article/details/84404264

以下模板基本出自《背包九讲》,参见https://www.kancloud.cn/kancloud/pack/70124

1.01背包

#include<bits/stdc++.h>
using namespace std;
const int __=50010;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}
int v;//背包容积
int c[__];//体积 
int w[__];//价值 
int f[__];//表示用第i件物品填充体积为v的背包得到的最大价值 
int main()
{
	n=read(),v=read();
	for (int i=1;i<=n;++i)//枚举 物品
	{
		c[i]=read(),w[i]=read();
		for (int j=v;j>=c[i];--j)//枚举体积
			f[j]=max(f[j],f[j-c[i]]+w[i]);//状态转移方程.
	}
	printf("%d\n",f[m]);
	return 0;
}

2.完全背包

#include<bits/stdc++.h>
using namespace std;
const int __=50010;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}
int v;//背包容积
int c[__];//体积 
int w[__];//价值 
int f[__];//表示用第i件物品填充体积为v的背包得到的最大价值 
int main()
{
	n=read(),v=read();
	for (int i=1;i<=n;++i)//枚举物品 
	{
		c[i]=read(),w[i]=read();
		for (int j=c[i];j<=v;++j)//枚举体积.注意这里的顺序 
			f[j]=max(f[j],f[j-c[i]]+w[i]);//状态转移 
	}
	printf("%d\n",f[m]);
	return 0;
}

3.多重背包

例题

描述 Description
在《Harry Potter and the Deathly Hallows》中,Harry Potter他们一起逃亡,现在有许多的东西要放到赫敏的包里面,但是包的大小有限,所以我们只能够在里面放入非常重要的物品,现在给出该种物品的数量、体积、价值的数值,希望你能够算出怎样能使背包的价值最大的组合方式,并且输出这个数值,赫敏会非常地感谢你。
输入格式 Input Format
(1)第一行有2个整数,物品种数n和背包装载体积v。
(2)2行到n+1行每行3个整数,为第i种物品的数量m、体积w、价值s。.
输出格式 Output Format
仅包含一个整数,即为能拿到的最大的物品价值总和。
样例输入 Sample Input

2 10
3 4 3
2 2 5

样例输出 Sample Output

13
【注释】
选第一种一个,第二种两个。
结果为31+52=13

时间限制 Time Limitation
1s
注释 Hint
对于100%的数据
1<=v<=5000
1<=n<=5000
1<=m<=5000
1<=w<=5000
1<=s<=5000

来源 Source
2007 湖北 noip 模拟赛

代码

//二进制优化
#include<bits/stdc++.h>
using namespace std;
const int __=100010;
inline long long read()
{
	long long f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}
long long c[__];//体积 
long long w[__];//价值 
long long f[__];//表示用第i件物品填充体积为v的背包得到的最大价值 
long long nn;
int main()
{
    long long n=read(),m=read();
    for(int i=1;i<=n;i++)
    {
      	long long num=read();//重量 
		long long x=read();//价值 
		long long y=read();//数量 
		long long t = 1;
       	while (num >= t)
        	{
            	c[++nn] = x*t;//相当于nn++;v[nn]=x*t;
            	w[nn] = y*t;
            	num -= t;
            	t *= 2;
        	}
        	c[++nn] = x * num;
        	w[nn] = y * num;//把s以2的指数分堆:1,2,4,…,2^(k-1),s-2^k+1,
    }
    for(int i=1;i<=nn;i++)
        for(int j=m;j>=c[i];j--)
           f[j] = max (f[j] , f[j - c[i]] + w[i]); 
    printf("%lld\n",f[m]);
    return 0;
}

4.混合背包

例题

【问题描述】
一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,…,Wn,它们的价值分别为C1,C2,…,Cn。有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
【输入格式】
第一行:二个整数,V(背包容量,V<=200),N(物品数量,N<=30);
第2…N+1行:每行三个整数Wi,Ci,Pi,前两个整数分别表示每个物品的重量,价值,第三个整数若为0,则说明此物品可以购买无数件,若为其他数字,则为此物品可购买的最多件数(Pi)。
【输出格式】
仅一行,一个数,表示最大总价值。
【样例输入】
10 3
2 1 0
3 3 1
4 5 4
【样例输出】
11
【样例解释】
选第一件物品1件和第三件物品2件。

代码

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}
int v, n;
int w[31], c[31], p[31];
int f[201];
int main()
{
    v=read(),n=read();
    for (int i = 1; i <= n; i++)
        w[i]=read(),c[i]=read(),p[i]=read();
    for (int i = 1; i <= n; i++)
        if (p[i] == 0)//完全背包
		{
            for (int j = w[i]; j <= v; j++)
                f[j] = max(f[j], f[j-w[i]]+c[i]);
        }
        else//01背包和多重背包
		{
            for (int j = 1; j <= p[i]; j++)           
                for (int k = v; k >= w[i]; k--)
                    f[k] = max(f[k],f[k-w[i]]+c[i]);
        }
    printf("%d\n",f[v]); 
    return 0;
}

5.二维费用的背包

例题

【问题描述】
潜水员为了潜水要使用特殊的装备。他有一个带2种气体的气缸:一个为氧气,一个为氮气。让潜水员下潜的深度需要各种的数量的氧和氮。潜水员有一定数量的气缸。每个气缸都有重量和气体容量。潜水员为了完成他的工作需要特定数量的氧和氮。他完成工作所需气缸的总重的最低限度的是多少?
例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119
如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。
你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。
【输入格式】
第一行有2整数m,n(1<=m<=21,1<=n<=79)。它们表示氧,氮各自需要的量。
第二行为整数k(1<=k<=1000)表示气缸的个数。
此后的k行,每行包括ai,bi,ci(1<=ai<=21,1<=bi<=79,1<=ci<=800)3整数。这些各自是:第i个气缸里的氧和氮的容量及汽缸重量。
【输出格式】
仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。
样例输入 Sample Input

5 60
5
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119

样例输出 Sample Output

249

代码

#include<bits/stdc++.h> 
using namespace std;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}
int O2[1001], N2[1001], w[1001];
int f[101][101];
int main()
{
    memset(f,127,sizeof(f));//初始化为一个很大的正整数
    f[0][0] = 0;
    int o=read(),n=read(),m=read();
    for (int i = 1; i <= m; i++)
        O2[i]=read(),N2[i]=read(),w[i]=read();
    for (int i = 1; i <= m; i++)
      	for (int j = o; j >= 0; j--)
    		for (int k = n; k >= 0; k--)
        	{
        		int x = j + O2[i];
			int y = k + N2[i];
        		if (x > o) x = o;//若氮、氧含量超过需求,可直接用需求量代换,
        		if (y > n) y = n;//不影响最优解
				f[x][y] = min (f[x][y] , f[j][k] + w[i]);
        	}
    printf("%d\n",f[o][n]);
    return 0;
}

6.分组背包

例题

【问题描述】
一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,…,Wn,它们的价值分别为C1,C2,…,Cn。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
【输入格式】
第一行:三个整数,V(背包容量,V<=200),N(物品数量,N<=30)和T(最大组号,T<=10);
第2…N+1行:每行三个整数Wi,Ci,P,表示每个物品的重量,价值,所属组号。
【输出格式】
仅一行,一个数,表示最大总价值。
【样例输入】group. in
10 6 3
2 1 1
3 3 1
4 8 2
6 9 2
2 8 3
3 9 3
【样例输出】group.out
20

代码

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}
int w[31], c[31];
int a[11][32], f[201];
int main()
{
    int v=read(),n=read(),t=read();
    for (int i = 1; i <= n; i++)
	{
    		w[i]=read(),c[i]=read();
		int p=read();
       	a[p][ ++a[p][0] ] = i;
    }
    for (int i = 1; i <= t; i++)//枚举组别 
        for (int j = v; j >= 0; j--)//枚举体积 
            for (int k = 1 , tmp; k <= a[i][0]; k++)//枚举第i组的物品 
                if (j >= w[tmp = a[i][k]])
                    f[j] = max (f[j] , f[j - w[tmp]] + c[tmp]); 
    printf("%d",f[v]);
    return 0;
}

7.有依赖性的背包

例题

戳这里 https://www.luogu.org/problem/show?pid=1064

代码

#include<bits/stdc++.h>
using namespace std;
const int __=32005;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0' , ch=getchar();
	return num*f;
}
int c[__],w[__];//主件的体积,价值
int c1[__][3],w1[__][3];//附件的体积,价值
int f[__]; 
int main()
{
	int n = read() , m = read();
	for (int i=1;i<=m;++i)
	{
		int v = read() , p = read() , q = read();
		if (!q)//是主件
			c[i] = v , w[i] = v * p;//价值=价格*重要度
		else//是附件且q表示这一组内的第几号附件 
		{
			++c1[q][0];//附件属于主件的第几号 
			c1[q][ c1[q][0] ] = v;
			w1[q][ c1[q][0] ] = v * p;//价值=价格*重要度
		}
	}
	
	for (int i = 1; i <= m; ++i)
		for (int j = n; c[i] != 0 && j >= c[i]; --j)
		{
			f[j] = max (f[j] , f[j - c[i]] + w[i]);//只装主件
			if (j >= c[i] + c1[i][1])
				f[j] = max ( f[j] , f[j - c[i] - c1[i][1]] + w[i] + w1[i][1]);//装主件和附件1
			if (j >= c[i] + c1[i][2])
				f[j] = max ( f[j] , f[j - c[i] - c1[i][2]] + w[i] + w1[i][2]);//装主件和附件2
			if (j >= c[i] + c1[i][1] + c1[i][2])
				f[j] = max ( f[j] , f[j - c[i] - c1[i][1] - c1[i][2]] + w[i] + w1[i][1] + w1[i][2]);
				//装主件,附件1和附件2
		}
	printf("%d\n",f[n]);
	return 0;
}

小结

NOIP2006的这道背包问题,通过引入“物品组”和“依赖”的概念可以加深对这题的理解,还可以解决它的推广问题。用物品组的思想考虑那题中极其特殊依赖关系:物品不能既作主件又作附件,每个主件最多有两个附件,可以发现一个主件和它的两个附件等价于一个由四个物品组成的物品组,这便揭示了问题的某种本质。

8.泛化物品

**在刚才学习有依赖性的背包时,就看到了一句话L:事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。这已经触及到了“泛化物品”的思想。看完P08后,你会发现这个“依赖关系树”每一个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和 **
**https://www.kancloud.cn/kancloud/pack/70132 **

9.背包问题问法的变化

(1).输出字典序最小的最优方案

首先,“字典序最小”是指1~N号物品的选择方案排列出来后字典序最小
下面开始尝试解释一下(以01背包为例)
1. 求一个字典序最小的最优方案,一般只需要在转移时注意一下策略即可
2. 那么,我们就需要把子问题的定义给改一下:(1.)我们注意到,如果选了物品1的最优方案,那么这个答案一定包括物品1,原问题转化成一个求背包容积为v-c【1】,物品为2…N的问题了,(2.)反之,若是不选物品1的最优方案,则原问题就会转化成背包容量为v,物品为2…N的问题了。
所以,不管怎么说,子问题都是以i…N的形式定义的,并不是前面所说的1…i定义的。所以说状态的定义和状态转移方程都需要改一下。
3. 但是,如果把已知物品逆序排列一下,或许就会更简单一些,(下面按照逆序排列的物品来叙述)
4. 在这种情况下,是可以用那个经典的状态转移方程了,只不过要注意该怎么输出方案:从N到1输入时,若

f[i][v] == f[i - 1][i - v] && f[i][v] == f[i - 1][i - c[i]] + w[i]

同时成立,应该选择后者,(即选择了物品1)来输出方案因为这样选择的话,我们会得到更小的字典序.(很明显吧)

(2.)输出方案

只需要开数组g[i][v]记录状态f[i][v]是有状态方程哪一项推出就行了

int g[__][__];
//开数组g[i][v]记录状态f[i][v]是有状态方程哪一项推出
void function()
{
	for (int i=1;i,=n;++i)
	{
		for (int j=v;j>=c[i];--j)
		{
			if (f[j]<f[j-c[i]]+w[i])
			{
				f[j]=f[j-c[i]]+w[i];
				g[i][j]=true;//选第i件物品 
			}
			else g[i][j]=false;//选第i件物品 
		}
	}
}
void print()
{
	int t=v;
	for (int i=n;i>=1;--i)
	{
		if (g[i][t])
		{
			printf("used %d",i);
			t-=c[i];//减去物品i的体积 
		}
	}
}

(3.)求次优解或第k优解

这个不再多说,因为有板子题

例题

https://www.luogu.org/problem/show?pid=1858

AC代码

//背包问题的变化
/*
*求次优解or第k优解 p1858 luogu.org
*/
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int f=1,num=0;
    char ch=getchar();
    while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
    while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
    return num*f;
}
int ans,now[55];
int V[288],W[288],f[5008][55];
int main()
{
    //freopen("data.out","w",stdout);
    //尝试输出中间变量我用了notepad,因为太长了啊! 辅助用
    int k=read(),v=read(),n=read();
    for(register int i=0;i<=5000;i++)
        for(register int j=0;j<=50;j++)
			f[i][j]=-20021003;//赋初值为-inf
    f[0][1]=0;//体积为0的最优解为0.
    for(register int i=1;i<=n;i++)
        V[i]=read(),W[i]=read();//V[i]为体积,W[i]为价值.
    for(register int i=1;i<=n;i++)
        for(register int j=v;j>=V[i];j--)
        {
            int c1=1,c2=1,cnt=0;
            while(cnt<=k)
            {
                if(f[j][c1]>f[j-V[i]][c2]+W[i])
                	now[++cnt]=f[j][c1++];//等于++cnt,now[cnt]=f[j][c1],++c1;
                else
				now[++cnt]=f[j-V[i]][c2++]+W[i];/*等于++cnt,
				now[cnt]=f[j-V[i]][c2]+W[i];++c2;*/
            }
            //这里我选择了开数组记录当前最优解的值,在下面直接赋值给f[j]即可。
            for(register int c=1;c<=k;c++)
				f[j][c]=now[c];
//          printf("now::%d\n",j);
//          for(register int w=1;w<=v;w++,puts(""))
//          for(register int c=1;c<=k;c++)printf("%d\t",f[w][c]);
//          puts("");//上面四行是输出中间变量的
        }
    for(register int i=1;i<=k;i++)
		ans+=f[v][i];
//  for(register int w=1;w<=v;w++,puts(""))
//      for(register int c=1;c<=k;c++)printf("%d ",f[w][c]);
    printf("%d",ans);
    return 0;
}

(4.)求方案总数

下面转自原文,实在是写不出来啊,虽然懂的这是怎么回事 。。。。。。。。
一般而言,背包问题是要求一个最优值,如果要求输出这个最优值的方案,可以参照一般动态规划问题输出方案的方法:记录下每个状态的最优值是由状态转移方程的哪一项推出来的,换句话说,记录下它是由哪一个策略推出来的。便可根据这条策略找到上一个状态,从上一个状态接着向前推即可。

还是以01背包为例,方程为f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。再用一个数组g[i][v],设g[i][v]=0表示推出f[i][v]的值时是采用了方程的前一项(也即f[i][v]=f[i-1][v]),g[i][v]表示采用了方程的后一项。注意这两项分别表示了两种策略:未选第i个物品及选了第i个物品。那么输出方案的伪代码可以这样写(设最终状态为f[N][V]):

 i=N
    v=V
    while(i>0)
        if(g[i][v]==0)
            print "未选第i项物品"
        else if(g[i][v]==1)
            print "选了第i项物品"
            v=v-c[i]

另外,采用方程的前一项或后一项也可以在输出方案的过程中根据f[i][v]的值实时地求出来,也即不须纪录g数组,将上述代码中的

g[i][v]==0改成f[i][v]==f[i-1][v],g[i][v]==1改成f[i][v]==f[i-1][v-c[i]]+w[i]

也可。

附录一.背包问题的搜索方法

1.简单的dfs

对于01背包,简单的dfs的复杂度是O(2n)。就是枚举出所有2n种将物品放入背包的方案,然后找最优解。代码如下(调用Try(1,0,0) 即可):

int best=0;
void Try (int i,int curw,int curv)
{//i是当前物品,curw是当前重量,curv是当前价值
	if (i > N)
	{
		best = max ( curv , best );
		return ;
	}
	if (curw + w[i] <= V) Try( i + 1, curw + w[i] , curv + v[i]);
	Try( i + 1 , curw , curv );
}

2.预处理和剪枝

预处理以下又是转自原文 ):在搜索中,可以认为顺序靠前的物品会被优先考虑。所以利用贪心的思想,将更有可能出现在结果中的物品的顺序提前,可以较快地得出贪心地较优解,更有利于最优性剪枝。所以,可以考虑将按照“性价比”(权值/费用)来排列搜索顺序

另一方面,若将费用较大的物品排列在前面,可以较快地填满背包,有利于可行性剪枝。

最后一种可以考虑的方案是:在开始搜索前将输入文件中给定的物品的顺序随机打乱。这样可以避免命题人故意设置的陷阱。

以上三种决定搜索顺序的方法很难说哪种更好,事实上每种方法都有适用的题目和数据,也有可能将它们在某种程度上混合使用。
剪枝
基本的剪枝方法不外乎可行性剪枝或最优性剪枝。
可行性剪枝:判断按照当前的搜索路径搜下去能否找到一个可行解,例如:若将剩下所有物品都放入背包仍然无法将背包充满(设题目要求必须将背包充满),则剪枝。

最优性剪枝:判断按照当前的搜索路径搜下去能否找到一个最优解,例如:若加上剩下所有物品的权值也无法得到比当前得到的最优解更优的解,则剪枝。

3.子集和问题

(以下可以忽略他的存在又是转自原文,课本上没有。
子集和问题是一个NP-Complete问题,与前述的(加权的)01背包问题并不相同。给定一个整数的集合S和一个整数X,问是否存在S的一个子集满足其中所有元素的和为X。

这个问题有一个时间复杂度为O(2^(N/2))的较高效的搜索算法,其中N是集合S的大小。

第一步思想是二分。将集合S划分成两个子集S1和S2,它们的大小都是N/2。对于S1和S2,分别枚举出它们所有的2^(N/2)个子集和,保存到某种支持查找的数据结构中,例如hash set。

然后就要将两部分结果合并,寻找是否有和为X的S的子集。事实上,对于S1的某个和为X1的子集,只需寻找S2是否有和为X-X1的子集。

假设采用的hash set是理想的,每次查找和插入都仅花费O(1)的时间。两步的时间复杂度显然都是O(2^(N/2))。

实践中,往往可以先将第一步得到的两组子集和分别排序,然后再用两个指针扫描的方法查找是否有满足要求的子集和。这样的实现,在可接受的时间内可以解决的最大规模约为N=42。

4.搜索还是DP

在看到一道背包问题时,应该用搜索还是动态规划呢?

首先,可以从数据范围中得到命题人意图的线索
1.如果一个背包问题可以用DP解,V一定不能很大,否则O(VN)的算法无法承受,
2.而一般的搜索解法都是仅与N有关,与V无关的。
所以,V很大时(例如上百万),命题人的意图就应该是考察搜索。
另一方面,N较大时(例如上百),命题人的意图就很有可能是考察动态规划了。

另外,当想不出合适的动态规划算法时,就只能用搜索了。例如看到一个从未见过的背包中物品的限制条件,无法想出DP的方程,只好写搜索以谋求一定的分数了。

小结

总算把这个博客写完了,写了快两天,虽然仍然不是很懂背包问题,但是也知道了一点,本打算是只写模板的,写着写着,就想写点理解了,哎,这就是一个蒟蒻的想要逞能的表现吧,
总之,背包问题,作为DP的一个分支,是需要我去深刻理解的,尤其是是各种背包模型之间的关系,意义的拓,是我们深刻理解DP的重要途径。

前些天搞图论搞蒙了,来这里看看DP,哎,一入DP深似海呀!!!!!!!!!我算是爬不出来了… 有错误欢迎指出,!!!!!!

猜你喜欢

转载自blog.csdn.net/huashuimu2003/article/details/84404264
今日推荐