【算法】【学习笔记】贪心
贪心算法是非常朴素的一种思想——每一步都找最好的那个选项。
贪心算法能拿到全局最优的严格前提
- 贪心选择性质:当前做出的局部最优选择,绝对不会破坏后续找到全局最优解的可能性。
- 最优子结构:问题的全局最优解包含其子问题的最优解。
❔题目
1. 柠檬水找零
解题思路
这个只要记录身上的面值数量,然后判断是否可以找零,找了就减少对应面值数,并且添加对应面值数即可。
然后因为越小的越可以构成更多的找零,所以优先选择大面值来找零。
实现
function lemonadeChange(bills: number[]): boolean {
let five = 0;
let ten = 0;
for (let i = 0; i < bills.length; i++) {
const bill = bills[i];
if (bill === 5) {
five++;
} else if (bill === 10) {
if (five === 0) return false;
five--;
ten++;
} else {
// 收到 20 元:优先用 10 + 5,没有 10 块再试 3 个 5 块
if (ten > 0 && five > 0) {
ten--;
five--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
public class Solution {
public bool LemonadeChange(int[] bills) {
int five = 0, ten = 0;
foreach (int bill in bills) {
if (bill == 5) {
five++;
} else if (bill == 10) {
if (five == 0) return false;
five--;
ten++;
} else {
if (ten > 0 && five > 0) {
ten--;
five--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
}
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0, ten = 0;
for (int bill : bills) {
if (bill == 5) {
five++;
} else if (bill == 10) {
if (five == 0) return false;
five--;
ten++;
} else {
if (ten > 0 && five > 0) {
ten--;
five--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
};
复杂度分析
- 时间复杂度: \(O(N)\)
- 空间复杂度: \(O(1)\)
2. 盛最多水的容器
解题思路
也就是这个问题是,找2条线a和b,其$(x_b - x_a) \times min(h_a, h_b) $是最大。
按贪心那就是从b离a最远时开始寻找,然后往左或者右收,收的时候,一定要找比本身高的,才停下。
实现
function maxArea(height: number[]): number {
let l = 0, r = height.length - 1;
let res = 0;
while (l < r) {
const currentArea = (r - l) * Math.min(height[l], height[r]);
res = Math.max(res, currentArea);
if (height[l] < height[r]) {
l++;
} else {
r--;
}
}
return res;
}
public class Solution {
public int MaxArea(int[] height) {
int l = 0, r = height.Length - 1;
int res = 0;
while (l < r) {
int currentArea = (r - l) * Math.Min(height[l], height[r]);
res = Math.Max(res, currentArea);
if (height[l] < height[r]) {
l++;
} else {
r--;
}
}
return res;
}
}
class Solution {
public:
int maxArea(vector<int>& height) {
int l = 0, r = height.size() - 1;
int res = 0;
while (l < r) {
// 1. 计算当前左右边界构成的盛水量
int currentArea = (r - l) * min(height[l], height[r]);
res = max(res, currentArea);
// 2. 核心贪心:舍弃短板,将较矮一侧的指针向内移动
if (height[l] < height[r]) {
l++;
} else {
r--;
}
}
return res;
}
};
复杂度分析
- 时间复杂度: \(O(N)\)
- 空间复杂度: \(O(1)\)
3. 分发糖果
解题思路
核心变量与几何含义
up(上坡长度):当前处于连续递增序列的步数。down(下坡长度):当前处于连续递减序列的步数。peak(山峰高度):上坡能达到的最高相对高度,即peak = up。res(总糖果数):累加的总需求量,默认第 1 个孩子分 1 个。
状态转换与计算规则
-
上坡(
ratings[i] > ratings[i-1]):- 重置
down = 0,令up++并记录顶点peak = up。 - 当前人领取的糖果数为
1 + up,直接累加至res。
- 重置
-
下坡(
ratings[i] < ratings[i-1]):- 重置
up = 0,令down++。 - 下坡整体抬高:为了保证递减,整个下坡段的孩子都需要被“抬高 1 个糖果”,总和增加
down。 - 山峰高度修正:当下坡长度超过山峰高度(
down > peak),说明下坡太长逆向把顶点推高了,必须为顶点补加 1 个糖果(res++)。
- 重置
为什么只用补一个,而不是整个坡的:
整个下坡的糖果其实已经在res += down里全部加过了!唯一被遗漏的,只有“山峰”这单个节点。
- 平地(
ratings[i] == ratings[i-1]):- 相邻评分相等时无递增递减约束,重置
up = 0, down = 0, peak = 0,给基础 1 个糖果(res += 1)。
- 相邻评分相等时无递增递减约束,重置
实现
function candy(ratings: number[]): number {
const n = ratings.length;
if (n <= 1) return n;
let res = 1; // 默认第 1 个孩子分 1 个糖果
let up = 0; // 当前连续上坡长度
let down = 0; // 当前连续下坡长度
let peak = 0; // 最近一次上坡达到的山峰高度
for (let i = 1; i < n; i++) {
if (ratings[i] > ratings[i - 1]) {
// 1. 上坡:增长上坡高度,更新山峰
up++;
down = 0;
peak = up;
res += 1 + up;
} else if (ratings[i] < ratings[i - 1]) {
// 2. 下坡:累加下坡段增量,下坡超长时给山峰补 1
down++;
up = 0;
if (down > peak) {
res++; // 修正顶点
}
res += down;
} else {
// 3. 平地:状态重置,给基础 1 个糖果
up = 0;
down = 0;
peak = 0;
res += 1;
}
}
return res;
}
public class Solution {
public int Candy(int[] ratings) {
int n = ratings.Length;
if (n <= 1) return n;
int res = 1; // 默认第 1 个孩子分 1 个糖果
int up = 0; // 当前连续上坡长度
int down = 0; // 当前连续下坡长度
int peak = 0; // 最近一次上坡达到的山峰高度
for (int i = 1; i < n; i++) {
if (ratings[i] > ratings[i - 1]) {
// 1. 上坡情况:累加 1 + up
up++;
down = 0;
peak = up;
res += 1 + up;
} else if (ratings[i] < ratings[i - 1]) {
// 2. 下坡情况:累加 down,超出山峰时补 1
down++;
up = 0;
if (down > peak) {
res++; // 顶点修正
}
res += down;
} else {
// 3. 平地情况:重置坡度,给 1 个糖果
up = 0;
down = 0;
peak = 0;
res += 1;
}
}
return res;
}
}
class Solution {
public:
int candy(vector<int>& ratings) {
int n = ratings.size();
if (n <= 1) return n;
int res = 1; // 默认第 1 个孩子分 1 个糖果
int up = 0; // 当前连续上坡长度
int down = 0; // 当前连续下坡长度
int peak = 0; // 最近一次上坡达到的山峰高度
for (int i = 1; i < n; ++i) {
if (ratings[i] > ratings[i - 1]) {
// 1. 上坡情况:更新山峰并累加当前上坡高度糖果
up++;
down = 0;
peak = up;
res += 1 + up;
} else if (ratings[i] < ratings[i - 1]) {
// 2. 下坡情况:整体抬高下坡段,必要时修正山峰高度
down++;
up = 0;
if (down > peak) {
res++; // 下坡长度反超山峰,顶点必须被迫补 1 个糖果
}
res += down; // 下坡段每个人整体增量累加
} else {
// 3. 平地情况:无需满足连续递增/递减,重置状态并给基础 1 个糖果
up = 0;
down = 0;
peak = 0;
res += 1;
}
}
return res;
}
};
复杂度分析
- 时间复杂度:\(O(N)\)
- 空间复杂度:\(O(1)\)
🤯拓展
引用
[1] 力扣探索模式
注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

浙公网安备 33010602011771号