贪心算法

二叉树的递归套路暂时先告一段落了,今天来聊聊贪心算法。

一、什么是贪心算法

1、最具自然智慧的算法

用最普通的思维就能想到的解决方法。

2、用一种局部最功利的标准,总是做出在当前看来是最好的选择

3、难点在于证明局部最功利的标准可以得到全局最优解

4、对于贪心算法的学习主要以增加阅历和经验为主

对于每一个利用贪心算法求解的题目,实际采取的贪心策略都是不同的,也就是说当前的贪心策略并不能帮助你解决另外的贪心题目,但是能够增加你的经验,让你学会如何尝试。

5、只要能够使用对数器校验策略是对的即可

对于每一个贪心策略,在提出之初,不能举出明显的反例,我们就可以尝试按此写出代码来,然后用最暴力的方式写出对数器来校验,校验通过则说明我们的贪心策略是对的,否则就需要换一个贪心策略了。

至于如何去证明验证通过的贪心策略真的就是对的,这不是我们需要关心的事情,把证明留给做学术研究的就可以了。

二、从头到尾讲一道贪心算法的题目

1、题目描述

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

字典序定义:Java中字符串的排序方式,比如 “abc” 和 “bce”相比,“abc” 第一位a的ASCII码小于 “bce” 第一位b的ASCII码,所以 “abc” < “bce”,所以“abc”的字典序更小。对于位数不等的字符串,“abc” 和 “be”,从各自的第一位开始比较,“abc”的a小于“be”的b,所以“abc” < "be"。

2、思路

(1)错误的

可能最直观的解法就是,所有字符串按字典序先排序,将排序后的字符串依次拼接起来就是最后的结果。

反例:["b", "ba"],各字符串按字典序先排序的结果是["b", "ba"],最后拼接的字符串是"bba",但是本例的最终结果是"bab",因为"bab" < "bba"。所以这样的贪心策略是错的。

(2)正确的

对于任意两个字符串 A和B,如果 A与B 的拼接结果小于 B与A 的拼接结果则A排在前面,否则B排在前面,按照这样的策略将整个字符串数组先排序一遍,再将排序后的数组挨个拼接起来得到的结果就是最终的结果。

这时候就存在一个问题了,对于任意的使用贪心策略求解的题,贪心策略的提出是比较容易的,但是如何证明它是正确的就比较难了。

不要忘了,我们有对数器啊,我们可以使用最暴力的解法获取正确答案,再和我们贪心策略获得的结果进行比较,不等则说明我们的贪心策略存在问题,那么,赶紧换另外的贪心策略

总结:任何贪心策略都是先提出再证明,既然我们有对数器,我们何必再花大力气去用证明的方式来确定贪心策略的正确性呢?证明还是留给做学术研究的人来做吧

3、笔试面试中出现的概率

贪心算法在笔试中出现的概率更高,在和面试官直接面试时,出现的概率反而不高。为啥?

因为贪心策略的代码都很简单,定义好比较的标准,也就是定义好比较器,按照此标准排序或使用堆,最后就得到结果了。也就是说

(1)起不到考察Coding的作用,只要定义好标准,然后使用排序或堆就能得到结果了;

(2)贪心策略的区分度不够,想对了策略就是满分,没有想对就是0分,分不出各种层次来;

4、代码

/**
 * @author Java和算法学习:周一
 */
public static class MyComparator implements Comparator {
    @Override
    public int compare(String o1, String o2) {
        return (o1 + o2).compareTo(o2 + o1);
    }
}

public static String lowestDictionary(String[] str) {
    if (str == null || str.length < 1) {
        return null;
    }
    Arrays.sort(str, new MyComparator());
    StringBuilder result = new StringBuilder();
    for (String s : str) {
        result.append(s);
    }
    return result.toString();
}

是不是发现只要贪心策略想好了,代码是及其的简单。

包含对数器的所有代码地址:https://github.com/monday-pro/algorithm-study/blob/master/src/basic/greedy/LowestDictionary.java

三、会议室问题

1、题目描述

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

2、贪心策略

可能我们会有以下的一些贪心想法:

(1)哪个会议开始时间早,我就安排这个会议。那这个想法对不对呢?

反例:[8, 21],[9, 10],[10, 12],[15, 20],很明显开始时间最早的会议是[8, 21],但是安排了这个会议后,其他的会议就安排不了了,但是此时的最优解是安排[9, 10],[10, 12],[15, 20]三个会议,所以这个想法不成立。

(2)哪个会议持续时间短,我就安排这个会议。那这个想法对不对呢?

反例:[8, 15],[14, 18],[17, 24],很明显会议持续时间最短的是[14, 18],但是选了这个会议其他两个就不能选了,但是此时的最优解是安排[8, 15],[17, 24]两个会议,所以这个想法不成立。

(3)哪个会议结束时间早,我就安排这个会议。那这个想法对不对呢?

对(至少目前看来是对的)。因为当前不能明显举出反例来,但是我们还得拿对数器来校验是否真的对。至于证明,留给做学术研究的来做吧。

3、思路

所有会议先根据结束时间按从小到大排序。

安排第一个会议A,将剩下会议的开始时间小于A会议结束时间的会议剔除,

剩下的再安排第一个会议B,将剩下会议的开始时间小于B会议结束时间的会议剔除,直到没有可以安排的会议。

4、代码

/**
 * 假设会议开始时间和结束时间都是 大于0 的数值
 * 
 * @author Java和算法学习:周一
 */
public static int bestArrange(Meeting[] meetings) {
    // 所有会议先根据结束时间按从小到大排序
    Arrays.sort(meetings, (o1, o2) -> o1.end - o2.end);
    // 已经安排的会议数量
    int result = 0;
    // 当前所处时间点
    int timeLine = 0;
    // 遍历所有会议,结束时间早的已经在前
    for (Meeting meeting : meetings) {
        // 剩下会议的开始时间在此时会议结束时间之后,则安排剩下会议中的第一个会议
        if (meeting.start >= timeLine) {
            result++;
            timeLine = meeting.end;
        }
    }
    return result;
}

包含对数器的所有代码地址:https://github.com/monday-pro/algorithm-study/blob/master/src/basic/greedy/BestArrange.java

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