经典算法学习之贪心算法

贪心算法也是用来求解最优化问题的,相比较动态规划很多问题使用贪心算法更为简单和高效,但是并不是所有的最优化问题都可以使用贪心算法来解决。

贪心算法就是在每个决策点都做出在当时看来最佳的选择。

贪心算法的设计步骤:

1、将最优化问题转换为:对其做出一次选择之后,只剩下一个问题需要求解的形式(动态规划会留下多个问题需要求解)

2、证明做出贪心选择之后,原问题总是存在最优解,即贪心算法总是安全的

3、证明做出贪心选择后,剩余的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解,这样就得到了最优子结构

其中2、3两步主要是为了证明一个问题适不适合使用贪心算法

下面是一个使用贪心算法解决问题的例子:

1、活动选择问题描述

    有一个需要使用每个资源的n个活动组成的集合S= {a1,a2,···,an },资源每次只能由一个活动使用。每个活动ai都有一个开始时间si和结束时间fi,且 0≤si<fi<∞ 。一旦被选择后,活动ai就占据半开时间区间[si,fi)如果[si,fi]和[sj,fj]互不重叠,则称ai和aj两个活动是兼容的。该问题就是要找出一个由互相兼容的活动组成的最大子集。例如下图所示的活动集合S,其中各项活动按照结束时间单调递增排序。

从图中可以看出S中共有11个活动,最大的相互兼容的活动子集为:{a1,a4,a8,a11,}和{a2,a4,a9,a11}。

2、动态规划解决过程

(1)活动选择问题的最优子结构

定义子问题解空间Sij是S的子集,其中的每个获得都是互相兼容的。即每个活动都是在ai结束之后开始,且在aj开始之前结束。假设Aij是Sij的最大的相互兼容的活动子集,再假设ak是Aij中的一个活动,由于最优解包含ak,所以得到了两个子问题寻找Sij中在ak之前的活动中的最优子结构Aik和ak之后的活动中的最优子结构Akj,这样可以得出Aij=Aik∪Akj∪{ak},最优解中活动的个数是|Aij|=|Aik|+|Akj|+1。

下面用剪切-粘贴法证明最优解Aij必然包含其子问题Aik和Akj的最优解。先假设Aij不包含Aik的最优解,那么Aik必然存在一个最优解Aik‘,使得|Aik’|>|Aik|,进而得出|Aik‘|+|Akj|+1>|Aik|+|Akj|+1=|Aij|与最初假设的Aij是Sij的最优解冲突,所以最优解Aij必然包含其子问题Aik的最优解。同理证明最优解Aij必然包含其子问题Akj的最优解。

综上最优子结构为:假设Sij的最优解Aij包含活动ak,则对Sik的解Aik和Skj的解Akj必定是最优的。

通过一个活动ak将问题分成两个子问题,下面的公式Aij=Aik∪Akj∪{ak}计算出Sij的解Aij

(2)一个递归解

  设c[i][j]为Sij中最大兼容子集中的活动数目,当Sij为空集时,c[i][j]=0;当Sij非空时,若ak在Sij的最大兼容子集中被使用,则则问题Sik和Skj的最大兼容子集也被使用,故可得到c[i][j] = c[i][k]+c[k][j]+1。

当i≥j时,Sij必定为空集,否则Sij则需要根据上面提供的公式进行计算,如果找到一个ak,则Sij非空(此时满足fi≤sk且fk≤sj),找不到这样的ak,则Sij为空集。

c[i][j]的完整计算公式如下所示:

(3)最优解计算过程

  根据递归公式,采用自底向下的策略进行计算c[i][j],引入复杂数组ret[n][n]保存中间划分的k值。程序实现如下所示:

 1 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1])
 2 {
 3     int i,j,k;
 4     int temp;
 5     //当i>=j时候,子问题的解为空,即c[i][j]=0
 6     for(j=1;j<=N;j++)
 7       for(i=j;i<=N;i++)
 8          c[i][j] = 0;
 9     //当i<j时,需要寻找子问题的最优解,找到一个k使得将问题分成两部分
10     for(j=2;j<=N;j++)
11      for(i=1;i<j;i++)
12       {
13          //寻找k,将问题分成两个子问题c[i][k]、c[k][j]
14          for(k=i+1;k<j;k++)
15             if(s[k] >= f[i] && f[k] <= s[j])   //判断k活动是否满足兼容性
16              {
17                temp = c[i][k]+c[k][j]+1;
18                if(c[i][j] < temp)
19                 {
20                   c[i][j] =temp;
21                   ret[i][j] = k;
22                 }
23             }
24       }
25 }

(4)构造一个最优解集合

  根据第三保存的ret中的k值,递归调用输出获得集合。采用动态规划方法解决上面的例子,完整程序如下所示:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3
 4 #define N 11
 5
 6 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]);
 7 void trace_route(int ret[N+1][N+1],int i,int j);
 8
 9 int main()
