回溯算法与动态规划基于背包问题的浅析

    经典问题描述:有n 个物品,它们有各自的重量,现有给定容量w的背包,如何让背包里装入的物品具有最大的重量?


                                                                              算法解背包问题

    假设我们有五个物品,重量分别为2,3,3,7,5,背包所能容纳的最大重量为10.怎么解呢?最笨的方法就是使用回溯算法穷举所有的情况,直到遇到临界情况,递归树如下:

    

       其中每个节点f(i,cw),代表决策第i个物品是否放入,并且此时背包里所有物品总重为cw。因此,回朔算法的时间复杂度成指数增长,并且包含了大量的重复运算(比如两个f(2,2))。如果我们的物品i遍历完毕,或者cw超出背包的总重,即临界退出条件,因此我们可以给出回溯算法的代码(物品重量用数组items表示,w代表背包容量,maxW存储当前最大重量,这些自己设置):

void knapsackProblem::backTracing(int i, int cw)
{
	if (i == n || cw == w) {//i==n代表所有物品加载完毕,cw==w代表背包填满,则直接跳出
		if (cw > maxW) maxW = cw;
		return;
	}
	//装与不装第i个物品两种情况
	backTracing(i + 1, cw);//选择不装第i个物品
	if (cw + items[i] <= w) {//如果第i个物品能放进去,就选择装第i个物品
		backTracing(i + 1, cw + items[i]);
	}
}

       可以看出,回溯算法对决策时的每种情况都进行了分析,时间复杂度为2^n,这实在太多庞大(比如n为10000时)。我们分析复杂度如此高的原因是回朔算法进行了大量重复的运算,如果我们能设置一个备忘录来避免这些重复的运算,那岂不是妙哉?既然每个节点的两个参数是物品次序和当前背包重量,我们可以采用一个bool类型的二维数组来记忆是否已经进行了相关运算。

    bool** mem = new bool*[n];
	for (int i = 0; i != n; ++i) {
		mem[i] = new bool[w + 1];
	}

    for (int i = 0; i != n; ++i) {
		for (int j = 0; j != w + 1; ++j) {
			mem[i][j] = false;
		}
	}

然后给出加载了备忘录的回溯算法:

void knapsackProblem::backTracing(int i, int cw)
{
	if (i == n || cw == w) {//i==n代表所有物品加载完毕,cw==w代表背包填满,则直接跳出
		if (cw > maxW) maxW = cw;
		return;
	}
	if (mem[i][cw]) return;//如果这个节点运算过,则直接返回
	mem[i][cw] = true;//否则将这个节点的运算状态设为true
	//装与不装第i个物品两种情况
	backTracing(i + 1, cw);//选择不装第i个物品
	if (cw + items[i] <= w) {//如果第i个物品能放进去,就选择装第i个物品
		backTracing(i + 1, cw + items[i]);
	}
}

         这样就避免了那些复杂的运算,用空间换来了时间,这其实就有点类似于动态规划的思想方法了。但是回溯算法尚且有局限性(比如节点有三个参数或者四个参数时很难使用备忘录),接下来我们看看动态规划如何实现背包问题。


                                                                         动态规划解背包问题

       其实上面带有备忘录的回朔算法已经十分接近动态规划了,动态规划中我们通过上一层的状态来决策下一层的状态,并且只统计那些不重复的节点,例如我们由f(1,0)和f(1,2)推出f(2,0),f(2,2),f(2,5).这样可以保证每层的节点数永远不会超过cw的最大值,即w(背包容量).整个过程依然可以通过一个表格来进行表示:

           

                     第一次决策                                                                               第二次决策

          

                    第三次决策                                                                                 第四次决策

                    第五次决策

                                                    注意:2列应当置1

分析:第一次决策是否装第一个物品,其重量为2,所以决策完毕有总重0和2两种情况。

           第二次决策第二个物品,其重量为3,我们只需要从第一行分析即可,不装第二个物品时,摘抄第一行即可,装第二个物品时就分别加3.

           以此类推~~

决策完毕只需要取出最后一行对应的最大重量即可。

 

因此,动态规划算法也需要一个辅助的二维数组来存储节点状态。

    bool** states = new bool*[n];
	for (int i = 0; i != n; ++i) {
		states[i] = new bool[w + 1];
	}
	for (int i = 0; i != n; ++i) {
		for (int j = 0; j != w + 1; ++j) {
			states[i][j] = false;
		}
	}
    //第一行需要特殊处理,设置为true;
	states[0][0] = true;
	states[0][items[0]] = true;

因此动态规划的代码如下,不是很复杂:

int knapsackProblem::dynamicProgramming(int * items, int n, int w)
{
	for (int i = 1; i != n; ++i) {
		//不装第i个物品
		for (int j = 0; j <= w; ++j) {
			if (states[i - 1][j] == true) states[i][j] = true;
		}
		//装第i个物品
		for (int j = 0; j <= w - items[i]; ++j) {
			if (states[i - 1][j] == true) states[i][j + items[i]] = true;
		}
	}
    //输出可装最大重量,即在最后一行从后往前遍历
	for (int i = w + 1; i >= 0; --i) {
		if (states[n - 1][i] == true)return i;
	}
	return 0;
}

此算法的时间复杂度为n*w,空间复杂度也为n*w。

有没有办法对空间复杂度进行改进呢,答案是有的,因为此二维数组有很多重复的地方,我们完全可以用一维数组代替。

int knapsackProblem::dynamicProgramming1(int * items, int n, int w)
{
	bool* states = new bool[w + 1];
	for (int i = 0; i != w + 1; ++i) {
		states[i] = false;
	}
	states[0] = true;
	for (int i = 1; i != n; ++i) {
		//因为如果不装第i个物品,此一维数组不会有任何变动,所以我们直接考虑装第i个物品
		for (int j = w - items[i]; j >= 0; --j) {
			//为什么要从大往小便利呢?因为下面的循环体是前值影响后值
			if (states[j] == true) states[j + items[i]] = true;
		}
	}
	for (int i = w; i >= 0; --i) {
		if (states[i] == true)return i;
	}
	return 0;
}

                                                                           背包问题的升级

       对于上面的背包问题已经解决,可如果再增加一个限制条件呢?如:刚刚的问题只涉及背包容量和物品重量,可如果我们引入物品价值这一变量,对一组不同重量不同价值的物品,我们选择一些物品加入背包,在不超出背包最大容量的限制下,可装入背包物品的总价值最大是多少呢?

        当然,我们依然可以使用笨重的回溯算法来解决这个问题,此时每个节点就有了三个参数分别为i(物品),cw(当前总重量),cv(当前总价值),具体实现代码如下:

void knapsackProblem::backTracing(int i, int cw, int cv)
{
	if (i == n || cw == w) {//i==n代表所有物品加载完毕,cw==w代表背包填满,则直接跳出
		if (cw > maxW) maxW = cw;
		return;
	}
	//装与不装第i个物品两种情况
	backTracing(i + 1, cw, cv);//选择不装第i个物品
	if (cw + items[i] <= w) {//如果第i个物品能放进去,就选择装第i个物品
		backTracing(i + 1, cw + items[i], cv + value[i]);
	}
}

假设物品价值为 int* value ={3,7,5,2,6},照样画出递归树示意图如下(每个节点代表物品,目前总重,目前总价):

       我们可以发现虽然暂时没见到重复的节点,可是对比f(2,2,7)和f(2,2,3),相同重量必然选择较高价值的,因此f(2,2,3)可以直接忽略不计。可是这又很难用备忘录来实现,那么动态规划如何解决这个问题呢?

        依然借助二维数组,不过此时二维数不再用bool来表示,而用int代表每个状态的value值,初始化为-1.如下:

v_states = new int*[n];
	for (int i = 0; i != n; ++i) {
		v_states[i] = new int[w + 1];
	}
	for (int i = 0; i != n; ++i) {
		for (int j = 0; j != w + 1; ++j) {
			v_states[i][j] = -1;
		}
	}
	states[0][0] = 0;
	states[0][items[0]] = value[0];

动态规划本身则和之前的算法大同小异,具体如下:

int knapsackProblem::dynamicProgramming(int * items, int * value, int n, int w)
{
	for (int i = 1; i != n; ++i) {
		//选择不装第i个物品
		for (int j = 0; j != w; ++j) {
			if (v_states[i - 1][j] >= 0) v_states[i][j] = v_states[i - 1][j];
		}
		//选择装第i个物品
		for (int j = 0; j <= w - items[i]; ++j) {
			if (v_states[i - 1][j] >= 0) {
				int v = v_states[i - 1][j] + value[i];
				if (v > v_states[i][j + items[i]]) {
					v_states[i][j + items[i]] = v;
				}
			}
		}
	}
	//寻找最大值
	int maxV;
	for (int j = 0; j <= w; ++j) {
		if (v_states[n - 1][j] > maxV) maxV = v_states[n - 1][j];
	}
	return maxV;
}

当然,此种方法也可以优化一下空间复杂度,留给读者自己分析。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值