0-1背包问题——暴力搜索,回溯法和剪枝限界

刷题笔记

  • 动态规划0-1背包问题
      • 回溯法和剪枝限界

动态规划0-1背包问题

回溯法和剪枝限界

运用回溯法解题通常包含以下三个步骤:

1、针对所给问题,定义问题的解空间;
2、确定易于搜索的解空间树;
3、以深度优先的方式搜索解空间树,并在搜索过程中用剪枝函数避免无效搜索

0-1背包问题,对于每一个物品只有两种决策,一种是该物品装入背包,x[i] = 1,另一种是该物品不装入背包,x[i] = 0。所以对于物品编号1-n所形成的解空间树的高度是n+1(包含叶子节点)。叶子节点最下面一层节点的数量是2^n,表示该问题的所有可能的解的数量。因为对于每一件物品的决策只有两种,用二叉树来构建解空间,设二叉树的节点i向左深度搜索表示选择编号为i的物品,x[i] = 1,向右深
度搜索表示不选择编号为i的物品,x[i] = 0。

首先需要确定深度搜索的返回条件,即 terminate condition,当到达叶子节点时,说明我们已经得到了一组解向量的结果x[1……n]的结果,所以当搜索到叶子节点时就需要return。当然,我们需要记录背包中能装入重量的最大值,即记录一下bestValue。

	void backTracking(int i) {
		if (i > n) {
			if (curValue > bestValue) {
				bestValue = curValue;
			}
			return;
		}
	}

那么考虑以下情况,在向左深度搜索,选择物品i装入背包时,应该考虑该物品装入背包是否能够装下的问题,如果背包当前的重量curWeight加上该物品的重量weight[i]的重量小于背包的容量capacity才能向左子树走,不然就说明该物品不能装下,递归函数不会向下再深搜了,相当于进行了剪枝操作。所以在搜索过程中需要用约束函数判断左子树是否可行。并且在选择将物品i加入背包中时,需要更新当前背包中的重量和价值,然后当回溯回退出来后,表示该节点往下左子树的情况已经处理完了,要恢复当前背包中的重量和价值为以前未加入物品i的状态,然后对该节点的右子树又进行深搜。

if (curWeight + weight[i] <= capacity) {    //约束条件
	x[i] = 1;
	curWeight += weight[i];
	curValue += value[i];
	backTracking(i + 1);
	curWeight -= weight[i];        //输出叶节点后回溯,直到前一个x[i] = 1祖先节点处,先恢复节点状态,再探索其右子树
	curValue -= value[i];
}

然后考虑右子树的情况,对于右子树而言,x[i] = 0,表示物品i不会加入背包,那么对于右子树而言肯定是满足约束条件的,因为物品i不会加入背包,背包中的重量不会增加。但是,这里对于右子树的处理有一个优化,就是采用了限界函数来判断一下,如果我不装入物品i之后,我的背包里面装满能够装下的最大价值是多少。设置这个限界函数的目的还是为了避免无效搜索,举个例子,比如在之前的搜索过程中,我们得到了一些解空间的解,也就是背包中的装物品的选择策略。假设,select 1 total value = 55, select 2 total value = 60, select 3 total value = 50。然后当你在深搜解空间时,遇到节点i处向右转,所以先判断一下当前不装入物品i,装入其他物品(i+1,……,n)把背包装满所得到的价值上界是多少,如果我们计算出的价值上界是58,那么我们就没有必要再深搜下去了,因为前面已经得到的解中best value = 60,而这一条路径上限才是58,也相当于剪枝了。所以我们要用界限函数考察右子树是否有可能最优,如果上界有可能超过当前的best value select,才有继续搜索下去的必要。

if (Bound(i + 1) > bestValue) {   //限界函数
	x[i] = 0;
	backTracking(i + 1);          //右子树搜索完毕后回溯,直到前一个x[i]=1祖先结点处,搜索其右子树		
}

下面考虑一下限界函数bound()如何生成,限界函数的目的是为了判断不选择物品i时,用背包剩余的空间装其他物品,能够得到的最大的价值。假设背包现在剩余的重量是5kg,那么肯定装入的物品value[i] / weight[i] 越高越好,比如物品1的value[1] / weight[1] = 2rmb/kg,物品1的weight[1] = 2。物品2的value[2] / weight[2] = 1rmb/kg,物品2的weight[2] = 2。物品3的value[3] / weight[3] = 1.5rmb/kg,物品3的weight[3] = 2。背包的重量只剩了5kg,所以当下最优的选择就是把物品1装入进去,此时背包的重量剩下了3kg,然后下一步肯定装入物品3,背包重量剩下了1kg,最后才装入物品2,不过这个时候剩余空间已经不足以装入物品2了,但是因为我们求的是上界限,就是理想情况下背包装满能够装入的最大价值。那这里就选择把物品2装入一半把背包塞满,计算价值上界。
涉及到的排序函数,为了方便期间,我们提前根据value[i] / weight[i]对value[]和weight[]进行从大到小的排序,使得排在前面的物品编号单位重量的价值最高。(贪心思想)

	void sortByPerWeightValue() {
		vector<double> perWgtVal(n + 1, 0);
		for (int i = 1; i <= n; i++) {
			perWgtVal[i] = (double)value[i] / (double)weight[i];
		}
		for (int i = 1; i <= n - 1; i++) {
			for (int j = n; j > i; j--) {
				if (perWgtVal[j] > perWgtVal[j - 1]) {
					swap(perWgtVal[j], perWgtVal[j - 1]);
					swap(weight[j], weight[j]);
					swap(value[j], value[j]);
				}
			}
		}
	}

