线性DP和背包问题

递归和递推

对于递归和地推我们以斐波那契数列来举个例子

        斐波那契数列的定义是这样的 f1=1, f2=1,fn=fn-1  +  fn-2;

根据这个定义,我们可以很容易的用递归实现,代码如下

#include<stdio.h>
long long f(int n)
{
	if(n==1||n==2)
		return 1;
	return f(n-1)+f(n-2);    //进行递归
}
int main()
{
	int n;
	scanf("%d", &n);    //读入需要求的斐波那契数列的第n个数
	long long t=f(n);    //调用函数进行递归实现
	printf("%lld", t);    //输出第n个数的值
	return 0;
}

用递归进行实现的过程会出现一个什么问题呢?就是对本来之前求过的数,又求了很多次,这是什么意思呢,举个例子

        我们要去求斐波那契数列的第5个数

        我们会调用:去求第4个和第3个数

        我们会调用:去求第3个和第2个数,以及去求第2个以及第1个(后面这部分调用结束,返回)

        我们会调用:去求第2个和第1个数(调用结束,返回)

就只是对于5来说,我们其中的3就求了两次,即重复子问题。如果我们要求的数更大,其中就会出现非常多的数,会被求非常多次,对于时间来说是极其致命的。

因此我们需要对这个递归进行优化,很明显,其中被多次求的数,我们其实一定是求过的,那么我们只需要把求出来的每个数都用数组存起来,那么我们之后在遇到要调用某个数时,就不需要去求这个数了,而只需要返回数组中所存的值。

依照这个思路我们就可以把代码优化成这样

#include<stdio.h>
const int N = 1e7+2;
long long a[N];
long long f(int n)
{
	if(a[n]!=0)
		return a[n];	//如果我们去调用的那个n存在,我们直接返回其中的值
	a[n]=f(n-1)+f(n-2);		//如果不存在,那么我们递归去求他,并把他存到数组中
	return a[n];
}
int main()
{
	int n;
	scanf("%d", &n);
	a[0]=1, a[1]=1;
	long long t=f(n-1);	//因为我们数组下标是从0开始存的,所以我们调用函数时所传的值是n-1
	printf("%lld", t);
	return 0;
}

如此,我们便可以不再去重复求某些数了。但既是是这样,我们依然还可以对他进行优化。我们先分析可以优化的点,因为我们求一个数,必然要知道他的前两个数,那么我们完全可以从前往后去求每个数的值并把所求的数存入数组,如此,我们和上面优化后的时间复杂度应该是一样的,但不一样的点在于,我们可以用for循环去实现这个操作,而不需要去一直调用函数,毕竟调用函数其实也是需要花费时间的

而如此,从初始往结果推的方法就叫递推,我们看一下代码

#include<stdio.h>
const int N = 1e7+2;
long long a[N];
int main()
{
	int n;
	scanf("%d", &n);
	for(int i=0;i<n;i++)
	{
		if(i==0||i==1)	//前两位的值为1
			a[i]=1;
		else
			a[i]=a[i-1]+a[i-2];	//后面的每一位都是前面两位的和
	}
	printf("%lld", a[n-1]);	//因为我们数组的下标从0开始存,所以第n个数,其实是存在了第n-1的下标处
	return 0;
}

 记忆化搜索

单听这个名字是不是感觉很有难度?其实他的原理并不复杂,因为刚刚对递归的第一种优化方法就是记忆化搜索。

记忆化搜索,意如其名,就是带有记忆化的搜索。其实现方法是用数组等容器将已经计算过的东西记录下来,在下一次需要再次计算这个东西的时候,就不需要再次计算,而是直接在数组中调用已经计算出来的结果就行。如此便可以大大的缩减时间复杂度,当然如此操作可能会增加空间复杂度。

动态规划(解决多阶段决策过程最优化问题)

在正式将动态规划之前,我们先看一到题,这道题现在看比较简单

POJ-1163 数字三角形

题目链接

1163 -- The Triangle

解题链接

动态规划的基本原理

分类加法原理

        假设我们要去完成一件事,而完成这件事的方式有n种,而由于每种方式,第一种有m1个方法去实现,第二种有m2个方法去实现,第三种有m3个方法去实现…第n种有mn个方法去实现,那么我们完成这件事的总方法数自然就是m1+m2+m3+…+mn个方法。这种操作就是分类加法

