0. 写在前面
说起贪心算法,可能是我们最为熟悉的算法了。正如我标题所说的,贪心算法之所以称之为贪心,就是由于它的核心思想和我们人的天性一模一样。都是选取当前情况下的最优值,但是如何选取一种度量标准,使得我们的贪心能获得全局最优,这是一个值得商榷的问题。下面,我们就开始全面的介绍贪心算法。
1. 贪心算法的定义
贪心算法主要使由于这样一个问题而产生的:
它有N个输入,而它的解就是由这n个输入的某个子集组成,但是要满足某些条件。
这些必须要满足的条件称为约束条件。
把满足约束条件的子集称为该问题的可行解。
度量可行解优劣的函数称为目标函数。
目标的取极值的可行解称为最优解。
以上就是贪心算法的大概描述,那么它的具体实现是如何呢,我们下面给出答案。
贪心算法的步骤:
1. 先选取一种度量标准。
2. 按照这种度量标准对这N个输入进行排序,然后一个一个输入。
3. 如果这个输入不能和当前部分最优解加在一起产生一个可行解,则不把此输入加入到这部分解中。
没错,这就结束了。但是具体的算法应该是这样写的:
//贪心算法的抽象化控制
procedure GREEDY(A,n)
//A(1:n)包含n个输入
solution<-Φ //将解向量初始化为空
for i<-1 to n do
x<-SELECT(A)
if FEASIBLE(solution,x)
then solution<-UNION(solution,x)
endif
repeat
return solution
end GREEDY
- 1
2. 贪心算法核心——背包问题
1.问题描述
之所以把背包问题单独列出来进行讨论,是因为它的特殊性。由于它是一个经典案例,所以在接下来的很多算法中,都会看到它的身影。
我们这里是普通的背包问题,也就是说可以进行拆分放入的。具体问题描述如下:
已知有N种物品,一个可容纳M重量的背包。每种物品i的重量为 wi ,假定讲物品i的一部分 xi 放入背包就会得到 pixi 的效益,怎样才能使装入物品总重量不超过M,且总收益最大。
2.背包问题的贪心算法
1. 问题描述
很显然这种问题我们经常在故事里听到,很显然,首先我们想到的就是把最贵的东西先装进去,可是最贵的东西一定是总价格最大的么?未必,由于总价格最大的潜在因素就有可能它的体积也非常庞大,因此单价不高。我们生活中嫌弃一个东西贵不贵,都是问单价,比如水果啊什么的,一斤多少钱,这里也是一样。那么我们就可以获得以下抽象化描述:
目标函数:总收益最大
度量标准:
效益值增量最大
单位效益值最大
2. 算法描述
有了上述问题分析后,我们就可以有这样一个算法:
//背包问题的贪心算法
procedure GREEDY-KNAPSACK(P,W,M,X,n)
//P,W分别按照单位效益由高到低排序,M是背包的容量,X是解向量。
real P(1:n),W(1:n),X(1:n),M,cu
integer i,n;
X←0
cu←M
for i←1 to n do
if W(i)>cu then exit endif
X(i)←1
cu←cu-W(i)
repeat
if i≤n then X(i)←cu/W(i)
endif
end GREEDY-KNAPSACK
- 1
对于这个算法是不是最优算法,我们可以给出证明,这是最优算法,但是这里由于篇幅有限,不在此说明,具体证明可以自行查阅。
3. 贪心算法若干应用
1. 带有期限的作业排序
1. 问题描述
有限期作业是这样的一个问题:有N个作业需要在1台机器上完成,每个作业均可在单位时间内完成。又假定每个作业i都有一个截止期限 di >0,当且仅当作业i在截止日期前被完成时方可获得 pi >0的效益。
2. 问题求解
通过上述描述,我们可以找到目标函数:总效益最大。而度量标准为:按照收益非增次序排序。这一点和背包问题相类似。
具体的算法如下:
//带有期限和效益的单位时间的作业排序贪心算法
procedure JS(D,J,n,k)
integer D(0:n),J(0:n),i,k,n,r
D(0)←J(0)←0
k←1;J(1)←1
for i←2 to n do
r←k
while D(J(r))>D(i) and D(J(r))≠r do
r←r-1
repeat
if D(J(r))≤D(i) and D(i)>r then
for i←k to r+1 by -1 do
J(i+1)←J(i)
repeat
J(i+1)←i;k←k+1
endif
repeat
end JS
- 1
这个算法还有优化的空间,大家可以思考一下。
2. 最优归并模式
1. 问题描述
最优归并模式问题描述如下:若两个分别包含n个和m个记录的已排序文件可以在O(m+n)时间内归并到一起而得到一个新排序的文件。现在给出n个文件,求最快归并完成的方案(使用二路归并)
2. 问题求解
看过这个问题,大家很容易想到哈夫曼树,没错,这个和哈夫曼树的想法是已知的,每次都归并尺寸最小的两个文件。具体算法如下:
procedure TREE(L,n)
for i←1 to n-1 do
call GETNODE(T)
LCHILD(T)←LEAST(L) //最小的长度
RCHILD(T)←LEAST(L)
WEIGHT(T)←WEIGHT(LCHILD(T))+WEIGHT(RCHILD(T))
call INSERT(L,T)
repeat
return (LEAST(L))
end TREE
- 1
这个代码应该很容易看懂,就是把最小的两个节点合并为新的节点。
3. 最小生成树
1. 问题描述
最小生成树是图论中一个经典的问题,它的解决方法也各不相同,最经典的有两种,一个是Prim算法,一个是Kruskal算法。该问题的具体描述如下:
设G=(V,E)是一个无向连通图。如果G的生成子图T(V,E’)是一棵树,则称T是G的一棵生成树。
而生成树和连通图的关系是:任何一个具有n个节点的连通图都必须至少有n-1条边,而所有具有n-1条边的n结点连通图都是树。
2. Prim算法
prim算法的贪心原则是:选择最短的点加入到解集中。
下面我们不介绍具体代码,我们对PRIM算法进行拆解。我么您所需的数据结构有:
COST(n,n) 这是成本的邻接矩阵
T(1:n-1,2)这是解集生成树
NEAR(n) 目标与解集树之间的成本最低的结点。
算法的具体的过程如下:
1. 找到成本最小的边
2. 初始化NEAR,即找出每个点与已生成树之间的成本最低的结点。
3. 在对其余的n-2条边:
对每一条边都计算一下已生成树的成本,并找到最小的一个成本边。
加入至已生成树后,对NEAR进行更新。
3. Kruskal算法
Kruskal算法与Prim算法不同,它的贪心原则是:选择最短的边加入到解集中。其算法如下:
procedure Kruskal
T←Φ
while T的边少于n-1条 do
从E中选取一条最小成本的边(V,W)
从E中删去(V,W)
if(V,W)在T中不生成环
then 将(V,W)加到T
else 舍弃(V,W)
endif
repeat
end Kruskal
- 1
Kruskal的算法的难点在于如何判断一个图是否为连通图,一个图是否存在回路。这个问题,我们会单独进行讲解。
4. 单点源最短路径
1. 问题描述
单源点的最短路径问题也是一个十分经典的算法问题,这个问题如下:从甲地到乙地有多种路径,求最短路径。
2. Dejesk算法
这种问题尤其是在地图路径规划上能够用到,它和多段图的区别在于,它的每个顶点的先后顺序是不固定的,如果用多段图的话,就可以使用动态规划算法了,这里我们使用的Dejesk算法,它的贪心规则为每次选择与 V0 最短的路径。
关于Dejesk算法,我们对其核心思想进行讲解,不写具体代码,具体代码可以在我的Github上找到。
我们所需的数据结构有COST(n,n),这是成本邻接矩阵,DIST(n),这是从V(出发点)到i点的最短距离,到自己的点为0,S(n)这是解集,里面只存0(未纳入)和1(已纳入)。
具体步骤如下:
1. 首先初始化解集S,全置为0,初始化DIST(n)为COST(V,i)
2. 然后把开始结点V纳入解集中S(V)←1,DIST(V)←0
3. 对于剩下的n-1条路径
1)找到DIST(n)中最小的DIST(u)
2) 把u纳入到解集中
3)修改DIST(n)中的所有S(i)=0的点,修改方法为DIST(w)←min(DIST(w),DIST(u)+cost(u,w))
这样,我们的Dejesk算法就介绍完毕了。这次贪心算法比较简单,大家可以自己实现一下。