算法设计与分析复习(二):算法设计策略-贪心法

来源:互联网 发布:下载壁纸软件 编辑:程序博客网 时间:2024/06/06 09:43

参考书籍:算法设计与分析——C++语言描述(第二版)

算法设计策略-贪心法

贪心法

  • 贪心法是一种求解最优化问题的算法设计策略贪心法是通过分步决策(stepwise decision)的方法来求解问题的。贪心法在求解问题的每一步上做出某种决策,产生n-元组解的一个分量。贪心法要求根据题意,选定一种最优量度标准(optimization criterion)或贪心准则(greedy criterion),也称贪心选择性质(greedy choice property)。这种量度标准通常只考虑局部最优行
  • 在初始状态下,解向量solution=,其中不包含任何分量。使用最优量度标准,一次选择一个分量,逐步形成解向量(x0,x1,,xn1)。算法执行过程中生成的向量(x0,x1,,xk),k<n,称为部分解向量,也称部分向量(partial vector)
  • 在根据最有量度标准选择分量的过程中,还需要使用一个可行解判定函数。设(x0,x1,,xk1)是贪心算法已经生成的部分解,根据最优量度标准,算法当前选取解向量的第k(k<n)个分量为xk,此时需要使用可行解判定函数来判断,在添加新的分量xk后所形成部分解(x0,x1,,xk)是否违反可行解约束条件。
  • 贪心法之所以被称为是贪心的,是因为它希望每一步决策都是正确的,即要求在算法的每一步上,仅根据最有量度标准选择分量,并只需要保证形成的部分解不违反约束条件,最终得到n-元组不仅是可行解,而且必定是最优解。
  • 事实上,最优量度标准一般并不从整体考虑,它只是在某种意义上的局部最优选择,每一步决策只是在当前看来是最优的。因此,贪心法不能保证对所有问题都得到整体最优解。
//贪心法框架SolutionType Greedy(SType a[], int n){  //初始时,解向量不包含任何分量  SolutionType solution=EmptySet;  //多步决策,每次选择解向量的一个分量  for(int i = 0; i<n;i++){    //遵循最优量度标准选择一个分量    SType x= Select(a);    //判定加入分量x后的部分解是否可行    if(Feasible(solution, x))      //形成新的部分解      solution=Union(solution, x);  }  return solution;//返回生成的最优解}

对于一个贪心算法,必须进一步证明该算法的每一步上所做出的选择,都必然最终导致问题的一个整体最优解。

背包问题

问题描述

背包问题:已知一个载重为M的背包和n件物品,第i件物品的重量为wi,如果将第i件物品全部放进背包,将有收益pi这里,wi>0,pi>0,0i<n。所谓背包问题,是指求一种最佳装载方案,使得收益最大。

  • 0/1背包问题:每一件物品不能分割,只能作为整体装入背包,或者不能装入。
  • 一般背包问题:物品时可以分割的,允许将其中的一部分装入背包。

用贪心法求解 0/1背包问题只能求得近似解。

一般背包问题的贪心法求解

一般背包问题:

max i=0n1pixis.t. i=0n1wixiMwi>0pi>00xi10i<n

最优量度标准:选择使单位重量收益最大的物品装入背包,即按pi/wi的非增次序选取物品。

//背包问题的贪心算法template<class T>  class Knapsack  {    public:    //创建一个一维数组w和p,并赋初值    Knapsack(int mSize, float cap, float *wei, T *prof);    //数组x为背包问题的最优解    void GreedyKnapsack(float* x);    ...      private:    float m, *w;//m为背包载重量,w指示存储n个物品重量的数组    T* p;//p指示存储n个物品收益的数组;    int n;//n为物品数量  };template<class T>  vois Knapsack<T>::GreedyKnapsack(float* x)  {    //前置条件:w[i]已按p[i]/w[i]的非增次序排列;    float u = m;//u为背包剩余载重量,初始时为m    for(int i = 0;i<n;i++)      x[i]=0;//对解向量初始化    for(i = 0;i<n;i++){      //按最优量度标准选择解的分量      if(w[i]>u)        break;      x[i]=1.0;      u=u-w[i];    }    if(i<n)      x[i]=u/w[i];  }

