对背包问题的理解(小白文)

前言:

这是自己对动态规划入门题背包问题的理解。

先来看一道不是01背包的问题:

描述 

你有一个神奇的背包,他的容积是m(0装满他,你才能拿走他,现在给你n(1<=n<=20)个物品Xi(Xi<=m),那么一共有几种方式,可以让你拿走背包?

 输入:

第一行 n,m

第二行 n个数字

输出: 

 输出方案数

样例: 

 输入:

3 40

20 20 20

输出:

3

分析 :

从题目可以看出来,是一道非常类似于01背包的问题:每个物品只有一个,问要把背包放满有多少种方法。因为很像01背包的问题,所以自然就联想到了01背包里的动态规划做法。既然谈到了动态规划,那首先要做的就是找出状态转移方程。

试着模仿了下01背包的思路:对每个物品,每个容量的背包遍历。(两个for循环)

 

 首先,像01背包那样,我们先定义:f[ i ] [ j ]为放到第 i 个物品时,放满容量为 j 的背包的方法有几种。f[0][1 ~ v] = 0意思就是我现在有0个物品,我用这0个物品去放 1~v的背包,结果肯定是0种。相对的,f[ 1 ~ n ][ 0 ]意思是,对于容量为0的背包,无论我有几个物品,都不可能放满(没办法放),所以是0种。

i 第 i 个物品 ,n 总共有n个物品,w,保存物品体积的数组,m 背包容量

上面是初始化。然后我们现在来逐个分析从而退出我们的状态转移方程。

首先,考虑对第 i 个物品,其体积为 w[ i ],加入我们要把他放进容量 为 j 的背包,我们可能要考虑三种情况:

1.如果 j < w[ i ],即 如果背包就放不下这个物品的时候,那么f[ i ][ j ]的值是不是应该等于 f[ i - 1][ j ]的值呢?即 对于 j ,如果我放不下,那么说明 对于 放满 j 背包的种数,我这个w[ i ]是帮不上忙,造不成影响,那么 f[ i ][ j ]应该等于什么?这时我们想到,如果我们在考虑放 i 的前面,即i - 1(我现在有i - 1个物品,然后去放 j)时,已经可以放满这个背包了,那么f[ i ][ j ]的值至少也应该是 f[i - 1][ j ]把。(不能说我有i - 1个物品去放 j,然后假设有两种,然后我再拿一个物品,我现在有 i 个物品,再去放 j ,结果难道会比 我只有 i - 1 时的小么?)想通了这点我们就可以得出一种情况:

 j < w[ i ]时,f[ i ][ j ] = f[i - 1][ j ]

2.考虑 j = w[ i ],如果 背包的容量 j 恰好等于我这个物品的体积,那不管前面如何,只放我自己(i)肯定可以放满把~~~             于是 首先f[ i ][ j ] += 1。但是,我们同时也想到,如果 i - 1时,我就已经有放满这个背包的方法了,那我现在 是 i,是不是应该也要加上 i - 1时的值呢?答案是肯定的,于是我们又得出一种情况:

 j = w[ i ]时,f[ i ][ j ] = f[ i - 1][ j ] + 1

3.最后只剩下一种情况:j > w[ i ]。如果 j > w[ i ],我们先这么想,假设这个背包的容量 是 8,我现在要考虑的物品 i 的体积w[ i ]是5.我现在考虑的是,如何在添加了我这个5之后,正好放满8。那,我们应该很容易想到:要用5放满8,我们是不是要去看看       8 - 5,即背包容量为3时,我们能把3放满的方法有几种呢?假设,i - 1 时我现在能把 3 放满的方法有 2种(f[ i - 1][ 3 ] = 2),那么回到 容量为 8 的背包,我们可以知道,这个背包里已经有体积为3 的物品了(且有2种),那么我现在把5放进去,是不是8就刚好满了呢?满了是满了,那这个值应该是什么?那么繁琐的话在说一遍,我现在有2  中方法把 3 放满,那么我用5放8,其值应该还是2 加上 i - 1时就可以放满8的方法,即

j > w[ i ]时,f[ i ][ j ] = f[ i - 1][ j  - w[i] ] + f[ i - 1][ j ]。

最后我们要求的值就是 f[ n ][ m ],这个不用我解释了把~~~

现在我们已经将可能出现的3中情况全部讨论完毕,那么就要该想代码该怎么写了。

代码:

#include
using namespace std;

int main()
{
	int n, m;                               //n 物品数量 m 背包容量
	cin >> n >> m;  
	int** v = new int*[n + 1];             //v 存放每种情况的方法
	for (int i = 0; i <= n; i++)
	{
		v[i] = new int[m + 1];
	}
	int* w = new int[n + 1];               //存放物品的体积
	for (int i = 0; i < n; i++)
	{
		cin >> w[i];
	}

	for (int i = 0; i <= n; i++)              //开始全部方法都为1(其实只需要初始化第一行和第一列)
	{
		for (int j = 0; j <= m; j++)
		{
			v[i][j] = 0;
		}
	}
	//------------------------------------------------------------以上是初始化------------------------------------------------------------------------------
	

	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++)         //从1开始遍历,将上一个物品的结果转移到这一层,因为统计的不是最优,而是所有种类
		{                                   //要想从w[i - 1]开始遍历,则需要在 获取一个值之后,用这个值更新其所在列,其所在层一下的所有值
				
			if (j < w[i - 1])                         //当j < w[i - 1]时,其值为放i - 1时的值
			{
				v[i][j] = v[i - 1][j];
			}
			else if (j == w[i - 1])               //当 j = w[i - 1]时,其值为i - 1时的值加一 (只放该物品即可放满,故直接加一种)
			{
				v[i][j] = v[i - 1][j] + 1;
			}
			else                               //当 j > w[i - 1]时,值为i - 1层时 j - w[i - 1]容量(刚好可以放进i)的值(种类)加上 上一层j容量的值
				v[i][j] = v[i - 1][j] + v[i - 1][j - w[i - 1]];
		}
	}

	cout << v[n][m] << endl ;


	//下面代码用来看最后v数组的结构的,可以试着推演一遍
	for (int i = 0; i <= n; i++)
	{
		for (int j = 0; j <= m; j++)
		{
			cout << v[i][j] << " ";
		}
		cout << endl;
	}
	cout << endl;
	
	system("pause");
	
}

