算法-贪心算法总结

说明:贪心的本质是选择每一阶段的局部最优,从而达到全局最优。

1 简单贪心

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

// 局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int count = 0;
        int start = s.length - 1;
        for (int i = g.length - 1; i >=0; i--) {
            if (start >= 0 && s[start] >= g[i]) {
                start--;
                count++;
            }
        }
        return count;
    }
}
// 1005. K 次取反后最大化的数组和
// 给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。重复这个过程恰好 k 次。可以多次选择同一个下标 i 。以这种方式修改数组后,返回数组 可能的最大和 。

// 1局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
// 2局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大.全局最优:整个数组和 达到最大。
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        Integer[] arr = Arrays.stream(nums).boxed().toArray(size->new Integer[size]);
        Arrays.sort(arr,(p1,p2)->Integer.compare(Math.abs(p1),Math.abs(p2)));
        nums = Arrays.stream(arr).mapToInt(i->i).toArray();
        for (int i = nums.length - 1; i >= 0; i--) {
            if (nums[i] < 0 && k > 0) {
                nums[i] = -nums[i];
                k--;
            }
        }
        if (k != 0) nums[0] = k % 2 == 1 ? -nums[0] : nums[0];
        return Arrays.stream(nums).sum();
    }
}
// 860. 柠檬水找零
// 在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。注意,一开始你手头没有任何零钱。给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

// 情况一:账单是5,直接收下。
// 情况二:账单是10,消耗一个5,增加一个10
// 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0,ten = 0,twenty = 0;
        for (int bill : bills) {
            if (bill == 5) five++;
            if (bill == 10) {
                if (five <= 0) return false;
                ten++;
                five--;
            }
            if (bill == 20) {
                if (ten > 0 && five > 0) {
                    twenty++;
                    ten--;
                    five--;
                } else if (five >= 3) {
                    twenty++;
                    five -= 3;
                } else {
                    return false;
                }
            }
        }
        return true;
    }
}

2 序列问题

// 376. 摆动序列
// 如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

// 局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)
// 统计峰值的时候,数组最左面和最右面是最不好统计的。例如序列[2,5],它的峰值数量是2,如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。所以可以针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即preDiff = 0。针对以上情形,result初始为1(默认最右面有一个峰值),此时curDiff > 0 && preDiff <= 0,那么result++(计算了左面的峰值),最后得到的result就是2(峰值个数为2即摆动序列长度为2)
class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums == null || nums.length == 0 || nums.length == 1) return nums.length;
        int preDiff = 0;
        int curDiff = 0;
        int count = 1;
        for (int i = 1; i < nums.length; i++) {
            curDiff = nums[i] - nums[i - 1];
            if (curDiff > 0 && preDiff <= 0 || curDiff < 0 && preDiff >= 0) {
                count++;
                preDiff = curDiff;
            } 
        }
        return count; 
    }
}
// 738. 单调递增的数字
// 给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)

// 局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。全局最优:得到小于等于N的最大单调递增的整数。
class Solution {
    public int monotoneIncreasingDigits(int n) {
        String[] strs = (n + "").split("");
        int start = strs.length;
        for (int i = strs.length - 1; i > 0; i--) {
            if (Integer.valueOf(strs[i]) < Integer.valueOf(strs[i - 1])) {
                strs[i - 1] = (Integer.valueOf(strs[i - 1]) - 1) + "";
                start = i;
            }
        }
        for (int i = start; i < strs.length; i++) {
            strs[i] = "9";
        }
        return Integer.valueOf(String.join("",strs));
    }
}

3 股票问题

// 122. 买卖股票的最佳时机 II
// 给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

// 局部最优:收集每天的正利润,全局最优:求得最大利润。把利润分解为每天为单位的维度。
class Solution {
    public int maxProfit(int[] prices) {
        int sum = 0;
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] - prices[i - 1] > 0) {
                sum += prices[i] - prices[i - 1];
            }
        }
        return sum;
    }
}
// 714. 买卖股票的最佳时机含手续费
// 给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

