算法——人的天性贪心算法

相应的练习代码:https://github.com/liuxuan320/Algorithm_Exercises

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

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

对于这个算法是不是最优算法,我们可以给出证明,这是最优算法,但是这里由于篇幅有限,不在此说明,具体证明可以自行查阅。

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

这个算法还有优化的空间,大家可以思考一下。

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

这个代码应该很容易看懂,就是把最小的两个节点合并为新的节点。

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条 doE中选取一条最小成本的边(V,W) 从E中删去(V,Wif(V,W)T中不生成环 then 将(V,W)加到T else 舍弃(V,Wendif repeat end Kruskal

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算法就介绍完毕了。这次贪心算法比较简单,大家可以自己实现一下。

    原文作者:贪心算法
    原文地址: https://blog.csdn.net/qq_35082030/article/details/55190693
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