贪心 (Greedy)
核心思想
每一步做当下看起来最好的选择(局部最优),并证明该选择不会影响后续步骤的最优性,从而局部最优拼出全局最优。
- 贪心没有状态:每一步只做一次决策,不回头、不记忆,这是与动态规划的本质区别。
- 贪心通常需要先排序(按某个键排),再线性扫描。
- 常见套路:区间调度类(按结束时间/起点排序)、每次取当前最值(配合堆)、差值/统计类。
适用条件与证明
贪心只有在前两条都成立时才正确:
- 最优子结构:全局最优解包含子问题的最优解(与 DP 相同);
- 贪心选择性质:贪心选的"当下最优"一定出现在某个全局最优解里。
常用证明方法:
- 交换论证(Exchange Argument):假设某个最优解不含贪心选择的元素,把它与贪心元素交换,证明不劣——最常见的证明方式。
- 归纳:证明贪心第 步之后,贪心前缀仍然可扩展成全局最优。
经典区间贪心三件套(均可用交换论证证明):
- 区间调度(选最多不相交区间):按结束时间升序,能选就选;
- 区间选点(最少点覆盖所有区间):按结束时间升序,每次把点放在当前区间右端点;
- 区间覆盖(最少区间覆盖一段目标):按起点排序,每次选能覆盖当前最左未覆盖点的、右端点最远的区间。
例题
517 超级洗衣机
n 台洗衣机排成一排,每台有一定量衣服;每步可让任意 m 台洗衣机各送 1 件衣服给相邻洗衣机。求使所有洗衣机衣服数相等的最少步数。
贪心魔法:答案 = max(单个洗衣机超出均值的最大量, 某前缀累计溢出量的绝对值最大值)。
class Solution {
public:
int findMinMoves(vector<int> &machines) {
int tot = accumulate(machines.begin(), machines.end(), 0);
int n = machines.size();
if (tot % n) {
return -1;
}
int avg = tot / n;
int ans = 0, sum = 0;
for (int num: machines) {
num -= avg;
sum += num;
ans = max(ans, max(abs(sum), num));
}
return ans;
}
};cpp
42 接雨水
给定 n 个非负整数表示每根柱子高度,计算下雨后能接多少雨水。
三遍循环的优雅贪心:每个位置能接的水 = min(左边最高, 右边最高) - 自身高度。
class Solution {
public:
int trap(vector<int>& height) {
int size = height.size();
vector<int> left_max(size), right_max(size);
left_max[0] = 0;
right_max[size - 1] = 0;
int max = height[0];
for (int i = 1; i < size; ++i){
left_max[i] = max;
max = std::max(max, height[i]);
}
max = height[size - 1];
for (int i = size - 2; i >= 0; --i){
right_max[i] = max;
max = std::max(max, height[i]);
}
int res = 0, con;
// final loop
for (int i = 0; i < size; ++i){
con = std::min(left_max[i], right_max[i]);
res += (con - height[i] < 0) ? 0 : con - height[i];
}
return res;
}
};cpp
另一种实现:用栈做"动态离散化",思路绕且容易写错,仅作记录:
class Solution {
public:
int trap(vector<int>& height) {
stack<pair<int, int>> s; // id, height
map<int, int> d;
int ans = 0;
for (int i = 0; i < height.size(); i++) {
int H = height[i];
d[H]++;
if (!s.empty()) {
int last = 0;
for (auto it = d.begin(); it != d.end(); it++) {
int h = it->first;
if (h == 0) continue;
if (h > H) break;
while (!s.empty() && s.top().second < h) {
// must remove useless discrete values, else TLE
if (--d[s.top().second] <= 0) d.erase(s.top().second);
s.pop();
}
if (!s.empty()) {
ans += (i - s.top().first - 1) * (h - last);
}
last = h;
}
}
if (H > 0) s.push({i, H});
}
return ans;
}
};cpp
407 接雨水 II(2D)
仍然是贪心,但 1D 的"左右最高点"在 2D 中变成了"该位置到任意边界所有路径的最高点中的最小值"——这正是最短路问题,用**优先队列 BFS(Dijkstra 思路)**解。可见 Dijkstra 不仅用于最短路(见 21_shortest_path)。
typedef pair<int,int> pii;
class Solution {
public:
int trapRainWater(vector<vector<int>>& heightMap) {
if (heightMap.size() <= 2 || heightMap[0].size() <= 2) {
return 0;
}
int m = heightMap.size();
int n = heightMap[0].size();
priority_queue<pii, vector<pii>, greater<pii>> pq;
vector<vector<bool>> visit(m, vector<bool>(n, false));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
pq.push({heightMap[i][j], i * n + j});
visit[i][j] = true;
}
}
}
int res = 0;
int dirs[] = {-1, 0, 1, 0, -1};
while (!pq.empty()) {
pii curr = pq.top();
pq.pop();
for (int k = 0; k < 4; ++k) {
int nx = curr.second / n + dirs[k];
int ny = curr.second % n + dirs[k + 1];
if( nx >= 0 && nx < m && ny >= 0 && ny < n && !visit[nx][ny]) {
if (heightMap[nx][ny] < curr.first) {
res += curr.first - heightMap[nx][ny];
}
visit[nx][ny] = true;
pq.push({max(heightMap[nx][ny], curr.first), nx * n + ny});
}
}
}
return res;
}
};cpp
475 供暖器
二分答案可行(见 01_binary_search),但更直接的是贪心:对每个房子找最近的供暖器,答案取所有最近距离的最大值。(排序)。
class Solution {
public:
int findRadius(vector<int>& houses, vector<int>& heaters) {
sort(houses.begin(),houses.end());
sort(heaters.begin(),heaters.end());
int ans = 0;
int h = 0;
// O(n)
for (int i=0;i<houses.size();i++){
while (h + 1 < heaters.size() &&
abs(houses[i] - heaters[h]) >= abs(houses[i] - heaters[h + 1])) h++;
ans = max(ans,abs(houses[i]-heaters[h]));
}
return ans;
}
};cpp
334 递增的三元子序列
只记录第一小、第二小,一旦出现更大的值就说明存在三元组——与 LIS 的贪心(lower_bound 替换)同思路,但只维护两个值。
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
int len = nums.size();
if (len < 3) return false;
int small = INT_MAX, mid = INT_MAX;
for (auto num : nums) {
if (num <= small) small = num;
else if (num <= mid) mid = num;
else if (num > mid) return true;
}
return false;
}
};cpp
(用完整 LIS 的解法见 07_dynamic_programming。)