贪心算法详解

贪心算法

一、基本概念

贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。

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

能使用贪心算法的题目很少,但是一经证明某问题可用贪心算法求解,贪心算法则是很有效的一种解法。

二、与动态规划的对比

动态规划和贪心算法都是用来求最优化问题,且二者都必须具有最有子结构。贪心算法可以解决的问题,动态规划都能解决,可以说,贪心算法是动态规划的一个特例。

贪心算法和动态规划最大的不同点:
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。

贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。动态规划主要运用于二维或三维问题,而贪心一般是一维问题。

三、常见贪心算法

1、背包相关问题

最优装载问题:
给出N个物体,有一定重量。请选择尽量多的物体,使总重量不超过C。

贪心策略:只关心物品的数量,因此将物品按照重量大小排序,依次放入小的,则放的是最多的物品。

2、区间相关问题

3、图的最小生成树

Kruskal算法:不断选取最短的边加入到点集中,最终是点集全连通。
具体步骤:
1、将图的所有边按照权值从小到大排序;
2、按照从小到大的顺序遍历每条边,如果当前这条边的两个顶点在一个连通图里(则不能添加,构成环了),否则添加进最小生成树里。重复这个过程直到图中任意两点都连通。

四、LeetCode经典题型

1、LeetCode 135. Candy

原题目连接

一排小孩,每个孩子有一个优先级,每个孩子至少要发给一个糖果,优先级高的比周围的孩子的糖果要多。
需要注意的是,优先级一样的没有要求说一样多糖果!

Example 1:
Input: [1,0,2]
Output: 5
Explanation: 分配方案为:2,1,2,共需要5个。

Example 2:
Input: [1,2,2]
Output: 4
Explanation:分配方案:1,2,1,共需要4个。

解题思路:
先初始化,每人一糖。
然后从左到右处理一次:右边的比左边优先级高的,在左边的基础上加一;
从右向左处理一次:左边的比右边优先级高的,在右边的基础上加一。(注意这趟遍历,需要考虑到不能改变第一趟从左向右的大小关系)

public int candy(int[] ratings) {
    if(ratings == null || ratings.length <= 0){
        return 0;
    }
    
    int[] c = new int[ratings.length];
    //1、每个小孩至少一颗糖
    for(int i=0;i ratings[i-1]){
            c[i] = c[i-1] + 1;
        }
    }
    
    //3、从右向左遍历,如果当前小孩的优先级比后一个小孩优先级高,   
    // 则当前小孩的糖果,应该为后一个小孩糖果+1 和 当前小孩糖果中的较大值(因为第一遍可能已经满足条件了)
    for(int i=ratings.length-1;i>0;i--){
        if(ratings[i] < ratings[i-1] && c[i] + 1 > c[i-1]){
            c[i-1] = c[i] + 1;
        }
        //c[i-1] = Math.max(c[i-1],c[i]+1);
    }
   
    int t = 0;
    for(int i=0;i

算法分析:
时间复杂度:从左向右O(N),从右向左O(N),总的复杂度为O(N);
空间复杂度:暂存每个小孩的糖果数,O(N);

2、LeetCode 455. Assign Cookies

原题链接

假设你是一个很棒的父母,并想给你的孩子一些饼干。 但是,你应该给每个孩子一个饼干。 每个孩子都有一个贪婪因子gi,这是孩子满意的cookie的最小尺寸; 并且每个cookie j的大小为sj。 如果sj> = gi,我们可以将cookie j分配给孩子i,而孩子i将是内容。 您的目标是使得尽量多的孩子接受你的蛋糕。

Example 1:
Input: [1,2,3], [1,1] //gi,si
Output: 1
Explanation: 有三个孩子,贪婪因子分别是1,2,3,有两个蛋糕,大小是1、1。
因此最多只能满足一个孩子的需求。

Example 2:
Input: [1,2], [1,2,3] //gi,si
Output: 2
Explanation: 有2个孩子,贪婪因子分别是1,2,有3个蛋糕,大小是1、2、3。
因此可以满足2个孩子的需求。

解题思路:
关键点:给每个小孩分配尽可能最小的蛋糕。这样蛋糕肯定是能满足最多的小孩。

public int findContentChildren(int[] g, int[] s) {
    //先对小孩按照贪婪因子从小到大排序,先满足最容易满足的小孩(难啃的骨头放到最后啃)
    Arrays.sort(g);
    //蛋糕也从小到大排序,每次只分配未分配蛋糕中的最小的那一个蛋糕。
    Arrays.sort(s);
    
    int res = 0;
    int i = 0;
    int j = 0;
    while(i < g.length && j < s.length){
        //分配最小的蛋糕j给孩子i
        if(s[j] >= g[i]){
            res ++;
            i++;
            j++;
        }else{ //无法分配,则找下一个蛋糕
            j++;
        }
    }
    
    return res;
}

3、LeetCode 55. Jump Game

原题链接

给定一个非负整数数组,您最初定位在数组的第一个索引处。
数组中的每个元素表示该位置的最大跳转长度。确定您是否能够到达最后一个索引。

Example 1:
Input: [2,3,1,1,4]
Output: true
Explanation: 从0位置跳1步到1,然后跳3步,到4(最后一个位置)

Example 2:
Input: [3,2,1,0,4]
Output: false
Explanation: 无论怎么跳,都会调到第4个位置(下标为3),它最大只能跳0步,因此无法到最后的位置。

解题思路:
从每个位置计算,计算这个位置能跳的最远的位置,最后如果这个最远的位置大于最后一个位置,那么就能跳到最后。
max表示当前位置能跳的最远位置,这是一个局部的最优贪心策略,在全局看来也是最优的,因为局部能够到达的最大范围也是全局能够到达的最大范围。

public boolean canJump(int[] nums) {
    int max = 0; //能跳的最远的位置
    
    //从每个位置开始跳,计算当前位置能跳到的最远位置(注意i<=max 必须满足当前位置能太跳到)
    for(int i=0;i max){
            max = i + nums[i];
        }
    }
    
    return max >= nums.length -1;
}