定理:如果p0/w0p1/w1pn1/wn1,则以上程序求得的背包问题的解是最优解。

C语言实验:设有载重M=20的背包,3件物品的重量为(w0,w1,w2)=(18,15,10),物品装入的收益为(p0,p1,p2)=(25,24,15)

#include <stdio.h>#include <stdlib.h>void GreedyKnapsack(float p[], float w[], float x[], int n, float m);void SortW(float* p, float* w, int n);int main(){    float p[3] = { 25,24,15 }, w[3] = { 18,15,10 };    float x[3] = { 0,0,0 };    float M = 20;    //对w[i]进行排序,按p[i]/w[i]的非增次序排列    SortW(p, w, 3);    printf("after sorting:\n");    for (int i = 0; i < 3; i++) {        printf("p[%d] = %f ", i, p[i]);    }    printf("\n");    for (int i = 0; i < 3; i++) {        printf("w[%d] = %f ", i, w[i]);    }    printf("\n");    GreedyKnapsack(p, w, x, 3, M);    printf("The solution is:\n");    for (int i = 0; i < 3; i++) {        printf("x[%d] = %f ", i, x[i]);    }    printf("\n");    system("pause");    return 0;}void SortW(float* p, float* w, int n){    int i = 0, j = n;    float tmp = 0;    for (i = 0; i < n; i++) {        for (j = 0; j < n - i-1; j++) {            if (p[j] / w[j] < p[j + 1] / w[j + 1]) {                tmp = p[j];                p[j] = p[j + 1];                p[j + 1] = tmp;                tmp = w[j];                w[j] = w[j + 1];                w[j + 1] = tmp;            }        }    }}void GreedyKnapsack(float p[], float w[], float x[], int n, float m){    //前置条件:w[i]已按p[i]/w[i]的非增次序排列;    float u = m;//u为背包剩余载重量,初始时为m    int i = 0;    for (i = 0; i<n; i++)        x[i] = 0;//对解向量初始化    for (i = 0; i<n; i++) {        //按最优量度标准选择解的分量        if (w[i]>u)            break;        x[i] = 1.0;        u = u - w[i];    }    if (i<n)        x[i] = u / w[i];}

实验结果:

after sorting:p[0] = 24.000000 p[1] = 15.000000 p[2] = 25.000000w[0] = 15.000000 w[1] = 10.000000 w[2] = 18.000000The solution is:x[0] = 1.000000 x[1] = 0.500000 x[2] = 0.000000请按任意键继续. . .

带时限的作业排序

问题描述

带时限得到作业排序问题:设有一个单机系统、无其他资源限制且每一个作业运行时间相等,假定每一个作业运行1个单位时间。现在有n个作业,每一个作业都有一个截止期限di>0,di为整数。如果作业能够在截止期限之内完成,可获得Pi>0的收益。问题要求得到一种作业调度方案,该方案给出作业的一个子集和该作业自己的一种排列,使得若按照这种排列次序调度作业运行,该子集中的每一个作业都能如期完成,并且能够获得最大收益。

贪心法求解

设n个作业以编号0 n1标识,每一个作业有唯一的作业编号,I=0,1,,n1是n个输入作业的集合。带时限的作业排序问题的解是I的一个子集X,可表示成一个n-元组:X=(x0,x1,,xr1),0<rn。每一个xi(0xin1)是一个作业的编号。如果存在某种排列次序,使得按该次序调度执行X中作业,X中所有的作业都能在子集的截止期限前完成,X为问题的一个可行解,可行解中的收益最大的解是最优解。

  • 最优量度标准:选择一个作业加入到部分解向量中,在不违反截止时限的前提下,使得至少就当前而言,已选入部分解向量中的那部分作业的收益最大。