当然,这个也可以在空间上 将 v数组从二维优化成一维的。

如果你已经看懂了上面我所讲的,那么你应该会发现,每个f[ i ][ j ]的值,都是由上一层 f[ i - 1][ j ]的值推出来的,而不是i - 2,i - 3什么的。这两个有什么区别?f[ i ][ j ] 是我拿 i 个物品,去放 容量为 j 的背包,而 f[i - 1][ j ]是我拿 i - 1个物品。即 f[ i - 1] [ j ]时还没有放 i 的时候的值(不废话??)。可以有点唠叨,但如果你已经觉得我唠叨了的话,说明你已经理解了我上面的话。

那么如何优化成一维的?

假设我现在用v[ j ]去表示 j,如果我们上面推出来的公式不变,只是把 i 和i - 1去掉(比如 v[j] = v[j - w[i - 1]] + v[j] ),那么我么只要保证 等号右边的 v[ j ]的值是 i - 1 时的值(即还没放 i)就可以了。

我们现在理一下逻辑:当我去 更新 v[ j ]的值时,等号左边的v[ j ]是还没有更新,等待更新的值。而此时等号右边的v[ j ]是已经有值,其值是在上一次,也就是 更新 i - 1层时的结果。看到这,你会发现,我们只是简单把[i - 1]去掉就好了,他们的关系依然如二维数组时的一样。即 完全没有必要将每一层的值记录下来,我们只需要知道 i - 1 层的值就好了。

优化后的代码是这样的

	//转换为一维数组,只要是从上往下遍历保证M[j]的值是M[i - 1][j]的值就好了
	for (int i = 1; i <= n; i++)
	{
		for (int j = m; j >= 1; j--)
		{
			if (j == w[i - 1])
			{
				v[j] = v[j] + 1;
			}
			else if (j < w[i - 1])
			{
				v[j] = v[j];
			}
			else
				v[j] = v[j - w[i - 1]] + v[j];
		}
	}

观察后,你会发现 只有 j 的遍历方式 从 1 - n 变成了 n - 1。

