一、最优化问题
- 本文及后面介绍的算法例子都是“最优化问题”。每个最优化问题都包含一组“限制条件”和一个“优化函数”。符合限制条件的问题求解方案称为“可行解”。使优化函数可能取得最佳值的可行解称为“最优解”
二、贪婪算法思想
- 在贪婪算法中,我们要逐步构造一个最优解。每一步,我们都在一定的标准下,作出一个最优决策。在每一步做出的决策,在以后的步骤中都不可更改。做出决策所依据的标准称为贪婪准则
- 贪婪算法又称贪心算法
三、实际应用之找零钱
问题描述
- 顾客用美元来购买糖果,售货员希望用数目最少的硬币找给小孩零钱。假设有面值为25美分、10美分、5美分以及1美分的硬币,而且数目不限
- 售货员每次选择一枚硬币,凑成要找的零钱。选择时所依据的是贪婪准则:在不超过要找的零钱总数的条件下,每一次都选择面试尽可能最大的硬币,直到凑成的零钱总数等于要找的零钱总数
- 问题描述:假设要找给顾客67美分
贪婪算法解决步骤
- 假设要找给顾客67美分:
- 前两步选择的是两个25美元的硬币(第三步就不能选择25美元了,否则总数就超过67美分了)
- 第三步选择10美分的硬币
- 第四步选择5美分的硬币
- 最后一步是两个1美分的硬币
- 贪婪算法使我们有一种感觉:这样凑出来的零钱,硬币数目最少或接近最少
四、实际应用之机器调度
应用分析
- 有任务n个,机器不限,任务在机器上处理。每个任务的开始时间为Si,任务的完成时间为Fi,Si<Fi。[Si,Fi]为任务i的处理时段
- 任务重叠:两个任务i和j重叠,当且仅当两个任务的处理时段有重叠。例如时段[1,4]与时段[2,4]有重叠,而[1,4]与时段[4,7]不算重叠
- 一人任务分配方案是可行的:是指没有两个时段重叠的任务分配给同一台机器,即每台机器在任何时刻最多只处理一个任务
- 最优分配:是指使用机器最少的可行分配方案
实际问题描述
- 假设n=7个任务,标号从a到g,它们的处理时段如下图所示:
- 问题描述:机器不限,现在要把这个7个任务分配到机器上运行,且要求总共使用的机器数目是最少的(时间不做要求)
贪婪算法解决步骤
- 贪婪准则步骤:根据任务的开始时间,若有“旧”机器可用,则将任务分配给它。否则,将任务分配给一台“新”机器
- 旧机器是指:已经被使用过的
- 新机器:重来没有被使用过的机器
- 根据任务的开始与结束时间,最终的执行结果如下图所示,所有的任务总在执行的过程中,共使用过3台机器
五、实际应用之最短路径
问题描述
- 一个有向网如下图所示,图中的数字代表两个节点之间的距离
- 现在有这样一个需求:从某一个点开始达到另外一个点,每一步都向路径上加入一个订单。假设当前的路径已经达到顶点q,但还未达到终点。要求下一次选择路径时:选择一个关联于q最近,且目前不在路径中的顶点
贪婪算法解决步骤
- 假设从顶点1达到顶点5,贪婪算法的解决办法为:
- 第一步选择1->3的路径,长度为2
- 第二步选择3->2的路径,长度为2
- 第三步选择4->2的路径,长度为1
- 第四步选择2->5的路径,长度为5
- 因此总共的长度为10
- 备注(重点):根据问题描述与贪婪算法,最终选择出的路径为10,但是不是最短路径(不是最优的)。例如可以执行1->4->5这条路径,长度总共为6。因此贪婪算法不一定是最优解
六、其他案例
七、一些其他术语概念
- 通过上面一些问题可以知道,贪婪法则虽然不能保证最优解(例如上面的最短路径问题),但是一般情况下它的解总是接近最优的。这是一种经验法则。所得的结果通常都接近最优解,这种算法称为“启发式方法”
- 如果启发式方法与最佳犯法之间还有一种限定关系,那么我们我们称这种启发式方法具有“限定性能”
- 具有限定限定性能的启发式方法称为“近似算法”
八、编码案例(货物装载)
- 问题描述:有一艘船要状态货物,所有的货箱的大小都一样,但是货箱的重量各不相同
- 现在我们的要求是:在不超载的情况下,在货船上状态数量最多的货物
贪婪算法解决
- 思想:选择货箱时,从生下的货箱中,选择重量最小的货箱,那么就可以保证装入的货箱的数目最多
- 例如:假设n=8个货箱,x为货箱的编号,重量w分别为[100,200,50,91,150,50,20,80],船的最大超载c=400。那么根据贪婪算法,可以在船上装载的货箱编号为7、3、6、8、4、1、5、2,总重量为390,此时得到的是最优解[x1,...x8]=[1,0,1,1,0,1,1,1]
- 定理:利用上述贪婪算法能产生最佳装载。该定理的证明如下
C++代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//用于表示货箱
struct container {
container(int _id, int _weight, int _flag = 0)
:id(_id), weight(_weight), flag(_flag) {}
int id; //货箱编号
int weight;//货箱重量
int flag; //用于表示是否装载进货船。0表示没有,1表示有
};
/*
参数:
c:货箱集合
capacity:船的最大超载容量
*/
void containterLoading(const std::vector<container*>& c, int capacity)
{
//如果船未超载,那么进行装箱操作
for (auto iter = c.begin(); ((iter != c.end()) && ((*iter)->weight <= capacity)); ++iter) {
(*iter)->flag = 1;
capacity -= (*iter)->weight;
}
}
bool isMax(const container* p1, const container* p2)
{
return p1->weight < p2->weight;
}
int main()
{
//初始哈8个货箱
std::vector<container*> vec;
vec.push_back(new container(1, 100));
vec.push_back(new container(2, 200));
vec.push_back(new container(3, 50));
vec.push_back(new container(4, 90));
vec.push_back(new container(5, 150));
vec.push_back(new container(6, 50));
vec.push_back(new container(7, 20));
vec.push_back(new container(8, 80));
//根据container的重量对所有的货箱进行排序
std::sort(vec.begin(), vec.end(), isMax);
//装箱操作
containterLoading(vec, 400);
//打印信息
std::cout << "The number of the case being loaded is :";
for (auto iter = vec.cbegin(); iter != vec.cend(); ++iter) {
if ((*iter)->flag)
std::cout << (*iter)->id << " ";
}
std::cout << std::endl;
return 0;
}
九、编码案例(0/1背包问题)
问题描述
- 有n个物品和一个容量为c的背包。物品i的重量为Wi,价值为p。现在要从n个物品中选出一些物品装入书包中
- 现在的最佳要求是:在装包的物品总重量不超过背包的容量下,使装入的物品总价值最高
- 问题描述是:
- Xi=1表示物品装入背包,Xi=0表示物品没有装入背包
- 0/1背包问题实际上是一个一般化的装载货箱问题,只是从每个货箱所获的价值不同
贪婪策略①
- 规则:从剩余的物品中选出可以装入背包的价值最大的物品
- 这种策略不能保证最优解
贪婪策略②
- 规则:从剩余的物品中选出可以装入背包的重量最小的物品
- 这种策略也不能保证最优解
贪婪策略③
- 规则:从剩余的物品中选出可以装入背包的Pi/Wi值最大的物品
- 这种策略也不能保证最优解
启发式贪婪方法
- 上面讨论的三种算法都不能保证最优解,但是我们不必沮丧。0/1背包问题是一个NP-复杂问题
- 其中策略③虽然不能保证最优解,但是我们认为它是一个好的启发式算法,而且很多时候,它的解非常接近最优解。在一项实验中,对随机产生的600个背包问题,利用这种启发式贪婪算法得到的解有239个为最优解,有583个解与最优解相差10%,因此,所有600个解与最优解只差全在25%之内。而且算法能在O(nlogn)时间内完成,性能非常好
K阶优化
- K阶优化的原理是:
- 首先将最多k件物品放入背包,不论它们的价值为多少
- 如果这k件物品超过了背包最大容量c,则放弃这种操作
- 如果这k件物品没有超过了背包最大容量c,继续从剩余的物品中按Pi/Wi值的递减顺序将物品逐个放入背包
- 现在假设n=4,w=[6,10,12,13],p=[6,10,12,13],c=11,Pi/Wi=[3,2.5,2,1.8]
- 当k=0时:将物品按其价值密度的非递增顺序放入背包。首先将物品1放入背包,然后是物品2。这是背包剩下的容量为5,不能再放入任何物品,因此解为x=[1,1,0,0],此解的价值为16
- 当K=1,K=2时,结果如下:
- 修改后的贪婪启发式方法得到的解为K阶优化。也就是说,如果从解中取出k件物品,并放入另外k件物品,那么获得的结果不会原来的好。而且用这种方式获得值在最优值的(100/(k+1))%以内。因此,我们把这种启发式方法称为有界性能启发式
- 当K=1时:保证最终结果在最佳值的50%以内
- 当K=2时:则在33.33%以内
- 有界性能启发式方法的执行时间随k的增加而增加,需要尝试的自己数目为O(),每一个子集所需时间为O(n)。还有,物品按价值比率排序所需时间为O(nlogn)。因此当k>0时,总时间为O()
- 实际考察的性能要好的多,下图给出了600种随机测试的统计结果:
十、编码案例(拓扑排序)
十一、编码案例(二分覆盖)
十二、编码案例(单源最短路径)
十三、编码案例(最小成本生成树)