//带时限作业排序的贪心算法void GreedyJob(int d[], Set X, int n){  //前置条件:p0>=p1>=p2>=...>=p(n-1)  X={0};  for(int i =1;i<n;i++){    //可行解判定    if(集合XU{i}中的作业都能在给定的时限内完成)      X=XU{i};  }}

定理X=(x0,x1,,xk)是k个作业的集合,α=(α0,α1,,αk)X的一种特定排列,它使得dα0dα1dαk,其中,dαj是作业αj的时限。X是一个可行解当且仅当X中的作业能够按照α次序调度而不会有作业超期。

  • 可行解判定方法:对任意一个部分解作业子集X=(x0,x1,,xk),使X中的作业按时限的非递减次序排列,设α=(α0,α1,,αk),dα0dα1dαkX这样的排列,为了判定α是否为可行的调度方案,只需对每个作业αj判断dαj>j+1是否成立。

设作业已按照收益非增次序排列,即p0p1pn1]。贪心算法按收益从大到小次序考察作业。初始时,x[0]=0,x=(x[0])只有一个分量,表示部分解向量x中当前已加入收益最大的作业。在一般情况下,设算法考察作业jx=(x[0],x[1],,x[k])为当前已入选的作业向量,且d[x[0]]d[x[1]]d[x[k]]。由于当前解是可行的,所以d[x[i]]i+1,0ik

为了判断作业j是否允许添加到部分解向量中去,具体做法是:将作业j按时限的非减次序插入向量(x[0],x[1],,x[k])中的某个位置,使得插入作业j后,由k+1个分量组成的部分解向量仍按时限的非减次序排列。假设作业j被插于下标r+1处。为了在位置r+1处添加作业j,作业x[r+1],,x[k]在向量中的位置都要向后移一位,形成一个新的部分解向量。为了保证在添加作业j后的作业子集仍构成可行解,必须满足以下两个要求:

  • d[x[j]]>j+1,r+1jk,否则作业x[r+1],,x[k]的后移将导致其中某些作业超期。
  • d[j]>r+1,否则作业j自己无法在时刻r+2前完成。
//带时限的作业排序程序int JS(int *d, int *x, int n){  //前置条件:p0>=p1>=...>=p(n-1)  int k = 0,x[0]=0;  for(int j = 1; j<n;j++){    int r=k;    while(r>=0 && d[x[r]]>d[j] && d[x[r]]>r+1)      r--;//搜索作业j的插入位置    //若条件不满足,选下一个作业    if((r<0||d[x[r]]<=d[j]) && d[j]>r+1){      //将x[r]以后的作业后移      for(int i = k;i>=r+1;i--){        x[i+1]=x[i];      }      //将作业j插入r+1处      x[r+1]=j;      k++;    }  }  return k;}

该程序最坏时间复杂度为O(n2)

实验:设有4个作业,每个作业的时限为(d0,d1,d2,d3)=(2,1,2,1),收益为(p0,p1,p2,p3)=(100,10,15,27)

#include <stdio.h>//带时限的作业排序算法int JS(float *p, int *d, int *x, int n);void SortP(float *p, int *d, int n);int main(){    int n = 4,k=0, d[4]={2,1,2,1};    float p[4] = {100,10,15,27};    int x[4]={0};    k = JS(p,d,x,n);//k+1表示解的个数    for(int i = 0; i<n;i++){        printf("p[%d]=%f ", i, p[i]);    }    printf("\n");    for(int i = 0; i<n;i++){        printf("d[%d]=%d ", i, d[i]);    }    printf("\n");    for(int i = 0; i<=k;i++){        printf("x[%d]=%d ", i, x[i]);    }    return 0;}int JS(float *p, int *d, int *x, int n){    SortP(p,d,n);    //前置条件:p0>=p1>=...>=p(n-1)    int k = 0;    x[0]=0;    for(int j = 1; j<n;j++){        int r=k;        while(r>=0 && d[x[r]]>d[j] && d[x[r]]>r+1)            r--;//搜索作业j的插入位置        //若条件不满足,选下一个作业        if((r<0||d[x[r]]<=d[j]) && d[j]>r+1){            //将x[r]以后的作业后移            for(int i = k;i>=r+1;i--){            x[i+1]=x[i];            }            //将作业j插入r+1处            x[r+1]=j;            k++;        }    }    return k;}void SortP(float *p, int *d, int n){    float tmp = 0;    int i = 0, j = 0;    for(i = 0; i<n; i++){        for(j = 0; j<n-i-1; j++){            if(p[j]<p[j+1]){                tmp = p[j];                p[j] = p[j+1];                p[j+1] = tmp;                tmp = d[j];                d[j] = d[j+1];                d[j+1] = tmp;            }        }    }}

