文章目录
使用rust学习基本算法(一)
Dijkstra算法是一种著名的算法,用于在加权图中找到最短路径。它由荷兰计算机科学家艾兹格·戴克斯特拉(Edsger W. Dijkstra)于1956年提出,并且在1959年发表。这个算法可以找到从图中一个指定的源节点到所有其他节点的最短路径。它能够处理有向和无向图,但前提是图中的边权重不能为负数。
Dijkstra算法的核心思想是贪心算法。
[!NOTE]
贪心算法
贪心算法是一种在每一步选择中都采取当前状态下最优的选择,从而希望导致结果是全局最优的算法策略。这种算法在解决某些优化问题时非常有效,尤其是当局部最优选择能够确保找到全局最优解的问题中。贪心算法的关键是,它做出选择时,仅考虑当前状态,不考虑结果将如何影响未来的选择。
贪心算法的特点:
- 局部最优选择:在每一步都做出当前看来最好的选择。
- 无回溯:一旦做出选择,就不再改变。
- 简单高效:通常情况下,贪心算法会比其他算法更简单、更快速地得到解决方案。
- 不一定能得到全局最优解:只有在某些特定问题上,贪心算法才能保证得到全局最优解。
贪心算法的应用场景:
- 硬币找零问题:给定不同面额的硬币和一个总金额,如何用最少的硬币组成这个金额。
- 活动选择问题:给定一系列活动的开始时间和结束时间,选择尽可能多的活动,使得活动之间不相互冲突。
- 哈夫曼编码:用于数据压缩的哈夫曼树构建也是一个贪心算法的例子。
- 最小生成树问题:如Prim算法和Kruskal算法。
学习贪心算法的建议:
- 理解问题的贪心性质:分析为什么局部最优选择能导向全局最优解。
- 实践多种贪心算法问题:通过解决不同类型的问题来加深理解。
- 对比其他算法:了解贪心算法与动态规划、回溯等算法在解决相同问题时的不同之处。
它维护了两组节点:已经找到最短路径的节点集合和还没有找到最短路径的节点集合。算法从源节点开始,逐步将距离源节点最近的节点加入到已找到最短路径的节点集合中,并更新其他节点到源节点的距离,直到所有节点都被处理过。
在Rust中实现Dijkstra算法,我们需要考虑几个关键点:
- 图的表示:可以使用邻接列表或邻接矩阵来表示图。
- 优先队列:为了高效地选择下一个最短路径候选节点,通常使用优先队列(最小堆)。
- 距离数组:用于跟踪从源节点到每个节点的当前最短距离。
- 前驱节点数组(可选):如果需要重构最短路径,则需要此数组来跟踪到达每个节点的最短路径上的前一个节点。
实现思路拆解:
定义边和优先队列中的元素
定义图的边,以及优先队列中的元素。在这里,我们创建了一个Edge结构体来表示边,它包含目标节点的索引node和从源节点到该节点的成本cost。
rust
#[derive(Clone, Eq, PartialEq)]
struct Edge {
node: usize,
cost: usize,
}
为了让Edge能够在优先队列(BinaryHeap)中按成本从小到大排序,我们需要为其实现Ord和PartialOrd特质。由于BinaryHeap是最大堆,我们通过反转比较结果来实现最小堆的效果。
rust
impl Ord for Edge {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.cost.cmp(&self.cost).then_with(|| self.node.cmp(&other.node))
}
}
impl PartialOrd for Edge {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
实现Dijkstra算法。此函数接受一个邻接列表表示的图和一个起始节点索引,返回一个包含从起始节点到所有其他节点的最短路径成本的向量。
rust
fn dijkstra(graph: &Vec<Vec<Edge>>, start: usize) -> Vec<usize> {
// 初始化距离向量,所有距离初始为usize::MAX
let mut dist = vec![usize::MAX; graph.len()];
let mut heap = BinaryHeap::new();
// 将起始节点的距离设置为0,并加入优先队列
dist[start] = 0;
heap.push(Edge { node: start, cost: 0 });
// 循环处理优先队列中的节点
while let Some(Edge { node, cost }) = heap.pop() {
// 如果当前节点的处理成本高于已知最小成本,则跳过
if cost > dist[node] {
continue;
}
// 遍历当前节点的所有邻接边
for edge in &graph[node] {
let new_cost = cost + edge.cost;
if new_cost < dist[edge.node] {
heap.push(Edge { node: edge.node, cost: new_cost }); // 直接创建并推入
dist[edge.node] = new_cost; // 更新成本
}
}
}
// 返回从起始节点到所有其他节点的最短路径成本
dist
}
在主函数中创建一个图,并调用dijkstra函数来计算从某个起点到图中所有其他点的最短路径成本。
rust
fn main() {
// 示例:创建一个图(这里应该是具体的图结构初始化代码)
let graph = vec![
vec![Edge { node: 1, cost: 2 }, Edge { node: 2, cost: 4 }],
vec![Edge { node: 2, cost: 1 }],
vec![Edge { node: 3, cost: 2 }],
vec![],
];
let start = 0;
let distances = dijkstra(&graph, start);
println!("从节点 {} 到其他所有节点的最短路径成本:{:?}", start, distances);
}
完整代码:
rust
use std::collections::BinaryHeap;
#[derive(Clone, Eq, PartialEq)]
struct Edge {
cost: usize,
node: usize,
}
impl Ord for Edge {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.cost.cmp(&self.cost).then_with(|| self.node.cmp(&other.node))
}
}
impl PartialOrd for Edge {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
fn dijkstra(graph: &Vec<Vec<Edge>>, start: usize) -> Vec<usize> {
let mut dist = vec![usize::MAX; graph.len()];
let mut heap: BinaryHeap<Edge> = BinaryHeap::new();
dist[start] = 0;
heap.push(Edge { node: start, cost: 0 });
while let Some(Edge { node, cost }) = heap.pop() {
if cost > dist[node] {
continue;
}
for edge in &graph[node] {
let new_cost = cost + edge.cost;
if new_cost < dist[edge.node] {
heap.push(Edge { node: edge.node, cost: new_cost }); // 直接创建并推入
dist[edge.node] = new_cost; // 更新成本
}
}
}
}
dist
}
fn main() {
let graph = vec![
vec![Edge { node: 1, cost: 2 }, Edge { node: 2, cost: 4 }],
vec![Edge { node: 2, cost: 1 }],
vec![Edge { node: 3, cost: 2 }],
vec![],
];
let start = 0;
let distances = dijkstra(&graph, start);
println!("从节点 {} 到其他所有节点的最短路径成本:{:?}", start, distances);
}
测试
rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dijkstra() {
// 构建一个简单的图
let graph = vec![
vec![Edge { node: 1, cost: 2 }, Edge { node: 2, cost: 4 }], // 从节点0到节点1和2的边
vec![Edge { node: 2, cost: 1 }], // 从节点1到节点2的边
vec![], // 节点2没有出边
];
// 打印输入图结构
println!("输入图结构:");
for (i, edges) in graph.iter().enumerate() {
print!("节点{} -> ", i);
for edge in edges {
print!("(节点{}, 成本{}) ", edge.node, edge.cost);
}
println!();
}
// 执行dijkstra算法
let dist = dijkstra(&graph, 0);
// 打印算法输出结果
println!("\n算法输出结果:");
println!("{:?}", dist);
// 验证结果
assert_eq!(dist, vec![0, 2, 3]);
}
}
节点0有两条边,一条到节点1(成本为2),另一条到节点2(成本为4)。
节点1有一条边到节点2(成本为1)。
节点2没有出边。
预期的输出是一个向量[0, 2, 3]。
Dijkstra算法的特点
- 单源最短路径:Dijkstra算法适用于从一个源点到其他所有点的最短路径问题。
- 非负权重限制:算法假设所有边的权重都是非负的,这是算法正确运行的前提。
- 贪心策略:在每一步选择中,算法都会寻找未处理的最近的顶点,即它采用了贪心策略。
- 时间复杂度:当使用优先队列(如二叉堆)时,算法的时间复杂度为O((V+E)logV),其中V是顶点数,E是边数。
应用场景
Dijkstra算法在计算机科学和相关领域中有广泛的应用,主要包括:
- 路由算法:在网络路由协议中,如OSPF(Open Shortest Path First)使用Dijkstra算法来计算最短路径。
- 地图服务:如Google Maps等地图和导航服务使用Dijkstra算法来找到两点之间的最短路径。
- 图形学:在图形学中,可以用来寻找图中的最短路径,比如在像素网络中寻找最短路径。
- 机器人路径规划:在机器人学中,Dijkstra算法可以用于寻找机器人从起点到终点的最短路径。
学习Dijkstra算法的建议
- 理解原理:首先理解Dijkstra算法的工作原理,包括它是如何通过贪心策略来逐步确定每个顶点的最短路径。
- 掌握实现:通过编程实践来加深理解。尝试在不同的编程语言中实现Dijkstra算法,并对比其性能。
- 学习优化:理解不同数据结构(如优先队列、斐波那契堆)对算法性能的影响,并尝试实现这些优化版本。
- 解决实际问题:尝试将Dijkstra算法应用于解决实际问题,如路由规划、网络流量分析等,以增强对算法应用场景的理解。
找机器人从起点到终点的最短路径。
学习Dijkstra算法的建议
- 理解原理:首先理解Dijkstra算法的工作原理,包括它是如何通过贪心策略来逐步确定每个顶点的最短路径。
- 掌握实现:通过编程实践来加深理解。尝试在不同的编程语言中实现Dijkstra算法,并对比其性能。
- 学习优化:理解不同数据结构(如优先队列、斐波那契堆)对算法性能的影响,并尝试实现这些优化版本。
- 解决实际问题:尝试将Dijkstra算法应用于解决实际问题,如路由规划、网络流量分析等,以增强对算法应用场景的理解。
- 对比学习:与其他路径搜索算法(如Bellman-Ford、Floyd-Warshall)进行对比学习,理解它们之间的区别和适用场景。