4、LeetCode 45. Jump Game II

原题链接
给定一个非负整数数组,您最初定位在数组的第一个索引处。
数组中的每个元素表示该位置的最大跳转长度。
您的目标是以最小跳跃次数到达最后一个索引。

【注】:假设总能到最后一个位置。

Example:
Input: [2,3,1,1,4]
Output: 2
Explanation: 从0跳到1,然后跳3步直接到最后位置,共需要跳2次。

解题思路:

  • 动态规划解法:

    用dp[i]表示跳到位置i需要的最少步数。

public int jump(int[] nums) {
    int[] dp = new int[nums.length];
    for(int i=1;i

算法分析:
时间复杂度:O(N*2);
空间复杂度:O(N);

  • 贪心算法:
public int jump(int[] nums) {
    int reached = 0;  //当前能到达的最远位置
    int max = 0;      //暂存从第i个位置能到的最远位置
    int times = 0;    //总共跳的次数
    for(int i=0;i

算法分析:
时间复杂度:O(N);
空间复杂度:O(1);

5、LeetCode 435. Non-overlapping Intervals

原题链接
给定间隔的集合,找到需要移除的最小间隔数,以使其余的间隔不重叠。

注:
1、可以认为每个间隔的终点一定大于起点
2、[1,2]和[2,3]认为不重叠

Example 1:
Input: [ [1,2], [2,3], [3,4], [1,3] ]
Output: 1
Explanation: 删除[1,3],所有间隔不重叠,因此最小的移除数量是1。

Example 2:
Input: [ [1,2], [1,2], [1,2] ]
Output: 2
Explanation: 需要删除两个[1,2]才能保证无重叠,因此结果为2。

解题思路:
首先思考到这种间隔问题,需要先对间隔进行排序,如活动安排问题,最多安排多少个活动,需要按照结束时间排序,优先满足结束时间早的活动。

同样的,本问题需要先按照间隔的start从小到大排序,排序完后,采用贪心策略,循环所有的间隔,比较当前间隔与最后一个不冲突间隔是否冲突,如果冲突,选择其中end较大的删除(这样肯定是最优的删除策略)。

public int eraseOverlapIntervals(Interval[] intervals) {
    Arrays.sort(intervals, (x,y) -> x.start - y.start);
    int count = 0;
    int j = 0; //标识上一个不冲突的间隔
    for(int i=1;i

后续待续。

你可能感兴趣的:(算法题目)