// 如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。
// 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
// 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
// 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
class Solution {
    public int maxProfit(int[] prices, int fee) {
        int buy = prices[0] + fee;
        int res = 0;
        for (int price : prices) {
            if (price + fee < buy) {
                buy = price + fee;
            } else if (price > buy) {
                res += price - buy;
                buy = price; //后面要继续收获利润,已经扣除了fee
            }
        }
        return res;
    }
}

4 两个维度权衡问题

// 135. 分发糖果
// n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。你需要按照以下要求,给这些孩子分发糖果:每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获得更多的糖果。请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

// 一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。
// 先确定右边评分大于左边的情况(也就是从前向后遍历)此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
// 再确定左孩子大于右孩子的情况(从后向前遍历)。因为如果从前向后遍历,根据 ratings[i + 1] 来确定 ratings[i] 对应的糖果,那么每次都不能利用上前一次的比较结果
class Solution {
    public int candy(int[] ratings) {
        int[] candyVec = new int[ratings.length];
        candyVec[0] = 1;
        for (int i = 1; i < ratings.length; i++) {
            if (ratings[i] > ratings[i - 1]) {
                candyVec[i] = candyVec[i - 1] + 1;
            } else {
                candyVec[i] = 1;
            }
        }
        for (int i = ratings.length - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) {
                candyVec[i] = Math.max(candyVec[i],candyVec[i + 1] + 1);
            }
        }
        int sum = 0;
        for (int candy : candyVec) {
            sum += candy;
        }
        return sum;
    }
}
// 406. 根据身高重建队列
// 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

// 局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性。全局最优:最后都做完插入操作,整个队列满足题目队列属性。
class Solution {
    public int[][] reconstructQueue(int[][] people) {
        Arrays.sort(people,(num1,num2) -> {
            if (num1[0] == num2[0]) {
                return num1[1] - num2[1];
            } else {
                return num2[0] - num1[0];
            }
        });
        List list = new LinkedList<>();
        for (int[] p : people) {
            list.add(p[1],p);
        }
        return list.toArray(new int[people.length][]);
    }
}

5 区间问题

// 55. 跳跃游戏
// 给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。

// 贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
class Solution {
    public boolean canJump(int[] nums) {
        int cover = 0;
        if (nums.length == 1) return true;
        for (int i = 0; i <= cover; i++) {
            cover = Math.max(cover,i + nums[i]);
            if (cover >= nums.length - 1) return true;
        }
        return false;
    }
}
// 45. 跳跃游戏 II
// 给你一个非负整数数组 nums ,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。

// 从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最小步数。这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
class Solution {
    public int jump(int[] nums) {
        if (nums.length == 1) return 0;
        int ans = 0;
        int curDistance = 0, nextDistance = 0;
        for (int i = 0; i < nums.length; i++) {
            nextDistance = Math.max(nextDistance,i + nums[i]);
            if (i == curDistance) {
                ans++;
                curDistance = nextDistance;
                if (curDistance >= nums.length - 1) break;
            }
        }
        return ans;
    }
}
// 452. 用最少数量的箭引爆气球
// 在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

// 局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。
class Solution {
    public int findMinArrowShots(int[][] points) {
        Arrays.sort(points,(p1,p2)->{
            if (p1[0] != p2[0]) {
                return Integer.compare(p1[0],p2[0]);
            } else {
                return Integer.compare(p1[1],p2[1]);
            }
        });
        int count = 1;
        int[] tmp = new int[]{points[0][0],points[0][1]};
        for (int i = 1; i < points.length; i++) {
            if (tmp[1] >= points[i][0]) {
                tmp[1] = Math.min(tmp[1],points[i][1]);
            } else {
                count++;
                tmp = new int[] {points[i][0],points[i][1]};
            }
        }
        return count;
    }
}
// 435. 无重叠区间
// 给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。注意:可以认为区间的终点总是大于它的起点。区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