10 {
11     int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12};
12     int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14};
13     int c[N+1][N+1]={0};
14     int ret[N+1][N+1]={0};
15     int i,j;
16     dynamic_activity_selector(s,f,c,ret);
17     printf("c[i][j]的值如下所示:\n");
18     for(i=1;i<=N;i++)
19     {
20         for(j=1;j<=N;j++)
21             printf("%d ",c[i][j]);
22         printf("\n");
23     }
24     //包括第一个和最后一个元素
25     printf("最大子集的个数为: %d\n",c[1][N]+2);
26     printf("ret[i][j]的值如下所示:\n");
27     for(i=1;i<=N;i++)
28     {
29         for(j=1;j<=N;j++)
30             printf("%d ",ret[i][j]);
31         printf("\n");
32     }
33     printf("最大子集为:{ a1 ");
34     trace_route(ret,1,N);
35     printf("a%d}\n",N);
36     system("pause");
37     return 0;
38 }
39
40 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1])
41 {
42     int i,j,k;
43     int temp;
44     //当i>=j时候,子问题的解为空,即c[i][j]=0
45     for(j=1;j<=N;j++)
46       for(i=j;i<=N;i++)
47          c[i][j] = 0;
48     //当i>j时,需要寻找子问题的最优解,找到一个k使得将问题分成两部分
49     for(j=2;j<=N;j++)
50      for(i=1;i<j;i++)
51      {
52          //寻找k,将问题分成两个子问题c[i][k]、c[k][j]
53          for(k=i+1;k<j;k++)
54             if(s[k] >= f[i] && f[k] <= s[j])   //判断k活动是否满足兼容性
55             {
56                temp = c[i][k]+c[k][j]+1;
57                if(c[i][j] < temp)
58                {
59                   c[i][j] =temp;
60                   ret[i][j] = k;
61                }
62             }
63      }
64 }
65
66 void trace_route(int ret[N+1][N+1],int i,int j)
67 {
68      if(i<j)
69      {
70          trace_route(ret,i,ret[i][j]);
71          if(ret[i][j] != 0 )
72             printf("a%d ", ret[i][j]);
73      }
74 } 

3、贪心算法解决过程

针对活动选择问题,认真分析可以得出以下定理:对于任意非空子问题Sij,设am是Sij中具有最早结束时间的活动,那么:

(1)活动am在Sij中的某最大兼容活动子集中被使用。

(2)子问题Sim为空,所以选择am将使子问题Smj为唯一可能非空的子问题。

有这个定理,就简化了问题,使得最优解中只使用一个子问题,在解决子问题Sij时,在Sij中选择最早结束时间的那个活动。

贪心算法自顶向下地解决每个问题,解决子问题Sij,先找到Sij中最早结束的活动am,然后将am添加到最优解活动集合中,再来解决子问题Smj

基于这种思想可以采用递归和迭代进行实现。递归实现过程如下所示:

 1 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret)
 2 {
 3      int *ptmp = ret;
 4      int m = i+1;
 5      //在Sin中寻找第一个结束的活动
 6      while(m<=n && s[m] < f[i])
 7         m = m+1;
 8      if(m<=n)
 9      {
10         *ptmp++ = m;  //添加到结果中
11         recursive_activity_selector(s,f,m,n,ptmp);
12      }
13 }

迭代实现过程如下:

 1 void greedy_activity_selector(int *s,int *f,int *ret)
 2 {
 3   int i,m;
 4   *ret++ = 1;
 5   i =1;
 6   for(m=2;m<=N;m++)
 7     if(s[m] >= f[i])
 8     {
 9        *ret++ = m;
10        i=m;
11     }
12 }

采用贪心算法实现上面的例子,完整代码如下所示:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3
 4 #define N 11
 5
 6 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret);
 7
 8 void greedy_activity_selector(int *s,int *f,int *ret);
 9
10 int main()
11 {
12     int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12};
13     int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14};
14     int c[N+1][N+1]={0};
15     int ret[N]={0};
16     int i,j;
17     //recursive_activity_selector(s,f,0,N,ret);
18     greedy_activity_selector(s,f,ret);
19     printf("最大子集为:{ ");
20     for(i=0;i<N;i++)
21     {
22        if(ret[i] != 0)
23          printf("a%d ",ret[i]);
24     }
25     printf(" }\n");
26     system("pause");
27     return 0;
28 }
29
30 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret)
31 {
32      int *ptmp = ret;
33      int m = i+1;
34      //在i和n中寻找第一个结束的活动
35      while(m<=n && s[m] < f[i])
36         m = m+1;
37      if(m<=n)
38      {
39         *ptmp++ = m;  //添加到结果中
40         recursive_activity_selector(s,f,m,n,ptmp);
41      }
42 }
43
44 void greedy_activity_selector(int *s,int *f,int *ret)
45 {
46   int i,m;
47   *ret++ = 1;
48   i =1;
49   for(m=2;m<=N;m++)
50     if(s[m] >= f[i])
51     {
52        *ret++ = m;
53        i=m;
54     }
55 }