分类乘法原理

        而分类乘法则是,假设我们要去完成一件事,而完成这件事要有n个步骤,而对于每个步骤,第一步有m1个方法去实现,第二步有m2个方法去实现,第三步有m3种方法去实现…第n步有mn个方法去实现,那么我们完成这件事就有m1*m2*m3*…*mn个方法。这种操作就是分类乘法

动态规划是解决多阶段决策过程最优化问题的一种方法

        阶段:把问题分为几个互相联系的有顺序的几个环节,这些环节就是阶段

        状态:某一阶段的出发位置称为状态,一般来说,一个阶段包含若干个状态

        决策: 从某个阶段的某个状态,演变到他下一个阶段的某个状态的选择(选与不选都是一种决策)

        策略:由起点到终点的全过程中,有每个决策组成的决策序列称为全过程的策略,简称策略

        状态转移方程:前一个阶段的终点是后一个阶段的起点,前一阶段的决策选择导出了后一阶段的状态,这种关系描述了由i 阶段到i+1 阶段状态的演变规律,称为状态转移方程。比如:斐波那契数列的f [ n ]=f [ n-1 ] + f [ n-2 ];就是一种状态转移方程

动态规划适用的基本条件

1.具有相同的子问题

        这是什么意思呢?即是,首先我们要把我们要求的问题分解成几个的子问题,而这些被分解出来的子问题又能被分解成几个子子问题,而这些子子问题分解后,最终能够被求出来,举个例子,我们假设一个问题可以被分为A、B、C三个问题,而A又可以被分为A1、B1、C1三个问题,B可以被分为A2、B2、C2三个问题,C可以被分为A3、B3、C3三个问题,而不会被分解为D、F、G等其他形式的子问题,如此便是具有相同的子问题。所以我们只关心最初始的状态,以及从当前状态如何转移到下一个状态

2.满足最优子结构

        即问题的最优解包含着他的子问题的最优解,也就是说,不论之前的决策是如何的,当前状态往后的状态必须是基于当前状态的最优解所得才行

3.满足无后效性

        什么是无后效性,就是指从当前状态转移到下一个状态不会受到当前状态以前的所有决策的影响,即是说当前状态已经包含了以前所有决策的所有可能性,而我们只需要在这所有可能性中选取最优的一种去转移到下一个状态。换句话说,如果当前状态决策的具体选择会影响到未来某个状态,这时候就不能去保证决策的最优性了。应该这样描述,“过去的决策只能通过当前状态影响未来的发展,而不能直接对未来产生影响,即是当前状态是对历史的总结。”

动态规划的一般步骤

这个一般步骤呢并不是一个固定的死的东西,并不是在做每个题的时候都要硬套,这个只是在我们刚做动态规划题的时候给我们提供一个大致的思路方向

        结合题目的原问题和子问题去确定状态

        (我是谁,我在哪)

                1、明确题目在求什么?要求答案我们需要知道什么?什么因素会影响答案?

                2、在描述题目的时候,一维描述不完整就用二维,二维还不行就上三维,三维还不行就继续加高维度,直至可以完整描述

        确定状态方程

        (我从哪里来?/我到哪去?)

                1、检查参数是否足够;

                2、分情况,比如在最后一次或者第一次等特殊状态的时候,注意特殊处理;

                3、注意边界,不要访问到边界外了;

                4、注意无后效性,比如求A需要求B,而求B需要求C,但是求C却又要求A,这是后就不满足无后效性;

        考虑需不需要优化(进阶操作)

        确定编程实现方式

                递推 或者 记忆化搜索

01背包

这类问题主要类似于 给你n个大小的体积,再给你m个物品,每个物品都有他对应的体积vi和价值wi, 每个物品只能选一次,然后让你去求选完物品后的最大总价值,或者让总价值最大的选法。

对于这一类问题我们当然不能直接去考虑直接先选价值比上体积最大的,举个例子:给你60个大小的体积,给你4个物品,体积分别为:60 1 6 5,其价值分别为:60 2 7 8;对于这个例子我们如果按价值比体积大的先选进行选择,那么我们将会选择第2、3、4个物品,而实际上,我们直接选第1个物品是最优的