// 按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。
class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals.length < 2) return 0;
        Arrays.sort(intervals,(p1,p2)->{
            if (p1[1] != p2[1]) {
                return Integer.compare(p1[1],p2[1]);
            } else {
                return Integer.compare(p1[0],p2[0]);
            }
        });
        int[] tmp = new int[] {intervals[0][0],intervals[0][1]};
        int validAns = 1;
        for (int i = 1; i < intervals.length; i++) {
            if (tmp[1] > intervals[i][0]) continue;
            validAns++;
            tmp = new int[] {intervals[i][0],intervals[i][1]};
        }
        return intervals.length - validAns;
    }
}
// 763. 划分字母区间
// 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

// 在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
class Solution {
    public List partitionLabels(String s) {
        List res = new ArrayList<>();
        int[] hash = new int[26];
        for (int i = 0; i < s.length(); i++) {
            hash[s.charAt(i) - 'a'] = i;
        }
        int curIndex = 0;
        int preIndex = -1;
        for (int i = 0; i < s.length(); i++) {
            curIndex = Math.max(curIndex,hash[s.charAt(i) - 'a']);
            if (i == curIndex) {
                res.add(curIndex - preIndex);
                curIndex = 0;
                preIndex = i;
            }
        }
        return res;
    }
}
// 56. 合并区间
// 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

// 按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。
class Solution {
    public int[][] merge(int[][] intervals) {
        List res = new ArrayList<>();
        Arrays.sort(intervals,(o1,o2) -> {
            return Integer.compare(o1[0],o2[0]);
        });
        int start = intervals[0][0];
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i][0] > intervals[i - 1][1]) {
                res.add(new int[]{start,intervals[i - 1][1]});
                start = intervals[i][0];
            } else {
                intervals[i][1] = Math.max(intervals[i][1],intervals[i - 1][1]);
            }
        }
        res.add(new int[]{start,intervals[intervals.length - 1][1]});
        return res.toArray(new int[res.size()][]);
    }
}

6 其他问题

// 53. 最大子数组和
// 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。

// 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。全局最优:选取最大“连续和”
class Solution {
    public int maxSubArray(int[] nums) {
        if (nums.length == 1) return nums[0];
        int sum = Integer.MIN_VALUE;
        int count = 0;
        for (int i = 0; i < nums.length; i++) {
            count += nums[i];
            sum = Math.max(sum,count);
            if (count < 0) count = 0;
        }
        return sum;
    }
}
// 134. 加油站
// 在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。说明: 如果题目有解,该答案即为唯一答案。输入数组均为非空数组,且长度相同。输入数组中的元素均为非负数。

// 局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置。
class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int curSum = 0;
        int totalSum = 0;
        int start = 0;
        for (int i = 0; i < gas.length; i++) {
            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];
            if (curSum < 0) {
                start = i + 1; // 更新起点 -> 局部最优
                curSum = 0;
            }
        }
        return totalSum < 0 ? -1 : start; // 如果总的油量小于总油耗量,说明怎么走都不符合。
    }
}
// 968. 监控二叉树
// 给定一个二叉树,我们在树的节点上安装摄像头。节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。计算监控树的所有节点所需的最小摄像头数量。

// 情况1:左右节点都有覆盖
// 情况2:左右节点至少有一个无覆盖的情况
// 情况3:左右节点至少有一个有摄像头
// 情况4:头结点没有覆盖
class Solution {
    // 0: 未覆盖,1:摄像头,2:已覆盖
    private int res = 0;

    public int minCameraCover(TreeNode root) {
        if (traversal(root) == 0) {
            res++;
        }   
        return res;
    }

    private int traversal(TreeNode root) {
        if (root == null) return 2;
        int left = traversal(root.left);
        int right = traversal(root.right);
        if (left == 2 && right == 2) return 0;
        if (left == 0 || right == 0) {
            res++;
            return 1;
        }
        if (left == 1 || right == 1) {
            return 2;
        }
        return -1;
    }
}

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