实验结果:

p[0]=100.000000 p[1]=27.000000 p[2]=15.000000 p[3]=10.000000d[0]=2 d[1]=1 d[2]=2 d[3]=1x[0]=1 x[1]=0

结果表明:运行收益为100和27的任务,先运行收益为27时限为1的任务

最佳合并模式

问题描述

合并n个有序子文件成为一个有序文件的合并过程可以有多种方式,称为合并模式。每执行一次合并需要将两个有序文件的全部记录依次从外村读入内存,还需要将合并后的新文件写入外村。在整个合并过程中,需从外存读/写的记录数最少的合并方案称为最佳合并模式(optimal merge pattern)。

可以用合并树来描述一种合并模式。两路合并排序过程中所需读/写的记录总数正是对应两路合并树的带权外路径长度,带权外路径长度是针对扩充二叉树而言的。扩充二叉树(extended binary tree)中除叶子结点外,其余结点都必须要有两个孩子。扩充二叉树的带权外路径长度(weighted external path length)定义为:

WPL=k=1mwklk

式中,m是叶子结点的个数,wk是第k个叶子结点的权,lk是从根到该叶子结点的路径长度。

贪心法求解

贪心法是一种多步决策的算法策略,一个问题能够使用贪心法求解,除了具有贪心法问题的一般特征外,关键问题是确定最优量度标准。两路合并最佳模式问题的最优量度标准为带权外路径长度最小。

两路合并最佳模式的贪心算法如下:

  1. W=w0,w1,,wn1是n个有序文件的长度,以每一个权值作为根节点值,构造n颗只有根的二叉树;
  2. 选择两颗根节点权值最小的树,作为左右子树构造一颗新二叉树。新树根的权值是两颗子树根的权值之和;
  3. 重复步骤2,直到合并成一棵二叉树为止。
//两路合并最佳模式的贪心算法template<class T>  struct HNode  {    //优先权队列中的元素类型    operator T()const{ return weight;}    BTNode<T> *ptr;    T weight;  };template<class T>  BTNode<T>* CreateHfmTree(T* w, int n){  //w为一维数组保存n个权值;  PrioQueen<HNode<T>> pq(2*n-1);//创建长度为2n-1的优先权队列pq  BTNode<T> *p;  HNode<T> a,b;  //权值作为根,构造n颗只有根的二叉树;  for(int i=0;i<n;i++){    //创建合并树的新结点    p=new BTNode<T>(w[i]);    //对象a包含指向根的指针和根的权值    a.ptr=p;    a.weight=w[i];    //将指向根的指针和根的权值进队列pq    pq.Append(a);  }  //两两合并n-1次,将n颗树合并成一颗  for(int i=1;i<n;i++){    //从pq依次取出根权值最小的两棵树;    pq.Serve(a);    pq.Serve(b);    a.weight += b.weight;    //将取出的两颗树合并,构造一颗新的二叉树;    p=new BTNode<T>(a.weight,a.ptr,b.ptr);    a.ptr=p;    //将指向新根的指针和根的权值进队列pq    pq.Append(a);  }  //取出生成的最佳合并树  pq.Serve(a);  return a.ptr;//a.ptr指向最佳合并树的根}

最小代价生成树

问题描述

