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(离散空间)

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

下面为第二次练习重新写的代码:

class ZeroOneBag
{
public:
	void initialFunc() {
		weight = { 0, 5, 15, 25, 27, 30 };
		value = { 0, 12, 30, 44, 46, 50 };
		opt = { 0, 0, 0, 0, 0, 0 };
		n = 5;
		curWeight = 0;
		curValue = 0;
		bestValue = 0;
		capacity = 50;
	}
	void sortByPerWeightValue() {
		vector<double> PerWgtVal;
		PerWgtVal.push_back(0);
		for (int i = 1; i <= n; i++) {
			PerWgtVal.push_back((double)value[i] / (double)weight[i]);
		}
		for (int i = 1; i <= n; i++) {      //用简单选择排序进行处理
			int maxIdx = i;
			for (int j = i + 1; j <= n; j++) {
				if (PerWgtVal[j] > PerWgtVal[maxIdx]) {
					maxIdx = j;
				}
			}
			if (maxIdx != i) {
				swap(PerWgtVal[i], PerWgtVal[maxIdx]);
				swap(value[i], value[maxIdx]);
				swap(weight[i], weight[maxIdx]);
			}
		}
	}
	double Bound(int i) {  //返回从第i到第n个物品中,在满足背包容量的条件下,我们还能获得的最大价值上界
		int surplusWeight = capacity - curWeight;
		double tmpValue = curValue;
		while (i <= n && surplusWeight >= 0) {
			tmpValue += value[i];
			surplusWeight -= weight[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;
				result = opt;
			}
			return;
		}
		if (curWeight + weight[i] <= capacity) {  //将第i个物品放入背包,满足约束条件,解空间树向左走
			opt[i] = 1;
			curWeight += weight[i];
			curValue += value[i];
			backTracking(i + 1);        //递归的处理剩余的结果
			curValue -= value[i];       //状态重置
			curWeight -= weight[i];
		}
		if (Bound(i + 1) > bestValue) {    //将第i个物品不放入背包,对于剩余情况进行处理,这里的判断是剪枝
			opt[i] = 0;
			backTracking(i + 1);
		}
	}
	void GetBestValue() const {
		cout << "select index: ";
		for (int i = 1; i <= n; i++) {
			if (result[i] == 1) {
				cout << i << " ";
			}
		}
		cout << endl;
		cout << "bestValue: " << bestValue << endl;
	}
	void PrintCommodity() const {
		for (int i = 1; i <= n; i++) {
			cout << "index:" << i << " weight: " << weight[i] << " value: " << value[i] << endl;
		}
	}
private: 
	vector<int> weight;  //物品重量数组
	vector<int> value;   //物品价值数组
	vector<int> opt;     //opt[i] == 1表示第i个物品放入背包, opt[i] == 0表示第i个物品不放入背包
	vector<int> result;
	int n;               //物品数量,也对应于物品的编号,编号从1开始
	int curWeight;       //当前重量
	int curValue;        //当前价值
	int bestValue;       //当前最优价值
	int capacity;        //背包容量
};

int main()
{
	ZeroOneBag obj;
	obj.initialFunc();
	obj.PrintCommodity();
	obj.sortByPerWeightValue();
	cout << "after call sortByPerWeightValue() result:" << endl;
	obj.PrintCommodity();
	obj.backTracking(1);
	obj.GetBestValue();
	return 0;
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
问题分析: 0-1背包问题是一个经典的组合优化问题,要求在一个给定的背包容量下,选择若干个物品放入背包中,使得物品的总价值最大,并且每个物品只能选择放入或不放入背包中。 建模: 我们可以将物品表示为一个二元组(w, v),其中w表示物品的重量,v表示物品的价值。我们需要考虑以下几个问题: 1.如何选择物品放入背包中,使得价值最大? 2.如何在选择物品的过程中,保证不超过背包的容量? 3.如何回溯到上一个状态,寻找下一个可行解? 算法描述: 回溯算法: 1.初始化背包容量为0,从第一个物品开始遍历,每个物品有两种选择:放入或不放入背包中。 2.如果放入该物品后不超过背包容量,则将该物品的价值加入总价值中,并继续遍历下一个物品。 3.如果不放该物品,则直接跳过该物品,继续遍历下一个物品。 4.当遍历完所有物品后,保存当前的总价值,并回溯到上一个状态,寻找下一个可行解。 5.重复以上步骤,直到找到所有的可行解。 分支限界法: 1.将物品按照单位重量价值从大到小排序,并按照排序后的顺序遍历。 2.对于每个物品,有两种选择:放入或不放入背包中。分别计算放入和不放入的上界,选择上界更高的分支进行扩展。 3.如果上界小于当前最优解,则剪枝。 4.重复以上步骤,直到找到最优解或者所有分支都被剪枝。 C++算法实现: 回溯算法: ```cpp void backtrack(vector<int>& weights, vector<int>& values, int capacity, int cur_weight, int cur_value, int start, int& max_value) { if (cur_weight > capacity) return; // 如果超过背包容量,返回 if (start == weights.size()) { // 遍历完所有物品 max_value = max(max_value, cur_value); // 更新最大价值 return; } backtrack(weights, values, capacity, cur_weight + weights[start], cur_value + values[start], start + 1, max_value); // 放入该物品 backtrack(weights, values, capacity, cur_weight, cur_value, start + 1, max_value); // 不放该物品 } int knapsack(vector<int>& weights, vector<int>& values, int capacity) { int max_value = 0; backtrack(weights, values, capacity, 0, 0, 0, max_value); return max_value; } ``` 分支限界法: ```cpp struct Node { int level; // 当前扩展到的层数 int value; // 当前价值 int weight; // 当前重量 double bound; // 当前上界 }; struct cmp { bool operator() (const Node& a, const Node& b) { return a.bound < b.bound; } }; double knapsack(vector<int>& weights, vector<int>& values, int capacity) { int n = weights.size(); vector<int> indices(n); for (int i = 0; i < n; ++i) { indices[i] = i; } sort(indices.begin(), indices.end(), [&](int a, int b) { return (double)values[a] / weights[a] > (double)values[b] / weights[b]; }); // 按照单位重量价值从大到小排序 priority_queue<Node, vector<Node>, cmp> q; q.push({-1, 0, 0, 0}); // 将根节点入队 double max_value = 0; while (!q.empty()) { auto u = q.top(); q.pop(); if (u.bound < max_value) continue; // 如果上界小于当前最优解,则剪枝 if (u.level == n - 1) { // 扩展到叶子节点 max_value = max(max_value, (double)u.value); continue; } int i = indices[u.level + 1]; if (u.weight + weights[i] <= capacity) { // 放入该物品 q.push({u.level + 1, u.value + values[i], u.weight + weights[i], u.bound}); } double bound = u.value + (double)(capacity - u.weight) * values[i] / weights[i]; // 计算不放该物品的上界 if (bound > max_value) { // 如果上界大于当前最优解,则继续扩展 q.push({u.level + 1, u.value, u.weight, bound}); } } return max_value; } ``` 算法分析: 回溯算法的时间复杂度是指数级别的,空间复杂度是O(n),其中n是物品个数。 分支限界法的时间复杂度是O(2^nlogn),空间复杂度是O(n),其中n是物品个数。虽然分支限界法的时间复杂度比回溯算法要低,但是在实际应用中,由于需要排序,因此常数较大,所以实际运行时间并不一定比回溯算法快。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值