算法分析与设计 CH4 贪心算法

 

目录

贪心算法

贪心算法的基本要素:

活动安排问题

0-1背包问题和背包问题

最优装载问题

哈夫曼编码

单源最短路径

最小生成树

多机调度问题

贪心算法

  • 贪心:利益最大化,或者代价(耗费)最小化
  • 贪心算法:就是一步步地做选择,每一步总是作出当前看来最好的选择(局部最优),不从整体考虑
  • 贪心算法对有些问题可以快速获得整体最优解,对有些问题虽不能得到整体最优解,确实近似最优解

贪心算法的基本要素:

  • 贪心选择性质: 是指所求问题的整体最优解,可以通过一系列局部最优的选择,即贪心选择来达到。
     证明方法:局部最优最终导致问题的整体最优解  具体方法:1.交替证明 2.数学归纳法
  • 最优子结构性质:一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质
     证明方法:问题的最优值包含其子问题的最优值  具体方法:反证法
  • 贪心算法与动态规划算法的差异:
     相同点:都要求问题具有最优子结构性质
     不同点:贪心算法要求问题具有贪心选择性质
      计算方式不同:
            动态规划算法通常以自底向上的方式解各子问题
            贪心算法以自顶往下的方式进行,每做一次贪心选择就将问题变为规模更小的子问题。

活动安排问题

  • 问题描述:n个需要使用某个公共资源的活动。S={a1,a2,...an},a1在半开区间[s1,f1)使用资源,其中s1=开始时间,f1=结束时间。目标:安排最大可能的相容的活动的集合   相容:活动时间不冲突 
  • 贪心选择性质:按照活动结束时间排序,即从小到大得到一个序列E{1,2,…n}。由于E中活动安排安结束时间的非减序排列,所以活动1具有最早完成时间。首先证明活动安排问题有一个最优解以贪心选择开始,即该最优解中包含活动1.设a是所给的活动安排问题的一个最优解,且a中活动也按结束时间非减序排列,a中的第一个活动是活动k。如k=1,则a就是一个以贪心选择开始的最优解。若k>1,则我们设b=a-{k}∪{1}。由于end[1] ≤end[k],且a中活动是互为相容的,故b中的活动也是互为相容的。又由于b中的活动个数与a中活动个数相同,且a是最优的,故b也是最优的。也就是说b是一个以贪心选择活动1开始的最优活动安排。因此,证明了总存在一个以贪心选择开始的最优活动安排方案,也就是算法具有贪心选择性质。 
  • 最优子结构性质:在选择了第一个活动后,原问题简化为对E中所有与活动1相容的活动进行安排的子问题,也就是1已经确定,然后就需要对剩下的活动进行安排,但是前提为剩下活动的开始时间,大于等于活动1的结束时间。故若A是原问题的最优解,则设A’=A-{1}是接下来的子问题的最优解,也就是活动开始时间>1活动的结束时间的剩余的活动,设为E’的最优解,利用反证法:假设 E’有一个解B’, B’比 A’包含更多的活动,则B= B’+{1},则B比最优解A还要包含更多的活动,与A的最优性矛盾,故不存在这样一个 B’。这样每一次所做的贪心选择都将原问题简化为与原问题具有相同形式的子问题,具有最优子结构性质。
  • 算法设计思想:先将活动按照结束时间从小到大排序,遍历找最大相容活动子集。
  •  核心代码:
    int greedySelector(int *s, int *f, boolean *a){
        //n个活动按照结束时间从小到大排序后放于s[],f[] 
        sort(s,f,n); //O(nlogn)
        a[1]=1; 
        int j=1; 
        int count=1;
        for(int i=2; i<=n; i++){ //O(n) 
            if(s[i]>f[j]){ //是否相容 
                a[i]=1;
                j=i;
                count++;
            }
            else{
                a[i]=0;
            }
        }
        return count;
    }

0-1背包问题和背包问题

  •  背包问题:贪心算法,每次选择单位重量价值最高的物品装入背包;如果背包尚有容量,将最后不能完全装入的物品切割一部分装满背包
  • 核心代码:
    void fun(int n, int W, int v[], int w[], int x[]){
        Sort(n,v,w);//按照单位重量价值排序
        int i;
        for(i=1; i<=n; i++){ //初始化 
            x[i]=0;
        } 
        int c=W;
        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];
        }
    }
  • 0-1背包问题:
    例如W=6,n=3(wi,vi)={(1,3),(3,6),(5,9)}已按照单位重量价值排序
    若用贪心算法,则Vmax=9  放入第一第二个, 但是动态规划算法得出:最优为12装入第一第三个
    对于0-1背包问题,不能用贪心算法得到最优解:无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低。

