五大常用算法——贪心算法

汇总几个常见的贪心算法实现的问题

  • 概述
  • IPO(最大投资收益)
  • 金砖最小分割代价
  • 会议室相关问题
  • 分发糖果
  • 柠檬水找零
  • 分发饼干

概述

这里主要总结一些涉及贪心算法的题目,对于贪心算法原理不作阐述。简单罗列一下搜集的资料。

贪心算法的定义:
贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

解题的一般步骤是:
1.建立数学模型来描述问题;
2.把求解的问题分成若干个子问题;
3.对每一子问题求解,得到子问题的局部最优解;
4.把子问题的局部最优解合成原来问题的一个解。

以上较为官方的一些阐述,那么个人认为贪心最大的工作就是尝试,在稿子上尝试写出几个简单示例的贪心策略,验证成功直接coding即可。


IPO问题

LeeCode502. IPO

假设 力扣(LeetCode)即将开始其 IPO。为了以更高的价格将股票卖给风险投资公司,力扣 希望在 IPO 之前开展一些项目以增加其资本。 由于资源有限,它只能在 IPO 之前完成最多 k 个不同的项目。帮助 力扣 设计完成最多 k 个不同项目后得到最大总资本的方式。

给定若干个项目。对于每个项目 i,它都有一个纯利润 Pi,并且需要最小的资本 Ci 来启动相应的项目。最初,你有 W 资本。当你完成一个项目时,你将获得纯利润,且利润将被添加到你的总资本中。

总而言之,从给定项目中选择最多 k 个不同项目的列表,以最大化最终资本,并输出最终可获得的最多资本。

  • 贪心策略思考:每次选择项目的时候,都在现有的资本可以实现的象中选取利润最大的项目。也就是说必须满足(cur)W >= Ci, choose = Max(Ci - Pi);
  • 实现: 首先将资本Ci构建成为一个小顶堆,所需资本小的放在前面,然后遍历这个小顶堆,将满足投资要求的项目扔进由利润值构建的大顶堆中去,那么每次选择项目时,就取出大顶堆的堆顶项目投资即可。
  • 代码:
    /*
    -----------------------------------创建Node辅助贪心算法----------------------------------
    */
    // 创建项目类
    class Node {
        int c;      // 资本
        int p;      // 利润

        public Node(int c, int p) {
            this.c = c;
            this.p = p;
        }
    }

    public int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) {
        // 首先创建大小堆
        PriorityQueue<Node> cHeap = new PriorityQueue<Node>(new MinCapitalComparator());
        PriorityQueue<Node> pHeap = new PriorityQueue<Node>(new MaxProfitComparator());
        // 将capitalHeap构造成为资本的小顶堆
        for (int i = 0; i < Capital.length; i++) {
            cHeap.add(new Node(Capital[i], Profits[i]));
        }
        int ans = W;
        // 遍历k,进行k次交易
        for (int j = 0; j < k; j++) {
            while (!cHeap.isEmpty() && cHeap.peek().c <= ans) {
                pHeap.add(cHeap.poll());
            }
            if (pHeap.isEmpty()) return ans;
            ans += pHeap.poll().p;
        }
        // 返回利润
        return ans;
    }

    // 小顶堆改造
    public class MinCapitalComparator implements Comparator<Node> {

        @Override
        public int compare(Node o1, Node o2) {
            return o1.c - o2.c;
        }
    }

    // 大顶堆改造
    public class MaxProfitComparator implements Comparator<Node> {

        @Override
        public int compare(Node o1, Node o2) {
            return o2.p - o1.p;
        }
    }

金砖最小分割代价

传送门——分割最小代价

题目:
一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的 金条,不管切成长度多大的两半,都要花费20个铜板。一群人想整分整块金 条,怎么分最省铜板?例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60. 金条要分成10,20,30三个部分。 如果, 先把长度60的金条分成10和50,花费50 再把长度50的金条分成20和30,花费50 一共花费110铜板。但是如果, 先把长度60的金条分成30和30,花费60 再把长度30金条分成10和20,花费30 一共花费90铜板。输入一个数组,返回分割的最小代价.

  • 贪心策略: 根据题意可知,如果当前的金砖总价较高,那么分割代价肯定较高,所以考虑优先挑选数组最小的两个元素分割,各分代价就是两个元素的和,求出和之和再扔进数组继续分割
  • 实现方法: 同样借用优先级队列,构造一个小顶堆,元素升序排列。
  • 实现思想,不断累加数组数种两个最小元素的分割代价。
  • 代码“
    public static int lessMoney(int[] arr) {
        //创建一个优先级队列(默认情况下按照升序进行排列)
        PriorityQueue<Integer> PQ = new PriorityQueue<Integer>(); 
        for (int i = 0; i < arr.length; i++) {
            PQ.add(arr[i]);
        }
        int res = 0;
        int sum = 0;
        while(PQ.size() > 1) {
            sum = PQ.poll() + PQ.poll();   // 弹出两个最小值求和
            res += sum;                            
            PQ.add(sum);                         // 累加sum之后再扔回堆中
        }
        return res;
    }