一个无向连通图的生成树是一个极小连通子图,它包括图中全部的结点,并且尽可能少的边。遍历一个连通图得到图的一颗生成树。

一颗生成树的代价是树中各条边上的代价之和。一个网络的各个生成树中,具有最小代价的生成树称为该网络的最小代价生成树(minimum-cost spanning tree)

贪心法求解

一个无向图的所有生成树都可看成是问题的可行解,其中代价最小的生成树就是所求的最优解,生成树的代价是问题的目标函数。

//最小代价生成树的贪心算法ESetType SpanningTree(ESetType E, int n){  //G=(V,E)为无向图,E是图G的边集,n是图中结点个数  ESetType S = EmptySet;//S为生成树上边的集合  int u,v,k=0;  EType e;//e=(u,v)为一条边  //选择生成树的n-1条边  while(k<n-1 && E中尚有未检查的边){    //按照最优量度标准选择一条边    e=select(E);    //判定可行性    if(S U e不包含回路){      //在生成树边集S中添加一条边      S=S U e;      k++;    }  }  return S;}

最简单的最优量度标准是:选择使得迄今为止已入选S中边的代价这和增重最小的边。对于最优量度标准的不同解释产生不同的构造最小代价生成树算法,对于上述最优量度标准有两种可能的理解,它们是普里姆(Prim)算法克鲁斯卡尔(Kruskal)算法

库鲁斯卡尔算法的贪心准则是:按边代价的非减次序考察E中的边,从中选择一条代价最小的边e=(u,v)。这种做法使得算法在构造生成树的过程中,边集S代表的图不一定是连通的。普里姆算法的贪心准则是:在保证S所代表的子图是一棵树的前提下选择一条最小代价的边e=(u,v)

普里姆(prim)算法

G=(V,E)是带权的连通图,F=(U,S)是图G的子图,它是正在构造中的生成树。普里姆算法从U=v0S=开始构造最小代价生成树,其中v0,v0V是任意选定的结点。普里姆算法的具体选边准则是:寻找一条边(u,v),它是所有一个端点u在构造的生成树上(uU),而另一个端点v不在该树上(vVU)的这些边中代价最小的边。算法按照上述选边准则,选取n-1条满足条件的最小边加到生成树上,最终U=V。这时,T=(V,S)是图G的一颗最小代价生成树。

设无向图中结点数为n,很明显,普里姆算法的时间复杂度是O(n2)

库鲁斯卡尔(Kruskal)算法

G=(V,E)是带权的连通图,F=(U,S)是正在构造中的生成树。初始时,U=v0S=克鲁斯卡尔的选边准则是:在E中选择一条权重最小的边(v,u),并将其从E中删除;若在S代表的子图中加入边e=(u,v)后不形成回路,则将该边加入S中。这就要求结点u和v分属于生成森林F的两颗不同的树上,当边(u,v)加入S后,这两颗树合并成一棵树。如果在S中加入e会形成回路,则应舍弃此边,继续选择下一条边,直到S中包含n-1条边,此时F=(V,S)便是图G的一颗最小代价生成树。也可能在所有的边都考察后|S|<n1,这种情况说明原图不是连通的。

设无向图有n个结点和e条边,一般有en,所以克鲁斯卡尔算法的时间复杂度为O(eloge)。克鲁斯卡尔算法对边数较少的带权图有较高效率。

定理:设图G=(V,E)是一个带权连通图,U是V的一个真子集。若边(u,v)E是所有uU,vVU的边中权值最小者,那么一定存在G的一颗最小代价生成树T=(V,S),(u,v)S。这一性质称为MST(minimum spanning tree)性质。

定理:普里姆算法和克鲁斯卡尔算法都将产生一个带权无向连通图的最小代价生成树。

单源最短路径

问题描述

单源最短路径问题是:给定带权的有向图G=(V,E)和图中结点sV,求从s到其余各结点的最短路径,其中s称为源点。这里所指的路径长度是指路径上的边所带的权值之和,并假定边上的权值为非负值。

贪心法求解

