vlambda博客
学习文章列表

常用算法之——贪心算法

算法简介

贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。


贪婪算法所得到的结果往往不是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。


贪婪算法并没有固定的算法解决框架,算法的关键是贪婪策略的选择,根据不同的问题选择不同的策略。


必须注意的是策略的选择必须具备无后效性,即某个状态的选择不会影响到之前的状态,只与当前状态有关,所以对采用的贪婪的策略一定要仔细分析其是否满足无后效性。


比如前边介绍的最短路径问题(广度优先、狄克斯特拉)都属于贪婪算法,只是在其问题策略的选择上,刚好可以得到最优解。


基本思路

其基本的解题思路为:

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

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

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

4.把子问题对应的局部最优解合成原来整个问题的一个近似最优解


案例详解

1.最佳支付方法


有1元、5元、10元、20元、100元、200元的钞票无穷多张。现使用这些钞票支付X元,最少需要多少张。


例如,X=628,最佳支付方法:3张200的,1张20的,3张1块的,共需要3+1+1+3=8张。


这里给出的钱金额:1,5,10,20,100是成倍数关系的,所以当使用一张较大面额的钞票时,若用较小面额钞票替换,一定需要更多的其他面额的钞票,所以尽可能使用面额较大的钞票,所以可以用贪心算法。


代码实现:


public class TanXin {

    public static void main(String[] args)

    {

        int[] a = {200,100,20,10,5,1};

        int X = 628;

        int count = 0;    

        for(int i=0;i<a.length;i++){

            if(a[i]<=X){

                count+=X/a[i];

                X = X%a[i];

            }

        }

        System.out.println("count: "+count);

    }

}

说明:这里能使用贪心的条件是,给出的面额成倍数,如果给出的面额不是成倍数,就不一定能使用贪心算法,举例如果给出钱金额:1,10,求组合达到金额为14的最少组合方法,这时候使用贪心算法就无能为力了,因为14=10+1+1+1+1,e而显然最优解是14=7+7。


2.分糖果问题


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


例如,需求因子数组g=[5,10,2,9,15,9];糖果大小数组s=[6,1,20,3,8],最多可以满足3个孩子。


思路:典型的贪心思想,最大的糖果满足需求因子最大的学生,次大的糖果满足次大需求的学生。先将需求因子数组和糖果大小数组都排序,得到s=[1,3,6,8,20],需求因子g=[2,5,9,910,15],如果最小的糖果能满足最小的需求因子,就将最小的糖果给最小的需求因子,例中的1无法满足需求因子2,所以试着将次小的糖果给最小的需求因子,仍然无法满足,然后将第三小的糖果给最小的需求因子,此时可以满足,然后将第四小的糖果给第二小的需求因子,依此下去。


代码实现:




class TanXin

        {

            public static void main(String[] args)

             {

                int[] g = {5,10,2,9,15,9};

                int[] s = {6,1,20,3,8};

                Arrays.sort(g);

                Arrays.sort(s);

                int i = 0;

                int j = 0;

                int count = 0;

                while(i<g.length&&j<s.length)

                {

                    if(s[j]>=g[i])

                    {

                        j++;

                        i++;

                        count++;

                    }

                    else

                    {

                        j++;

                    }

                }

               System.out.println("满足的小朋友个数:                 "+count);

            }

      }

3.摇摆序列


一个整数序列,如果两个相邻元素的查恰好正负(负正)交替出现,则该序列被称为摇摆序列。


例如:序列[1,7,4,9,2,5],相邻元素的差(6,-3,5,-7,3),该序列为摇摆序列。


理解思路,写并不好写,这里使用状态机做,但是基本思想是简单的,如果一组数字是递增的,那么就选取这组数字中最大的一个,如果一组数字是单调递减的,那么就选取这组数字中最小的那个,这里体现了贪心的思想。


4.移除k个数字


