贪心算法及常见题目

1.什么是贪心算法。

       贪心算法是在一种功利的标准下所产生的解,是一种绝对标准下的解,他总是做出对于某一个局部标准来说最好的解,看看以此能否得到全局最优解。如果它以局部想法贪出一个全局最优解,我们就说他的贪心策略有效,如果没有搞错全局最优解,我们就说它的贪心策略无效。

       对于我们程序员来讲,他是最没有道理的算法,也是最具自然智慧的算法,它的难点在于证明局部最功利的标准可以得到全局最优解。

       我们在学习贪心算法时,主要以增加阅历和经验为主。

       举个贪心没有贪对的栗子

有一个小人,他想从左上角走到右下角然后再回去,它再往右下角走时,只能往下或者往有走,而它在回左上角时,只能往左或者往上走。而他的目标是收集最多的  1  ,1  再被收集一次后,就会变成 0 。

根据贪心算法对该栗子进行拆分,我们可以把该过程分为两部分,去的时候一部分,回来的时候一部分,根据贪心策略思想,我们只需要在去的时候获取到一个局部的最优解,回来的时候再获得一个局部的最优解,就能获取到全局最优解,当然贪心算法能在大部分情况下获取到最优解,但还是存在个别反例的情况,例如上述例子,  

按照贪心算法,去的时候走绿色这条路,会获取到去的这个局部的最大值,而回去的时候,因为只能向左或者向上,所以黄色的就只能走一条,从而有一个  1  没有获取到,那他的最优解应该是什么呢?

如上图,去的时候我不贪图局部最优,走蓝色路线,回去的时候走红色路线,这样就能收集到全部的  1 

这是一个贪心失败的典型栗子。

2.贪心算法的一些题目

  2.1给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果。   

在写该题之前,我们需要知道什么是字典序。

字典序,简单来说就是在编程语言中字符串比较大小的顺序。字符串比较有两种情况,在两个字符串长度相同时,总左到右,依次比较各个字符的ASCII码,譬如 两个字符串 "abc" 和 "bcd" ,因为a的ASCII码小于b的ASCII码,所以 "abc"的字典序小于"bcd"的字典序,如果两个字符长度不相等,则较短的字符后面补最小的ASCII码,补至和另一个长度一样后再比。

接下来看这题,我们可以根据字典序进行排序,然后我们再把每个字符拼接起来即可。

代码如下:

public static String minDictResult(String[] strs){
        if (strs == null || strs.length == 0){
            return "";
        }
        Arrays.sort(strs,(a,b)-> (a+b).compareTo(b+a));
        StringBuilder sb = new StringBuilder();
        for (String str : strs) {
            sb.append(str);
        }
        return sb.toString();
    }

2.2 用贪心算法返回最多会议室宣讲场次问题

        一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给以每一个项目开始的时间和结束的时间你来安排宣讲的日程,要求会议室进行的宣讲的场次最多,返回最多的宣讲场次。

看到题之后,我们能想到很多种贪心方案,但是,我们想的贪心方案都是有效的吗?

栗如:

1.所有时间中,谁开会时间早,我先安排谁,通过对开始时间的贪心,看看最多能安排几场。

明显该贪心策略不对   例如  [1,100][2,3][3,5][7,10]如果我们按照开始时间进行贪心,那么就会选中[1,100]这场会议,明显不对,贪心策略失效

2.那按照会议持续时间最短进行贪心呢?  明显也不对 ,反例   [1,50][45,55][50,100]在该反例中,如果选最小持续时间的话,就会选[45,55],明显[1,50][50,100]才是正确答案。

3.按照会议结束时间进行贪心,首先对结束时间按小到大进行排序,结束时间一样,就按照开始时间进行开会,该贪心策略有效

代码如下:

public class Metting{
        int start;
        int end;
        Metting(int start,int end){
            this.start = start;
            this.end = end;
        }
    }
    public static int getMaxMettingCount(Metting[] metting){
        if (metting == null || metting.length == 0){
            return 0;
        }
        int count = 0;
        int offSet = 0;
        Arrays.sort(metting,(a,b)-> a.end - b.end);
        //依次遍历每一个会议,结束时间早的先遍历
        for (int i = 0; i < metting.length; i++) {
            if ( offSet <= metting[i].start){
                count ++;
                offSet = metting[i].end;
            }
        }
        return count;
    }

2.3 黄金分割问题

加入有一块黄金,它的长度是一个数组内元素的总和,每一次分割都需要付出等长的代价,问最小的代价是多少?