从源点到另一个结点的任意一条路径均可看成是问题的可行解,其中长度最小的路径就是所求的最优解,路径的长度是问题的目标函数。

迪杰斯特拉(Dijkstra)提出了按路径长度的非递减次序逐一产生最短路径的算法:首先求得长度最短的一条最短路径,再求得长度次短的一条最短路径,其余类推,直到从源点到其他所有结点之间的最短路径都已被求出为止。

S={v0,v1,,vk}是已求得的最短路径的结点集合,一个结点vi属于S当且仅当从源点s到vi的最短路径已经计算,单源最短路径的最优量度标准是:使得从s到S的所有结点的路径长度之和的增量最小。所以迪杰斯特拉算法总是在集合VS中选择“当前最短路径”长度最小的结点加入集合S中。

迪杰斯特拉(Dijkstra)算法

设集合S存放已经求得最短路径的终点,则VS为尚未求得最短路径的终点集合。初始状态时,集合S中只存在一个源点,设为结点s。迪杰斯特拉算法的具体做法是:首先将源点s加入到S中;在算法的每一步中,按照最短路径值的非减次序,产生下一条最短路径st,并将该路径的终点tVS加入S中;直到S=V,算法结束。

在算法执行中,一个结点tVS当前最短路径,是一条从源点s到结点t的路径,在该路径上,除结点t外,其余结点都属于S,当前最短路径是所有这些路径中的最小者。于是可将最优量度标准设计为:从VS中选择当具有最短的“前最短路径”的结点加入集合S中。

迪杰斯特拉算法用到以下数据结构:

  1. 一维数组d[i]中存放从源点s到结点i的当前最短路径的长度。
  2. 一位整型数组path[i]存放从源点s到结点i的当前最短路径上,结点i的前一个结点。因此,从源点到结点i的路径可以根据path的反向溯源来创建。
  3. 一维布尔数组inS,若inS[i]为true,表示结点i在S中;否则,表示i不在VS中。

迪杰斯特拉算法计算过程
+ 求第一条最短路径

在初始状态下,集合S中只有一个源点s,S=s,所以

