贪心算法

1、前言

求解最优化问题的算法通常会经历一系列步骤,在每个步骤都会面临多种选择,而许多最优化问题并不需要计算每个选择,它的选择非常明确。

贪心算法就是这样的算法,它在每一步中都做出当时看起来最佳的选择,它总是做出局部最优选择从而实现最优解

2、贪心算法原理

贪心算法的步骤:

  • 确定总是的最优子结构
  • 设计一个递归算法
  • 证明如果我们做出一个贪心选择,则只剩下一个子总是
  • 证明贪心选择总是安全的
  • 设计一个递归算法实现贪心策略
  • 将递归算法转换为迭代算法

什么时候才能使用贪心算法的呢?书中给出了贪心算法的两个性质,只有最优化问题满足这些性质,就可采用贪心算法解决问题。

(1)贪心选择性质:一个全局最优解可以通过最优解(贪心)选择来达到。即:当考虑做选择时,只考虑对当前问题最佳的选择而不考虑子问题的结果。

(2)最优子结构:问题的一个最优解包含了其子问题的最优解。

贪心算法一般来说都能用动态规划解决,因为动态规划需要的条件,贪心算法都满足,而贪心算法像是简化版的动态规划,它不需要计算每个选择。贪心算法只需要选择一个当下最优选择即可。

3、活动选择问题

假定由n个活动组成的集合S= {a1,a2,···,an },这些活动使用同一个资源。每个活动ai都有一个开始时间si和结束时间fi,且 0≤sii<∞ 。被选择后,活动ai就占据半开时间区间[si,fi)。如果[si,fi]和[sj,fj]互不重叠,则称ai和aj两个活动是兼容的。

该问题就是要找出一个由互相兼容的活动组成的最大子集。例如下图所示的活动集合S,其中各项活动按照结束时间单调递增排序。

根据上图可知,最大子集为1、4、8、11。

4、动态规划解题

最大活动子集问题是否满足最优子结构呢?仔细思考发现此问题类似于钢条切割问题,使用复制剪切法,将最优解分为两个规模相当的子问题,那么子问题必然也是最优解。

具有最优子结构,那么它的状态方程式是什么呢?

定义子问题解空间Sij是S的子集,其中每个活动都是在ai结束之后开始,且在aj开始之前结束。当 i>=j 时,Sij为空集。

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

由此可知状态方程式为:

贪心算法_第1张图片
  /**
 * 动态规划计算最大活动数。
 * 此问题满足最优子问题条件,而且可推导公式为:
 * max = Math.max(max, c[i][k] + c[k][j] + 1);
 * 类似钢条切割,一定是中间某点分割,两边都是最优选
 */
public static int dpSelect(){
    int[] st = {1,3,0,5,3,5,6,8,8,2,12}; 
    int[] ft = {4,5,6,7,8,9,10,11,12,13,14};
    int length = st.length;
    int[][] c = new int[length][length];
    int[][] ret = new int[length][length];
    
    for (int i = 0; i < length; i++) {
        for (int j = 1; j < length; j++) {
            if (i == j) {
                c[i][j] = 0;
            }else {
                int max = 0;
                for (int k = i+1; k < j; k++) {
                    if (st[k] >= ft[i] && ft[k] <= st[j]) {
                        max = Math.max(max, c[i][k] + c[k][j] + 1);
                        ret[i][j] = k;
                    }
                }
                /*
                 * max等于0,即没有合适的k。
                 * 如果集合的首位和末位时间合适,所以会有两个活动
                 * 如果集合的首位和末位时间不合适,所以会有一个活动
                 */
                if (max == 0) {
                    if (st[j] >= ft[i]) {
                        max += 2;
                    }else {
                        max += 1;
                    }
                }
                c[i][j] = max;
            }
        }
    }
    System.out.println(c[0][10]);
    System.out.println("----------");
    printResult(st, ft, 0, 10, ret);
    return c[0][length-1];
}

printResult方法,根据ret数组中的值,计算一共选择了哪些活动。

  public static void printResult(int[] st, int[] ft, int i, int j, int[][] ret){
    int r = ret[i][j];
    if (r > 0) {
        System.out.println(r);
    }
    while (r > 0 && r < st.length && (r=ret[i][r]) > 0) {
        System.out.println(r);
    }
    if (st[j] >= ft[i]) {
        System.out.println(i);
        System.out.println(j);
    }else {
        System.out.println(i);
    }
}

5、贪心算法

贪心算法是动态规划的简化版本。

此问题使用贪心算法非常简单,因为活动结束时间已经按照从小到大顺序排列了,为了实现最大子集的目标,所以第1个活动一定要入选,因为第1个活动最早结束,能够给出最多的时间让其它活动入选。

接下来遍历每一个活动,只要新活动的开始时间大于或等于已经入选的活动,则此活动也可以入选。因为结束时间从小到大排列的原因,新入选的活动一定是最优的。

代码如下:

/**
 * 贪心算法计算最大活动数
 */
public static void greedSelect(){
    int[] st = {1,3,0,5,3,5,6,8,8,2,12}; 
    int[] ft = {4,5,6,7,8,9,10,11,12,13,14};
    
    int length = st.length;
    int current = 0;
    System.out.println("0 is selected");
    for (int i = 1; i < length; i++) {
        //因为活动结束时间已经按从小到大顺序排列,那么只要活动开始时间大于current结束时间
        //那么此活动一定会是最优的。有可能其它活动开始时间也满足要求,但他们的结束时间一定比i大。
        if (st[i] >= ft[current]) {
            current = i;
            System.out.println(current + " is selected");
        }
    }
}

ps:所有代码均已上传至本人的github,欢迎访问

你可能感兴趣的:(贪心算法)