已知一个字符串表示的非负整数num,num中的k个数字移除,求移除k个数字后,可以获得的最小的可能的新数字。(num不会以0开头,num长度小于10002)。


举例:输入num="1432219",k=3,去掉3个数字后,得到的最小的数字是1219。


思考:让得到的数字最小,需要尽可能让得到的新数字优先最高位最小,其次次高位最小,再一次高位最小。例如输入num是1432219,此时可以使用栈存储最终的结果或删除工作,从高位向低位遍历num,如果遍历的数字大于栈顶元素,则将该数字push入栈,如果小于栈顶元素则进行pop弹栈,直到栈为空,或者不能再删除数字(k==0)或栈顶小于当前元素为止。


第一种特殊情况:一种特殊情况是在栈为空的时候,进去的为0,例如数字是num=100200,此时先取出1,1进入栈,但是第二个0的时候,此时1出栈,但0不能进栈,因为此时栈内为空。


第二种特殊情况:数字是单调递增的时候,例如12345678,此时在入栈完成后,检查栈的大小,如果栈的size()大于原先的字符串中的数字的长度减去2,此时要做处理,直接删除最后的两个数字。


代码实现:


class TanXin

    {

        public static void main(String[] args)

        {

        //测试用例

        //String num = "1432219";

        //String num = "12345678";

        //String num = "1002345";

        //String num = "100200";

         String num = "123411";

         int k = 2;

        Stack<Integer> al = new Stack<>();

        for(int i=0;i<num.length();i++)

         {

            int number = num.charAt(i) - '0';

            while(al.size()!=0&&al.peek()>number&&k>0)//如果进入一个小于栈顶数的数字

              {

                al.pop();

                k--;

              }

            if(number!=0||al.size()!=0)//栈为空的时候不能进去值为0

            {

                al.push(number);

            }

         }

            //如果是顺序的那么把最后的k位数字删除

        while(al.size()>0&&k>0)

        {

            al.pop();

            k--;

        }

//打印最终结果,因为栈实现了Iterator接口,所以可以使用for each语句

        System.out.println(al.toString());

        for(int c:al)

          {

            System.out.print(c+" ");

          }

     }

}


说明:最终的栈的输出要从栈底输出,因为栈实现了Iterator接口,所以这里用for each语句最为方便,否则从栈顶出栈后,还要颠倒一次顺序。


5.跳跃游戏


一个数组存储了非负整型数据,数组中第i个元素nums[i],代表了可以从数组第i个位置最多向前跳跃nums[i]步,已知数组各元素的情况下,求是否可以从数组第0个位置跳跃到数组的最后一个元素的位置。


跳跃游戏有两种问题:一种问能不能从第0个位置跳跃到数组的最后一个元素的位置,另一个中是从第0个位置跳跃到数组最后一个位置,最少需要跳跃多少步。


思路:例如:nums = [2,3,1,1,4],从第0个位置(2)可以跳至第一个位置(3)和第二个位置(1),从第一个位置(3)又可以跳至第二(1)、第三(1)、第四个位置(4),从第二个位置(1)又可以跳至第三个位置,所以从第0个位置是跳到第一个位置还是跳到第二个位置呢?此时选择跳到该位置后能够继续跳到的更远的位置。例如跳到第一个位置后最远能跳到第4个位置,而跳到第二个位置后最远能跳到第三个位置,所以我们就选择从第0个位置,跳到第1个位置,这也是典型的一种贪心思想。


如果不能到达的情况,例如nums=[3,2,1,0,4],此时不能达到,那么这该怎么判断呢?可以继续使用上面的办法,这时会出现在第3个位置(值为0)的地方停止,此时发现走一步后在原地,且该位置不是最终位置,说明停滞在该位置,说明无法到达最后位置了,代码描述:nums[i]+i==i&&i!=nums.length-1。


class TanXin

{

            public static void main(String[] args)

