从贪心法、动态规划、回溯法、分支限界法谈背包问题————持续更新

贪心算法,(其实是很简单很简单的算法,就是排序然后按顺序取就完事)顾名思义,我们每次都要贪婪地得到最好的,但是既然是最好的肯定有个比较标准,体现在算法中也就是特定的排序函数,我们利用此排序函数对集合进行排序。接下来就是每次都从拍好序的集合中找到我们想要的。
在部分背包问题中,贪心算法不会像在0-1背包问题那样浪费任何容量。因此,总是能给出最优解。
代码如下:

/*
背包问题 贪心
不要忘记给结构体内变量赋初值
变量为double类型
*/
#include<algorithm>
#include<iostream>
#include<vector>
using namespace std;

struct ware {
	int num;//物品序号
	double P;//效益
	double W;//重量
	double p_w;//单位效益
	ware()
	{
		num = 0;
		P = 0.0;
		W = 0.0;
		p_w = 0.0;
	}
};
struct Bag {
	int num;//背包号码
	double weight;//重量(单位化)
	Bag() { num = 0; weight = 0.0; }
};

bool cmp(ware a, ware b)//从大到小排列
{
	return a.p_w > b.p_w;
}

int main()
{
	int n;
	double M;
	cin >> n >> M;

	vector<ware> W_P(n);//可以用变量值初始化

	for (int i = 0; i < n; i++)
	{
		//此种方式事前不必定义大小或大小随意
		ware temp;
		temp.num = i + 1;//背包号码
		cin >> temp.P >> temp.W;
		temp.p_w = temp.P / temp.W;
		W_P.push_back(temp);

		//当不会发生越界的时候可用此种方式
		/*
		cin >> W_P[i].P >> W_P[i].W;
		W_P[i].p_w = W_P[i].P / W_P[i].W;
		W_P[i].num = i + 1;
		*/
	}


	sort(W_P.begin(), W_P.end(), cmp);

	cout << "排好序的结果为:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << W_P[i].num << " " << W_P[i].p_w << endl;
	}

	vector<Bag> J(n);

	for (int i = 0; i < n; i++)
	{
		if (W_P[i].W < M)
		{
			M = M - W_P[i].W;
			J[i].num = W_P[i].num;
			J[i].weight = 1.0;
		}
		else {
			J[i].num = W_P[i].num;
			J[i].weight = M / W_P[i].W;
			break;
		}
	}

	int i = 0;
	cout << "背包问题最优结果为:" << endl;
	while (J[i].num != 0)
	{
		cout << J[i].num << " " << J[i].weight << endl;
		i++;
	}
	return 0;
}

在这里插入图片描述
动态规划
(其实动态规划也不难,真心不难)
如果可以证明最优性原理适用,就可以使用动态规划解决0-1背包问题。
最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。
对于0-1背包问题来说,找最优解无非就是求一个集合。不妨假设我们已经考虑到最后一个物品n,故最有解集合对于第n个物品来说,有两种情况:或者最后一个物品n在本集合中,或者不在本集合中。我们可以将上面给出的最后一步一般化:对于每一个物品i来说,也有两种状态,我们可以将它放入先前的集合中,也可以不放入。由于最优性原理成立,我们可以利用动态规划一步步推至最后的结果。
我们为了方便考虑以下的物品情况:

重量/kg收益/元
21
32
45

背包可容纳6kg的物品。你肯定很高兴,因为你不出30秒就知道取第一件和第三件物品。但是,你用的一定是枚举法!!!但是如果规模更大呢?100件物品,背包变成卡车呢?来,施主,给贫道算一个。如果一一枚举,假设n件物品,我们将会有 2 n 2^n 2n种情况需要枚举,体现在程序中,你只需要写n个for循环,然后判断 2 n 2^n 2n下是否超出容量,在初始化一个max来记录你获得的最大值……画面很美好,或许计算机算一个小时可以得出答案。其实这种方式是最容易想到的,但是我们最容易想到的为什么不能用呢,有点违背天理啊。其实,枚举法是非常正确的做法,但是由于计算量太大,现有计算技术跟不上,只能放弃。举个例子,
1+1=2
1+1+1=3
1+1+1+1=4
……
1+1+1+1+1+1+1+1=?
容易知道该式子满足最优性原理,动态规划是这样的:要计算1+1+1+1+1+1+1+1你肯定已经知道1+1+1+1+1+1+1=7了,那么直接得到1+1+1+1+1+1+1+1=7+1=8。
上面的加法是一维的。体现在算法中我们只需要初始化一个int dp,来保存我们先前计算过的值,然后拿它加上现在的1。
但是背包问题是二维的,牵扯到背包的重量,以及取的物品数量。所以我们想按动态规划的决策思想来一步步计算出结果,那么我们的起点就是背包重量为1,只有一件物品的时候可以拿什么,然后不断扩充至背包重量为6,物品数为3的情况。

在这里插入图片描述
体现在算法中就是开一个二维数组 int dp[100][1000] = { 0 }(dp是dynamic programming的缩写),然后再写一个两层循环,一层为了背包重量递增,一层为了物品序号递增。

for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= w; j++)
		{
			if (j >= t[i].weight)
			{
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - t[i].weight] + t[i].profit);
			}
			else
			{
				dp[i][j] = dp[i - 1][j];
			}
		}
	}

你要知道动态规划的过程也是枚举法一步步得来的,只不过他枚举的更多的是有规划的,避免了大量盲目的计算。有规划的,你可以这样理解,我们不是一个一个可能性去试,而是我们知道怎么做,并且知道这样做是对的,于是我们从第一步起一直做到最后一步,得到正确答案。动态的,你可以这样理解,我们目前得到的最优结果都是在先前工作的基础上动态改变的。

