贪心算法

贪心算法

概念

贪婪算法(Greedy algorithm)是一种对某些求最优解问题的更简单、更迅速的设计技术。用贪婪法设计算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间,它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯。
贪婪算法是一种改进了的分级处理方法。其核心是根据题意选取一种量度标准。然后将这多个输入排成这种量度标准所要求的顺序,按这种顺序一次输入一个量。如果这个输入和当前已构成在这种量度意义下的部分最佳解加在一起不能产生一个 可行解,则不把此输入加到这部分解中。这种能够得到某种量度意义下最优解的分级处理方法称为贪婪算法。
对于一个给定的问题,往往可能有好几种量度标准。初看起来,这些量度标准似乎都是可取的,但实际上,用其中的大多数量度标准作贪婪处理所得到该量度意义下的最优解并不是问题的最优解,而是次优解。因此,选择能产生问题最优解的最优量度标准是使用贪婪算法的核心。
一般情况下,要选出最优量度标准并不是一件容易的事,但对某问题能选择出最优量度标准后,用贪婪算法求解则特别有效。 最优解可以通过一系列局部最优的选择即贪婪选择来达到,根据当前状态做出在当前看来是最好的选择,即局部最优解选择,然后再去解做出这个选择后产生的相应的子问题。每做一次贪婪选择就将所求问题简化为一个规模更小的子问题,最终可得到问题的一个整体最优解。

编辑本段特性

贪婪算法可解决的问题通常大部分都有如下的特性:
⑴ 有一个以最优方式来解决的问题。为了构造问题的解决方案,有一个候选的对象的集合:比如不同面值的硬币。
⑵ 随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
⑶ 有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
⑷ 还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
⑸ 选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
⑹ 最后,目标函数给出解的值。
为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。

编辑本段基本思路

⒈建立 数学模型来描述问题。
⒉把求解的问题分成若干个子问题。
⒊对每一子问题求解,得到子问题的局部最优解。
⒋把子问题的解局部最优解合成原来解问题的一个解。
实现该 算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出 可行解的一个解 元素
由所有解 元素组合成问题的一个可行解。
下面是一个可以试用贪心算法解的题目,贪心解的确不错,可惜不是最优解。

编辑本段例题分析

[0-1背包问题]有一个背包,背包容量是M=150。有7个物品,物品不可以分割成任意大小。
要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 A B C D E F G
重量 35 30 60 50 40 10 25
价值 10 40 30 50 35 40 30
分析:
目标函数:∑pi最大
约束条件是装入的物品总重量不超过背包容量:∑wi<=M(M=150)
⑴根据贪心的 策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
⑵每次挑选所占重量最小的物品装入是否能得到最优解?
⑶每次选取单位重量价值最大的物品,成为解本题的策略。
值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。
贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。
一般来说,贪心算法的证明围绕着:整个问题的 最优解一定由在贪心策略中存在的子问题的最优解得来的。
对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:
⑴贪心策略:选取价值最大者。
反例
W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。
⑵贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。
⑶贪心策略:选取单位重量价值最大的物品。
反例:
W=30
物品:A B C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。
【注意:如果物品可以分割为任意大小,那么策略3可得最优解】
对于选取单位重量价值最大的物品这个策略,可以再加一条优化的规则:对于单位重量价值一样的,则优先选择重量小的!这样,上面的反例就解决了。
但是,如果题目是如下所示,这个策略就也不行了。
W=40
物品:A B C
重量:25 20 15
价值:25 20 15
附:本题是个DP问题,用贪心法并不一定可以求得最优解,以后了解了 动态规划算法后本题就有了新的解法。

编辑本段备注

贪心算法当然也有正确的时候。求 最小生成树Prim算法Kruskal算法都是漂亮的贪心算法。
贪心法的应用算法有 Dijkstra的单源最短路径和Chvatal的贪心 集合覆盖启发式
所以需要说明的是,贪心算法可以与 随机化算法一起使用,具体的例子就不再多举了。(因为这一类算法普及性不高,而且技术含量是非常高的,需要通过一些反例确定随机的对象是什么,随机程度如何,但也是不能保证完全正确,只能是极大的几率正确)

编辑本段例题

马踏棋盘的贪心算法
123041-23 XX
【问题描述】
马的遍历问题。在8×8方格的棋盘上,从任意指定方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条最短路径。
【初步设计】
首先这是一个搜索问题,运用 深度优先搜索进行求解。算法如下:
⒈ 输入初始位置坐标x,y;
⒉ 步骤 c:
如果c> 64输出一个解,返回上一步骤c--
(x,y) ← c
计算(x,y)的八个方位的子结点,选出那此可行的子结点
循环遍历所有可行子结点,步骤c++重复2
显然⑵是一个递归调用的过程,大致如下:
#define N 8
……
……
void dfs(int x,int y,int count)
{
int i,tx,ty;
if(count> N*N)
{
output_solution();//输出一个解
return;
}
for(I=0;i <8;++i)
{
tx=hn[i].x;//hn[]保存八个方位子结点
ty=hn[i].y;
s[tx][ty]=count;
dfs(tx,ty,count+1);//递归调用
s[tx][ty]=0;
}
}
Pascal程序:
Program YS;
Const FXx:array[1..8]of -2..2=(1,2,2,1,-1,-2,-2,-1);
FXy:array[1..8]of -2..2=(2,1,-1,-2,-2,-1,1,2);
Var
Road:array[1..10,1..10]of integer;
x,y,x1,y1,total:integer;
Procedure Find(x,y:integer);
var Nx,Ny,i:integer;
Begin
For i:=1 to 8 do begin {8个方向}
If (x+FXx[i] in [1..8]) and (y+FXy[i] in [1..8]) Then{确定新坐标是否越界}
If Road[x+Fxx[i],y+Fxy[i]]=0 Then begin{判断是否走过}
Nx:=x+FXx[i]; Ny:=y+FXy[i]; Road[Nx,Ny]:=1;{建立新坐标}
If (Nx=x1) and (Ny=y1) Then inc(total)
else Find(Nx,Ny); {递归}
Road[Nx,Ny]:=0 {回朔}
end
end
End;
BEGIN{Main}
Total:=0;
FillChar(Road,sizeof(road),0);
Readln(x,y); {读入开始坐标}
Readln(x1,y1); {读入结束坐标}
If (x>10) or (y>10) or (x1>10) or (y1>10) Then writeln('Eorror') {判断是否越界}
Else Find(x,y);
Writeln('Total:',total) {打出总数}
END.
这样做是完全可行的,它输入的是全部解,但是马遍历当8×8时解是非常之多的,用 天文数字形容也不为过,这样一来求解的过程就非常慢,并且出一个解也非常慢。
怎么才能快速地得到部分解呢?
【贪心算法】
其实马踏棋盘的问题很早就有人提出,且早在1823年,J.C.Warnsdorff就提出了一个有名的算法。在每个结点对其子结点进行选取时,优先选择‘出口’最小的进行搜索,‘出口’的意思是在这些子结点中它们的可行子结点的个数,也就是‘孙子’结点越少的越优先跳,为什么要这样选取,这是一种局部调整最优的做法,如果优先选择出口多的子结点,那出口少的子结点就会越来越多,很可能出现‘死’结点(顾名思义就是没有出口又没有跳过的结点),这样对下面的搜索纯粹是徒劳,这样会浪费很多无用的时间,反过来如果每次都优先选择出口少的结点跳,那出口少的结点就会越来越少,这样跳成功的机会就更大一些。这种算法称为为贪心算法,也叫贪婪算法或 启发式算法,它对整个求解过程的局部做最优调整,它只适用于求较优解或者部分解,而不能求最优解。这样的调整方法叫贪心策略,至于什么问题需要什么样的贪心策略是不确定的, 具体问题具体分析。实验可以证明马遍历问题在运用到了上面的贪心策略之后求解速率有非常明显的提高,如果只要求出一个解甚至不用回溯就可以完成,因为在这个算法提出的时候世界上还没有 计算机,这种方法完全可以用手工求出解来,其效率可想而知。

