贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
贪心没有套路,说白了就是常识性推导加上举反例。
题型总结
455 分饼干
从代码中可以看出我用了一个 index 来控制饼干数组的遍历,遍历饼干并没有再起一个 for 循环,而是采用自减的方式,这也是常用的技巧。
有的同学看到要遍历两个数组,就想到用两个 for 循环,那样逻辑其实就复杂了
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int j=0;//控制胃口的遍历
int count=0;//满足的总人数
for(int i=0;i<s.length && j < g.length;i++){
if(s[i]>=g[j]){//从最小的饼干开始看能喂饱谁
j++;//这样一个for遍历就行了。
count++;
}
}
return count;
}
}
区间问题
区间问题:
55 45两跳跃 763.划分字母区间 这三题都是要更新最大索引
452用最少数量的箭引爆气球 453使其无重叠区间 56合并区间 这三题区别就是在于对重叠区间的处理不同
| 题目 | 问题特征 | 重叠时的操作 | 判断逻辑 |
|---|---|---|---|
| 55. 跳跃游戏 | 判断能否从起点跳到终点 | 更新当前能到达的最远位置 | 如果当前位置 > 当前最远位置,则失败 |
| 45. 跳跃游戏 II | 求从起点跳到终点的最少步数 | 更新当前步的边界和下一步最远位置 | 每次到达当前边界时,步数+1,更新边界 |
| 763. 划分字母区间 | 将字符串划分为最多片段,同一字母只出现在一个片段 | 更新当前区间内字母最后出现的位置 | 当遍历到当前右边界时,分割片段 |
| 452. 用最少数量的箭引爆气球 | 用最少的箭射爆所有重叠的气球 | 更新最小右边界 | 若新区间左边界 > 当前右边界,需新增一支箭 |
| 435. 无重叠区间 | 移除最少数量的区间使剩余区间不重叠 | 更新最小右边界 | 若当前区间左边界 < 前一个区间的右边界,则需移除 |
| 56. 合并区间 | 合并所有重叠的区间 | 更新最大右边界 | 若新区间左边界 ≤ 当前右边界,则合并;否则开启新区间 |
452 435 56 处理重叠时的逻辑不一样是因为 前两个删除就要更新最小右边界。后一个添加就是更新最大右边界。
45 跳跃游戏
class Solution {
public int jump(int[] nums) {
if(nums==null||nums.length==0||nums.length==1) return 0;
int count=0;//记录跳跃的次数
int curconver=0;//记录当前覆盖的最大区域
int maxconver=0;//最大的覆盖区域
for(int i=0;i<nums.length;i++){
maxconver=Math.max(maxconver,i+nums[i]);
//说明当前一步,再跳一步就到达了末尾
if(maxconver>=nums.length-1){
count++;
break;
}
//走到当前覆盖的最大区域时,更新下一步可达的最大区域
if(i==curconver){
curconver=maxconver;
count++;
}
}
return count++;
}
}
452用最少数量的箭引爆气球
class Solution {
public int findMinArrowShots(int[][] points) {
Arrays.sort(points,(a,b)->Integer.compare(a[0],b[0]));
int count=1;// points不为空初始射一箭,后面两球没有重叠再射一剑,重叠不用管。
for(int i=1;i<points.length;i++){
if(points[i][0]>points[i-1][1])//points[i][0] 0是第i个气球的左端点,1是右端点。
//也就是右边的左端点大于临近左边气球的右端点。也就是临近的这两个气球没有重叠部分时。
{
count++;
}else{
points[i][1]=Math.min(points[i][1],points[i-1][1]);//维护当前箭能覆盖的最小右边界
}
}
return count;
}
}
435.无重叠区间
和452其实是一类题;记得二维数组先排序。然后画图找重叠逻辑。右边东西的左边界大于左边东西的右边界就是有重叠区间。如果要移除,要记得更新移除后的最小右边界。
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals,(a,b)->Integer.compare(a[0],b[0]));
int count=0;
for(int i=1;i<intervals.length;i++){
if(intervals[i][0]<intervals[i-1][1]){
count++;//临近的有重叠部分
intervals[i][1]=Math.min(intervals[i][1],intervals[i-1][1]);//移除上一个重叠的,所以要更新
}
}
return count;
}
}
56 合并区间
和前面两个区别在于 对于重叠的区间不是删除而是合并添加。
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> res = new LinkedList<>();//每个节点存储一个 int[] 类型的数组
if (intervals.length == 0)
{
return intervals;
}
Arrays.sort(intervals, (x, y) -> Integer.compare(x[0], y[0]));
//区间初始化为第一个
int start = intervals[0][0];
int rightmostRightBound = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
//如果当前的左边界大于前面最大右边界,也就是无重叠。
if (intervals[i][0] > rightmostRightBound) {
//当前区间不重叠加入到结果中。
res.add(new int[]{start, rightmostRightBound});
//记录当前的数组区间,如果和下一个还不重叠就再次加到res里。
start = intervals[i][0];
rightmostRightBound = intervals[i][1];
} else {//重叠了要合并,更新最右边界就行了。结果不在这添加,因为可能好几个重叠的。
//更新最大右边界
rightmostRightBound = Math.max(rightmostRightBound, intervals[i][1]);
}
}
// 添加最后一个合并的区间
res.add(new int[]{start, rightmostRightBound});
// 将List<int[]>转为int[][]
return res.toArray(new int[res.size()][]);
}
}
763.划分字母区间
将字符串分割,使得每个部分中的字符不会出现在其他部分中(即每个字符只属于一个片段)。
class Solution {
public List<Integer> partitionLabels(String s) {
char[] chars=s.toCharArray();//字符串s先转成char数组。
List<Integer> result = new LinkedList<>();
int lastIndex = 0; // 当前片段的最后位置
int start = 0; // 当前片段的起始位置
for(int i=0;i<chars.length;i++){
// 找到当前字符在字符串中最后出现的位置
int currentLast = s.lastIndexOf(chars[i]);
// 更新当前片段的最后位置
lastIndex = Math.max(lastIndex, currentLast);
// 如果当前位置等于当前片段的最后位置,说明可以分割
if (i == lastIndex) {
result.add(lastIndex - start + 1); // 计算片段长度并加入结果
start = i + 1; // 更新下一个片段的起始位置
}
}
return result;
}
}
股票问题 求最大的利润

