算法专题:贪婪算法

在众多的计算机解题策略中,贪心策略可以算得上是最接近人们日常思维的一种解题策略,正基于此,贪心策略在各级各类信息学竞赛、尤其在对NPC类问题的求解中发挥着越来越重要的作用。

贪心策略的定义

贪心策略是:指从问题的初始状态出发,通过若干次的贪心选择而得出最优值(或较优解)的一种解题方法。

其实,从“贪心策略”一词我们便可以看出,贪心策略总是做出在当前看来是最优的选择,也就是说贪心策略并不是从整体上加以考虑,它所做出的选择只是在某种意义上的局部最优解,而许多问题自身的特性决定了该题运用贪心策略可以得到最优解或较优解。

例1:在n行m列的正整数矩阵中,要求从每一行中选一个数,使得选出的n个数的和最大。

本题可用贪心策略:选n次,每一次选相应行中的最大值即可。

例2:在一个N×M的方格阵中,每一格子赋予一个数(即为权)。规定每次移动时只能向上或向右。现试找出一条路径,使其从左下角至右上角所经过的权之和最大。

本题用贪心策略不能得到最优解,我们以2×4的矩阵为例。

3

4

6

1

2

10

若按贪心策略求解,所得路径为:1,3,4,6;

若按动态规划法求解,所得路径为:1,2,10,6。

例3:设定有n台处理机p1,p2,......pn,和m个作业j1,j2,...jm,处理机可并行工作,作业未完成不能中断,作业ji在处理机上的处理时间为ti,求解最佳方案,使得完成m项工作的时间最短?

本题不能用贪心算法求解:理由是若n=3,m=6 各作业的时间分别是11 7 5 5 4 7

用贪心策略解(每次将作业加到最先空闲的机器上)time=15,用搜索策略最优时间应是14,但是贪心策略给我们提供了一个线索那就是每台处理上的时间不超过15,给搜索提供了方便。

总之:

  1. 不能保证求得的最后解是最佳的;
  2. 只能用来求某些最大或最小解问题;
  3. 能确定某些问题的可行解的范围,特别是给搜索算法提供了依据。

贪心策略的特点

贪心算法有什么样的特点呢?我认为,适用于贪心算法解决的问题应具有以下2个特点:

  1. 贪心选择性质:所谓贪心选择性质是指应用同一规则f,将原问题变为一个相似的、但规模更小的子问题、而后的每一步都是当前看似最佳的选择。这种选择依赖于已做出的选择,但不依赖于未做出的选择。从全局来看,运用贪心策略解决的问题在程序的运行过程中无回溯过程。关于贪心选择性质,读者可在后文给出的贪心策略状态空间图中得到深刻地体会。
  2. 局部最优解:我们通过特点2向大家介绍了贪心策略的数学描述。由于运用贪心策略解题在每一次都取得了最优解,但能够保证局部最优解得不一定是贪心算法。如大家所熟悉得动态规划算法就可以满足局部最优解,但贪心策略比动态规划时间效率更高站用内存更少,编写程序更简单。

典型例题与习题

1. 例4:背包问题

有一个背包,背包容量是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. 每次选取单位容量价值最大的物品,成为解本题的策略。

注意:这个方法得到的解不一定是背包问题的最优解

程序如下:

program beibao; 

const
  m=150;
  n=7;
var
  xu:integer;
  i,j:integer;
  goods:array[1..n,0..2] of integer;
  ok:array[1..n,1..2] of real; 

procedure init;
  var
    i:integer;
  begin
    xu:=m;
    for i:=1 to n do
      begin
        write('Enter the price and weight of the ',i,'th goods:');
        goods[i,0]:=i;
        read(goods[i,1],goods[i,2]);
        readln;
        ok[i,1]:=0; ok[i,2]:=0;
      end;
  end; 

procedure make;
  var
    bi:array[1..n] of real;
    i,j:integer;
    temp1,temp2,temp0:integer;
  begin
    for i:=1 to n do
      bi[i]:=goods[i,1]/goods[i,2];
    for i:=1 to n-1 do
      for j:=i+1 to n do
        begin
          if bi[i]<bi[j] then begin
             temp0:=goods[i,0]; temp1:=goods[i,1]; temp2:=goods[i,2];
             goods[i,0]:=goods[j,0]; goods[i,1]:=goods[j,1]; goods[i,2]:=goods[j,2];
             goods[j,0]:=temp0; goods[j,1]:=temp1; goods[j,2]:=temp2;
          end;
        end;
  end;

begin
  init;
  make;
  for i:=1 to 7 do
    begin
      if goods[i,2]>xu then break;
      ok[i,1]:=goods[i,0]; ok[i,2]:=1;
      xu:=xu-goods[i,2];
    end;
  j:=i;
  if i<=n then
    begin
      ok[i,1]:=goods[i,0];
      ok[i,2]:=xu/goods[i,2];
    end;
  for i:=1 to j do
      writeln(ok[i,1]:1:0,':',ok[i,2]*goods[i,2]:2:1);
end.

2. 例5:旅行家的预算问题

一个旅行家想驾驶汽车以最少的费用从一个城市到另一个城市,给定两个城市间的距离d1,汽车油箱的容量是c,每升汽油能行驶的距离d2,出发时每升汽油的价格是p,沿途加油站数为n(可为0),油站i离出发点的距离是di,每升汽油的价格是pi。

计算结果四舍五入保留小数点后两位,若无法到达目的地输出“No answer"

若输入:

d1=275.6 c=11.9 d2=27.4 p=8 n=2

d[1]=102 p[1]=2.9

