一、贪心算法的核心定义与本质
贪心算法是一种在每一步选择中都采取当前状态下最优(即局部最优)的选择,以期最终获得全局最优解的启发式算法。其核心思想可概括为:'走一步看一步,每步都选最好的,不回头'。
与动态规划(DP)需要存储子问题的最优解并回溯不同,贪心算法——它通过每一步的局部最优积累,直接推导全局最优。这种'短视'的特性使其实现简单、效率极高,但也决定了它并非适用于所有问题,必须满足严格的前提条件。
本文介绍贪心算法的核心定义、适用条件(贪心选择性质与最优子结构)及解题步骤。通过活动选择、哈夫曼编码、Dijkstra 最短路径和 Kruskal 最小生成树四个经典问题的 C++ 实现,展示了贪心策略的设计与验证方法。文章对比了贪心算法与动态规划的区别,分析了其优缺点及实际应用场景,强调在满足特定条件下利用局部最优推导全局最优的高效性。

贪心算法是一种在每一步选择中都采取当前状态下最优(即局部最优)的选择,以期最终获得全局最优解的启发式算法。其核心思想可概括为:'走一步看一步,每步都选最好的,不回头'。
与动态规划(DP)需要存储子问题的最优解并回溯不同,贪心算法——它通过每一步的局部最优积累,直接推导全局最优。这种'短视'的特性使其实现简单、效率极高,但也决定了它并非适用于所有问题,必须满足严格的前提条件。
判断一个问题能否用贪心算法解决,必须同时满足以下两个核心性质,缺一不可:
每一步的局部最优选择,能够导向全局最优解。即:在选择当前最优解时,不需要考虑后续的决策,其选择结果不会影响后续子问题的最优性。
全局最优解中必然包含其子问题的最优解。即:问题的最优解可以分解为若干个子问题的最优解的组合。
v 的最短路径'必然包含'从源点到路径上某中间节点 u 的最短路径'——若存在更短的 源点→u 路径,替换后可得到更短的 源点→v 路径,与全局最优矛盾。以'找零钱问题'为例:
若硬币面额为 [1,3,4],需找 6 元。直觉贪心策略(选最大面额优先)会得到 4+1+1=6(3 枚硬币),但最优解是 3+3=6(2 枚硬币)。此时'最大面额优先'的贪心策略不满足'贪心选择性质',导致全局最优失效。
使用贪心算法解决问题需遵循固定流程,核心是策略设计与正确性证明:
贪心算法的应用场景高度集中,以下为 4 类核心问题的详细实现:
给定 n 个活动,每个活动有开始时间 start[i] 和结束时间 end[i],选择最多不重叠的活动集合。
按活动的结束时间升序排序,优先选择最早结束的活动——该选择能为后续活动预留最多时间,最大化总活动数。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// 定义活动结构体
struct Activity {
int start; // 开始时间
int end; // 结束时间
};
// 排序规则:按结束时间升序
bool compare(const Activity& a, const Activity& b) {
return a.end < b.end;
}
// 选择最多不冲突活动
vector<Activity> selectMaxActivities(vector<Activity>& activities) {
vector<Activity> result;
if (activities.empty()) return result;
// 1. 按结束时间排序
sort(activities.begin(), activities.end(), compare);
// 2. 选择第一个活动(最早结束)
result.push_back(activities[0]);
int lastEnd = activities[0].end;
// 3. 遍历后续活动,选择不冲突的(开始时间>=上一个结束时间)
for (int i = 1; i < activities.size(); i++) {
if (activities[i].start >= lastEnd) {
result.push_back(activities[i]);
lastEnd = activities[i].end; // 更新最后一个活动的结束时间
}
}
return result;
}
int main() {
vector<Activity> activities = {{1,4},{3,5},{0,6},{5,7},{3,9},{5,9},{6,10},{8,11},{8,12},{2,14},{12,16}};
vector<Activity> selected = selectMaxActivities(activities);
// 输出结果
cout << "选择的活动(开始时间,结束时间):" << endl;
for (auto& act : selected) {
cout << "(" << act.start << ", " << act.end << ")" << endl;
}
cout << "最多可选择 " << selected.size() << " 个活动" << endl;
return 0;
}
选择的活动(开始时间,结束时间): (1, 4) (5, 7) (8, 11) (12, 16) 最多可选择 4 个活动
给定字符的频率分布(如 a:5, b:9, c:12, d:13),构造前缀编码(无编码是另一编码的前缀),使总编码长度(频率×编码长度之和)最小。
0,右分支记为 1,叶子节点的路径即为对应字符的编码。#include<iostream>
#include<queue>
#include<vector>
using namespace std;
// 计算哈夫曼编码的总长度
int huffmanCodeTotalLength(const vector<int>& frequencies) {
// 最小堆:priority_queue<Type, Container, Compare>
priority_queue<int, vector<int>, greater<int>> minHeap;
// 1. 将所有频率入堆
for (int freq : frequencies) {
minHeap.push(freq);
}
int totalLength = 0; // 总编码长度
// 2. 合并节点,直到堆中只剩 1 个节点
while (minHeap.size() > 1) {
// 取出两个最小频率
int first = minHeap.top(); minHeap.pop();
int second = minHeap.top(); minHeap.pop();
// 合并后的新节点频率
int merged = first + second;
totalLength += merged; // 合并节点的频率即编码长度贡献
// 新节点入堆
minHeap.push(merged);
}
return totalLength;
}
int main() {
// 字符频率:a:5, b:9, c:12, d:13, e:16, f:45
vector<int> frequencies = {5,9,12,13,16,45};
int total = huffmanCodeTotalLength(frequencies);
cout << "哈夫曼编码的总长度:" << total << endl;
// 输出:224
return 0;
}
总长度 224 的计算逻辑:
合并过程为 5+9=14(贡献 14)→12+13=25(贡献 25)→14+16=30(贡献 30)→25+30=55(贡献 55)→45+55=100(贡献 100),总和 14+25+30+55+100=224。
在带非负权的无向/有向图中,找到从源点 S 到所有其他节点的最短路径长度。
dist[] 数组记录源点到各节点的当前最短距离(初始为 INF,dist[S]=0);u(当前距离源点最近的未确定节点),标记为'已确定';u 的所有邻接节点 v,执行松弛操作:若 dist[v] > dist[u] + 边权 w,则更新 dist[v],并将(dist[v], v)入堆;#include<iostream>
#include<vector>
#include<queue>
#include<climits>
#include<stdexcept>
using namespace std;
const int INF = INT_MAX;
vector<int> dijkstra(int n, int source, const vector<vector<pair<int,int>>>& adj) {
// 边界校验:源点合法性
if (source < 0 || source >= n) {
throw invalid_argument("源点超出节点范围!");
}
vector<int> dist(n, INF);
dist[source] = 0;
// 最小堆:(当前距离,节点)
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> minHeap;
minHeap.push({0, source});
while (!minHeap.empty()) {
auto [currentDist, u] = minHeap.top(); // C++17 结构化绑定
minHeap.pop();
if (currentDist > dist[u]) continue;
// 遍历 u 的所有邻接节点
for (auto [v, w] : adj[u]) {
// 松弛操作:更新 dist[v]
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
minHeap.push({dist[v], v});
}
}
}
return dist;
}
int main() {
int n = 6; // 节点数(0~5)
vector<vector<pair<int,int>>> adj(n); // 局部邻接表,替代全局变量
// 构建图(边:u->v,权 w)
adj[0].emplace_back(1,2); emplace_back(2,4);
adj[1].emplace_back(2,1); emplace_back(3,7);
adj[2].emplace_back(4,3);
adj[3].emplace_back(5,1);
adj[4].emplace_back(3,2); emplace_back(5,5);
int source = 0;
try {
vector<int> dist = dijkstra(n, source, adj);
// 输出正确结果
cout << "源点 " << source << " 到各节点的最短距离:" << endl;
for (int i = 0; i < n; ++i) {
if (dist[i] == INF) {
cout << "到节点 " << i << ":不可达" << endl;
} else {
cout << "到节点 " << i << ":" << dist[i] << endl;
}
}
} catch (const invalid_argument& e) {
cerr << "错误:" << e.what() << endl;
return 1;
}
return 0;
}
源点 0 到各节点的最短距离:
到节点 0:0
到节点 1:2
到节点 2:3(0→1→2)
到节点 3:8(0→1→2→4→3)
到节点 4:6(0→1→2→4)
到节点 5:9(0→1→2→4→3→5)
在无向带权图中,找到一棵连接所有节点、总边权和最小的生成树(Minimum Spanning Tree, MST)。
n-1 条边(n 为节点数)。#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// 定义边结构体
struct Edge {
int u; // 起点
int v; // 终点
int weight;// 边权
};
// 并查集(Union-Find):维护连通分量
class UnionFind {
private:
vector<int> parent; // 父节点
vector<int> rank; // 秩(用于路径压缩优化)
public:
UnionFind(int n) {
parent.resize(n);
rank.resize(n, 0);
for (int i = 0; i < n; i++) {
parent[i] = i; // 初始父节点为自身
}
}
// 查找根节点(路径压缩)
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 递归压缩路径
}
return parent[x];
}
// 合并两个连通分量(按秩合并)
bool unite(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return false; // 已在同一分量
// 秩小的树合并到秩大的树
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
if (rank[rootX] == rank[rootY]) {
rank[rootX]++;
}
}
return true;
}
};
// Kruskal 算法:返回 MST 的总边权
int kruskal(int n, vector<Edge>& edges) {
// 1. 按边权升序排序
sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
return a.weight < b.weight;
});
UnionFind uf(n);
int mstTotal = 0; // MST 总边权
int edgeCount = 0; // 已选边数
// 2. 遍历边,选择不构成环的边
for (auto& edge : edges) {
if (uf.unite(edge.u, edge.v)) {
mstTotal += edge.weight;
edgeCount++; // MST 需 n-1 条边,提前退出
if (edgeCount == n - 1) break;
}
}
// 若边数不足 n-1,说明图不连通
return (edgeCount == n - 1) ? mstTotal : -1;
}
int main() {
int n = 5; // 节点数(0~4)
vector<Edge> edges = {{0,1,2},{0,3,6},{1,2,3},{1,3,8},{1,4,5},{2,4,7},{3,4,9}};
int mstTotal = kruskal(n, edges);
if (mstTotal == -1) {
cout << "图不连通,无法构建 MST" << endl;
} else {
cout << "最小生成树的总边权:" << mstTotal << endl; // 输出:16
}
return 0;
}
MST 的边为 (0,1,2)、(1,2,3)、(1,4,5)、(0,3,6),总权 2+3+5+6=16,覆盖所有 5 个节点且无环。
贪心与 DP 均依赖'最优子结构',但核心差异在于子问题的处理方式:
| 对比维度 | 贪心算法(Greedy) | 动态规划(DP) |
|---|---|---|
| 核心思想 | 局部最优→全局最优,不回溯 | 存储子问题最优解,回溯推导全局最优 |
| 子问题处理 | 不存储子问题解,每步直接选最优 | 存储子问题解(如 dp 数组),避免重复计算 |
| 适用场景 | 满足'贪心选择性质'的问题 | 不满足贪心选择性质,但有最优子结构 |
| 时间复杂度 | 低(通常 O(nlogn),排序主导) | 较高(通常 O(n²) 或 O(nm)) |
| 典型问题 | 活动选择、哈夫曼编码、Dijkstra | 0-1 背包、最长公共子序列、斐波那契 |
| 最优解保证 | 需证明策略正确性,否则不保证 | 只要状态转移正确,必为全局最优 |
O(nlogn)(排序)或 O(MlogN)(优先队列),远低于 DP;O(n)。贪心算法是一种'高效但挑剔'的算法:它通过局部最优的积累快速推导全局最优,但仅适用于满足'贪心选择性质'和'最优子结构'的问题。掌握贪心算法的核心在于:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online