d[i]={w(s,i)<s,i>E<s,i>E

式中,w(s,i)是边<s,i>的权值。所以对于所有结点i,d[i]有当前最短路径的长度。

第一条最短路径是所有最短路径中的最短者,它必须只包含一条边<s,k>,并满足

d[k]=min{d[i]|iV{s}}

  • 更新d和path

    将结点k加入到S中,并对所有的iVS按式d[k]=min{d[i]|iV{s}}修正,即

    d[j]=min{d[j],d[k]+w(k,j)}

    式中,w(k,j)是边<k,j>的权值。

  • 求下一条最短路径

    VS中,选择具有最短的当前最短路径值的结点k,满足d[k]=min{d[i]|iVS}

算法的主要步骤如下(有向图用邻接矩阵表示):

  • 初始化:创建长度为n的一维数组inS、d和path,并将每个inS[i]初始化为false,d[i]为a[v][i],如果ivd[i]<,则path[i]=v,否则path[i]=1
  • 将源点v加入集合S中:inS[v]=true;d[v]=0
  • 使用for循环,按长度的非递减次序,依次产生n-1条最短路径:选出最小的d[k];将结点k加入集合S中,inS[k]=true;使用内层for语句更新数组d和path的值,使得其始终代表当前最短路径。

上述迪杰斯特拉算法的时间复杂度为O(n2)

磁带最优存储

单带最优存储

问题描述

设有n个程序编号分别为0,1,,n1,要存放在长度为L的磁带上,程序i在磁盘上的存储长度为ai,0i<n。假定存放在磁带上的程序随时可能被检索,且磁带在每次检索前均已倒带到最前端。那么,如果n个程序在磁带上的存放次序一维γ=(γ0,γ1,,γn1),则检索程序γk所需的时间tkki=0aγi成正比。假定每个程序被检索的概率相等,则平均检索时间(mean retrieval time, MRT)定义为1nn1k=0tk。单带最优存储问题是求这n个程序的一种排列,使得MRT有最小值。这也等价于求使D(γ)=n1k=0ki=0aγi有最小值的排列。

贪心法求解

最优量度标准:计算迄今为止已选的那部分程序的D值,选择下一个程序的标准应使得该值的增加最小。

定理:设有n个程序{0,1,,n1},程序i的长度为ai,0i<n,且有a0a1an1γ=(γ0,γ1,,γn1)是这n个作业的一种排列。那么只有当γj=j,0j<n时,D(γ)=n1k=0ki=0aγi有最小值。

多带最优存储

问题描述

设有m(m>1)条磁带T0,T1,,Tm1和n个程序,要求将此n个程序分配到这m条磁带上,令Ij(0j<m)是存放在第j条磁带上的程序子集的某种排列,D(Ij)的定义与前面相同,那么,求m条磁带上检索一个程序的平均检索时间的最小值等于求0j<mD(Ij)的最小值。令TD=0j<mD(Ij)。多带最优存储问题是求n个程序在m条磁带上的一种存储方式,使得TD有最小值。

贪心法求解

在多带情况下计算最优平均检索时间,可以先将程序按照长度的非减次序排列,即a0a1an1,其中ai(0i<n)是程序i的长度。从程序0和磁带T0开始分配,将程序0放在T0上,程序1存放在T1上,…….,程序员m-1存放在Tm1上,接着再将程序m存放在T0上…….。一般将程序i存放在磁带imodm上。在这种存储方式下,每一条磁带上的程序都是按长度非减次序排列的。

//多带最优存储#include <iostream.h>void Store(int n, int m)  {    int j=0;    for(int i=0;i<n;i++){        cout<<"将程序"<<i<<"加到磁带"<<j<<endl;        j=(j+1)%m;    }    cout<<endl;  }

定理:设有n个程序0,1,,n1,程序i的长度为ai(0i<n),且有a0a1an1,以上程序实现将n个程序分配到m条磁带上,且有最小TD值。

贪心法的基本要素

一般来说,适用于贪心法求解的问题大都具有下面两个特征:最优量度标准和最优子结构。

最优量度标准

所谓贪心法的最优量度标准,是指可以根据该量度标准,实行多步决策进行求解,虽然在该量度意义下所做的这些选择都是局部最优的,但最终得到的解却是全局最优的。选择最优量度标准是使用贪心法求解问题的核心问题。值得注意的是,贪心算法每步做出的选择可以依赖以前做出的选择,但决不依赖将来的选择,也不依赖于子问题的解。虽然贪心算法的每一步选择也将问题简化为一个规模更小的子问题,但由于贪心算法每一步选择不依赖子问题的解,每步选择只按最优量度标准进行,因此,对于一个贪心算法,必须证明所采用的量度标准能够导致一个整体最优解

贪心法的当前选择可能会依赖于已经做出的选择,但不依赖于尚未做出的选择和子问题,因此它的特征是自顶向下,一步一步地做出贪心决策。

最优子结构

所谓最优子结构特征是 关于问题最优解的特征。当一个问题的最优解中包含了子问题的最优解时,则称该问题具有最优子结构特性(optimal substructure)。一个可用贪心法求解的问题往往呈现最优子结构特性。

一般而言,如果一个最优化问题的解结构具有元组形式,并具有最优子结构特性,我们可以尝试选择量度标准。如果经过证明(一般是归纳法),确认该量度标准能够导致最优解,便可容易地按贪心法的算法框架设计出求解该问题的具体的贪心算法。

并非所有的具有最优子结构特性的最优化问题,都能够找到最优量度标准,此时,可以考虑采用动态规划法来求解

小结

一个问题能够用贪心策略的条件是该问题的解是向量结构的,具有最优子结构特性,还要求能够通过分析问题获取最优量度标准。但是,按照该量度标准一次生成解的分量所形成的解是否确实是最优解仍需证明。

原创粉丝点击