最优装载问题

  • 问题描述:某艘船的载重量为C,每件物品的重量为wi,要将尽量多的物品装入到船上。
  • 贪心策略:每次都选择最轻的,然后再从剩下的n-1件物品中选择最轻的。
  • 最优子结构性质:                                                                                                                        设(x1,x2,……xn)是最优装载问题的满足贪心选择性质的最优解,则易知,x1=1,(x2,x3,……xn)是轮船载重量为c-w1,待装船集装箱为{2,3,……n}时相应最优装载问题的最优解。因此,最优装载问题具有最优子结构性质。

哈夫曼编码

  • 贪心选择性质                                                                                                                                设一个字符数组a,其中每个字符a[i]都具有频度f[i]。设a[x],a[y]是a中具有最低频度的两个字符,则存在一种最优前缀编码,其中a[x],a[y]的编码长度相同但是最后一位不同。这就相当于a[x],a[y]是兄弟节点,即通过合并来构造一棵最优树的过程,可以贪心选择两个频度最低的字符开始,因为我们认为一次合并的代价就是被合并的两个字符的频度之和。选择频度最低的两个字符合并,累计加的更多,使得最终哈夫曼树的合并代价最低。      
  • 最优子结构:                                                                                                                           设一个字符数组a,其中每个字符a[i]都具有频度f[i]。设a[x],a[y]是a中具有最高频度的两个字符,假设数组a1,其中a1=a-a[x]-a[y]+a[z],其中a[z]是由a[x]和a[y]合成的节点,f(z)=f(x)+f(y);假设T’是a1的最优前缀编码的任意一棵树,那么可以将T’中的叶子节点替换成具有x和y孩子的内部节点,也就得到了树T,表示的是a的一个最优前缀编码,故满足最优子结构性质。       
  • 补充:使用优先队列来实现哈夫曼编码问题                                                                               