这一步是必要的。

假设我们从 1 - n遍历,比如 我现在是要用 3 放 8。我们从1开始遍历。在遍历到 大于5 的背包之前,我们将 v[ 1 - 3]的值根据上一层存下来的值更新了下来。然后,j = 3,我们将 v[ 3 ] + 1,然后当我们遍历到 6 的时候,我们已经知道了我们要去 根据 v[ 3 ]的值来更新 v[ 6 ],但你会发现,此时v[ 3 ]的值已经不是 i - 1时的值了,我们在 j = 3的时候更新过他,给他加了1,现在 v[ 3 ]的值已经是 i 层的了(相当于我已经放过这个 i 物品了)。看到这,你会发现,从 1 - n的遍历方式并不行得通。

然后我们考虑 n - 1的遍历方式。

我们现在会在遍历到3之前先遍历到6,我们去更新v[ 6 ]的值,然后去找这时v[ 3 ]的值,这时我们还没有遍历到v[ 3 ],即v[ 3 ]的值还没有被更新,他还是 i - 1层的,那个我们想要的值。

看到这里,我想你应该就明白为什么 n-1遍历才行了。当然二维数组遍历的话是可以从1 - n的,你可以想一下为什么。

01背包

01背包,网上的代码有很多,这里我就不放了。但是通过刚才的背包问题,我们在回过头看01背包的那个转移方程:

二维:    M[i][j] = M[i - 1][j] > M[i - 1][j - W[i - 1]] + V[i - 1] ? M[i - 1][j] : M[i - 1][j - W[i - 1]] + V[i - 1]

一维:    M[j] = M[j] > M[j - w[ i ] ] + v[i] ? M[j] : M[j - w[ i ]] + v[ i ]

现在看这个方程,你会不会觉得更好理解些了呢?

完全背包

完全背包就是每个物品都有无限个。我们可以简单的将他转化为01背包,即每个物品无限个转化为 最多 j -w[i] 个 体积为w[ i ]的单个物品。然后还是01背包的做法。我们当然可以这么做,但是这样时间复杂度会比较大,更优化的方式是这样的:

我们只需要简单的 将一维数组的01背包中的 对 j的遍历方式从n - 1 改成 1 - n就好了。

你可能会要问了,为什么改回来就好了?那让我们再来分析一遍。

在01背包时,我们将 1 - n 改成 n - 1,是因为 1- n 遍历时,我们去取 v[ j - w[ i ]]的值,这个值可能已经在 i 层就已经被更新了。没错,背包里,这个值可能是已经放过一个 i 的背包了。也就是说这个值已经是 v[ i ][ j ]了。重新用 将3 放 8 的例子来讲。

1 - n 遍历。现在我们已经将v[ 3 ]更新过(假设v[ 3 ]的值比v[ 0 ]的值大(当然啦喂ヽ(●-`Д´-)ノ)),那么在01背包里,我们就已将v[ 3 ]的值设置为 放了一个w 为3 的物品,然后遍历到6了,我们回去找v[ 3 ]的值,并问,是你本身大,还是你加上我更大,就是 M[ j ] > M[j - w[ i ] ] + v[ i ] ? M[j] : M[j - w[ i ]] + v[ i ]。。当然是加起来更大啦。于是又放了一个3。。嗯,这时我们就放了2个3了。就这样继续向n遍历,你懂得~~~

事实上,我们这样遍历到j,其实就相当于 v[ 1 ~ (j - 1)]就已经是可能放过一个 i 物品的状态了。自己推敲一遍,发现这段代码是真的巧--

理解到这里,感觉自己有那么一丢丢理解了动态规划的状态转移:说白了就是如果一个问题可以以类似递归的方式分解成一个个小问题的话,那么我们就可以找到这些递归的关系,用方程表示出来。而这个方程一般表示的是  i 层的值,可以由 i - 1层的值推出来。因此我们只要用一个数组抽象化地去记录 第 i - 1层的值,然后用他去更新 第 i 层的值就好了~~~

以上就是我目前的理解,如果有错还请大佬指正ヾ(・ω・` 。)    。。。。。

 

你可能感兴趣的:(对背包问题的理解(小白文))