1、1,第4章 贪心算法,找钱问题,要找给顾客3角7分钱 硬币面值为1角、5分、2分、1分. 贪心算法:1角:3枚; 5分:1枚;2分:1枚 合计:5枚能得到最优解面值为11分、7分、5分、1分, 贪心算法:11分: 3枚; 1分:4枚 合计:7枚 但最优解应该是5枚,贪心法不成功,贪心算法概述,在贪婪算法中采用逐步构造最优解的方法。在每个阶段,都作出一个看上去最优的决策它并不一定对所有问题都成功,但是对某些问题特别简单、有效。决策一旦作出,就不可再更改。作出贪婪决策的依据称为贪婪准则( criterion)。对一些NP完全问题或规模很大的优化问题,可通过仿贪心算法能得到很好的近世解,而用动态规划
2、根本无法解。,4.1 活动安排问题,设有n个活动的集合E=1,2,n,其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si fi 如果选择了活动i,则它在半开时间区间si, fi)内占用资源若区间si, fi)与区间sj, fj)不相交,则称活动i与活动j是相容的也就是说,当sifj或sjfi时,活动i与活动j相容,5,4.1 活动安排问题,例:设待安排的11个活动的开始时间和结束时间按结束时间的非减序排列如下:,6,4.1 活动安排问题,算法greedySelector 的计算过程如左图
3、所示。图中每行相应于算法的一次迭代。阴影长条表示的活动是已选入集合A的活动,而空白长条表示的活动是当前正在检查相容性的活动。,7,4.1 活动安排问题,在下面所给出的解活动安排问题的贪心算法greedySelector : public static int greedySelector(int s, int f, boolean a)int n=s.length;a1=true;int j=1;int count=1;for (int i=2;i=fj) ai=true;j=i;count+;else ai=false;return count;,各活动的起始时间和结束时间存储于数组s和f中
4、且按结束时间的非减序排列,8,4.1 活动安排问题,由于输入的活动以其完成时间的非减序排列,所以算法greedySelector每次总是选择具有最早完成时间的相容活动加入集合A中。直观上,按这种方法选择相容活动为未安排活动留下尽可能多的时间。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。算法greedySelector的效率极高。当输入的活动已按结束时间的非减序排列,算法只需O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。,9,4.2 贪心算法的特点,所求问题的整体最优解
5、可以通过一系列局部最优的选择,即贪心选择来达到。动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。,10,4.2 贪心算法-0-1背包问题,给定n种物品和一个背包。物品i的重量是Wi,其价值为Vi,背包的容量为C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?,在选择装入背包的物品时,对每种物品i只有2种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。,11,4.2 贪心算法-背包问题,背包问题: 与0-1背包问题类似,所不同的是在选
6、择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包,1in。,这2类问题都具有最优子结构性质,极为相似,但背包问题可以用贪心算法求解,而0-1背包问题却不能用贪心算法求解。,至少有三种看似合理的贪心策略: (1)选择价值最大的物品,因为这可以尽可能快地增加背包的总价值。但是,虽然每一步选择获得了背包价值的极大增长,但背包容量却可能消耗得太快,使得装入背包的物品个数减少,从而不能保证目标函数达到最大。(2)选择重量最轻的物品,因为这可以装入尽可能多的物品,从而增加背包的总价值。但是,虽然每一步选择使背包的容量消耗得慢了,但背包的价值却没能保证迅速增长,从而不能保证目标函数达到最大
7、。(3)选择单位重量价值最大的物品,在背包价值增长和背包容量消耗两者之间寻找平衡。,120 50 背包 180 190 200(a) 三个物品及背包 (b) 贪心策略1 (c) 贪心策略2 (d) 贪心策略3,例如,有3个物品,其重量分别是20, 30, 10,价值分别为60, 120, 50,背包的容量为50,应用三种贪心策略装入背包的物品和获得的价值如图所示。,void Knapsack(int n,float M,float v ,float w ,float x ) Sort(n, v, w); /按单位价值排序/int i;for (i =1;i c) break;xi= 1;c-=
8、 wi; if(i= n) xi = c/wi; ,算法分析:,排序为主要算法时间,所以 T(n)=O(nlogn),背包问题的贪心算法,背包问题中的物体不能分拆,只能整个装入称为0-1背包问题.,算法证明:该算法能得到在最优解,用贪心算法能得到0-1背包的最优解吗?,0 / 1背包问题的几种贪婪策略: 1)从剩余的物品中,选出可以装入背包的价值最大的物品 2)从剩下的物品中选择可装入背包的重量最小的物品 3)从剩余物品中选择可装入包的pi /wi 值最大的物品 这三种策略都不能保证得到最优解。0 / 1背包问题是一个NP复杂问题。对于这类问题,也许根本就不可能找到具有多项式时间的算法。,为什
9、么贪心策略不适用于0/1背包问题? 例:c=50, w=(10,20,30), V=(60,100,120) 单价 v/w=(6,5,4) 按贪心策略,应装入前两个物品,价值160;最优解应为装入后两种物品,价值220。原因:贪心法不能保证0/1背包装满,闲置部分使背包价值降低。考虑0/1背包问题,应比较选择wi与不选择wi所导致的结果,然后作出选择,由此导致相互重叠的子问题。所以可用动态规划法。,虽然按pi /wi 非递增的次序装入物品不能保证得到最优解,但它是一个直觉上近似的解。我们希望它是一个好的启发式算法,且大多数时候能很好地接近最后算法。据统计,在600个随机产生的背包问题中,用这种
10、启发式贪婪算法来解有239题为最优解。有583个例子与最优解相差10%,所有600个答案与最优解之差全在25%以内。该算法能在O(n log n)时间内获得如此好的性能,18,4.3 最优装载,有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。 1.算法描述最优装载问题可用贪心算法求解。采用重量最轻者先装的贪心选择策略,可产生最优装载问题的最优解。具体算法描述如下页。,19,例 设n=8,w1,w8=100, 200, 50, 90, 150, 50, 20, 80,c=400。,算法思路 将装船过程划
11、为多步选择,每步装一个货箱,每次从剩下的货箱中选择重量最轻的货箱.如此下去直到所有货箱均装上船或船上不能再容纳其他任何一个货箱。,所考察货箱的次序为 :7, 3, 6, 8, 4, 1, 5, 2。货箱7, 3, 6, 8, 4, 1的 总重量为390个单位且已被装载, 剩下的装载能力为10 ,小于任意 货箱.所以得到解x1,.x8= 1, 0, 1, 1, 0, 1, 1, 1,20,最优装载的贪心算法,template void Loading(int x, Type w, Type c, int n ) int *t = new int n + 1;Sort(w, t, n) ; /按货
12、箱重量排序/for (int i = 1; i = n; i +)xi = 0;for (int i = 1;i= n /调整剩余空间/,算法分析:,排序为主要算法时间,所以 T(n)=O(nlogn),算法证明:该算法能得到最优解.,21,4.4 Huffman编码,Huffman编码用于数据压缩 例:字符出现频率与编码方案a b c d e f 频率 45 13 12 16 9 5 定长码 000 001 010 011 100 101 变长码1 0 1 00 01 10 11 变长码2 0 101 100 111 1101 1100 但是变长码1 的识别困难,因为不具有前缀性质,22,变
13、长码2属于前缀码,即任何一个字符的编码都不是其它字符编码的前缀。这样能保证译码迅速、唯一确定。 例:0010111010 0 101 1101aabe 前缀码的二叉树表示树叶:字符 前缀码:树根到树叶的路径 例:变长码2对应的二叉树,100,55,25,30,a:45,14,f:5,d:16,b:13,c:12,e:9,0,1,0,0,0,0,1,1,1,23,对应前缀码的二叉树为完全二叉树,即每个结点有两个儿子。 n个字符对应n个叶结点,n-1个内部结点。 设字符c在文件中出现频率为f(c),其前缀码编码方案对应一棵二叉树,字符c在树中的深度为d(c),则编码的平均码长为:B(T)= f(c
14、)d(c) 平均码长最小的方案为最优前缀码 Huffman编码是一种最优前缀码,可用贪心算法构造此编码。,24,构造过程: 以|c|个叶结点开始,执行|c|-1次“合并”运算,最后生成T。 字符c的频率为f(c); 队列Q以f(c)为键值存放二叉树各结点,通过贪心选择,将最小频率的两个二叉树合并,然后将新树(频率为上述两个二叉树频率之和)插入Q中。,24,25,设在1000个字母的文章中各字母出现的频率为:a:83, b:14, c:28, d:38, e:131, f:29, g:20, h:53,14 20 28 29 38 53 83 131,34 28 29 38 53 83 131,
15、34 57 38 53 83 131,57 72 53 83 131,72 110 83 131,110 155 131,155 241,396,396,155,241,110,131,53,57,72,83,34,38,28,29,14,20,1,0,1,0,1,0,1,0,1,0,1,0,1,0,最佳编码: a:10 ; b:1111; c:0101; d:110; e:00; f:0100; g:1110; h:011,1)将权从小到大排序 2)每次选取最小权合并,例 题,算法设计与分析 贪心算法 哈夫曼编码,26,templateBinaryTreeHuffmanTree(T f, i
16、nt n)/根据权f1:n构造霍夫曼树/创建一个单节点树的数组Huffman *W=newHuffman n+1;BinaryTree z,zero;for(int i=1;i Q(1);Q.Initialize(w,n,n);/将堆中的树不断合并Huffman x, yfor(i=1;in;i+),Q.DeleteMin(x);Q.DeleteMin(y);z.MakeTree(0, x.tree, y.tree);x.weight+=y.weight;x.tree=z;Q.Insert(x); Q. DeleteMin(x);/最后的树Q. Deactivate();delete w;re
17、turn x.tree;,霍夫曼树算法,27,4.4 哈夫曼编码,在书上给出的算法huffmanTree中,编码字符集中每一字符c的频率是f(c)。以f为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。一旦2棵具有最小频率的树合并后,产生一棵新的树,其频率为合并的2棵树的频率之和,并将新树插入优先队列Q。经过n1次的合并后,优先队列中只剩下一棵树,即所要求的树T。算法huffmanTree用最小堆实现优先队列Q。初始化优先队列需要O(n)计算时间,由于最小堆的DeleteMin和Insert运算均需O(logn)时间,n1次的合并总共需要O(nlogn)计算时间。
18、因此,关于n个字符的哈夫曼算法的计算时间为O(nlogn) 。,28,时间复杂度:用f(c)初始化对列Q的时间:O(n)DeleteMin、Insert需要时间:O(log n)n-1次合并需要时间:O(nlog n)、,29,4.5 单源最短路径问题,有向图G的每条边都有一个非负的长度c i j ,路径的长度即为此路径所经过的边的长度之和。,例:具有五个顶点的有向图,各边上的数即为长度。对于给定源顶点v1,需找出从它到图中其他任意顶点的最短路径。,30,E. Dijkstra的贪婪算法通过分步方法求出最短路径。每一步产生一个到达新的目的顶点的最短路径。下一步所能达到的目的顶点通过如下贪婪准则
19、选取:在还未产生最短路径的顶点中,选择路径长度最短的目的顶点。即按路径长度顺序产生最短路径。最初产生从v 到它自身的路径,这条路径没有边,其长度为0。在贪婪算法的每一步中,产生下一个最短路径。,31,设u是图中某个顶点,将从源点v到u且中间只经过S中顶点的路径称为从源点v到u的特殊路径。 用distu来记录从源点v到u的特殊路径长度。 每次从V-S中寻找distu最小的u,将u从V-S中去掉,加入S中。 u加入S可能导致distj减少, (jV-S),这是由于新增的路径(v)(u)(j)有可能比原路径(v)(j)短,故用distu+c(u,j)取代原来的distj。,S,v,u1,u2,V-S
20、,用贪心选择来扩充集合S。起初,S中仅包含源点v。 某顶点uS 从源点v到u的最短路径已知。,32,算法设计与分析 贪心算法,算法描述:(1) 用带权的邻接矩阵c来表示带权有向图, cij表示弧上的权值. 若 V,则置cij为设S为已知最短路径的终点的集合,它的初始状态为v.从源点v到图上其余各点vi的当前最短路径长度的初值为:disti=cvi viV-S(2) 选择vj, 使得distj=Mindisti | viV-Svj就是长度最短的最短路径的终点。令S=SUj(3) 修改从v到集合V-S上任一顶点vk的当前最短路径长度:如果 distj+cjk distk 则修改 distk= di
21、stj+cjk (4) 重复操作(2),(3)共n-1次.,33,算法设计与分析 贪心算法 单源最短路径,例 题,1) v1 v2: 10,2) v1 v3: 50,3) v1 v4: 30,4) v1 v5: 60,34,算法设计与分析 贪心算法,voidDijkstra(int n, int v,Type dist, int prev, Type *c) bool smaxint;for (int i=1;i=n; i+)disti=cvi;si=false;if(disti= =maxint) previ=0;else previ=v ; distv=0;sv=true; for (in
22、t i=1;in;i+) int temp=maxint;int u= v; for (int j = 1;j=n;j+),if (!sj) ,单源最短路径问题的Dijkstra算法,分析:用带权邻接矩阵表示有n个顶点和e条边的带权有向图,主循环体需要O(n)时间,循环需要执行n-1次,所以完成循环需要O(n2).,35,算法说明: Si=false,表示顶点i不在S集合中开始,只有Sv=true Previ存放顶点i的前驱结点,即从源点到顶点i的最短路径上i的前一个结点。 当distu+cuidisti时, Previ=u 根据Prev的值,产生最短路径。例: Prev2=1, Prev3=
23、4,Prev4=1, Prev5=3 顶点1到顶点5的路径: 1435,1,2,3,4,5,36,计算复杂性,对于具有n个顶点和e条边的带权有向图,如果用带权邻接矩阵表示这个图,那么Dijkstra算法的主循环体需要 时间。这个循环需要执行n-1次,所以完成循环需要 时间。算法的其余部分所需要时间不超过 。,37,4.6 最小生成树,图G=(V,E)是无向连通带权图,即一个网络。若子图G包含G的全部顶点,则称G为G的生成树。生成树上各边的权之和称为生成树的耗费。耗费最小的生成树叫最小生成树。 最小的生成树在通信网络、电路设计中应用广泛。 贪心算法可用于构造最小生成树(Prim 算法或Krusk
24、al算法),38,算法设计与分析 贪心算法 最小生成树,问题陈述:设G(V,E)是一个无向连通带权图。E中每条边(v, w)的权为cvw,若G的一个子图G是一棵包含G的所有顶点的树,则称G为G的生成树。生成树各边权的总和称为该生成树的耗费。在G的所有生成树中,耗费最小的生成树称为G的最小生成树.,抽象描述: 输入:任一连通生成子图 (该子图的边集合)可行解:图的生成树, 优化函数:生成树的各边权值之和最优解:使优化函数达到最小的生成树.,4.6 最小生成树,39,最小生成树性质: 设G=(V,E)是连通带权图,U是V的真子集。若(u,v) E,uU, vV-U, 且在所有这样的边中, (u,v
25、) 的权cuv最小,那么一定存在G的一棵最小生成树包含边(u,v)。,U,V-U,u,u ,v ,v,40,算法思路: 首先置S=1, T= .若SV, 就作如下的贪心选择:选取满足条件iS, jV-S,且cij最小的边(i, j),将顶点j添加到S中边(i, j)添加到T中.这个过程一直进行到S=V时为止. T中的所有边构成G的一棵最小生成树。,void Prim(int n, Type * * c) T= ;S =1;while (S!= V) (i, j) = i S且 jV- S的最小权边;T=TU(i,j);S= S Uj; ,算法描述,Prim算法,设G=(V,E)是一个连通带权图
26、, y=l, 2, , n。,例 题,算法设计与分析 贪心算法 最小生成树,41,例 题,算法设计与分析 贪心算法 最小生成树,42,算法说明: T中最后包含n-1条边。 时间复杂度O(n2)为了找出满足iS, j V-S且cij最小的边(i,j),设置两个数组:closest和Lowcostclosestj表示j (j V-S)在S中一个邻接顶点,并且cjclosestj cjk,k是j在S中的其它邻接顶点Lowcostj= cjclosestj,43,Template Void Prim(int n,Type *c) Type lowcostmaxint;int closestmaxint
27、;bool smaxint;s1=true;for(int i=2;i=n;i+) lowcosti=c1i;closesti=1;si=false;for(int i=1;in;k+)/n-1条边 Type min=inf;int j=1;for(int k=2;k=n;k+)/每次从V-S中选使lowcost距离最小的点jif(lowcostkmin),sj=true; /j加入S后,引起lowcost的变化 for(int k=2;k=n;k+)if(cjklowcostk ,44,Kruskal算法:(假设有n个顶点,e条边) 1)将图G的n个顶点看作n个孤立的连通分支 2)将边按权值
28、从小到大排列 3)在不构成环路的情况下,依次选取权值最小的边,直到有n-1条边为止。 当查看第k条边(v,w)时,若v和w属于两个不同的连通分支T1和T2,就用边(v,w)将T1和T2连成一个连通分支,并继续处理第k+1条边。若v和w属于同一个连通分支,则直接查看第k+1条边。,45,例 题,算法设计与分析 贪心算法 最小生成树,46,template bool Kruskal(int n, int e, EdgeNode E , EdgeNode t )MinHeap H(1);H. initialize(E, e, e);UnionFind U(n);int k = 0;while (e
29、,e-; int a = U.Find(x.u);int b = U.Eind(x.v);/a=b,表同一连通分支 if (a! = b) tk + = x;U. Union(a, b);H. Deactivate( ) renturn (k = = n-1),Kruska算法,e1(1),e2(6),e8(9),e6 (2),e9(4),e5(5),e4(7),e10(8),e11(3),e7(10),e3(11),1) 以G 中全部点为点作图,2) 按权的大小次序依次添加 各边,若出 现回路则忽略此边.,3) 加入n-1条边后就得到最小生成树.,1,2,5,3,7,求最小生成树(Krusc
30、al),最优解: (e1, e6, e11, e5, e4),48,按权的递增顺序查看等价于对优先队列执行removeMin运算。可以用堆实现这个优先队列。Kruskal算法的时间复杂度为O(e log e) 。 Prim 算法或Kruskal算法的比较两个算法的时间复杂度分别为O(n2) 和O(e log e) 当图的边数较少时,Kruskal算法优于Prim算法;当图的边数较多时,用Prim算法为好。,49,4.7 多机调度问题,N个独立的作业1,2,n,m台相同的机器。每个作业需要在机器上加工ti时间。每个作业不能拆分成更小的作业,只能在一台机器上完成,而且处理过程不能中断。 如何调度,
31、使得全部作业在最短时间内完成? 当n m时,很简单。 当n m时,属于NP完全难题,迄今未有效解决。,50,4.7 多机调度问题,例如,设7个独立作业1,2,3,4,5,6,7由3台机器M1,M2和M3加工处理。各作业所需的处理时间分别为2,14,4,16,6,5,3。按算法greedy产生的作业调度如下图所示,所需的加工时间为17。,贪心策略: 处理时间长的作业优先调度。将n个作业按处理时间,由大到小排序,依次分给空闲机器。,51,算法设计与分析 贪心算法,class JobNode friend void Greedy(JobNode * , int, int);friend void m
32、ain(void);public:operator int () const return time; private:int iD,time; class MachineNode friend void Greedy(JobNode *, int, int);public:operator int( ) const return avail; private:int iD,avail; ,多机调度问题的贪心近似算法,52,算法设计与分析 贪心算法,多机调度问题的贪心近似算法,template void Greedy(Type a,int n,int m) if (nH(m); Machine
33、Node x; /m台机器初始化,占用时间均为0 for(int:i=1;i=1;i-)HDeleteMin(x);cout”将机器”xiD“从” xavail“到”(xavail十aitime)”的时间段分配给作业”ai. iDendl;xavail+=aitime;Hinsert(x); ,53,4.7 多机调度问题,采用最长处理时间作业优先的贪心选择策略可以设计出解多机调度问题的较好的近似算法。按此策略,当 时,只要将机器i的0, ti时间区间分配给作业i即可,算法只需要O(1)时间。当 时,首先将n个作业依其所需的处理时间从大到小排序。然后依此顺序将作业分配给空闲的处理机。算法所需的计算时间为O(nlogn)。,