1、0-1背包
#include<iostream>
#include<vector>
#include<algorithm>
#define maxn  100;
using namespace std;
struct good
{
	int weight;
	int profit;
};

int main()
{
	cout << "请输入背包重量" << endl;
	int w;
	cin >> w;
	cout << "请输入物品个数:" << endl;
	int n;
	cin >> n;
	vector<good> t(n + 1);
	for (int i = 1; i <= n; i++)
	{
		cout << "请输入物品" << i << "的重量";
		cin >> t[i].weight;
		cout << "请输入物品" << i << "的收益";
		cin >> t[i].profit;
	}

	int dp[100][1000] = { 0 };

	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= w; j++)
		{
			if (j >= t[i].weight)
			{
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - t[i].weight] + t[i].profit);
			}
			else
			{
				dp[i][j] = dp[i - 1][j];
			}
		}
	}

	for (int i = 0; i <= n; i++)
	{
		for (int j = 0; j <= w; j++)
		{
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
		//打印挑选出的物品
	cout << "挑选出的物品是:" << endl;
	int c = n;
	int j = w;
	while (c != 1)
	{
		if (c >= 0)
		{
			if (dp[c][j] != dp[c - 1][j])
			{
				cout << c << " ";
				j = j - t[c].weight;
				c = c - 1;
			}
			else
			{
				c = c - 1;
			}
		}
	}
	if (dp[c][j] != 0)
	{
		cout << 1;
	}
}

在这里插入图片描述
其中要打印出选取的物品,可根据以下规则:
在这里插入图片描述
回溯法:
最优化问题,在查找完成之前,我们无法确定是否已经得到一个最优解。但是我们可以做到尽量减少查找次数(因为此类枚举数量实在太大)并且不影响最后的结果。回溯法先看代码比较好理解,以下是经典的八皇后问题,怎么做到回溯的呢,一个check函数,再加continue语法就ok了。

#include <iostream>
using namespace std;
bool check(int a[], int n)//检查是否可以放置皇后
{//多次被调用,只需一重循环 
	for (int i = 1; i <= n - 1; i++)
	{
		if ((abs(a[i] - a[n]) == n - i) || (a[i] == a[n]))
			return false;
	}
	return true;
}

void queens()
{
	int a[9];
	int count = 0;
	for (a[1] = 1; a[1] <= 8; a[1]++)
	{
		for (a[2] = 1; a[2] <= 8; a[2]++)
		{
			if (!check(a, 2))  continue;
			for (a[3] = 1; a[3] <= 8; a[3]++)
			{
				if (!check(a, 3))  continue;
				for (a[4] = 1; a[4] <= 8; a[4]++)
				{
					if (!check(a, 4))  continue;
					for (a[5] = 1; a[5] <= 8; a[5]++)
					{
						if (!check(a, 5))  continue;
						for (a[6] = 1; a[6] <= 8; a[6]++)
						{
							if (!check(a, 6))  continue;
							for (a[7] = 1; a[7] <= 8; a[7]++)
							{
								if (!check(a, 7))  continue;
								for (a[8] = 1; a[8] <= 8; a[8]++)
								{
									if (!check(a, 8))
										continue;
									else
									{
										for (int i = 1; i <= 8; i++)
										{
											cout << a[i];
										}
										cout << endl;
										count++;
									}
								}
							}
						}
					}
				}
			}

		}
	}
	cout << count << endl;
}

void main()
{
	queens();
}

八皇后问题递归方法,本质还是回溯,只不过递归是由递归栈自动帮我们保存所需的元素:

/*
八皇后问题
*/
#include <iostream>
using namespace std;


int a[100], n, count1;
bool check(int a[], int n)//检查是否可以放置皇后
{//多次被调用,只需一重循环 
	for (int i = 1; i <= n - 1; i++)
	{
		if ((abs(a[i] - a[n]) == n - i) || (a[i] == a[n]))
			return false;
	}
	return true;
}


void backtrack(int k)
{
	if (k > n)//找到解
	{
		for (int i = 1; i <= n; i++)
		{
			cout << a[i];
		}
		cout << endl;
		count1++;
	}
	else
	{
		for (int i = 1; i <= n; i++)
		{
			a[k] = i;//上一个不合适的数据会在这里被覆盖掉
			if (check(a, k) == 1)
			{
				backtrack(k + 1);
			}
		}
	}

}

void main()
{
	n = 8, count1 = 0;
	backtrack(1);
	cout << count1 << endl;
}

为什么书上讲解的时候都爱把回溯法化成树来讲解呢?**因为用for循环枚举本来就跟树很相似,越顶层的for循环越接近树根。以上算法的continue其实就是将以它为根的子树忽略不计,我们知道如果单纯枚举,是所有节点都有考虑的,但是如果continue,子树中的节点都不再考虑,continue越靠近上层for循环,我们不需要考虑的节点越多,枚举的数量越少,由此,增加了效率,因为减少的都是肯定不符合条件的。你也可以这样理解,枚举不过是回溯法的特殊化,如果你只将check函数(代码需要改变一下)放在最内层的循环,一直到树的叶节点才continue,那这就是枚举。
分支-限界法
你可能不知道分支-限界法跟回溯法有什么区别。那么你联想一下树的遍历方式:深度遍历,广度遍历。
其中深度遍历对应回溯法,广度遍历对应分支-限界法。其实道理很简单,难点也相同:即该什么时候该回溯,也可以说成什么时候这个这个分支达到界限,可以舍弃以它为根的所有子树节点。就是设计回溯算法的check函数,以及分支-限界法的限界函数。
我们知道树的深度遍历可以用递归实现,但是广度遍历却不能这么优雅。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值