单源最短路径

  •  问题描述:图中某一顶点到其他各顶点的最短路径
  • Dijkstra算法:

    通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算)。                       此外,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。                             初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是"起点s到该顶点的路径"。然后,从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 然后,再从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 ... 重复该操作,直到遍历完所有顶点。

  • 贪心选择性质:                                                                                                                        一个图中的点分为两个集合,一个是V:所有点的集合;另一个是S:表明已经找到最短路径的点,即一个点找到了最短路就加入S,而我们要证的就是加入S的点的最短路都已确定。设d[u]表示u点到源点的当前距离,z[u]表示u到源点的最短路。                                                  假设一个点u是加入S集合中的第一个不满足d[u]=z[u]的点,如果u点到源点s没有路,那么d[u]=z[u]=无穷,就不满足z[u]=d[u]这个条件了,所以可以得出s到u一定有条最短路。我们假设y点是V-S中的一点,y-u不一定存在,也就是说y有可能就是u点,然后假设x是y的紧邻前驱,但s-x也不一定存在,s点有可能为x点,因为x已经加入S了,x又是y的紧邻前驱,所以在松弛时已经计算出d[y]=z[y],(因为图中的s-u是一条最短路了,所以此路上的s-y也是s到y的最短路,否则s-u就不是s到u的最短路了)(根据收敛性质:(此中的字母与本文无关,只是描述收敛性质用到)s-u-v是图G某u点,v点属于V的最短路径,而且在松弛边(u,v)之前的任何时间d[u]=z[u],则在操作后总有d[v]=z[v]),到了这里我们就得出了两个不等式,1.在这条路径中看得出d[u]>=z[u]>=z[y]=d[y],2.在选择u点时,只有d[u]<=d[y]时,才会选到u点加入S,从而得到d[u]=d[y]=z[u]=z[y]。 

  • 最优子结构性质:

    如果P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,k和s是这条路径上的一个中间顶点,那么P(k,s)必定是从k到s的最短路径。下面证明该性质的正确性。

       假设P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,则有P(i,j)=P(i,k)+P(k,s)+P(s,j)。而P(k,s)不是从k到s的最短距离,那么必定存在另一条从k到s的最短路径P'(k,s),那么P'(i,j)=P(i,k)+P'(k,s)+P(s,j)<P(i,j)。则与P(i,j)是从i到j的最短路径相矛盾。因此该性质得证。

  • Dijkstra算法操作步骤:                                                                                                           (1) 初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为"起点s到该顶点的距离"[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞]。             (2) 从U中选出"距离最短的顶点k",并将顶点k加入到S中;同时,从U中移除顶点k。       (3) 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离。                                                                                                             (4) 重复步骤(2)和(3),直到遍历完所有顶点。

  • 补充:适用于稠密图,代码采用优先队列来替代课本上的数组,采用邻接矩阵,算法的时间复杂度为O((|v|+|e|)log|v|).                                                                                                                 不适用于有负边的情况,假如一张图里有一个总长为负数的环,那么Dijkstra算法有可能会沿着这个环一直绕下去。另外,如果一张图里有负数边,但没有总长为负数的环,此时可以用Bellman-Ford算法计算,虽然它比Dijkstra慢了一些。                                                                 题解:              P3371 【模板】单源最短路径(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

最小生成树

  •  前提补充:                                                                                                                               生成树,所有顶点连通,但是不存在回路(极小连通子图),去掉一条边则非连通,加上一条边则形成回路;n个顶点,n-1条边。                                                                                       最小生成树:生成树的所有边的带权和最小。
  • 最小生成树性质(MST):设G=(V,E)是一个连通网络,U是顶点集V的一个真子集。若(u,v)是G中一条“一个端点在U中(例如:u∈U),另一个端点不在U中的边(例如:v∈V-U),且(u,v)具有最小权值,则一定存在G的一棵最小生成树包括此边(u,v)。
  • 贪心选择性质:设T是最优MST,选定其中的一条边(u,v),其中u在子树T1中,v在子树T2中,由贪心选择策略可知(u,v)是所有连接两颗子树的最短边,此时w(T)=w(u,v)+w(T1)+w(T2),设存在一条边(u',v'),其中u’在子树T1中,v‘在子树T2中,则w(T’)=w(u‘,v’)+w(T1)+w(T2)>w(T)。故选择最短策略成立,由此可以推得,按此贪心策略一步步选择可得最小生成树。
  • 最优子结构性质:设T是最优MST,选定其中的一条边(u,v),其中u在子树T1中,v在子树T2中,此时w(T)=w(u,v)+w(T1)+w(T2),假设存在子树T1’,w(T1')<w(T1),因为T=T1U{(u,v)}UT2,则T‘=T1‘U{(u,v)}UT2,则w(T')<w(T),矛盾。故T1子树是MST,同理可证T2也是MST,故子问题具有最优子结构。
  • Prim算法                                                                                                                                  首先置S={1},然后,只要S是V的真子集,就做如下贪心选择:选取满足条件i∈S,j∈V-S,且i,j这条边是最小的,并将顶点j添加到S中。一直到S=V为止。                                           使用优先队列,算法时间复杂度为O((|v|+1)log|v|).
  • Kruskal算法                                                                                                                              将所有边按权从小到大排序,然后从第一条边开始,依边权递增的顺序查看每条边,当查看到第k条边(u,v)时,如果两个端点分别是当前两个不同的连通分支,就用边(u,v)将两个连通分支连接,然后继续查看k+1条边;若u和v在同一个连通分支中,就直接查看k+1条边。看连通分支也就是看是否形成回路,使用并查集操作。                                                                         时间复杂度为O(eloge)
  • Prim算法适用于稠密图,Kruskal算法适用于稀疏图。

多机调度问题

  •  问题描述:设有n个独立的作业{1,2,...n},由m台相同的机器进行加工处理。作业i所需的处理时间为ti。现规定,任何作业可以在任何一台机器上加工处理,但未完工之前不允许中断处理。任何作业不能拆分成更小的子作业。
  • 解决思路:需要实现将n个独立的作业按照时间从大到小排序;在这里需要考虑的是:
    如果n<=m,则需要的时间就是n个作业当中最长处理时间t。
    如果n>m,先给每个机器分配作业,这一趟下来就分配了m个作业。然后对每个作业而言,选取处理时间最短的机器区域处理。
    以上就是贪婪算法的思路。
  • 注意:此问题是一个完全NP问题,到目前位置还没有有效的解法,只是用贪心选择策略设计出较好的近似算法。 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值