贪心算法,(其实是很简单很简单的算法,就是排序然后按顺序取就完事)顾名思义,我们每次都要贪婪地得到最好的,但是既然是最好的肯定有个比较标准,体现在算法中也就是特定的排序函数,我们利用此排序函数对集合进行排序。接下来就是每次都从拍好序的集合中找到我们想要的。
在部分背包问题中,贪心算法不会像在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 | 收益/元 |
---|---|
2 | 1 |
3 | 2 |
4 | 5 |
背包可容纳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函数,以及分支-限界法的限界函数。
我们知道树的深度遍历可以用递归实现,但是广度遍历却不能这么优雅。