举个例子     假如说一个数组[10,20,30] 那么该黄金的长度为  10 + 20 + 30 = 60,我需要把这个长度为60的黄金分割为 长度为 10 20  30  的三块,假如说我要先分割成 长度为10 和 50 的,我需要付出  60 的代价,然后再把50的分割成 20 的和30的,我需要付出50的代价,总共需要付出110的代价。而这道题还有其他更好的解法吗?答案是肯定的,我先把这个60的黄金分割成30  30 的,我需要付出60 的代价 ,我再把一个30的分割成 10 和 20 的,需要付出 30 的代价  ,同样能分割成 10  20  30 ,我这次需要付出的代价是60 + 30 = 90,那我们就可以想了,我们只要从大到小分割不就可以啦,答案是否定的,举个反例  [10,9,8,7] 黄金总长10 +9 + 8 + 7 = 34 ,从大到小分割代价 34 +24 + 15  + 8 = 81 这是最优答案吗?如果先把 长度为34 的黄金分为 15 和 19 ,然后再分为  10 9 8 7 所需要付出的代价 34 + 15 + 19 = 68 显然比刚才的代价小,那么我们该如何进行贪心呢?

答案:就是哈夫曼编码

我们新建一个小根堆,把数组中的值放在小根堆中,然后我们依次弹出两个值,求和后放入小根堆,再弹出两个值,求和后再放入小根堆中,依次往复,直到小根堆中只剩一个值,该过程中所有的求和累加在一起,就是所需的最小代价。

代码如下:

public static int goldenSelection(int[] arr){
        if (arr == null || arr.length == 0){
            return  0;
        }
        PriorityQueue<Integer> queue = new PriorityQueue();
        for (int i = 0; i < arr.length; i++) {
            queue.add(arr[i]);
        }
        int sum = 0;
        int cur = 0;
        while (queue.size() > 1){
            cur = queue.poll() + queue.poll();
            sum += cur;
            queue.add(cur);
        }
        return sum;
    }

2.4  输入正整数组costs、正整数组profits、正数k、正数M、输出你最后获得的最大钱数

costs[i] 表示i号项目的花费,profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润)

k表示你只能串行的最多左k个项目,M表示你初始的资金。

说明,没做完一个项目,马上获得的受益,可以支持你去做下一个项目。不能并行的做项目

输出:你最后获得的最大钱数

这一道题我们可以建一个根据花费排序的小根堆,再建一个根据利润排序的大根堆,开始时,我们把所有的项目放在花费的小根堆里面,大根堆是空的。我们把花费小于K的项目全弹出,放在根据利润排序的大根堆里面,然后我们把利润的大根堆弹出一个,把利润加到获得的最大钱数中,直到做了K个项目结束。

代码如下:

public class Profit {
    public static class Program{
        int cost;
        int profit;

        Program(int cost,int profit){
            this.cost = cost;
            this.profit = profit;
        }
    }

    public static int getProfit(int[] costs,int[] profits,int K,int M){
        if (costs == null || costs.length == 0){
            return 0;
        }
        Program[] programs = new Program[costs.length];
        //建出每一个项目
        for (int i = 0; i < costs.length; i++) {
            programs[i] = new Program(costs[i],profits[i]);
        }
        PriorityQueue<Program> programQueue = new PriorityQueue<>((a,b)->a.cost - b.cost);
        PriorityQueue<Program> profitQueue = new PriorityQueue<>((a,b)->b.profit - a.profit);

        //把项目丢到小根堆中去
        for (int i = 0; i < programs.length; i++) {
            programQueue.add(programs[i]);
        }
        for (int i = 0; i < K; i++) {
            while (!programQueue.isEmpty()&&programQueue.peek().cost <=  M){
                profitQueue.add(programQueue.poll());
            }
            if (profitQueue.isEmpty()){
                return M;
            }
            Program poll = profitQueue.poll();
            M += poll.profit;
        }
        return M;
    }
}

2.5 点灯问题

给定一个字符串str,只由 'X' 和 '.' 两种字符构成。

'X'表示墙,不能放灯,也不需要点亮

'.'表示居民点,可以放灯,需要点亮

如果灯灯放在 i 位置, 可以让 i - 1, i 和 i + 1 三个位置被点亮

如果点亮str中所有需要点亮的位置 至少需要几盏灯。

分析:

情况1:

如果  i 位置是 'X'                                                       ---> i+1 位置讨论

情况2:

如果  i 位置是 '.'  i+1位置是'X'     i 位置点灯                --->i位置点灯    灯+1    i+2 位置讨论

情况3:如果 i + 1 也是  '.' 就再往下讨论 i + 2 位置

如果  i 位置是 '.'  i+1位置是'.'      i+2 位置是 'X'          --->i+1位置点灯  灯+1   i+3 位置讨论

情况4:同2,只不过 i+2 位置是 '.'

如果  i 位置是 '.'  i+1位置是'.'      i+2 位置是 '.'          --->i+1位置点灯  灯+1   i+3 位置讨论

代码如下:

public class MinLight {

    public static int minLight(String str){
        if (str == null || str == ""){
            return 0;
        }
        int light = 0;
        int i = 0;
        char[] chars = str.toCharArray();
        while (i < chars.length) {
            if ('X'== chars[i]){
                i = i + 1;
            }else {
                light++;
                if (i+1 == chars.length){
                    break;
                }else {
                    if ('X' == chars[i+1]){
                        i = i + 2;
                    }else {
                        i = i + 3;
                    }
                }
            }
        }
        return light;
    }
}

总之,学习贪心算法没有什么捷径可走,需要我们不断刷题来提高我们的经验和阅历。

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值