4、总结

  活动选择问题分别采用动态规划和贪心算法进行分析并实现。动态规划的运行时间为O(n^3),贪心算法的运行时间为O(n)。动态规划解决问题时全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有最优解。贪心算法的主要思想就是对问题求解时,总是做出在当前看来是最好的选择,产生一个局部最优解。

声明:本文部分内容改自:Anker—学习成长笔记:http://www.cnblogs.com/Anker/archive/2013/03/16/2963625.html

时间: 2024-12-27 17:59:47

经典算法学习之贪心算法的相关文章

JAVA算法基础-贪心算法

前言 学无止境.算法博大精深啊,一个贪心算法里面就隐含了这么多不同的场景实现,每个场景下的算法就有多种不同的实现,个人写法不一也成就了各种不同的漂亮算法,看了这些实现,也让我开拓了思维,这个世界的方案永远没有最完美的只有最合适的- ! 1.贪心算法概念 贪心算法也叫贪婪算法,当然叫法随意.主要目的是在问题求解时,做出最正确的判断= =,这不是贪心是啥?在计算机工程领域当中,就是说不考虑整体最优算法而是从局部做到最优解.当然贪心是算法不能对所有的问题都能得到整体都最优解,但对多数个问题还是能得到近

算法导论之贪心算法

参考:http://www.cnblogs.com/Creator/archive/2011/06/07/2074227.html 贪心算法在几个基本算法里面算是相对简单的算法了,思路也是非常简单的,每一步总是做出在当前看来最好的选择.也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择.基本思路就是从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解.当达到某算法中的某一步不能再继续前进时,算法停止. 贪心算法存在的问题是: 1. 不能保证求得的最

算法学习笔记 KMP算法之 next 数组详解

最近回顾了下字符串匹配 KMP 算法,相对于朴素匹配算法,KMP算法核心改进就在于:待匹配串指针 i 不发生回溯,模式串指针 j 跳转到 next[j],即变为了 j = next[j]. 由此时间复杂度由朴素匹配的 O(m*n) 降到了 O(m+n), 其中模式串长度 m, 待匹配文本串长 n. 其中,比较难理解的地方就是 next 数组的求法.next 数组的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀,也可看作有限状态自动机的状态,而且从自动机的角度反而更容易推导一些. "前

学习日志---贪心算法

贪心算法: 贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解.贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解.       简单来说就是从一点开始,以每一步最优的方式去寻求最优解,但不保证是全局最优,只保证是每步最优. 例子: 有十一个快件,每个快件有送货的起始时间,要求用最少的车,运送完所有的快件.快件1:1:00-4:00快件2

【算法导论】贪心算法之活动选择问题

动态规划总是在追求全局最优的解,但是有时候,这样有点费时.贪心算法,在求解过程中,并不追求全局最优解,而是追求每一步的最优,所以贪心算法也不保证一定能够获得全局最优解,但是贪心算法在很多问题却额可以求得最优解. 一.问题概述 活动选择问题: 假定一个有n个活动(activity)的集合S={a1,a2,....,an},这些活动使用同一个资源(例如同一个阶梯教室),而这个资源在某个时刻只能供一个活动使用.每个活动ai都有一个开始时间si和一个结束时间fi,其中0<=si<fi<正无穷.如

【算法导论】贪心算法之赫夫曼编码

概述 讨论赫夫曼编码问题,赫夫曼编码的思想就是变长编码.变长编码就是让字符表中出现概率高的字符的编码长度尽可能小,而出现概率高的字符的编码长度相对较长.然后还要遵循前缀码的要求,就是任意一个编码都不是其他编码的前缀码,这样方便解码. 思路及实现 对于下表中的字符和相应的出现概率,有对应图中的编码树: 可以比较容易的看出来,每个叶节点就代表一个字符,从根节点到叶节点走过的路径拼接起来,就代表这个字符的编码,比如f是1100,e是1101,而f和e是深度最深的节点也是概率最小的两个节点.这也就是我们

(转)五大常用算法之三:贪心算法

http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741375.html 贪心算法 一.基本概念: 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解. 贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择.必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态

五大常用算法之三:贪心算法

一.基本概念: 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解. 贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择.必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关. 所以对所采用的贪心策略一定要仔细分析其是否满足无后效性. 二.贪心算法的基本思路: 1.建立数学模型来描述问题.2.把求解的问题分成若

算法设计分析之贪心算法

贪心算法,是一种“只顾眼前”的价值观.远古的人会有这样的人生态度,我只关注每天能吃饱,就可以活下去.在几百万年前未尝不是一种好算法. 贪心算法的条件 贪心算法需要满足无后效性.什么是有后效性呢?我当前这一步会给整个问题的情况带来改变.比如有一个路径问题,规定了走过的路不能再走,那这就是一个有后效性的问题. 贪心算法也不适应那些活动收益不同的问题.比如背包问题:假设现在有几样价值不同,体积不同的东西需要装进容积为F的背包里,要求装入的总价值最大.用贪心算法来解,不能保证得到最优解. 贪心策略1:先