那么我们该怎么考虑呢?我们可以这样考虑:

        首先,我们去开一个数组 a[ m+1 ][ n+1 ] 其行数表示,选取物品的次数,因为有m个物品所以要选m次,而又因为我们的下标是从1开始的(原因待会讲)所以行数为m+1,体积是因为我们总的体积为n的大小,所以我们要开n列,同样因为下标从1 开始,所以列数为n+1

        对于每一个物品我们考虑这样选取

                对于第一个物品,我们只会有选和不选两种状态,而因为在他之前是没有选任何物品的,所以只有a[ 0 ][ 0 ]=0一种状态,表示选0个的时候,占了0个体积,价值为0。如果选第一个物品时体积没超出,那么我们将会得到两种状态a[ 1 ][ 0 ]=0+0和a[ 1 ][ v1 ]=0+w1;

                对于第二个物品,我们还是只有选和不选两种状态,但因为第一次选完后有两种状态,所以选第二个物品后的状态应该是基于第一次选完后的每种状态考虑选不选第二个物品,如此便得到了2*2种状态,即a[ 2 ][ 0 ]=0+0+0、a[ 2 ][ v2 ]=0+0+w2和a[ 2 ][ v1 ]=0+w1+0、a[ 2 ][ v1+v2 ]=0+w1+w2。这里我们假设所有的选择的体积都不超过所给体积(当然我们在解题时,需要抛弃所选物品的总体积已经超过所给体积的情况)

                对于第三个物品,我们依然基于上一个物品所选后的所有合法状态(不合法的状态就是体积已经超出的状态),进行选或不选,所以就有2*3种状态,即a[ 3 ][ 0 ]=0+0+0+0、a[ 3 ][ v3 ]=0+0+0+w3和a[ 3 ][ v2 ]=0+0+w2+0、a[ 3 ][ v2+v3 ]=0+0+w2+w3和a[ 3 ][ v1 ]=0+w1+0+0、a[ 3 ][ v1+v3 ]=0+w1+0+w3和a[ 3 ][ v1+v2 ]=0+w1+w2+0、a[ 3 ][ v1+v2+v3 ]=0+w1+w2+w3。(这里依然假设所有选法是合法的)

                对于第四个物品,他是基于第三个物品所选后的所有合法状态进行选或不选,我就不一一举例了,待会会有代码演示

        如此我们考虑完所有物品的选择之后我们数组的最后一行就会包含所有合法的对于所有物品选与不选的所有情况,如此我们只需遍历一遍这一行就可以得到最大值

聪明的你可能会发现两个问题:

        一是如果某两个物品选择后,他们所占的体积相同怎么办,比如v1=3,w1=4;v2=3,w1=5;v3=3,w1=3;因为这三个物品体积一样,所以我们选的时候会变成a[ 1 ][ 3 ]=4,a[ 2 ][ 3 ]=5,a[ 3 ][ 3 ]=3;而其实我们的选择应该是a[ 1 ][ 3 ]=4,a[ 2 ][ 3 ]=5,a[ 3 ][ 3 ]=5;即是我们需要对于这些合法的位置选择总价值更大的那个值

        二是为什么我们要开m+1行n+1列,行很好理解,因为我们的第1行是从第0行转移过来的,所以需要m+1行,而有m+1列,其实是因为对于每一个物品的选择,我们都有一个物品都不选的选择。为什么我们不直接抛弃这一种选择呢?是因为他可能会影响下一个物品的选择结果,举个例子:给你5个大小的体积,2个物品,第一个物品是2的大小4的价值,第二个物品是4的大小7的价值,如果我们抛弃一个物品都不选的情况,那么选完第一个物品后,我们就只有a[ 1 ][ 2 ]=4这一种状态,那么对于第2个物品我们就只能选择不选,所以最终结果为a[ 2 ][ 2 ]=4,而实际上,我们正确的选择后,我们应该有a[ 2 ][ 4 ]=7,即不选第一个物品只选第二个物品这种情况

接下来我们用代码实现上述的过程,代码如下:

#include<stdio.h>
#include<algorithm>
using namespace std;
typedef long long LL;
LL a[50][1010];
int main()
{
	int m, n;
	scanf("%d%d", &m, &n);
	LL v[m+1], w[m+1];
	for(int i=1;i<=m;i++)
		scanf("%lld%lld", &v[i], &w[i]);
	a[0][0]=-1;		//我们将选0个物品占0的体积标记为-1,是为了便于后面我们选某个物品时,看他是不是从之前一个都没选的状态转移而来
	for(int i=1;i<=m;i++)
	{
		for(int j=0;j<=n;j++)
		{
			if(a[i-1][j]==-1)
			{
				a[i][j]=a[i-1][j];
				if(j+v[i]<=n)
					a[i][j+v[i]]=max(a[i][j+v[i]], w[i]);
			}
			else if(a[i-1][j]>0)
			{
				a[i][j]=max(a[i][j], a[i-1][j]);
				if(j+v[i]<=n)
					a[i][j+v[i]]=max(a[i][j+v[i]], a[i-1][j]+w[i]);
			}
		}
	}
	for(int i=0;i<=m;i++)
	{
		for(int j=0;j<=n;j++)
		{
			printf("%lld ", a[i][j]);
		}
		printf("\n");
	}
	LL maxx=-1;
	for(int j=0;j<=n;j++)
		maxx=max(maxx, a[m][j]);
	printf("%lld", maxx);
	return 0;
}

 运行过程是这样的

4 60    //输入
60 60
1 2
6 7
5 8
-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
-1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
-1 2 0 0 0 0 7 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
-1 2 0 0 0 8 10 9 0 0 0 15 17 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
60    //输出

 对于上述的整个过程,虽然思路很清晰,但代码却不够简洁,接下来我们看看平常看到的DP公式是如何来的,对于上述思路,我们是基于前一个选择后所以合法的状态才进行转移,如此我们便有两个判断,一个是判断当前体积在上一个物品选后是否存在一种状态,一个是判断选后是否合法。当然这两个并不是弊端,其弊端在于如果我们要知道最后的答案便需要对最后一行进行遍历,当然我们可以在转移过程中就进行判断,不过依然会多花时间。

于是乎我们便有了这样的思路:对于每一个位置我们都进行转移,只要合法就转移,于是我们就有了这样的代码,代码如下:

#include<stdio.h>
#include<algorithm>
using namespace std;
typedef long long LL;
LL a[50][1010];
int main()
{
	int m, n;
	scanf("%d%d", &m, &n);
	LL v[m+1], w[m+1];
	for(int i=1;i<=m;i++)
		scanf("%lld%lld", &v[i], &w[i]);
	//a[0][0]=-1;		这里我们不需要标记,因为每一个位置我们都会进行转移
	for(int i=1;i<=m;i++)
	{
		for(int j=0;j<=n;j++)
		{
			a[i][j]=a[i-1][j];
			if(j-v[i]>=0)
				a[i][j]=max(a[i][j], a[i-1][j-v[i]]+w[i]);
		}
	}
	printf("%lld", a[m][n]);
	return 0;
}

其运行过程是这样的:

4 60    //输入
60 60
1 2
6 7
5 8
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 60
0 2 2 2 2 2 7 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 60
0 2 2 2 2 8 10 10 10 10 10 15 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 60
60    //输出

滚动DP

通过上面的代码我们会发现,其实影响当前一个选择的所有结果的因素一定是选完上一次后的所有状态,即和上一次选择之前的所有状态已经无关了,换句话说就是上一次选完后的所有状态已经包含是上一次选择之前的所有状态的所有合法可能,于是乎我们就只需要保留两行数组,让其交换着转移,如此便是滚动DP了,这样做可以极大的降低空间复杂度。

代码如下:

#include<stdio.h>
#include<algorithm>
using namespace std;
typedef long long LL;
LL a[2][1010];
int main()
{
	int m, n;
	scanf("%d%d", &m, &n);
	LL v[m+1], w[m+1];
	for(int i=1;i<=m;i++)
		scanf("%lld%lld", &v[i], &w[i]);
	//a[0][0]=-1;		这里我们不需要标记,因为每一个位置我们都会进行转移
	for(int i=1;i<=m;i++)
	{
		for(int j=0;j<=n;j++)
		{
			a[i%2][j]=a[(i-1)%2][j];	//我们通过余2来判断当前转移的是第奇数次选择还是第偶数次选择,而第奇数次选择是由第偶数次转移而来,第偶数次选择是由第奇数次转移而来
			if(j-v[i]>=0)
				a[i%2][j]=max(a[i%2][j], a[(i-1)%2][j-v[i]]+w[i]);
		}
	}
	printf("%lld", a[m%2][n]);
	return 0;
}

 其运行过程是这样的:

4 60    //输入
60 60
1 2
6 7
5 8
第1次转移后的所有过程:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
第2次转移后的所有过程:
0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 60
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
第3次转移后的所有过程:
0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 60
0 2 2 2 2 2 7 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 60
第4次转移后的所有过程:
0 2 2 2 2 8 10 10 10 10 10 15 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 60
0 2 2 2 2 2 7 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 60
60    //输出

就地滚动

就地滚动也是对空间复杂度的优化方案,他可以直接把二维数组优化为一维。因为我们可以这样考虑,其实每次状态的转移都是从上一个的当前位置 j 和 j-v[i] 两个位置转移而来,那么我们其实只需要比较当前的 j 和 由 j-v[i]转移来的两价值的大小就行了。但我们需要注意的是,如果我们从前往后遍历,那么我们的每种物品就可能会被选择很多次(选很多次,这样就变成完全背包了,待会讲),所以我们需要从后往前遍历,即如下面代码所展示的一般

代码如下:

#include<stdio.h>
#include<algorithm>
using namespace std;
typedef long long LL;
LL a[1010];
int main()
{
	int m, n;
	scanf("%d%d", &m, &n);
	LL v[m+1], w[m+1];
	for(int i=1;i<=m;i++)
		scanf("%lld%lld", &v[i], &w[i]);
	//a[0][0]=-1;		这里我们不需要标记,因为每一个位置我们都会进行转移
	for(int i=1;i<=m;i++)
	{
		for(int j=n;j>=v[i];j--)	//因为我们是从后往前遍历,所以当j-v[i]<0时就已经不能转移了,所以到v[i]就结束进行下一次选择了
		{
			a[j]=max(a[j], a[j-v[i]]+w[i]);
		}
	}
	printf("%lld", a[n]);
	return 0;
}

其运行过程是这样的:

4 60    //输入
60 60
1 2
6 7
5 8
第1次转移后的过程:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
第2次转移后的过程:
0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 60
第3次转移后的过程:
0 2 2 2 2 2 7 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 60
第4次转移后的过程:
0 2 2 2 2 8 10 10 10 10 10 15 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 60
60    //输出

完全背包

这类问题主要类似于 给你n个大小的体积,再给你m个物品,每个物品都有他对应的体积vi和价值wi, 每个物品都可以选任意多次,然后让你去求选完物品后的最大总价值,或者让总价值最大的选法。

刚刚上面也有提到,把01背包的就地滚动,从前往后遍历就可以实现完全背包问题,我就直接给出代码了。

代码如下:

#include<stdio.h>
#include<algorithm>
using namespace std;
typedef long long LL;
LL a[1010];
int main()
{
	int m, n;
	scanf("%d%d", &m, &n);
	LL v[m+1], w[m+1];
	for(int i=1;i<=m;i++)
		scanf("%lld%lld", &v[i], &w[i]);
	for(int i=1;i<=m;i++)
	{
		for(int j=v[i];j<=n;j++)	//因为我们是从前往后遍历,所以我们直接从v[i]开始
		{
			a[j]=max(a[j], a[j-v[i]]+w[i]);
		}
	}
	printf("%lld", a[n]);
	return 0;
}

 举一组数据

4 60
60 60
6 7
1 2
5 8

其运行过程是这样的:

4 60    //输入
60 60
6 7
1 2
5 8
第1次转移后的过程:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 60
第2次转移后的过程:
0 0 0 0 0 0 7 7 7 7 7 7 14 14 14 14 14 14 21 21 21 21 21 21 28 28 28 28 28 28 35 35 35 35 35 35 42 42 42 42 42 42 49 49 49 49 49 49 56 56 56 56 56 56 63 63 63 63 63 63 70
第3次转移后的过程:
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100 102 104 106 108 110 112 114 116 118 120
第4次转移后的过程:
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100 102 104 106 108 110 112 114 116 118 120
120    //输出

多重背包

补充知识

1.快速输入/输出

我们为什么需要学习快速输入/输出,首先,我们应该知道cin/cout这种流输出输出是没有scanf/printf快的,其原因在于流为了同步stdio会有一个缓冲区,如果我们关闭缓冲区,那么流的输入输出据说甚至比stdio快(为什么我说据说呢?因为我自己没去测过)即使是我们用关闭缓冲区的流,或者直接用stdio,在某些输入输出数据特别特别多的时候,依然会因为输入输出太慢的而超时。在遇到这种情况的时候,我们就需要快速输入输出了。

下面我们先来看看实现快速输入输出的代码

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值