算法设计方法1:贪心算法

前言:走出数据结构的世界,进入算法设计的天地 。为了满足人们对大数据量信息处理的渴望,为解决各种实际问题,计算机算法学得到了飞速的发展,线性规划、动态规划、贪心策略等一系列运筹学模型纷纷运用到计算机算法学中,产生了解决各种现实问题的有效算法。虽然设计一个好的求解算法更像是一门艺术而不像是技术,但仍然存在一些行之有效的、能够用于解决许多实际问题的算法设计方法,你可以使用这些方法来设计算法,并观察这些算法是如何工作的。一般情况下,为了获得较好的性能,必须对算法进行细致的调整。但是在某些情况下,算法经过调整之后性能仍无法达到要求,这时就必须寻求另外的算法设计方法来求解该问题。从此篇博客起的算法设计方法1-5会学习五种基本的算法设计方法:贪婪算法、分而治之算法、动态规划、回溯和分支定界。此外,还有一些更高级的而且常用的方法,如线性规划、整数规划、中心网络、遗传算法、模拟退火等,有需要的朋友可以去买《算法导论》来学习,计算机本科生大三时都会接触到的一本好书。

一、贪心算法的概念

1、贪心算法定义

解决最优化问题的算法一般包含一系列的步骤,每一步都有若干的选择。对于很多最优化问题,只需要采用简单的贪心算法就可以解决,而不需要采用动态规划方法。贪心算法使所做的局部选择看起来都是当前最佳的,通过局部的最优化选择来产生全局最优解,本质上是一种改进了的分级处理方法。

贪心算法(greedy algorithm)又称贪婪算法,是指在对问题求解时,总是做出在当前看来是最好的选择即局部最优解,通过局部的最优化选择来产生全局最优解。

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

简单来说,贪心算法是指:在每一步求解的步骤中,它要求“贪婪”的选择最佳操作,并希望通过一系列的局部最优选择,能够产生一个问题(全局的)最优解。

①贪心算法基本思路:

1.建立数学模型来描述问题。

2.把求解的问题分成若干个子问题。

3.对每一子问题求解,得到子问题的局部最优解。

4.把子问题的解局部最优解合成原来解问题的一个解。

②贪心算法每一步必须满足一下条件:

 a、可行的:即它必须满足问题的约束。

 b、局部最优:他是当前步骤中所有可行选择中最佳的局部选择。

 c、不可取消:即选择一旦做出,在算法的后面步骤就不可改变了。

2、贪心算法思想

贪心算法是一种非常直观的求解方法,虽然设计一个问题的贪婪算法通常是很容易的,但是设计出来的方法不一定能产生最优的解。但即使贪婪算法不能保证最优解,它们依然是有用的,因为它们常常让我们得到近似最优解。

在贪婪算法中,我们逐步构造一个最优解。每一步,我们都在一定的标准下,作出一个最优的决策。在每一步做出的决策,在以后的步骤中都不可更改。作出决策所依据的标准称为贪婪准则。

比如经典的找零钱问题中,售货员为凑成要找的零钱的选择时所依赖的贪婪准则在不超过要找的零钱总数条件下,每一次都选择面值尽可能大的硬币,直到凑成的零钱整数等于要找的零钱总数。这样需要的硬币数最少。

3、贪心算法应用

可以求解货箱装载问题、背包问题、拓扑排序问题、二分覆盖问题、最短路径问题和最小代价生成树问题。而IT常见笔试题会有找零钱问题、分发糖果问题和跳一跳问题。

二、贪心算法经典案例

1、钱币找零问题
这个问题在我们的日常生活中就更加普遍了。假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?用贪心算法的思想,很显然,每一步尽可能用面值大的纸币即可。在日常生活中我们自然而然也是这么做的。在程序中已经事先将Value按照从小到大的顺序排好。

package GreedyAlgorithm;

public class CoinChange 
{
    int count = 0;//记录总共需要多少张钱
    public static void main(String[] args) 
    {
        //人民币面值集合
        int[] values = { 1, 2, 5, 10, 20, 50, 100 };
        //各种面值对应数量集合
        int[] counts = { 3, 1, 2, 1, 1, 3, 5 };
        //求442元人民币需各种面值多少张
        int[] num = change(442, values, counts);
        print(num, values);
    }