会议室相关问题

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

  • 分析: 只有一个会议室,所以每个项目的开始时间和结束时间必须要错开,保证不会有两个项目同时使用一个会议室的冲突。
  • 贪心策略:优先选取最早结束时间的项目,然后从开始时间在此处后面项目中再选最长结束时间的项目。
  • 实现方法: 优先级队列,创建一个队列,以每个项目的最早结束时间升序排列,依次遍历查看队列中元素,如果最迟时间为0或者当前的最早时间大于等于最迟时间,ans加1。
  • 代码如下:
    // 创建项目的实体类
    public static class Program{
        int start;  //项目开始时间
        int end;    //项目结束时间

        public Program(int start, int end){
            this.start = start;
            this.end = end;
        }
    }

    // 创建一个项目按照结束时间从小到大的排列顺序
    public static class MinEndTimeComparator implements Comparator<Program> {
        @Override
        public int compare(Program o1, Program o2) {
            return o1.end - o2.end;
        }
    }

    /**
     * 主函数入口
     * @param programs  给定项目的数组(包含项目的开始时间和结束时间)
     * @param start     会议室可以使用的开始时间
     * @return          最多会议室的使用次数
     */
    public static int bestArrange(Program[] programs, int start) {
        PriorityQueue<Program> queue = new PriorityQueue<>(new MinEndTimeComparator());
        for (int i = 0; i < programs.length; i++) {
            queue.add(programs[i]);
        }
        int res = 0;
        while(!queue.isEmpty()){
            if(start <= queue.peek().start) {
                res++;
                start = queue.poll().end;
            }else{
                queue.poll();
            }
        }
        return res;
    }

LintCode920——会议室