            {

            //int[] nums = {2,3,1,1,4};

            int[] nums = {3,2,1,0,4};

            //int[] nums = {3,2,1,0};

            int count = 0;

            int i = 0;

            boolean  flag = true;

            while(count<nums.length&&(i+nums[i]<=nums.length-1)){//防止到达不到停死在那,和已经走过两种情况

                if(nums[i]+i==i&&i!=nums.length-1){//如果停滞在那边,就说明无法到达

                    flag = false;

                    break;

                }

                if(nums[i]+i==nums.length-1){         //如果下一步能到达,那么加一后退出

                    count++;

                    break;

                }

                int k = i + nums[i];

                int maxReach=0;

                int j;

                int save = 0; //这一个保存变量很重要,记录上次到达的位置

                for(j=i+1;j<=k;j++){

                    if(maxReach<=(nums[j]+j))                //这边的等于号必须加,{3,2,1,0,4}

                    {

                        System.out.println("maxReach:                 "+maxReach+"nums[j]+j: "+(nums[j]+j));

                        save = j;

                        maxReach = nums[j]+j;

                    }

                }

                i = save;

                System.out.println("i: "+i);

                count++;

            }

            System.out.println("count: "+count);

            if(flag==false){

                System.out.println("无法到达");

            }

            else

                System.out.println("经过"+count+"步后到达");

        }

}


6.射气球问题


已知在一个平面上有一定数量的气球,平面可以看作是一个坐标系,在平面的x轴的不同位置安排弓箭手向y轴方向射箭,弓箭可以向y轴走无穷远,给定气球的宽度xstart<=x<=xend,问至少需要多少弓箭手,将全部气球 打爆?


例如:四个气球:[[10,16],[2,8],[1,6],[7,12]],至少需要2个弓箭手。


分析:对于某个气球,至少需要使用1只弓箭将它击穿,在这只气球将其击穿的同时,尽可能击穿其他更多的气球。(这里体现了贪心的思想)。做法:使用一个类Node代表一个气球,类中的first代表的气球的左坐标,类中的second代表气球的右坐标,对各个气球(即Node类)进行排序,按照气球的左端点从小到大排序(即按照Node类的first属性排序),遍历气球数组,同时维护一个射击区间(通过两个变量[shoot_begin,shoot_end]),在满足就将当前气球射穿的情况下,尽可能击穿更多的气球,每击穿一个新的气球,更新一次射击区间。具体的更新方法:如果下一个first属性大于上一个的first属性,那么shoot_beign更新,如果下一个second属性下雨上一个的second属性,那么更新。


代码实现:


class Node implements Comparable<Node>

{

        int first;

        int second;

        public Node(int first,int second)

        {

            this.first = first;

            this.second = second;

        }


@Override

        public int compareTo(Node o) {

            // TODO Auto-generated method stub

            return first-o.first;

        }

 }

class TanXin

{

        public static void main(String[] args)

        {

            Node n1 = new Node(10,16);

            Node n2 = new Node(2,8);

            Node n3 = new Node(1,6);

            Node n4 = new Node(7,12);

            ArrayList<Node> al = new ArrayList<>();

            al.add(n1);

            al.add(n2);

            al.add(n3);

            al.add(n4);

            Collections.sort(al);//排序,等会总结一下排序

            //for(Node n:al)

            //{

               //System.out.println(n.first+" "+n.second);

            //}

            int shoot_num = 1;

            int shoot_begin = n1.first;

            int shoot_end = n1.second;

            for(int i=1;i<al.size();i++)

            {

                if(al.get(i).first<=shoot_end)

                {

                    shoot_begin = al.get(i).first;        //因为已经排序过了,所以直接赋值

                    if(shoot_end>al.get(i).second)        //跟新设计区间的右坐标

                    {

                        shoot_end = al.get(i).second;

                    }

                }

                else

                {

                    shoot_num++;                    

                    shoot_begin = al.get(i).first;    //更新射击区间

                    shoot_end = al.get(i).second;

                }

            }

            System.out.println("shootNum: "+shoot_num);

        }

}