一、什么是贪心:
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
二、贪心的策略:
举反例,如果想不到反例,那么就试一试贪心。
三、步骤:
-
将问题分解为若干个子问题;
-
找到适合的贪心策略;
-
求子问题的最优解;
-
局部最优推全局最优
四、例题:
-
分发饼干
-
题目:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子
i
,都有一个胃口值g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j
,都有一个尺寸s[j]
。如果s[j] >= g[i]
,我们可以将这个饼干j
分配给孩子i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。示例 1:
输入: g = [1,2,3], s = [1,1] 输出: 1 解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。
-
思路
-
用尽可能的小饼干喂胃口小的孩子
-
-
题解:
class Solution { public int findContentChildren(int[] g, int[] s) { if (s.length == 0) { return 0; } Arrays.sort(g); Arrays.sort(s); int res = 0; //小饼干先喂小胃口 for (int i = 0; i < s.length && res < g.length; i++) { if (s[i] >= g[res]) { res++; } continue; } return res; } }
-
总结:思考局部最优,如何推出全局最优,以及是否存在反例
-
-
摆动序列
-
题目
-
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
-
示例 1:
输入:nums = [1,7,4,9,2,5] 输出:6 解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
-
-
思路
- 包含所有的波峰与波谷
-
题解
class Solution { public int wiggleMaxLength(int[] nums) { if (nums.length < 2) { return nums.length; } int res = 1; int pre = 0; for (int i = 1; i < nums.length; i++) { int cur = nums[i] - nums[i - 1]; if (cur > 0 && pre <= 0 || cur < 0 && pre >= 0) { res++; pre = cur; } } return res; } }
-
总结:整个序列有最多的峰值
-
-
最大子序和
-
题目
-
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
-
-
思路
-
局部最优:当前“连续和”为负数时放弃,从下一个元素重新计算“连续和”。负数加上下一个元素 “连续和”只会越来越小。
-
全局最优:选取最大“连续和”
-
-
题解
class Solution { public int maxSubArray(int[] nums) { int res = Integer.MIN_VALUE; int sum = 0; for (int i = 0; i < nums.length; i++) { sum += nums[i]; res = Math.max(res, sum); if (sum < 0) { sum = 0; } } return res; } }
-
总结:
-
-
跳跃游戏
-
题目
-
给你一个非负整数数组
nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以,返回
true
;否则,返回false
。
-
-
思路
- 扩大最大范围,判断最大范围能否覆盖数组长度;
-
题解
class Solution { public boolean canJump(int[] nums) { if (nums.length < 2) { return true; } int range = 0; for (int i = 0; i <= range; i++) { if (range >= nums.length - 1) { return true; } range = Math.max(range, nums[i] + i); } return false; } }
-
总结:不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。
-
-
跳跃游戏II
-
题目
-
给定一个长度为
n
的 0 索引整数数组nums
。初始位置为nums[0]
。每个元素
nums[i]
表示从索引i
向前跳转的最大长度。换句话说,如果你在nums[i]
处,你可以跳转到任意nums[i + j]
处: -
返回到达
nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达nums[n - 1]
。
-
-
思路
- 每一次都跳到最大范围;
- 最大范围变化几次,则需要几步;
-
题解
class Solution { public int jump(int[] nums) { if (nums.length == 1) { return 0; } int res = 0; int range = 0; while (range < nums.length - 1) { int temp = 0; for (int i = 0; i <= range; i++) { temp = Math.max(temp, i + nums[i]); } range = temp; res++; } return res; } //优化 public int jump(int[] nums) { int range = 0; int res = 0; int end = 0; for (int i = 0; range < nums.length - 1; i++) { end = Math.max(end, i + nums[i]); if (i == range) { res++; range = end; } } return res; } }
-
总结:
-
-
K次取反最大化的数组和
-
题目
-
给你一个整数数组
nums
和一个整数k
,按以下方法修改该数组:选择某个下标i
并将nums[i]
替换为-nums[i]
。重复这个过程恰好k
次。可以多次选择同一个下标i
。以这种方式修改数组后,返回数组 可能的最大和 。
-
-
思路
- 先从最小的负数开始操作,直至k = 0 || 没有负数
- 然后操作一次或者0次最小的整数;
- 累加数组和
-
题解
class Solution { public int largestSumAfterKNegations(int[] nums, int k) { Arrays.sort(nums); for (int i = 0; i < nums.length && k > 0 && nums[i] < 0; i++, k--) { nums[i] = 0 - nums[i]; } Arrays.sort(nums); nums[0] = k % 2 == 0 ? nums[0] : 0 - nums[0]; return Arrays.stream(nums).sum(); } }
-
总结:存在负数:让绝对值大的负数变为正数;无负数:只找数值最小的正整数进行反转
-
-
加油站
-
题目
-
在一条环路上有
n
个加油站,其中第i
个加油站有汽油gas[i]
升。你有一辆油箱容量无限的的汽车,从第
i
个加油站开往第i+1
个加油站需要消耗汽油cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。给定两个整数数组
gas
和cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回-1
。如果存在解,则 保证 它是 唯一 的。
-
-
思路
- 遍历cur += gas[i] - cost[i];
- 一旦cur< 0,则之前遍历的都不是起点;
- 整个消耗小于0,则不存在符合条件的起点;
-
题解
class Solution { public int canCompleteCircuit(int[] gas, int[] cost) { int start = 0; int cur = gas[0] - cost[0]; int sums = gas[0] - cost[0]; for (int i = 1; i < gas.length; i++) { sums += gas[i] - cost[i]; if (cur < 0) { start = i; cur = 0; } cur += gas[i] - cost[i]; } return sums < 0 ? -1 : start; } }
-
总结:
-
-
分发糖果
-
题目
-
n
个孩子站成一排。给你一个整数数组ratings
表示每个孩子的评分。你需要按照以下要求,给这些孩子分发糖果:每个孩子至少分配到
1
个糖果。相邻两个孩子评分更高的孩子会获得更多的糖果。 - 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
-
-
思路:
- 存在左右两个维度;
- 逐一维度进行比较;
- 去最大值,则满足要求;
-
题解
class Solution { public int candy(int[] ratings) { int res = 0; //比左孩子 int[] left = new int[ratings.length]; //比右孩子 int[] right = new int[ratings.length]; for (int i = 0, j = ratings.length - 1; i < ratings.length && j > -1; i++, j--) { if (i > 0 && ratings[i] > ratings[i - 1]) { left[i] = left[i - 1] + 1; } else { left[i] = 1; } if (j < ratings.length - 1 && ratings[j] > ratings[j + 1]) { right[j] = right[j + 1] + 1; } else{ right[j] = 1; } } for (int i = 0; i < ratings.length; i++) { res += Math.max(left[i], right[i]); } return res; } }
-
总结:
-
-
根据身高重建队列
-
题目
-
假设有打乱顺序的一群人站成一个队列,数组
people
表示队列中一些人的属性(不一定按顺序)。每个people[i] = [hi, ki]
表示第i
个人的身高为hi
,前面 正好 有ki
个身高大于或等于hi
的人。请你重新构造并返回输入数组
people
所表示的队列。返回的队列应该格式化为数组queue
,其中queue[j] = [hj, kj]
是队列中第j
个人的属性(queue[0]
是排在队列前面的人)。
-
-
思路
- 根据身高降序,身高相同,位置小的在前
- 根据位置插入
-
题解
class Solution { public int[][] reconstructQueue(int[][] people) { // 根据身高降序,身高相同,位置小的在前 Arrays.sort(people, new Comparator<int[]>() { @Override public int compare(int[] o1, int[] o2) { if (o1[0] == o2[0]) { return o1[1] - o2[1]; } return o2[0] - o1[0]; } }); List<int[]> list = new ArrayList<>(); for (int[] per : people) { list.add(per[1], per); } return list.toArray(new int[list.size()][2]); } }
-
总结:两个维度一起考虑一定会顾此失彼,先确定一个维度
-
-
最少数量引爆气球
-
题目
-
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组
points
,其中points[i] = [xstart, xend]
表示水平直径在xstart
和xend
之间的气球。你不知道气球的确切 y 坐标。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标
x
处射出一支箭,若有一个气球的直径的开始和结束坐标为x
start
,x
end
, 且满足xstart ≤ x ≤ x
end
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。给你一个数组
points
,返回引爆所有气球所必须射出的 最小 弓箭数 。
-
-
思路
- 根据start从小到大排序
- 遍历不断扩大区间,重合区间则箭数++;
- 不重合,则重置区间
-
题解
class Solution { public int findMinArrowShots(int[][] points) { if (points.length < 2) { return points.length; } Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0])); int range = points[0][1]; int res = 1; for (int i = 1; i < points.length; i++) { if (points[i][0] <= range) { range = Math.min(range, points[i][1]); continue; } range = points[i][1]; res++; } return res; } }
-
总结
-
-
无重叠区间
-
题目
- 给定一个区间的集合
intervals
,其中intervals[i] = [starti, endi]
。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
- 给定一个区间的集合
-
思路
- 对start进行升序排序
- 遍历
- 若重叠res++,去除end大的区间,达到局部最优
- 推出全局最优
-
题解
class Solution { public int eraseOverlapIntervals(int[][] intervals) { Arrays.sort(intervals, new Comparator<int[]>(){ @Override public int compare(int[] o1, int[] o2){ return o1[0] - o2[0]; } }); //Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); int res = 0; for (int i = 1; i < intervals.length; i++) { if (intervals[i][0] < intervals[i - 1][1]) { intervals[i][1] = Math.min(intervals[i][1], intervals[i - 1][1]); res++; } } return res; } }
-
总结:先确定一个维度,避免顾此失彼
-
-
划分字母区间
-
题目
-
给你一个字符串
s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是s
。返回一个表示每个字符串片段的长度的列表。
-
-
思路
- 遍历字符串,记录每个字母的起始位置与终止位置
- 起始位置升序排序
- 合并区间
-
题解
class Solution { public List<Integer> partitionLabels(String s) { int[][] arr = new int[26][2]; for (int[] array : arr) { Arrays.fill(array, -1); } for (int i = 0; i < s.length(); i++) { int index = s.charAt(i) - 'a'; if (arr[index][0] == -1) { arr[index][0] = i; } arr[index][1] = i; } Arrays.sort(arr, (a, b) -> Integer.compare(a[0], b[0])); List<Integer> list = new ArrayList<>(); int start = 0; int end = 0; for (int i = 0; i < arr.length; i++) { if (arr[i][0] == -1) { continue; } if (arr[i][0] <= end) { end = Math.max(arr[i][1], end); } else { list.add(end - start + 1); start = arr[i][0]; end = arr[i][1]; } } list.add(end - start + 1); return list; } }
-
总结:
-
-
合并区间
-
题目
- 以数组
intervals
表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
- 以数组
-
思路
- 根据start升序
- 重叠,则合并区间
- 不重叠,则记录;
-
题解
class Solution { public int[][] merge(int[][] intervals) { Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); List<int[]> list = new ArrayList<>(); int start = intervals[0][0]; int end = intervals[0][1]; for (int i = 0; i < intervals.length; i++) { if (intervals[i][0] <= end) { end = Math.max(intervals[i][1], end); } else { list.add(new int[]{start, end}); start = intervals[i][0]; end = intervals[i][1]; } } list.add(new int[]{start, end}); return list.toArray(new int[list.size()][]); } }
-
总结:
-
-
单调递增的数字
-
题目
-
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
-
-
思路
- 从个位往前遍历
- 若小于前一位,则这位及以后均变为9,前一位减1
-
题解
class Solution { public int monotoneIncreasingDigits(int n) { char[] ch = String.valueOf(n).toCharArray(); int minus = 0; for (int i = ch.length - 1; i >= 0; i--) { if (i > 0 && ch[i] - minus < ch[i - 1]) { for (int j = i; j < ch.length; j++) { ch[j] = '9'; } minus = 1; continue; } ch[i] -= minus; minus = 0; } int res = 0; for (int i = 0; i < ch.length; i++) { res = (ch[i] - '0') + res * 10; } return res; } }
-
总结:
-
-
监控二叉树
-
题目
-
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
-
-
思路
- 为覆盖所有节点,从叶子节点往根节点遍历(递归)
- 确定三种状态:-1监控无覆盖,0监控覆盖,1监控节点
-
题解
class Solution { int res = 0; public int minCameraCover(TreeNode root) { //-1监控无覆盖,0监控覆盖,1监控节点 //叶子结点父节点安置监控 return dfs(root) == -1 ? ++res : res; } public int dfs(TreeNode root) { if (root == null) { return 0; } int left = dfs(root.left); int right = dfs(root.right); if (left == -1 || right == -1) { res++; return 1; } if (left == 0 && right == 0) { return -1; } return 0; } }
-
-
总结:局部最优-让叶子节点的父节点安摄像头,所用摄像头最少。整体最优0全部摄像头数量所用最少!
未完待续......