d[2]=220 p[2]=2.2

output

26.95

本问题的贪心策略是:找下一个较便宜的油站,根据距离确定加满、不加、加到刚好到该站。

程序如下:

program jiayou;
const maxn=10001;
zero=1e-16;
type
 jd=record
  value,way,over:real;
 end;
var oil:array[1..maxn] of ^jd;
 n:integer;
 d1,c,d2,cost,maxway:real;
function init:boolean;
var i:integer;
begin
 new(oil[1]);
 oil[1]^.way:=0;
 read(d1,c,d2,oil[1]^.value,n);
 maxway:=d2*c;
 for i:=2 to n+1 do
  begin
   new(oil[i]);
   readln(oil[i]^.way,oil[i]^.value);
   oil[i]^.over:=0;
  end;
 inc(n,2);
 new(oil[n]);
 oil[n]^.way:=d1;
 oil[n]^.value:=0;
 oil[n]^.over:=0;
 for i:=2 to n do
  if oil[i]^.way-oil[i-1]^.way>maxway then
   begin
    init:=false;
    exit
   end;
init:=true;
end;
procedure buy(i:integer;miles:real);
begin
 cost:=cost+miles/d2*oil[i]^.value;
end;
procedure solve;
var i,j:integer;
  s:real;
begin
 i:=1;j:=i+1;
 repeat
  s:=0.0;
  while( s<=maxway+zero) and (j<=n-1) and (oil[i]^.value<=oil[j]^.value) do
   begin
   inc(j);
   s:=s+oil[j]^.way-oil[j-1]^.way
   end;
  if s<=maxway+zero then
   if (oil[i]^.over+zero>=oil[j]^.way-oil[i]^.way) then
    oil[j]^.over:=oil[i]^.over-(oil[j]^.way-oil[i]^.way) else
    begin
     buy(i,oil[j]^.way-oil[i]^.way-oil[i]^.over);
     oil[j]^.over:=0.0;
    end
   else begin
    buy(i,maxway-oil[i]^.over);
    j:=i+1;
    oil[j]^.over:=maxway-(oil[j]^.way-oil[i]^.way);
    end;
   i:=j;
 until i=n;
 end;
begin
 cost:=0;
 if init then begin
  solve;
  writeln(cost:0:2);
  end else writeln('No answer');
end. 

3. 例6

n个部件,每个部件必须经过先A后B两道工序。已知部件i在A,B 机器上的时间分别为ai,bi。如何安排加工顺序,总加工时间最短?

输入:

5  1 2 3 4 5 
ai 3 5 8 7 10 
bi 6  2 1 4 9 

输出:

34

1 5 4 2 3

本问题的贪心策略是A机器上加工短的应优先,B机器上加工短的应靠后。

程序如下:

program workorder;
const maxn=100;
type jd=record
 a,b,m,o:integer;
 end;
var n,min,i:integer;
 c:array[1..maxn] of jd;
 order:array[1..maxn] of integer;
procedure init;
var i:integer;
begin
 readln(n);
 for i:=1 to n do
 read(c[i].a);
 readln;
 for i:=1 to n do
 read(c[i].b);
 readln;
 for i:=1 to n do
  begin
   if c[i].a<c[i].b then c[i].m:=c[i].a else c[i].m:=c[i].b;
   c[i].o:=i;
  end;
end;
procedure sort;
var i,j,k,t:integer;
    temp:jd;
begin
 for i:=1 to n-1 do
  begin
   k:=i;t:=c[i].m;
  for j:=i+1 to n do
   if c[j].m<t then begin t:=c[j].m;k:=j end ;
  if k<>i then begin temp:=c[i];c[i]:=c[k];c[k]:=temp end
  end;
end;
procedure playorder;
var i,s,t:integer;
begin
 fillchar(order,sizeof(order),0);
 s:=1;
 t:=n;
 for i:=1 to n do
  if c[i].m=c[i].a then begin order[s]:=i;s:=s+1 end
                   else begin order[t]:=i;t:=t-1;end;
end;
procedure calc_t;
var i,t1,t2:integer;
begin
 t1:=0;t2:=0;
 for i:=1 to n do
  begin
   t1:=t1+c[order[i]].a;
   if t2<t1 then t2:=t1;
   t2:=t2+c[order[i]].b;
  end;
min:=t2;
end;
begin
 init;
 sort;
 playorder;
 calc_t;
 writeln(min);
 for i:=1 to n do
  write(c[order[i]].o,' ');
 writeln;
end.

习题

1. 最佳浏览路线问题

某旅游区的街道成网格状(见图),其中东西向的街道都是旅游街,南北向的街道都是林荫道。由于游客众多,旅游街被规定为单行道。游客在旅游街上只能从西向东走,在林荫道上既可以由南向北走,也可以从北向南走。阿隆想到这个旅游区游玩。他的好友阿福给了他一些建议,用分值表示所有旅游街相邻两个路口之间的道路值得浏览得程度,分值从-100到100的整数,所有林荫道不打分。所有分值不可能全是负值。

例如下图是被打过分的某旅游区的街道图:

d36_ht1.jpg

阿隆可以从任一路口开始浏览,在任一路口结束浏览。请你写一个程序,帮助阿隆寻找一条最佳的浏览路线,使得这条路线的所有分值总和最大。

2. 删数问题

键盘输入一个高精度的正整数N,去掉其中任意S个数字后剩下的数字按左右次序组成一个新的正整数。对给定的N和S,寻找一种删数规则使得剩下得数字组成的新数最小。

算法专题:贪婪算法 (2008-02-23 15:34:08由localhost编辑)