假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i
,都有一个胃口值 gi
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j
,都有一个尺寸 sj
。如果 sj >= gi
,我们可以将这个饼干j
分配给孩子 i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
注意:
示例 1:
输入: [1,2,3], [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: [1,2], [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
思路:将最小的饼干给最不贪心的小朋友,如果最小的饼干满足不了最不贪心的小朋友,就放弃这个最小的饼干。在剩下的饼干和小朋友中继续考虑。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int gi = 0, si = 0;
while (gi < g.length && si < s.length) {
if (s[si] >= g[gi]) {
si++;
gi++;
}else {
si++;
}
}
return gi;
}
}
另外一种贪心思想:将最大的饼干给最贪心的小朋友。如果最大的饼干都满足不了这个最贪心的小朋友,就放弃这个小朋友。在剩下的小朋友和饼干中,依旧是将最大的饼干给最贪心的小朋友。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Integer[] gg = new Integer[g.length];
Integer[] ss = new Integer[s.length];
for (int i = 0; i < g.length; i++) {
gg[i] = g[i];
}
for (int i = 0; i < s.length; i++) {
ss[i] = s[i];
}
Arrays.sort(gg, (a,b) -> (b-a));
Arrays.sort(ss, (a,b) -> (b-a));// 逆序排序
int gi = 0;
int si = 0;
int res = 0;
while (gi < gg.length && si < ss.length) {
if (gg[gi] <= ss[si]) {
res++;
gi++;
si++;
}else {
gi++;
}
}
return res;
}
}
在以上的解法中,我们只在每次分配饼干时选择一种看起来是当前最优的分配方法,但无法保证这种局部最优的分配方法最后能得到全局最优解。我们假设能得到全局最优解,并使用反证法进行证明,即假设存在一种比我们使用的贪心策略更优的最优策略。如果不存在这种最优策略,表示贪心策略就是最优策略,得到的解也就是全局最优解。
证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,可以给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。
注:以上证明来自 CyC2018
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
贪心思想:对于每次选择,区间的结尾很重要。结尾越小,留给后面区间越大的空间,后面越有可能容纳更多区间。所以按照区间的结尾排序,每次选择结尾最早的,且和前一个区间不重叠的区间。
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
int len = intervals.length;
if (len == 0) return 0;
Arrays.sort(intervals, (a, b) -> (a[1] == b[1] ? a[0]-b[0] : a[1]-b[1]));
int sum = 1;// 含义:构成最长无重叠区间的区间个数
int pre = 0;
for (int i = 1; i < len; i++) {
if (intervals[i][0] >= intervals[pre][1]) {
sum++;
pre = i;
}
}
return len - sum;
}
}
问题:是否可以用动态规划来求解的题目都可以用贪心法来求解?
这显然是不行的,举出反例即可。只有具有贪心选择性质,才能使用贪心法来求解。使用反证法来证明上面这个问题具有贪心选择性质。
某次选择的是[s(i), f(i)]
,其中f(i)
是当前所有选择中结尾最早的。假设这个选择不是最优的,并且假设最优解在这一步选择[s(j), f(j)]
,f(j) > f(i)
。此时,显然可以将 [s(i), f(i)]
替换[s(j), f(j)]
,而不影响后续的区间选择。也就是说 [s(i), f(i)]
可以替换[s(j), f(j)]
成为一个最优解,与 [s(i), f(i)]
不是最优解的假设矛盾。因此这个问题具有贪心选择性质。
总体思路:贪心算法为A;最优算法为O;发现A完全能替代O,且不影响求出最优解。
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
Example:
输入:[[10,16], [2,8], [1,6], [7,12]]
输出:2
解释:对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。
思路:是否需要新的箭关键在于区间的末尾,所以首先需要对区间排序,排序规则是区间末尾升序,末尾相同的话区间起始点升序。
代码中的end
标记,它表示:在遍历的过程中使用当前这只箭能够击穿所有气球的最远距离。这个最远距离,在每遍历一个新区间的时候,都会检查一下。结合图示理解end的含义。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utTt3GpN-1594738258842)(.\images\LeetCode452-引爆气球示意图.png)]
参考题解:liweiwei1419:贪心算法(Python 代码、Java 代码)
class Solution {
public int findMinArrowShots(int[][] points) {
if (points == null || points.length == 0) {
return 0;
}
Arrays.sort(points, (a, b) -> (a[1]==b[1] ? a[0]-b[0] : a[1]-b[1]));
int minCount = 1;
int end = points[0][1];// 表示最远距离
for (int i = 1; i < points.length; i++) {
if (points[i][0] > end) {
minCount++;
end = points[i][1];
}
}
return minCount;
}
}
假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。
注意:总人数少于1100人。
示例:
输入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
输出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
思路:先排序再插入。排序规则:先按高度h
降序,如果高度相同,则按照k
升序。插入的过程就是:遍历排序之后的数组,将每个整数对插入到k
位置(这个位置是指数组中的位置)上,也就是说k
就是这个整数对在数组中应该在的位置。
为什么k
有这样的性质?
首先题目定义k
是排在这个人前面且身高大于或等于h
的人数,而且对数组进行了身高的降序排序。所以对于某个整数对,他的前面肯定都是比他高的,那么把他插入到k
这个位置,他的前面也就有k
个人,也就符合了k
的定义。
class Solution {
public int[][] reconstructQueue(int[][] people) {
if (people == null || people.length == 0) {
return people;
}
Arrays.sort(people, (a, b) -> (a[0]==b[0] ? a[1]-b[1] : b[0]-a[0]));
for (int i = 1; i < people.length; i++) {
int pos = people[i][1];// 这个人实际应该在的位置
int[] copy = people[i];// 保存这个人的一个副本
// 往前移位操作
int j = i;
for (; j > pos; j--) {
people[j] = people[j-1];
}
people[pos] = copy;
}
return people;
}
}
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。注意:你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
思路:代码中min
记录的是前i
个元素中的最小值,如果第i
天的股票价格高于min
就会产生利润,最后得出一个最大利润。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int min = prices[0];
int res = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] > min) {
res = Math.max(res, prices[i]-min);
}else {
min = prices[i];
}
}
return res;
}
}
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
思路:遍历整个股票交易日价格列表 prices
,策略是所有上涨交易日都买卖(赚到所有利润),所有下降交易日都不买卖(永不亏钱)。
注意:对于连续上涨交易日的情况,设此上涨交易日股票价格分别为 p1,p2,…,pn,则第一天买最后一天卖收益最大,即 pn−p1,等价于每天都买卖,即 pn−p1=(p2−p1)+(p3−p2)+…+(pn−pn−1)。
参考题解:Krahets:买卖股票的最佳时机 II (贪心,清晰图解)
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int res = 0;
for (int i = 0; i < prices.length-1; i++) {
if (prices[i+1] > prices[i]) {
res += prices[i+1]-prices[i];
}
}
return res;
}
}
假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。
示例 1:
输入: flowerbed = [1,0,0,0,1], n = 1
输出: True
示例 2:
输入: flowerbed = [1,0,0,0,1], n = 2
输出: False
注意:
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int len = flowerbed.length;
int count = 0;
for (int i = 0; i < len; i++) {
if (flowerbed[i]==0
&& (i==0 || flowerbed[i-1]==0)
&& (i==len-1 || flowerbed[i+1]==0)) {
count++;
flowerbed[i] = 1;
}
}
return count>=n;
}
}
以上解法参考官方题解,边界条件写得太棒了。
if (flowerbed[i]==0 && (i==0 || flowerbed[i-1]==0) && (i==len-1 || flowerbed[i+1]==0))
对于首尾需要特别考虑,还有一种思路,在开头的左边和结尾的右边分别补零,这样首尾就可以和中间元素一起考虑了。
给定字符串 s
和 t
,判断 s
是否为 t
的子序列。
你可以认为 s
和 t
中仅包含英文小写字母。字符串 t
可能会很长(长度 ~= 500,000),而 s
是个短字符串(长度 <=100)。
名词解释:字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
s = "abc", t = "ahbgdc"
返回 true.
示例 2:
s = "axc", t = "ahbgdc"
返回 false.
后续挑战 : 如果有大量输入的 S
,称作S1, S2, ... , Sk
其中 k >= 10亿
,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
class Solution {
public boolean isSubsequence(String s, String t) {
int si = 0;
int sLen = s.length();
if (sLen == 0) return true;
int tLen = t.length();
for (int i = 0; i < tLen; i++) {
if (t.charAt(i) == s.charAt(si)) {
si++;
}
if (si == sLen) return true;
}
return false;
}
}
给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。
我们是这样定义一个非递减数列的: 对于数组中所有的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。
示例 1:
输入: nums = [4,2,3]
输出: true
解释: 你可以通过把第一个4变成1来使得它成为一个非递减数列。
示例 2:
输入: nums = [4,2,1]
输出: false
解释: 你不能在只改变一个元素的情况下将其变为非递减数列。
说明:
思路:
1、当数组长度小于3时,最多需要调整一次就能满足条件,返回true;
2、当数组长度大于等于3时,出现前一个元素y大于后一个元素z时,如果y的前元素x不存在,让y=z即可;若x存在,根据x和z的大小关系,做如下调整:若x>z,就让z=y,否则让y=z。
参考题解:but的题解
class Solution {
public boolean checkPossibility(int[] nums) {
int len = nums.length;
if (len < 3) {
return true;
}
int cnt = 0;
for (int i = 0; i < len-1; i++) {
if (nums[i] > nums[i+1]) {
cnt++;
if (cnt > 1) {
break;
}
if ((i-1>=0) && nums[i-1] > nums[i+1]) {
nums[i+1] = nums[i];
}else {
nums[i] = nums[i+1];
}
}
}
return cnt <= 1;
}
}
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
class Solution {
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) {
return Integer.MIN_VALUE;
}
int maxSum = nums[0];
int subSum = nums[0];
for (int i = 1; i < nums.length; i++) {
if (subSum < 0) {
subSum = nums[i];
}else {
subSum += nums[i];
}
if (subSum > maxSum) {
maxSum = subSum;
}
}
return maxSum;
}
}
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段。返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:S = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca", "defegde", "hijhklij"。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。
提示:
思路:对于遇到的每一个字母,去找这个字母最后一次出现的位置,用来更新当前的最小区间。
class Solution {
public List<Integer> partitionLabels(String S) {
// 首先记录下每个字符最后一次出现的位置
int[] last = new int[26];
for (int i = 0; i < S.length(); i++) {
last[S.charAt(i)-'a'] = i;
}
// 根据字符最后一次出现的位置来确定区间
List<Integer> res = new ArrayList<>();
int start = 0, end = 0;
for (int i = 0; i < S.length(); i++) {
end = Math.max(end, last[S.charAt(i)-'a']);
if (i == end) {
res.add(end-start+1);
start = i+1;
}
}
return res;
}
}