买卖限次数问题 以及 冷冻期问题,设立状态,整个流程可以抽象成m个状态,那dp就用二维的表示第i天第m个状态时利润的最大值。
dp初始化:m为买入时初始为-price[0],卖出或者冷冻初始化为0就行了。
121.买卖股票的最佳时机 只能买卖一次
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
class Solution {
public int maxProfit(int[] prices) {
int result=0;
int low=prices[0];
for(int i=0;i<prices.length;i++){
low=Math.min(prices[i],low);
result=Math.max(prices[i]-low,result);
}
return result;
}
}
122.买卖股票的最佳时机 II 可以买卖多次
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大利润 。

使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。
class Solution {
public int maxProfit(int[] prices) {
int max=0;
for(int i=1;i<prices.length;i++){
if(i>0){
max+=Math.max(prices[i]-prices[i-1],0);
}
}
return max;
}
}
714.买卖股票的最佳时机含手续费 可以买卖多次
本题有了手续费,就要关心什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以扣减手续费的情况。
买入日期:其实很好想,遇到更低点就记录一下。
卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。
class Solution {
public int maxProfit(int[] prices, int fee) {
int buy=prices[0]+fee;//假设第一天就买了
int sum=0;
for(int p:prices){
if(p+fee<buy)//说明第一天买的不合算啊
{
buy=p+fee;//更新,这时候才买。
}
else if(p>buy){
sum=sum+p-buy;
buy=p;//卖出后更新,比p小的时候就买
}
}
return sum;
}
}
122.买卖股票的最佳时机II 最多买卖两次 第一次卖是最低成本
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
public int maxProfit(int[] prices) {
// 初始化四个状态
int firstBuy = Integer.MAX_VALUE; // 第一次买入的最低价格
int firstSell = 0; // 第一次交易的最大利润
int secondBuy = Integer.MIN_VALUE; // 第二次买入后的剩余金额(考虑第一次利润)
int secondSell = 0; // 第二次交易后的总利润
for (int price : prices) {
// 1. 更新第一次买入的最低成本
firstBuy = Math.min(firstBuy, price);
// 2. 更新第一次卖出的最大利润
firstSell = Math.max(firstSell, price - firstBuy);
// 3. 更新第二次买入后的剩余金额(需叠加第一次利润)
secondBuy = Math.max(secondBuy, firstSell - price);
// 4. 更新第二次卖出的总利润
secondSell = Math.max(secondSell, secondBuy + price);
}
return secondSell;
}
}
188.买卖股票的最佳时机IV k笔交易
给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
public int maxProfit(int k, int[] prices) {
int[] dp=new int[2*k];//偶数索引表示进行第i次交易的买入状态,奇数表示卖出
//初始化
for(int i=0;i<2*k;i++){
if(i%2==0) dp[i]=-prices[0];//每次交易的买入
else dp[i]=0;//每次交易的卖出
}
for(int i=0;i<prices.length;i++){// 遍历每一天
for(int j=0;j<2*k;j++){ // 遍历每个交易状态
if(j%2==0){ // 买入状态
if(j>0){
dp[j]=Math.max(dp[j],dp[j-1]-prices[i]);
}else{
dp[j]=Math.max(dp[j],-prices[i]);//第一次买入
}
}else{ // 卖出状态
dp[j]=Math.max(dp[j],dp[j-1]+prices[i]);
}
}
}
return dp[2*k-1];
}
}
309.最佳买卖股票时机含冷冻期
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。
具体可以区分出如下四个状态:
状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
不持有股票状态,这里就有两种卖出股票状态
状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
状态三:今天卖出股票
状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
然后写出每个状态对应的递推公式。
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) return 0;
int n = prices.length;
// dp[i][j]:第i天状态为j时的最大利润
// j的四个状态:
// 0: 持有股票
// 1: 保持卖出股票状态(非冷冻期)
// 2: 今天卖出股票
// 3: 冷冻期
int[][] dp = new int[n][4];
// 初始化
dp[0][0] = -prices[0]; // 第一天买入
dp[0][1] = 0; // 第一天无法保持卖出状态
dp[0][2] = 0; // 第一天无法卖出
dp[0][3] = 0; // 第一天无冷冻期
for (int i = 1; i < n; i++) {
// 状态0:持有股票(可能是之前买入,或今天从非冷冻期/保持卖出状态买入)
dp[i][0] = Math.max(
dp[i - 1][0], // 保持持有
Math.max(
dp[i - 1][1], // 从保持卖出状态买入
dp[i - 1][3] // 从冷冻期买入
) - prices[i]
);
// 状态1:保持卖出股票状态(可能是之前已卖出,或从冷冻期解除)
dp[i][1] = Math.max(
dp[i - 1][1], // 继续保持
dp[i - 1][3] // 冷冻期解除
);
// 状态2:今天卖出股票
dp[i][2] = dp[i - 1][0] + prices[i];
// 状态3:冷冻期(只有在前一天卖出时进入)
dp[i][3] = dp[i - 1][2];
}
// 最终利润取最后一天的非持有状态中的最大值
return Math.max(
Math.max(dp[n - 1][1], dp[n - 1][2]),
dp[n - 1][3]
);
}
}
双维度问题
两个相互制约的维度需要分别处理。
135.分发糖果
巧妙
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] candies = new int[n];
Arrays.fill(candies, 1); // 每人至少1颗糖
// 从左到右:保证右边高分孩子比左边多
for (int i = 1; i < n; i++) {
if (ratings[i] > ratings[i - 1]) {
candies[i] = candies[i - 1] + 1;
}
}
// 从右到左:保证左边高分孩子比右边多
for (int i = n - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candies[i] = Math.max(candies[i], candies[i + 1] + 1);
}
}
// 统计总糖果数
int total = 0;
for (int candy : candies) {
total += candy;
}
return total;
}
}
406.根据身高重建队列
本题有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后再按照另一个维度重新排列。
在按照身高从大到小排序后:
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性

class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people,(a,b)->{
if(a[0]==b[0]) return a[1]-b[1];// a - b 是升序排列,故在a[0] == b[0]的狀況下,會根據k值升序排列
return b[0]-a[0]; //b - a 是降序排列,在a[0] != b[0],的狀況會根據h值降序排列
});
LinkedList<int[]> que=new LinkedList<>();
for(int[] p:people){
que.add(p[1],p);//Linkedlist.add(index, value),會將value插入到指定index裡。
}
return que.toArray(new int[people.length][]);
}
}