一、贪心算法
贪心算法是一种思想,其核心思想在于每一步的决策,只考虑当前状态下的最优(局部最优),而不考虑对未来的影响,从而获取全局最优结果。它不会像动态规划法一样进行回溯。其应用逻辑的前提是局部最优可累积全局最优和最优子结构。
贪心算法的优势在于时间复杂度低,整体考虑较少,逻辑简单,不需要处理中间子问题。所以其代码实现清晰简单、执行效率高。但其局限性也很明显,它只适合于前面提到的特定场景下的全局最优解的获取。
二、Dijkstra算法
Dijkstra算法是1956年由荷兰的艾兹赫尔·迪杰斯特拉提出(1959发表)的单源最短路径算法。它是贪心算法的一个特例。主要用于计算带非负权的图中从指定的起点到其它所有节点的最短路径。
迪杰斯卡尔算法的限制条件是其边的权重不允许出现负值,这是迪杰斯卡尔算法成立的严格条件。也是后续的算法迭代的重要基础。要想明白迪杰斯卡尔算法就必须先搞清楚一个名词:松弛操作即Relaxation。其主要内容是:判断"起点→中转顶点u→目标顶点v"的路径总长度,是否小于当前记录的起点到v的最短距离,若更小则更新v的距离值,公式为:distv = min(distv, distu + weight(u,v))。松弛操作是一种原子类型的操作。
网上有一个经典的例子,用手拉着代表连接两个点的皮筋,那么这根皮筋就直观的表示了两个点间的长度(假设两点间还有几个点,都可以连接这两个点)。一开始的情况下,把皮筋拉得很紧;然后,如果发现通过两点间的某个点连接后,原来两点间的距离会变短。这时就需要松开一端的手,让皮筋收缩到新的过这个点的长度的路径。这个过程,不就是皮筋松弛操作么。松弛就是把紧的宽上限值放松到一个更优的松的窄下限值(由大到小的过程)。
三、二者的不同
Dijkstra算法作为一种贪心算法的特例,肯定有其自己的特点。所以其和传统的贪心算法的不同在于:
| 对比项 | 贪心算法 | Dijkstra 算法 |
|---|---|---|
| 类型 | 通用算法思想 | 特定实例算法 |
| 解决问题 | 多种优化问题 | 单源最短路径 |
| 是否最优 | 不一定,需要证明 | 非负权图上最优 |
| 决策依据 | 仅考虑局部最优 | 局部最优+松弛操作 |
| 是否回溯 | 无 | 通过松弛回溯 |
| 常用数据结构 | 排序、扫描、堆等 | 邻接表和优先队列 |
| 适用对象 | 区间、调度、编码、图等 | 有向图或无向图 |
| 负权边 | 取决于具体算法 | 不支持 |
可以把二者理解为:
贪心算法是方法论,Dijkstra是使用贪心思想解决非负权图最短路径问题的具体算法。
可以把贪心算法当成一种经验的直觉,也就是跟着感觉走。而Dijkstra算法则通过一定方法进行验证,保证这种直觉的正确性。前者未必正确,但后者在保证条件的前提下一定是正确的。
四、应用场景
Dijkstra应用场景
地图导航:如GPS导航系统计算两地之间的最快或最短路线。
网络路由:互联网中的OSPF(开放最短路径优先)协议底层即使用Dijkstra算法计算数据包的最佳传输路径。
交通规划:城市公交、地铁线路的最优换乘方案计算。
机器人路径规划:在已知环境中寻找从起点到终点的最优移动轨迹。
贪心算法的应用场景:
图论领域:迪杰斯特拉最短路径算法、普利姆最小生成树算法、克鲁斯卡尔最小生成树算法。
编码领域:霍夫曼编码,通过贪心策略选择权重最小的两个节点合并,实现数据的最优无损压缩。
日常问题:活动选择问题、分数背包问题(物品可分割)。
五、例程
下面看一个Dijkstra的算法求最短路径的例子:
c
#include <algorithm>
#include <functional>
#include <iostream>
#include <limits>
#include <queue>
#include <utility>
#include <vector>
struct Edge {
int to;
int weight;
};
std::vector<int> shortestPath(const std::vector<std::vector<Edge>> &g, int source, int target) {
const long long INF = std::numeric_limits<long long>::max();
std::vector<long long> dis(g.size(), INF);
std::vector<int> prev(g.size(), -1);
using State = std::pair<long long, int>;
std::priority_queue<State, std::vector<State>, std::greater<State>> pq;
dis[source] = 0;
pq.push({0, source});
while (!pq.empty()) {
auto [curDistance, curNode] = pq.top();
pq.pop();
if (curDistance != dis[curNode]) {
continue;
}
for (const auto &edge : g[curNode]) {
long long newDistance = curDistance + edge.weight;
if (newDistance < dis[edge.to]) {
dis[edge.to] = newDistance;
prev[edge.to] = curNode;
pq.push({newDistance, edge.to});
}
}
}
if (dis[target] == INF) {
return {};
}
std::vector<int> path;
for (int node = target; node != -1; node = prev[node]) {
path.push_back(node);
}
std::reverse(path.begin(), path.end());
return path;
}
int main() {
std::vector<std::vector<Edge>> g(5);
g[0].push_back({1, 2});
g[0].push_back({2, 5});
g[1].push_back({2, 1});
g[1].push_back({3, 3});
g[2].push_back({3, 1});
g[3].push_back({4, 2});
const auto path = shortestPath(g, 0, 4);
if (path.empty()) {
std::cout << "path no exist!" << std::endl;
} else {
std::cout << "shortest path: ";
for (std::size_t i = 0; i < path.size(); ++i) {
if (i != 0) {
std::cout << " -> ";
}
std::cout << path[i];
}
std::cout << std::endl;
}
return 0;
}
六、总结
算法是思想,数据是基础。现实世界中,解决问题就是用算法的思维去解决资源分配的过程。计算机是一种工具,不同点在于,它是对人类的全方位的延伸,特别是大脑。所以,对基础的算法的学习和掌握,对于在现实的工作中也是有着重要的作用的。