    public static int[] change(int money, int[] values, int[] counts)
   {
        //用来记录需要的各种面值张数
        int[] result = new int[values.length];

        for (int i = values.length - 1; i >= 0; i--)
        {
            //需要最大面值人民币张数
            int c = min(money / values[i], counts[i]);
            //剩下钱数
            money = money - c * values[i];
            //将需要最大面值人民币张数存入数组,并记录所需钱的张数
            num += c;
            result[i] = c;
        }
        return result;
    }

    /**
     * 返回最小值
     */
    private static int min(int i, int j) {
        return i > j ? j : i;
    }
    
    private static void print(int[] num, int[] values) {
        for (int i = 0; i < values.length; i++) {
            if (num[i] != 0) {
                System.out.println("需要面额为" + values[i] + "的人民币" + num[i] + "张");
            }
        }
    }
}

运行结果:可以看出,求出的结果也刚好等于442元。正好为最优解。

需要面额为2的人民币1张
需要面额为5的人民币2张
需要面额为10的人民币1张
需要面额为20的人民币1张
需要面额为100的人民币4张

但是,当面额及数量为下种特殊情况时,贪心算法就无法给出最优解:

 5         //人民币面值集合
 6         int[] values = { 3, 5, 10, 20, 50, 100 };
 7         //各种面值对应数量集合
 8         int[] counts = { 3, 2, 1, 1, 3, 5 };
需要求得money = 416元

运行结果如下:

需要面额为5的人民币1张
需要面额为10的人民币1张
需要面额为100的人民币4张

求出结果为415元与所需不符合,于是我们可以看出,有些情况,贪心算法确实可以给出最优解,然而,还有一些问题并不是这种情况。对于这种情况,我们关心的是近似解,或者只能满足于近似解,所以贪心算法也是有价值的。

2、分发糖果问题1(LeetCode)

问题描述:老师想给孩子们分发糖果,有N个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:每个孩子至少分配到 1 个糖果。相邻的孩子中,评分高的孩子必须获得更多的糖果。那么这样下来,老师至少需要准备多少颗糖果呢?

示例 1:
输入: [1,0,2]
输出: 5
解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。
示例 2:
输入: [1,2,2]
输出: 4
解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

题目分析:

对于这个问题,主要的条件是要相邻孩子,得分高的拿的糖果要多(不包括相等得分),还有就是每个人最少都要有一个。
解决这个问题我们还是采用贪心算法,首先初始化每个人分配的糖果数量都是1,然后这个算法需要遍历两遍,第一遍从左往右遍历,如果当前孩子的分数大于前一个孩子的分数,则当前孩子得到的糖果在前一个孩子的基础上加1;然后,第二遍从右往左遍历,如果当前孩子的分数大于他右边孩子的分数,并且他的糖果不比他右边孩子多,则糖果数在他基础上加1;最后,将所有孩子的糖果数相加即可。第二次遍历是为了处理数组中降序和有出现相邻孩子分数相同的情况。

典型的贪心算法题本身可以用贪心法来做,我们用candy[n]表示每个孩子的糖果数,遍历过程中,如果孩子i+1的rate大于孩子i 的rate,那么当前最好的选择自然是:给孩子i+1的糖果数=给孩子i的糖果数+1;如果孩子i+1的rate小于等于孩子i 的rate咋整?这个时候就不大好办了,因为我们不知道当前最好的选择是给孩子i+1多少糖果。解决方法是:暂时不处理这种情况。等数组遍历完了,我们再一次从尾到头遍历数组,这回逆过来贪心,就可以处理之前略过的孩子。最后累加candy[n]即得到最小糖果数。

Java代码:

public class Solution
 { 
    public int candy(int[] ratings) 
    { 
        if(ratings==null || ratings.length<=0) 
           return 0; 
        int[] num = new int[ratings.length];  //表示每个孩子的糖果数的数组
        Arrays.fill(num, 1); 

        for(int i=1;iratings[i-1]) 
             num[i]=num[i-1]+1;
        } 

        for(int i=ratings.length-2;i>=0;i--) 
        { 
           if(ratings[i]>ratings[i+1] && num[i] < num[i+1]+1) 
              num[i]=num[i+1]+1; 
       } 

       int sum=0; 
       for(int i=0;i

3、分发糖果问题2(LeetCode455)

问题描述:已知有一些孩子和一些糖果,每个孩子都有需求因子g,每个糖果有大小s;如果某个糖果的大小s>=某个孩子的需求因子时,代表该糖果可以满足该孩子,使用这些糖果,最多可以满足多少孩子?(注意:某个孩子最多只能被一块糖果满足)

①举个实例
孩子的需求因子为g = [5, 10, 2,9,15,9];糖果的大小数组为:s = [6,1,20,3,8],那么,这种情况下,最多可以满足3个孩子。

②贪心规律
(1)某个糖果不能满足某个孩子,那么,该糖果一定不能满足更大需求因子的孩子。
(2)某个孩子可以用更小的糖果满足,则没必要用更大的糖果,留着更大的糖果去满足需求因子更大的孩子。(贪心!!)
(3)孩子的需求因子更小则其更容易被满足,故优先从需求因子小的孩子开始,因为用一个糖果满足一个较大需求因子的孩子或满足较小需求因子的孩子效果一样。(最终总量不变)(贪心!!)

所以,贪婪准则为:从需求因子小的孩子开始,某个孩子可以用更小的糖果满足,则没必要用更大的糖果,留着更大的糖果去满足需求因子更大的孩子。

算法思想

(1)对需求因子数组g和糖果大小数组s进行从小到大排序;
(2)按照从小到大的顺序使用各糖果,尝试是否可以满足某个孩子,每个糖果只尝试一次;若成功,则换下一个糖果尝试;直到发现没有孩子或者没有糖果,循环结束。

Java代码:

public int candy(int[] childs,int[] candys)
{
    //第一步,先把孩子的需求因子数组childs和糖果大小数组candys进行升序排序
    Arrays.sort(childs);
    Arrays.sort(candys);

    //第二步,按照从小到大的顺序使用糖果,尝试是否满足某个孩子
    int childCount = 0;
    int candyCount = 0;
     
    //直到发现没有孩子或者没有糖果,循环结束。
    while(childCount

4、跳一跳游戏

有n个盒子排成一行,每个盒子上面有一个数字a[i],表示最多能向右跳a[i]个盒子;
小明站在左边第一个盒子,请问能否到达最右边的盒子?
比如说:[1, 2, 3, 0, 4] 可以到达第5个盒子;
[3, 2, 1, 0, 4] 无法到达第5个盒子;

我们自然而然能产生一种贪心解法:尽可能的往右跳,看最后是否能到达。

问题描述:给定一个非负整数数组,假定你的初始位置为数组第一个下标。数组中的每个元素代表你在那个位置能够跳跃的最大长度。请确认你是否能够跳跃到数组的最后一个下标。

例如:A = [2,3,1,1,4], 能够跳跃到最后一个下标,输出true;

       (先跳跃1步,从下标0到 1,然后跳跃3步,到达最后一个下标。一共两次)

          A = [3,2,1,0,4],不能跳跃到最后一个下标,输出false。

输入格式

第一行输入一个正整数 n(1≤n≤100) ,接下来的一行,输入n 个整数,表示数组 A。

输出格式

如果能跳到最后一个下标,输出true,否则输出false。

样例输入

5

2  0  2  0  1

样例输出

true

分析,需要注意的一点是:只要跳跃到值为0的位置时,跳跃就会停止,如果此时不能到达最后的数组下标,那就不能跳跃到最后的数组下标位置了。

算法思想:从第一个数开始, 寻找可以一个可以跳最远的点。

          * 例如:3 1 2 4 1 0 0
         * 1.从第一个位置0,可以跳到位置1和位置2和位置3;
         * 2.如果跳到位置1,那么最远就可以跳到位置(1+1);
         * 3.如果跳到位置2,那么最远就可以跳到位置(2+2);
         * 4.如果跳到位置3,那么最远就可以跳到位置(3+4);
         * 5.故选择跳到位置3 ,重复1.2.3步;

Java代码:

public void jump()
{
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();

        int[]a = new int[n];
        for(int i= 0 ;i< n ;i++) 
        {
           a[i] = sc.nextInt();
        }

        int i;
        int l;	//左边界,控制搜索的起始位置
        int r;	//右边界,控制搜索的终止位置
        for( i= 0 ;i< n && a[i]!= 0 ;) 
       {	//当a[i]==0 时 , 该位置为可到达的最远位置
        	r = i + a[i];
        	l = i + 1; 
        	for(int j= i+1 ;j< n && j<= i+a[i] ;j++) 
                {
        		if(j+a[j] >= r)
                        {	//遍历可到达的最远位置
        			r = j+ a[j];	//更新左右边界
        			l = j;
        		}
        	}
        	i = l;	//左边界
        }

        if(i< n-1) 
           System.out.println("false");
        else
           System.out.println("true");
}

拓扑排序、最短路径问题和最小代价生成树问题以后闲下来再写博客。

参考链接:

程序员算法基础——贪心算法

你可能感兴趣的:(数据结构与算法)