编辑本段数学应用

如把3/7和13/23分别化为三个 单位分数的和
【贪心算法】
设a、b为互质正整数,a<b 分数a/b 可用以下的步骤分解成若干个单位分数之和:
步骤一: 用b 除以a,得商数q1 及余数r1。(r1=b - a*q1)
步骤二:把a/b 记作:a/b=1/(q1+1)+(a-r)/b(q1+1)
步骤三:重复步骤2,直到分解完毕
3/7=1/3+2/21=1/3+1/11+1/231
13/23=1/2+3/46=1/2+1/16+1/368
以上其实是 数学家 斐波那契提出的一种求解 埃及分数的贪心算法,准确的算法表述应该是这样的:
设某个 真分数的分子为a,分母为b;
把b除以a的商部分加1后的值作为埃及分数的某一个分母c;
将a乘以c再减去b,作为新的a;
将b乘以c,得到新的b;
如果a大于1且能整除b,则最后一个 分母为b/a;算法结束;
或者,如果a等于1,则,最后一个 分母为b;算法结束;
否则重复上面的步骤。
备注:事实上,后面判断a是否大于1和a是否等于1的两个判断可以合在一起,及判断b%a是否等于0,最后一个分母为b/a,显然是正确的。

贪心算法

一、算法思想

贪心法的基本思路:
——从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到某算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1. 不能保证求得的最后解是最佳的;
2. 不能用来求最大或最小解问题;
3. 只能求满足某些约束条件的可行解的范围。


实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
   求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;

二、例题分析

1、[背包问题]有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。
要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。

物品 A  B  C D E F G 
重量 35  30  60  50  40  10  25  
价值  10  40  30 50  35  40  30 


分析:

目标函数: ∑pi最大
约束条件是装入的物品总重量不超过背包容量:∑wi<=M( M=150)