之前已经解释过了,限界函数如下:

	double Bound(int i) {
		double surplusWeight = capacity - curWeight;
		double tmpValue = curValue;
		while (i <= n && weight[i] <= surplusWeight) {
			surplusWeight -= weight[i];
			tmpValue += value[i];
			i++;
		}
		if (i <= n) {  //说明物品没装完但背包容量装不下下一个物品了
			tmpValue += (double)value[i] / (double)weight[i] * surplusWeight;
		}
		return tmpValue;
	}

完整的代码如下:

class knapsackone
{
public:
	void sortByPerWeightValue() {
		vector<double> perWgtVal(n + 1, 0);
		for (int i = 1; i <= n; i++) {
			perWgtVal[i] = (double)value[i] / (double)weight[i];
		}
		for (int i = 1; i <= n - 1; i++) {
			for (int j = n; j > i; j--) {
				if (perWgtVal[j] > perWgtVal[j - 1]) {
					swap(perWgtVal[j], perWgtVal[j - 1]);
					swap(weight[j], weight[j]);
					swap(value[j], value[j]);
				}
			}
		}
	}

	double Bound(int i) {
		double surplusWeight = capacity - curWeight;
		double tmpValue = curValue;
		while (i <= n && weight[i] <= surplusWeight) {
			surplusWeight -= weight[i];
			tmpValue += value[i];
			i++;
		}
		if (i <= n) {  //说明物品没装完但背包容量装不下下一个物品了
			tmpValue += (double)value[i] / (double)weight[i] * surplusWeight;
		}
		return tmpValue;
	}

	void backTracking(int i) {
		if (i > n) {
			if (curValue > bestValue) {
				bestValue = curValue;
			}
			return;
		}
		if (curWeight + weight[i] <= capacity) {
			x[i] = 1;
			curWeight += weight[i];
			curValue += value[i];
			backTracking(i + 1);
			curWeight -= weight[i];        //输出叶节点后回溯,直到前一个x[i] = 1祖先节点处,先恢复节点状态,再探索其右子树
			curValue -= value[i];
		}
		if (Bound(i + 1) > bestValue) {
			x[i] = 0;
			backTracking(i + 1);          //右子树搜索完毕后回溯,直到前一个x[i]=1祖先结点处,搜索其右子树		
		}
	}

	void initialFunc() {
		weight = { 0, 5, 15, 25, 27, 30 };
		value = { 0, 12, 30, 44, 46, 50 };
		x = { 0, 0, 0, 0, 0, 0 };
		n = 5;
		curWeight = 0;
		curValue = 0;
		bestValue = 0;
		capacity = 50;
		
	}

	int knapsack() {
		initialFunc();
		sortByPerWeightValue();
		backTracking(1);
		return bestValue;
	}


private:
	vector<int> weight;  //物品重量数组
	vector<int> value;   //物品价值数组
	vector<int> x;       //x[i] == 1表示第i个物品放入背包, x[i] == 0表示第i个物品不放入背包
	int n;               //物品数量
	int curWeight;       //当前重量
	int curValue;        //当前价值
	int bestValue;       //当前最优价值
	int capacity;        //背包容量
};

int main()
{
	knapsackone S;
	int value = S.knapsack();
	return 0;
}

补充:

贪心算法的特点是每个阶段所作的选择都是局部最优的,它期望通过所作的局部最优选择产生出一个全局最优解。

动态规划:每个阶段产生的都是全局最优解,第i阶段的“全局”:问题解空间为(a1,a2,……,ai)。第i阶段的“全局最优解”:问题空间为(a1,a2,……,ai)时的最优解(连续空间)

贪心:每个阶段产生的都是局部最优解。第i个阶段的“局部”:问题空间为按照贪心策略中的优先级排好序的第i个输入ai。第i个阶段的“局部最优解”:ai(离散空间)

在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择。而在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择,然后再去解做出这个选择后产生的相应的子问题。

你可能感兴趣的:(数据结构与算法刷题专栏,动态规划,剪枝,算法)