给定一系列的会议时间间隔,包括起始和结束时间[[s1,e1],[s2,e2],…(si < ei),确定一个人是否可以参加所有会议。

思路: 类似上一题(使用优先级队列)

/**
 * Definition of Interval:
 * public class Interval {
 *     int start, end;
 *     Interval(int start, int end) {
 *         this.start = start;
 *         this.end = end;
 *     }
 * }
 */

public class Solution {
    /**
     * @param intervals: an array of meeting time intervals
     * @return: if a person could attend all meetings
     */
    public boolean canAttendMeetings(List<Interval> intervals) {
        // Write your code here
        if(intervals.size() < 2) return true;
        PriorityQueue<Interval> heap = new PriorityQueue<Interval>(new MyConparator());
        for(Interval pro: intervals){
            heap.add(pro);
        }
        // h构建小顶堆结束,遍历检查后一项的start是否>=前一项的end
        int end = heap.poll().end;
        while(!heap.isEmpty()){
            if(heap.peek().start < end){
                return false;
            }
            end = heap.poll().end;
        }
        return true;
    }
    
    // n比较器,按照开始时间排序
    class MyConparator implements Comparator<Interval> {
        @Override
        public int compare(Interval o1, Interval o2){
            return o1.start - o2.start;
        }
    }
}

LeeCode1353. 最多可以参加的会议数目(经典)

给你一个数组 events,其中 events[i] = [startDayi, endDayi] ,表示会议 i 开始于startDayi ,结束于 endDayi 。

你可以在满足 startDayi <= d <= endDayi 中的任意一天 d 参加会议 i 。注意,一天只能参加一个会议。

请你返回你可以参加的 最大 会议数目

  • 思路分析: 这题跟一个会议室供多个项目使用有区别。对于某一天我可以参加的所有会议中,我肯定会选择最早结束的那个,因此贪心的思想就出现了:每次在可以进行的会议中选择拥有最早结束时间的会议来实现。
  • 代码如下:

    public int maxEvents(int[][] events) {
        // 首先将所有会议按照会议开始时间进行排序
        Arrays.sort(events, new Comparator<int[]>(){
            @Override
            public int compare(int[] a, int[] b){
                return a[0] - b[0];
            }
        });
        // 创建优先级队列
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        int cur = 0;     // 当前处于第0天
        int index = 0;      // 会议按照最早开始时间存储的编号
        int n = events.length;   // 会议总数
        int ans = 0;             // 结果集
        // 循环结束条件
        while(index < n || !queue.isEmpty()){
            // 如果队列为空,将当前的项目结束时间扔进去,day调整为当前的时间
            if(queue.isEmpty()){
                queue.add(events[index][1]);
                cur = events[index++][0];
            }
            // 遍历后面的会议。如果开始时间在day时间之间,都将他们入独对列
            while(index < n && events[index][0] <= cur){
                queue.add(events[index++][1]);
            }
            // 如果当前会议可以进行
            if(queue.peek() >= cur){
                cur++;
                ans++;
            }
            //弹出会议
            queue.poll();
        }
        return ans;
    }


分发糖果

135. 分发糖果

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

你需要按照以下要求,帮助老师给这些孩子分发糖果:

每个孩子至少分配到 1 个糖果。
相邻的孩子中,评分高的孩子必须获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢?

思路分析:(贪心策略)

  • 首先保证每个孩子手上有一颗糖
  • 从左到右遍历数组,如果当前孩子比他左边的孩子分数高,则这个孩子再给一颗糖。即ratings[i] > ratings[i-1] 的时候,i孩子在前一个孩子的基础上再给一颗糖。一次遍历之后只考虑了左孩子的情况,那么现在还要考虑右孩子。从以同样的方式从到左再来一遍
  • 从右到左遍历数组,如果当前孩子比他右边的孩子分数高,则这个孩子再给一颗糖。即ratings[i] > ratings[i+1] 的时候,i孩子在后一个孩子的基础上再给一颗糖
  • 代码如下:
class Solution {
    // 贪心算法求解
    public int candy(int[] ratings) {
        if (ratings.length == 1) return 1;
        int n = ratings.length;
        int[] candy = new int[n];
        Arrays.fill(candy,1);// 首先需要保证每个孩子手上有一颗糖
        // 从左至右遍历数组
        for (int i = 1; i < n; i++) {
            if(ratings[i] > ratings[i - 1]){
                candy[i] = candy[i - 1] + 1;
            }
        }
        // 从右到左再来一遍
        for (int i = n - 2; i >= 0; i--) {
            if(ratings[i] > ratings[i + 1] && candy[i] <= candy[i + 1]){
                candy[i] = candy[i + 1] + 1;
            }
        }
        // 返回结果集
        int ans = 0;
        for (int c : candy) {
            ans += c;
        }
        return ans;
    }
}

柠檬水找零

LeeCode860. 柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。

顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

思路:只会收到5,10,20三种面个的货币,20无论如何都不会用作找零的面额不考虑。没后每次找零的时候优先用大额面额(5,10) , 如果找零后5元成负数,则一定不可能找零。
代码:

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0;   // 代表5元面额
        int ten = 0;    // 代表10元面额
        //遍历数组
        for(int m : bills){
            if(m == 5){
                five++;
                continue;
            }
            if(m == 10){
                five--;
                ten++;
            }else{
                if(ten != 0){
                    five--;
                    ten--;
                }else{
                    five -= 3;
                }
            }
            // 每次找零之后,如果five变成了负,则无法找零
            if(five < 0) return false;
        }
        return true;
    }
}

分发饼干

LeeCode455. 分发饼干
难度简单145假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

思路: 典型贪心策略,尽可能的用小饼干喂饱胃口小的孩子

  • 首先将两个数组分别排序
  • 遍历两个数组,如果当前饼干喂不饱当前的小孩,那么后面所有的小孩肯定也喂不饱,所以直接查看下一个饼干能否喂饱当前的小孩。
  • 如果当前的饼干可以喂饱当前的小孩,那么小孩和饼干都从列表中移除,结果集加1.
  • 代码如下:
class Solution {
    public int findContentChildren(int[] g, int[] s) {
        // 首先将两个数组进行排序
        Arrays.sort(g);
        Arrays.sort(s);
        int i = 0, j = 0, ans = 0;
        while(i < g.length && j < s.length){
            // 满足了一个小孩,s和g均后移,ans加1
            if(s[j] >= g[i]){
                i++;
                ans++;
            }
            // 不管当前饼干是否可以满足此小孩,指针都向后移
            j++;
        }
        return ans;
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 年轻即出发... 简书://www.greatytc.com/u/7110a2ba6f9e 知乎:htt...
    囧么肥事阅读 340评论 0 1
  • 贪心的过程要么是最大要么是最小,堆可以很好的满足这个要求。 问题1:一块金条切成两半,是需要花费和长度数值一样的铜...
    放开那个BUG阅读 298评论 0 0
  • 1.随时找到数据流的中位数 【题目】有一个源源不断地吐出整数的数据流,假设你有足够的空间来保存吐出的数。请设计一个...
    Miss_麦兜阅读 1,021评论 0 1
  • 题目 :一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费...
    名字是乱打的阅读 164评论 0 1
  • 贪心算法是指,在对问题求解时,将整体的问题划分为一系列局部问题,对局部的问题求出最优解,再通过数学归纳法证明每一步...
    憨憨二师兄阅读 394评论 0 0