(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
(2)每次挑选所占空间最小的物品装入是否能得到最优解?
(3)每次选取单位容量价值最大的物品,成为解本题的策略。

http://baike.baidu.com/view/298415.htm

 

    实现这个算法是学习算法分析与设计这门课程的需要。

    贪心算法是所接触到的第一类算法。算法从局部的最优出发,简单而快捷。对于一个问题的最

优解只能用穷举法得到时,用贪心法是寻找问题次优解的较好算法。

    贪心法是一种改进了的分级处理方法。用贪心法设计算法的特点是一步一步地进行,根据某个

优化测度(可能是目标函数,也可能不是目标函数),每一步上都要保证能获得局部最优解。每一

步只考虑一个数据,它的选取应满足局部优化条件。若下一个数据与部分最优解连在一起不再是可

行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加为止。这种能够

得到某种度量意义下的最优解的分级处理方法称为贪心法。

    选择能产生问题最优解的最优度量标准是使用贪心法的核心问题。

    假定有n个物体和一个背包,物体i 有质量wi,价值为pi,而背包的载荷能力为M。若将物体i的

一部分xi(1<=i<=n,0<=xi<=1)装入背包中,则有价值pi*xi。在约束条件

(w1*x1+w2*x2+…………+wn*xn)<=M下使目标(p1*x1+p2*x2+……+pn*xn)达到极大,此处

0<=xi<=1,pi>0,1<=i<=n.这个问题称为背包问题(Knapsack problem)。

    要想得到最优解,就要在效益增长和背包容量消耗两者之间寻找平衡。也就是说,总应该把那

些单位效益最高的物体先放入背包。

    在实现算法的程序中,实现算法的核心程序倒没碰到很大的问题,然而实现寻找最优度量标准

程序时麻烦不断!

    在寻找最优度量标准时,大致方向是用冒泡排序算法。也就是根据p[i]/w[i]的大小来对w[i]来

排序。

    在直接用此算法时,可以有如下的一段代码:

    //根据效益tempArray[i]对重量w[i]排序,为进入贪心算法作准备

1   void sort(float tempArray[], flaot w[], int n)

2   {

3       int i = 0, j = 0;

4       int index = 0;

5    

6       //用类似冒泡排序算法,根据效益p[i]/w[i]对w[i]排序

7       for (i = 0; i < n; i++)

8    {

9           float swapMemory = 0;

10          float temp;

11

12          temp = tempArray[i];

13          index = i;

14

15          for (j = i + 1; j < n; j++)

16          {          

17              if (temp < tempArray[j])

18        {

19                  temp = tempArray[j];

20                  index = j;

21        }

22       }

23   

24          //对w[i]排序

25          swapMemory = w[index];

26          w[index] = w[i];

27          w[i] = swapMemory;

28      }

29

30      return;

31  }

    然而仔细对算法分析后可以发现,“拿来主义”在这里用不上了!

    对算法的测试用例是p[3] = {25, 24, 15};w[3] = {18, 15, 10}。得到的结果如下:

    please input the total count of object: 3

    Please input array of p :

    25 24 15

    Now please input array of w :

    18 15 10

    sortResult[i] is :

    1   -107374176.000000   1   1.600000   2    1.600000

    after arithmetic data: x[i]

    0.000000        0.333333        0.000000 

    可以看到其效益为x[3] = {1.4, 1.6, 1.5},于是在M = 20的情况下,其预想中的输出结果是

0,1,0.5。然而事实上是不是就这样呢?

    当程序进入此函数经过必要的变量初始化后,进入了外围循环,也就是程序的第7行。第一轮循

环中,temp = tempArray[0] = 1.4,index = i = 0;程序运行到第15行,也就是进入了内层循环。

内层循环的主要任务是从第i + 1个元素之后找到一个最大的效益并保存此时的下标。到了第24行后

,就开始对w[i]进行排序。

    问题就在这里了!排序后的w[i] = {1.6, 1.6, 1.5},因此对w[i]排序后就既改变了w[i]的原

有顺序,还改变了w[i]的原来值!

    据此,做出一些修改,得到了如下的一段代码:

1   void sort(float tempArray[], int sortResult[], int n)

2   {

3       int i = 0, j = 0;

4       int index = 0, k = 0;

5

6       for (i = 0; i < n; i++)//对映射数组赋初值0

7    {

8           sortResult[i] = 0;

9    }

10

11      for (i = 0; i < n; i++)

12      {

13          float swapMemory = 0;

14          float temp;

15

16          temp = tempArray[i];

17          index = i;

18

19          for (j = i; j < n; j++)

20          {          

21              if ((temp < tempArray[j]) && (sortResult[j] == 0))

22        {

23                  temp = tempArray[j];

24                  index = j;

25        }

26       }

27

28          if (sortResult[index] == 0)

29       {

30              sortResult[index] = ++k;

31       }

32      }

33

34      for (i = 0; i < n; i++)

35      {

36          if (sortResult[i] == 0)

37       {

38              sortResult[i] = ++k;

39       }

40      }

41

42      return;

43  }

    修改后最大的一个改变是没有继续沿用直接对w[i]排序,而是用w[i]的一个映射数组

sortResult[i]。sortResult[i]中元素值存放的是根据效益计算得w[i]的大小顺序!这样w[i]原有

的值和位置都没有改变,从而使算法得以实现!

    至于有没有更好的实现版本,还在探索中!

#include <stdio.h>

#define MAXSIZE 100  //假设物体总数

#define M 20      //背包的载荷能力

//算法核心,贪心算法

void GREEDY(float w[], float x[], int sortResult[], int n)

{

    float cu = M;

    int i = 0;

    int temp = 0;

    for (i = 0; i < n; i++)//准备输出结果

    {

        x[i] = 0;

    }

    for (i = 0; i < n; i++)

    {

        temp = sortResult[i];//得到取物体的顺序

        if (w[temp] > cu)  

        {

            break;

        }

        x[temp] = 1;//若合适则取出

        cu -= w[temp];//将容量相应的改变

    }

    if (i <= n)//使背包充满

    {

        x[temp] = cu / w[temp];

    }

    return;

}

void sort(float tempArray[], int sortResult[], int n)

{

    int i = 0, j = 0;

    int index = 0, k = 0;

    for (i = 0; i < n; i++)//对映射数组赋初值0

    {

        sortResult[i] = 0;

    }

    for (i = 0; i < n; i++)

    {

        float temp = tempArray[i];

        index = i;

        //找到最大的效益并保存此时的下标

        for (j = 0; j < n; j++)

        {          

            if ((temp < tempArray[j]) && (sortResult[j] == 0))

            {

                temp = tempArray[j];

                index = j;

            }

        }

        //对w[i]作标记排序

        if (sortResult[index] == 0)

        {

            sortResult[index] = ++k;

        }

    }

    //修改效益最低的sortResult[i]标记

    for (i = 0; i < n; i++)

    {

        if (sortResult[i] == 0)

        {

            sortResult[i] = ++k;

        }

    }

    return;

}

//得到本算法的所有输入信息

void getData(float p[], float w[], int *n)

{

    int i = 0;

    printf("please input the total count of object: ");

    scanf("%d", n);

    printf("Please input array of p :\n");

    for (i = 0; i < (*n); i++)

    {

        scanf("%f", &p[i]);

    }

    printf("Now please input array of w :\n");

    for (i = 0; i < (*n); i++)

    {

        scanf("%f", &w[i]);

    }

    return;

}

void output(float x[], int n)

{

    int i;

    printf("\n\nafter arithmetic data: advise method\n");

    for (i = 0; i < n; i++)

    {

        printf("x[%d]\t", i);

    }

    printf("\n");

    for (i = 0; i < n; i++)

    {

        printf("%2.3f\t", x[i]);

    }

    return;

}

void main()

{

    float p[MAXSIZE], w[MAXSIZE], x[MAXSIZE];

    int i = 0, n = 0;

    int sortResult[MAXSIZE];

    getData(p, w, &n);

    for (i = 0; i < n; i++)

    {

        x[i] = p[i] / w[i];

    }

    sort(x, sortResult, n);

    GREEDY(w, x, sortResult, n);

    output(x, n);

    getch();

}

一.贪心算法的基本概念 

    当一个问题具有最优子结构性质时,我们会想到用动态规划法去解它。但有时会有更简单有效的算法。我们来看一个找硬币的例子。假设有四种硬币,它们的面值分别为二角五分、一角、五分和一分。现在要找给某顾客六角三分钱。这时,我们会不假思索地拿出2个二角五分的硬币,1个一角的硬币和3个一分的硬币交给顾客。这种找硬币方法与其他的找法相比,所拿出的硬币个数是最少的。这里,我们下意识地使用了这样的找硬币算法:首先选出一个面值不超过六角三分的最大硬币,即二角五分;然后从六角三分中减去二角五分,剩下三角八分;再选出一个面值不超过三角八分的最大硬币,即又一个二角五分,如此一直做下去。这个找硬币的方法实际上就是贪心算法。顾名思义,贪心算法总是作出在当前看来是最好的选择。也就是说贪心算法并不从整体最优上加以考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,我们希望贪心算法得到的最终结果也是整体最优的。上面所说的找硬币算法得到的结果就是一个整体最优解。找硬币问题本身具有最优子结构性质,它可以用动态规划算法来解。但我们看到,用贪心算法更简单,更直接且解题效率更高。这利用了问题本身的一些特性。例如,上述找硬币的算法利用了硬币面值的特殊性。如果硬币的面值改为一分、五分和一角一分3种,而要找给顾客的是一角五分钱。还用贪心算法,我们将找给顾客1个一角一分的硬币和4个一分的硬币。然而3个五分的硬币显然是最好的找法。虽然贪心算法不是对所有问题都能得到整体最优解,但对范围相当广的许多问题它能产生整体最优解。如图的单源最短路径问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,但其最终结果却是最优解的很好的近似解。

二.求解活动安排问题算法 

    活动安排问题是可以用贪心算法有效求解的一个很好的例子。该问题要求高效地安排一系列争用某一公共资源的活动。贪心算法提供了一个简单、漂亮的方法使得尽可能多的活动能兼容地使用公共资源。

    设有n个活动的集合e={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi。如果选择了活动i,则它在半开时间区间[si,fi]内占用资源。若区间[si,fi]与区间[sj,fj]不相交,则称活动i与活动j是相容的。也就是说,当si≥fi或sj≥fj时,活动i与活动j相容。活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合。

    在下面所给出的解活动安排问题的贪心算法gpeedyselector中,各活动的起始时间和结束时间存储于数组s和f{中且按结束时间的非减序:.f1≤f2≤…≤fn排列。如果所给出的活动未按此序排列,我们可以用o(nlogn)的时间将它重排。

template< class type>

void greedyselector(int n, type s[ 1, type f[ ], bool a[ ] ]

 { a[ 1 ] = true;

 int j = 1;

 for (int i=2;i< =n;i+ + ) {

     if (s[i]>=f[j]) {

        a[i] = true;

        j=i;

}

else a[i]= false;

}

}

    算法greedyselector中用集合a来存储所选择的活动。活动i在集合a中,当且仅当a[i]的值为true。变量j用以记录最近一次加入到a中的活动。由于输入的活动是按其结束时间的非减序排列的,fj总是当前集合a中所有活动的最大结束时间,即:

     

    贪心算法greedyselector一开始选择活动1,并将j初始化为1。然后依次检查活动i是否与当前已选择的所有活动相容。若相容则将活动i加人到已选择活动的集合a中,否则不选择活动i,而继续检查下一活动与集合a中活动的相容性。由于fi

总是当前集合a中所有活动的最大结束时间,故活动i与当前集合a中所有活动相容的充分且必要的条件是其开始时间s 不早于最近加入集合a中的活动j的结束时间fj,si≥fj。若活动i与之相容,则i成为最近加人集合a中的活动,因而取代活动j的位置。由于输人的活动是以其完成时间的非减序排列的,所以算法greedyselector每次总是选择具有最早完成时间的相容活动加入集合a中。直观上按这种方法选择相容活动就为未安排活动留下尽可能多的时间。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。算法greedyselector的效率极高。当输人的活动已按结束时间的非减序排列,算法只需g(n)的时间来安排n个活动,使最多的活动能相容地使用公共资源。

例:设待安排的11个活动的开始时间和结束时间按结束时间的非减序排列如下:

i

1

2

3

4

5

6

7

8

9

10

11

s[i]

1

3

0

5

3

5

6

8

8

2

12

f[i]

4

5

6

7

8

9

10

11

12

13

14

 

算法greedyselector的计算过程如图所示。

 

    图中每行相应于算法的一次迭代。阴影长条表示的活动是已选人集合a中的活动,而空白长条表示的活动是当前正在检查其相容性的活动。若被检查的活动i的开始时间si小于最近选择的活动了的结束时间fj,则不选择活动i,否则选择活动i加入集合a中。

三.算法分析 

    贪心算法并不总能求得问题的整体最优解。但对于活动安排问题,贪心算法greedyse—1ector却总能求得的整体最优解,即它最终所确定的相容活动集合a的规模最大。我们可以用数学归纳法来证明这个结论。

    事实上,设e={1,2,…,n}为所给的活动集合。由于正中活动按结束时间的非减序排列,故活动1具有最早的完成时间。首先我们要证明活动安排问题有一个最优解以贪心选择开始,即该最优解中包含活动1。设 是所给的活动安排问题的一个最优解,且a中活动也按结束时间非减序排列,a中的第一个活动是活动k。若k=1,则a就是一个以贪心选择开始的最优解。若k>1,则我们设 。由于f1≤fk,且a中活动是互为相容的,故b中的活动也是互为相容的。又由于b中活动个数与a中活动个数相同,且a是最优的,故b也是最优的。也就是说b是一个以贪心选择活动1开始的最优活动安排。因此,我们证明了总存在一个以贪心选择开始的最优活动安排方案。

    进一步,在作了贪心选择,即选择了活动1后,原问题就简化为对e中所有与活动1相容的活动进行活动安排的子问题。即若a是原问题的一个最优解,则a’=a—{i}是活动安排问题 的一个最优解。事实上,如果我们能找到e’的一个解b’,它包含比a’更多的活动,则将活动1加入到b’中将产生e的一个解b,它包含比a更多的活动。这与a的最优性矛盾。因此,每一步所作的贪心选择都将问题简化为一个更小的与原问题具有相同形式的子问题。对贪心选择次数用数学归纳法即知,贪心算法greedyselector最终产生原问题的一个最优解。

四.贪心算法的基本要素 

    贪心算法通过一系列的选择来得到一个问题的解。它所作的每一个选择都是当前状态下某种意义的最好选择,即贪心选择。希望通过每次所作的贪心选择导致最终结果是问题的一个最优解。这种启发式的策略并不总能奏效,然而在许多情况下确能达到预期的目的。解活动安排问题的贪心算法就是一个例子。下面我们着重讨论可以用贪心算法求解的问题的一般特征。

    对于一个具体的问题,我们怎么知道是否可用贪心算法来解此问题,以及能否得到问题的一个最优解呢?这个问题很难给予肯定的回答。但是,从许多可以用贪心算法求解的问题中

我们看到它们一般具有两个重要的性质:贪心选择性质最优子结构性质

1.贪心选择性质

    所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。在动态规划算法中,每步所作的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能作出选择。而在贪心算法中,仅在当前状态下作出最好选择,即局部最优选择。然后再去解作出这个选择后产生的相应的子问题。贪心算法所作的贪心选择可以依赖于以往所作过的选择,但决不依赖于将来所作的选择,也不依赖于子问题的解。正是由于这种差别,动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为一个规模更小的子问题。

对于一个具体问题,要确定它是否具有贪心选择性质,我们必须证明每一步所作的贪心选择最终导致问题的一个整体最优解。通常可以用我们在证明活动安排问题的贪心选择性质时所采用的方法来证明。首先考察问题的一个整体最优解,并证明可修改这个最优解,使其以贪心选择开始。而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步作贪心选择,最终可得到问题的一个整体最优解。其中,证明贪心选择后的问题简化为规模更小的类似子问题的关键在于利用该问题的最优子结构性质。

2.最优子结构性质

    当一个问题的最优解包含着它的子问题的最优解时,称此问题具有最优子结构性质。问题所具有的这个性质是该问题可用动态规划算法或贪心算法求解的一个关键特征。在活动安排问题中,其最优子结构性质表现为:若a是对于正的活动安排问题包含活动1的一个最优解,则相容活动集合a’=a—{1}是对于e’={i∈e:si≥f1}的活动安排问题的一个最优解。

3.贪心算法与动态规划算法的差异

    贪心算法和动态规划算法都要求问题具有最优子结构性质,这是两类算法的一个共同点。但是,对于一个具有最优子结构的问题应该选用贪心算法还是动态规划算法来求解?是不是能用动态规划算法求解的问题也能用贪心算法来求解?下面我们来研究两个经典的组合优化问题,并以此来说明贪心算法与动态规划算法的主要差别。

五. 0-背包问题 

给定n种物品和一个背包。物品i的重量是w ,其价值为v ,背包的容量为c.问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大? 在选择装入背包的物品时,对每种物品i只有两种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。

    此问题的形式化描述是,给定c>0,wi>0,vi>0,1≤i≤n,要求找出一个n元0—1向

量(xl,x2,…,xn), ,使得 ≤c,而且 达到最大。

      背包问题:与0-1背包问题类似,所不同的是在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包。

    此问题的形式化描述是,给定c>0,wi>0,vi>0,1≤i≤n,要求找出一个n元向量

(x1,x2,...xn),0≤xi≤1,1≤i≤n 使得 ≤c,而且 达到最大。

    这两类问题都具有最优子结构性质。对于0—1背包问题,设a是能够装入容量为c的背包的具有最大价值的物品集合,则aj=a-{j}是n-1个物品1,2,…,j—1,j+1,…,n可装入容量为c-wi叫的背包的具有最大价值的物品集合。对于背包问题,类似地,若它的一个最优解包含物品j,则从该最优解中拿出所含的物品j的那部分重量wi,剩余的将是n-1个原重物品1,2,…,j-1,j+1,…,n以及重为wj-wi的物品j中可装入容量为c-w的背包且具有最大价值的物品。

    虽然这两个问题极为相似,但背包问题可以用贪心算法求解,而0·1背包问题却不能用贪心算法求解。用贪心算法解背包问题的基本步骤是,首先计算每种物品单位重量的价值

vj/wi然后,依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。若将这种物品全部装入背包后,背包内的物品总重量未超过c,则选择单位重量价值次高的物品并尽可能多地装入背包。依此策略一直进行下去直到背包装满为止。具体算法可描述如下:

 void knapsack(int n, float m, float v[ ], float w[ ], float x[ ] )

 sort(n,v,w);

 int i;

 for(i= 1;i<= n;i++) x[i] = o;

 float c = m;

 for (i = 1;i < = n;i ++) {

     if (w[i] > c) break;

    x[i] = 1;

    c-= w[i];

    }

 if (i < = n) x[i] = c/w[i];

算法knapsack的主要计算时间在于将各种物品依其单位重量的价值从大到小排序。因此,算法的计算时间上界为o(nlogn)。当然,为了证明算法的正确性,我们还必须证明背包问题具有贪心选择性质。 

这种贪心选择策略对0—1背包问题就不适用了。看图2(a)中的例子,背包的容量为50千克;物品1重10千克;价值60元;物品2重20千克,价值100元;物品3重30千克;价值120元。因此,物品1每千克价值6元,物品2每千克价值5元,物品3每千克价值4元。若依贪心选择策略,应首选物品1装入背包,然而从图4—2(b)的各种情况可以看出,最优的选择方案是选择物品2和物品3装入背包。首选物品1的两种方案都不是最优的。对于背包问题,贪心选择最终可得到最优解,其选择方案如图2(c)所示。

 

    对于0—1背包问题,贪心选择之所以不能得到最优解是因为它无法保证最终能将背包装满,部分背包空间的闲置使每千克背包空间所具有的价值降低了。事实上,在考虑0—1背包问题的物品选择时,应比较选择该物品和不选择该物品所导致的最终结果,然后再作出最好选择。由此就导出许多互相重叠的于问题。这正是该问题可用动态规划算法求解的另一重要特征。动态规划算法的确可以有效地解0—1背包问题。


顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路经问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。

问题一、活动安排问题

问题表述:设有n个活动的集合E = {1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活i都有一个要求使用该资源的起始时间si和一个结束时间fi,si < fi 。如果选择了活动i,则它在半开时间区间[si, fi)内占用资源。若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的。也就是说,当si >= fjsj >= fi时,活动i与活动j相容。

由于输入的活动以其完成时间的非减序排列,所以算法greedySelector每次总是选择具有最早完成时间的相容活动加入集合A中。直观上,按这种方法选择相容活动为未安排活动留下尽可能多的时间。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。

算法greedySelector的效率极高。当输入的活动已按结束时间的非减序排列,算法只需O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。

例:设待安排的11个活动的开始时间和结束时间按结束时间的非减序排列如下:


算法greedySelector 的计算过程如下图所示。图中每行相应于算法的一次迭代。阴影长条表示的活动是已选入集合A的活动,而空白长条表示的活动是当前正在检查相容性的活动。


若被检查的活动i的开始时间Si小于最近选择的活动j的结束时间fi,则不选择活动i,否则选择活动i加入集合A中。 

贪心算法并不总能求得问题的整体最优解。但对于活动安排问题,贪心算法greedySelector却总能求得的整体最优解,即它最终所确定的相容活动集合A的规模最大。这个结论可以用数学归纳法证明。

活动安排问题实现:

 

复制代码
   
   
/* 主题:活动安排问题
* 作者:chinazhangjie
* 邮箱:chinajiezhang@gmail.com
* 开发语言:C++
* 开发环境:Vicrosoft Visual Studio
* 时间: 2010.11.21
*/

#include
< iostream >
#include
< vector >
#include
< algorithm >
using namespace std ;

struct ActivityTime
{
public :
ActivityTime (
int nStart, int nEnd)
: m_nStart (nStart), m_nEnd (nEnd)
{ }
ActivityTime ()
: m_nStart (
0 ), m_nEnd ( 0 )
{ }
friend
bool operator < ( const ActivityTime & lth, const ActivityTime & rth)
{
return lth.m_nEnd < lth.m_nEnd ;
}
public :
int m_nStart ;
int m_nEnd ;
} ;

class ActivityArrange
{
public :
ActivityArrange (
const vector < ActivityTime >& vTimeList)
{
m_vTimeList
= vTimeList ;
m_nCount
= vTimeList.size () ;
m_bvSelectFlag.resize (m_nCount,
false ) ;
}
// 活动安排
void greedySelector ()
{
__sortTime () ;
// 第一个活动一定入内
m_bvSelectFlag[ 0 ] = true ;
int j = 0 ;
for ( int i = 1 ; i < m_nCount ; ++ i) {
if (m_vTimeList[i].m_nStart > m_vTimeList[j].m_nEnd) {
m_bvSelectFlag[i]
= true ;
j
= i ;
}
}

copy (m_bvSelectFlag.begin(), m_bvSelectFlag.end() ,ostream_iterator
< bool > (cout, " " ));
cout
<< endl ;
}

private :
// 按照活动结束时间非递减排序
void __sortTime ()
{
sort (m_vTimeList.begin(), m_vTimeList.end()) ;
for (vector < ActivityTime > ::iterator ite = m_vTimeList.begin() ;
ite
!= m_vTimeList.end() ;
++ ite) {
cout
<< ite -> m_nStart << " , " << ite -> m_nEnd << endl ;
}
}

private :
vector
< ActivityTime > m_vTimeList ; // 活动时间安排列表
vector < bool > m_bvSelectFlag ; // 是否安排活动标志
int m_nCount ; // 总活动个数
} ;

int main()
{
vector
< ActivityTime > vActiTimeList ;
vActiTimeList.push_back (ActivityTime(
1 , 4 )) ;
vActiTimeList.push_back (ActivityTime(
3 , 5 )) ;
vActiTimeList.push_back (ActivityTime(
0 , 6 )) ;
vActiTimeList.push_back (ActivityTime(
5 , 7 )) ;
vActiTimeList.push_back (ActivityTime(
3 , 8 )) ;
vActiTimeList.push_back (ActivityTime(
5 , 9 )) ;
vActiTimeList.push_back (ActivityTime(
6 , 10 )) ;
vActiTimeList.push_back (ActivityTime(
8 , 11 )) ;
vActiTimeList.push_back (ActivityTime(
8 , 12 )) ;
vActiTimeList.push_back (ActivityTime(
2 , 13 )) ;
vActiTimeList.push_back (ActivityTime(
12 , 14 )) ;

ActivityArrange aa (vActiTimeList) ;
aa.greedySelector () ;
return 0 ;
}
复制代码

 

贪心算法的基本要素

对于一个具体的问题,怎么知道是否可用贪心算法解此问题,以及能否得到问题的最优解呢?这个问题很难给予肯定的回答。

但是,从许多可以用贪心算法求解的问题中看到这类问题一般具有2个重要的性质:贪心选择性质和最优子结构性质。

1、贪心选择性质

所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。

动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。 

对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。

2、最优子结构性质

当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。 

3、贪心算法与动态规划算法的差异

贪心算法和动态规划算法都要求问题具有最优子结构性质,这是2类算法的一个共同点。但是,对于具有最优子结构的问题应该选用贪心算法还是动态规划算法求解?是否能用动态规划算法求解的问题也能用贪心算法求解?下面研究2个经典的组合优化问题,并以此说明贪心算法与动态规划算法的主要差别。

0-1背包问题:

给定n种物品和一个背包。物品i的重量是Wi,其价值为Vi,背包的容量为C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

在选择装入背包的物品时,对每种物品i只有2种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i

背包问题:

0-1背包问题类似,所不同的是在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包,1 <= i <= n

2类问题都具有最优子结构性质,极为相似,但背包问题可以用贪心算法求解,而0-1背包问题却不能用贪心算法求解。

用贪心算法解背包问题的基本步骤:

首先计算每种物品单位重量的价值Vi/Wi,然后,依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。若将这种物品全部装入背包后,背包内的物品总重量未超过C,则选择单位重量价值次高的物品并尽可能多地装入背包。依此策略一直地进行下去,直到背包装满为止。

伪代码:

void Knapsack(int n,float M,float v[],float w[],float x[])

{

  Sort(n,v,w);

  int i;

  for (i = 1 ; i <= n ; i++) 

    x[i] = 0;

    float c=M;

    for (i=1;i<=n;i++) {

      if (w[i] > c) break;

    x[i]=1;

    c-=w[i];

  }

  if (i <= n) 

    x[i]=c / w[i];

}

算法knapsack的主要计算时间在于将各种物品依其单位重量的价值从大到小排序。因此,算法的计算时间上界为 Onlogn)。

为了证明算法的正确性,还必须证明背包问题具有贪心选择性质。

对于0-1背包问题,贪心选择之所以不能得到最优解是因为在这种情况下,它无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低了。事实上,在考虑0-1背包问题时,应比较选择该物品和不选择该物品所导致的最终方案,然后再作出最好选择。由此就导出许多互相重叠的子问题。这正是该问题可用动态规划算法求解的另一重要特征。实际上也是如此,动态规划算法的确可以有效地解0-1背包问题。

问题二、 哈夫曼编码

哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。其压缩率通常在20%90%之间。哈夫曼编码算法用字符在文件中出现的频率表来建立一个用01串表示各字符的最优表示方式。

给出现频率高的字符较短的编码,出现频率较低的字符以较长的编码,可以大大缩短总码长。

 

a

b

c

d

e

f

频率(千次)

45

13

12

16

9

5

定长码

000

001

010

011

100

101

变长码

0

101

100

111

1101

1100

定长码:

  3*(45+13+12+16+9+5) = 300 千位

变长码:

  1*45+3*13+3*12+3*16+4*9+4*5 = 224 千位

1、前缀码

对每一个字符规定一个0,1串作为其代码,并要求任一字符的代码都不是其它字符代码的前缀。这种编码称为前缀码

编码的前缀性质可以使译码方法非常简单。 

表示最优前缀码的二叉树总是一棵完全二叉树,即树中任一结点都有2个儿子结点。

f(c)表示字符c出现的概率,dt(c)表示c的码长

平均码长定义为:

使平均码长达到最小的前缀码编码方案称为给定编码字符集C最优前缀码

2、构造哈夫曼编码

哈夫曼提出构造最优前缀码的贪心算法,由此产生的编码方案称为哈夫曼编码

哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树T

算法以|C|个叶结点开始,执行|C|1次的“合并”运算后产生最终所要求的树T。 

f为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。一旦2棵具有最小频率的树合并后,产生一棵新的树,其频率为合并的2棵树的频率之和,并将新树插入优先队列Q。经过n1次的合并后,优先队列中只剩下一棵树,即所要求的树T

算法huffmanTree用最小堆实现优先队列Q。初始化优先队列需要O(n)计算时间,由于最小堆的removeMinput运算均需O(logn)时间,n1次的合并总共需要O(nlogn)计算时间。因此,关于n个字符的哈夫曼算法的计算时间为O(nlogn) 

3、哈夫曼算法的正确性

要证明哈夫曼算法的正确性,只要证明最优前缀码问题具有贪心选择性质和最优子结构性质。

(1)贪心选择性质

(2)最优子结构性质

实现:

复制代码
   
   
/* 主题: Haffman编码
* 作者: chinazhangjie
* 邮箱: chinajiezhang@gmail.com
* 开发环境 : Microsoft Visual Studio 2008
* 时间 : 2010.11.21
*/

#include
< iostream >
#include
< vector >
#include
< queue >
using namespace std ;


class HaffmanNode
{
public :
HaffmanNode (
int nKeyValue,
HaffmanNode
* pLeft = NULL,
HaffmanNode
* pRight = NULL)
{
m_nKeyValue
= nKeyValue ;
m_pLeft
= pLeft ;
m_pRight
= pRight ;
}

friend
bool operator < ( const HaffmanNode & lth, const HaffmanNode & rth)
{
return lth.m_nKeyValue < rth.m_nKeyValue ;
}

public :
int m_nKeyValue ;
HaffmanNode
* m_pLeft ;
HaffmanNode
* m_pRight ;
} ;

class HaffmanCoding
{
public :
typedef priority_queue
< HaffmanNode *> MinHeap ;
typedef HaffmanNode
* HaffmanTree ;

public :
HaffmanCoding (
const vector < int >& weight)
: m_pTree(NULL)
{
m_stCount
= weight.size () ;
for (size_t i = 0 ; i < weight.size() ; ++ i) {
m_minheap.push (
new HaffmanNode(weight[i], NULL, NULL)) ;
}
}
~ HaffmanCoding()
{
__destroy (m_pTree) ;
}

// 按照左1右0编码
void doHaffmanCoding ()
{
vector
< int > vnCode(m_stCount - 1 ) ;
__constructTree () ;
__traverse (m_pTree,
0 , vnCode) ;
}

private :
void __destroy(HaffmanTree & ht)
{
if (ht -> m_pLeft != NULL) {
__destroy (ht
-> m_pLeft) ;
}
if (ht -> m_pRight != NULL) {
__destroy (ht
-> m_pRight) ;
}
if (ht -> m_pLeft == NULL && ht -> m_pRight == NULL) {
// cout << "delete" << endl ;
delete ht ;
ht
= NULL ;
}
}
void __traverse (HaffmanTree ht, int layers, vector < int >& vnCode)
{
if (ht -> m_pLeft != NULL) {
vnCode[layers]
= 1 ;
__traverse (ht
-> m_pLeft, ++ layers, vnCode) ;
-- layers ;
}
if (ht -> m_pRight != NULL) {
vnCode[layers]
= 0 ;
__traverse (ht
-> m_pRight, ++ layers, vnCode) ;
-- layers ;
}
if (ht -> m_pLeft == NULL && ht -> m_pRight == NULL) {
cout
<< ht -> m_nKeyValue << " coding: " ;
for ( int i = 0 ; i < layers; ++ i) {
cout
<< vnCode[i] << " " ;
}
cout
<< endl ;
}
}

void __constructTree ()
{
size_t i
= 1 ;
while (i < m_stCount) {
HaffmanNode
* lchild = m_minheap.top () ;
m_minheap.pop () ;
HaffmanNode
* rchild = m_minheap.top () ;
m_minheap.pop () ;

// 确保左子树的键值大于有子树的键值
if (lchild -> m_nKeyValue < rchild -> m_nKeyValue) {
HaffmanNode
* temp = lchild ;
lchild
= rchild ;
rchild
= temp ;
}
// 构造新结点
HaffmanNode * pNewNode =
new HaffmanNode (lchild -> m_nKeyValue + rchild -> m_nKeyValue,
lchild, rchild ) ;
m_minheap.push (pNewNode) ;
++ i ;
}
m_pTree
= m_minheap.top () ;
m_minheap.pop () ;
}

private :
vector
< int > m_vnWeight ; // 权值
HaffmanTree m_pTree ;
MinHeap m_minheap ;
size_t m_stCount ;
// 叶结点个数
} ;


int main()
{
vector
< int > vnWeight ;
vnWeight.push_back (
45 ) ;
vnWeight.push_back (
13 ) ;
vnWeight.push_back (
12 ) ;
vnWeight.push_back (
16 ) ;
vnWeight.push_back (
9 ) ;
vnWeight.push_back (
5 ) ;

HaffmanCoding hc (vnWeight) ;
hc.doHaffmanCoding () ;
return 0 ;
}
复制代码

 

问题三、单源最大路径

给定带权有向图G =(V,E),其中每条边的权是非负实数。另外,还给定V中的一个顶点,称为源。现在要计算从源到所有其它各顶点的最短路长度。这里路的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。

1、算法基本思想

Dijkstra算法是解单源最短路径问题的贪心算法。

其基本思想是,设置顶点集合S并不断地作贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知。

初始时,S中仅含有源。设uG的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到所有其它顶点之间的最短路径长度。

例如,对下图中的有向图,应用Dijkstra算法计算从源顶点1到其它顶点间最短路径的过程列在下表中。


Dijkstra算法的迭代过程:

迭代

s

u

dist[2]

dist[3]

dist[4]

dist[5]

初始

{1}

-

10

maxint

30

100

1

{1,2}

2

10

60

30

100

2

{1,2,4}

4

10

50

30

90

3

{1,2,4,3}

3

10

50

30

60

4

{1,2,4,3,5}

5

10

50

30

60

2、算法的正确性和计算复杂性

(1)贪心选择性质

(2)最优子结构性质

(3)计算复杂性

对于具有n个顶点和e条边的带权有向图,如果用带权邻接矩阵表示这个图,那么Dijkstra算法的主循环体需要O(n)时间。这个循环需要执行n-1次,所以完成循环需要O(n)时间。算法的其余部分所需要时间不超过O(n^2)

实现:

 

复制代码
   
   
/* 主题: Dijkastra算法
* 作者: chinazhangjie
* 邮箱: chinajiezhang@gmail.com
* 开发环境 : Microsoft Visual Studio 2008
* 时间 : 2010.11.23
*/
#include
< iostream >
#include
< vector >
#include
< limits >
using namespace std ;

class BBShortestDijkstra
{
public :
BBShortestDijkstra (
const vector < vector < int > >& vnGraph)
:m_cnMaxInt (numeric_limits
< int > ::max())
{
m_vnGraph
= vnGraph ;
m_stCount
= vnGraph.size () ;
m_vnDist.resize (m_stCount) ;
for (size_t i = 0 ; i < m_stCount; ++ i) {
m_vnDist[i].resize (m_stCount) ;
}
}

void doDijkatra ()
{
int nMinIndex = 0 ;
int nMinValue = m_cnMaxInt ;
vector
< bool > vbFlag (m_stCount, false ) ;
for (size_t i = 0 ; i < m_stCount; ++ i) {
m_vnDist[
0 ][i] = m_vnGraph[ 0 ][i] ;
if (nMinValue > m_vnGraph[ 0 ][i]) {
nMinValue
= m_vnGraph[ 0 ][i] ;
nMinIndex
= i ;
}
}

vbFlag[
0 ] = true ;
size_t k
= 1 ;
while (k < m_stCount) {
vbFlag[nMinIndex]
= true ;
for (size_t j = 0 ; j < m_stCount ; ++ j) {
// 没有被选择
if ( ! vbFlag[j] && m_vnGraph[nMinIndex][j] != m_cnMaxInt ) {
if (m_vnGraph[nMinIndex][j] + nMinValue
< m_vnDist[k - 1 ][j]) {
m_vnDist[k][j]
= m_vnGraph[nMinIndex][j] + nMinValue ;
}
else {
m_vnDist[k][j]
= m_vnDist[k - 1 ][j] ;
}
}
else {
m_vnDist[k][j]
= m_vnDist[k - 1 ][j] ;
}
}
nMinValue
= m_cnMaxInt ;
for (size_t j = 0 ; j < m_stCount; ++ j) {
if ( ! vbFlag[j] && (nMinValue > m_vnDist[k][j])) {
nMinValue
= m_vnDist[k][j] ;
nMinIndex
= j ;
}
}
++ k ;
}

for ( int i = 0 ; i < m_stCount; ++ i) {
for ( int j = 0 ; j < m_stCount; ++ j) {
if (m_vnDist[i][j] == m_cnMaxInt) {
cout
<< " maxint " ;
}
else {
cout
<< m_vnDist[i][j] << " " ;
}
}
cout
<< endl ;
}
}
private :
vector
< vector < int > > m_vnGraph ;
vector
< vector < int > > m_vnDist ;
size_t m_stCount ;
const int m_cnMaxInt ;
} ;

int main()
{
const int cnCount = 5 ;
vector
< vector < int > > vnGraph (cnCount) ;
for ( int i = 0 ; i < cnCount; ++ i) {
vnGraph[i].resize (cnCount, numeric_limits
< int > ::max()) ;
}
vnGraph[
0 ][ 1 ] = 10 ;
vnGraph[
0 ][ 3 ] = 30 ;
vnGraph[
0 ][ 4 ] = 100 ;
vnGraph[
1 ][ 2 ] = 50 ;
vnGraph[
2 ][ 4 ] = 10 ;
vnGraph[
3 ][ 2 ] = 20 ;
vnGraph[
3 ][ 4 ] = 60 ;

BBShortestDijkstra bbs (vnGraph) ;
bbs.doDijkatra () ;
}
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值