使用rust学习基本算法(二)
贪心算法
贪心算法是一种在每一步选择中都采取当前状态下最优的选择,以期望通过一系列的局部最优解来达到全局最优解的算法策略。它的主要特点是局部最优解的选择,希望通过这种方式来解决某些优化问题。但需要注意的是,贪心算法并不保证能够得到全局最优解,它的正确性需要根据具体问题来分析。
贪心算法的基本思路
- 建立数学模型:将问题抽象成数学问题。
- 定义局部最优解策略:确定在每一步选择中应该遵循的规则,以确保每一步都是当前状态下的最优选择。
- 求解和构建全局解:通过不断地采取局部最优解,累积最终求得全局最优解或者接近全局最优解的答案。
贪心算法的特点
- 简单高效:算法通常较为简单,且执行效率高。
- 局部最优解:每一步都采取当前看来最优的选择。
- 不可回溯:一旦选择,就不会改变。
- 适用范围有限:只适用于能够通过局部最优解确保能够得到全局最优解的问题。
适用场景
贪心算法适用于问题的最优解可以通过一系列局部最优解构建的情况。
常见的适用于贪心算法的问题包括:
-
活动选择问题:如何安排活动使得使用同一个会议室的活动数目最多。
-
哈夫曼编码:用于数据压缩的编码方法。
-
最小生成树:如Kruskal算法和Prim算法。
-
单源最短路径:如Dijkstra算法。
[!TIP]
点击链接即可查看博客详情介绍。
活动选择问题
活动选择问题是贪心算法中的一个经典案例,它描述的是这样一个问题:给定一个活动集合,每个活动都有一个开始时间和结束时间,我们需要选择尽可能多的活动,使得这些活动之间不相互冲突。
简而言之,目标是在给定的时间内安排尽可能多的活动。
活动选择问题的贪心选择策略
贪心算法解决活动选择问题的关键在于如何选择活动。一种有效的策略是总是选择结束时间最早的活动,因为结束得越早,留给其他活动的时间就越多,从而有可能安排更多的活动。当然,前提是这个活动与已经选择的活动不冲突。
算法步骤
输入:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 活动集合 ( S = { a 1 , a 2 , . . . , a n } ) ,其中每个活动 ( a i ) 由一个开始时间 ( s i ) 和结束时间 ( e i ) 组成。 活动集合 (S = \{a_1, a_2, ..., a_n\}),其中每个活动 (a_i) 由一个开始时间 (s_i) 和结束时间 (e_i) 组成。 </math>活动集合(S={a1,a2,...,an}),其中每个活动(ai)由一个开始时间(si)和结束时间(ei)组成。
排序 :按照活动的结束时间从小到大对活动进行排序。 选择活动:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 选择结束时间最早的活动(假设为 a 1 将其加入到结果集合中。从剩下的活动中继续选择结束时间最早且与已选择的活动不冲突的活动,将其加入到结果集合中。重复上述步骤,直到没有更多的活动可以被选择。 选择结束时间最早的活动(假设为 a_1将其加入到结果集合中。 从剩下的活动中继续选择结束时间最早且与已选择的活动不冲突的活动,将其加入到结果集合中。 重复上述步骤,直到没有更多的活动可以被选择。 </math>选择结束时间最早的活动(假设为a1将其加入到结果集合中。从剩下的活动中继续选择结束时间最早且与已选择的活动不冲突的活动,将其加入到结果集合中。重复上述步骤,直到没有更多的活动可以被选择。
算法实现实例
python
activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)]
# 每个元组代表一个活动,元组的第一个元素是开始时间,第二个元素是结束时间
# 按照结束时间对活动进行排序
activities.sort(key=lambda x: x[1])
def select_activities(activities):
selected_activities = [activities[0]] # 选择结束时间最早的活动
last_finish_time = activities[0][1]
for start_time, finish_time in activities[1:]:
if start_time >= last_finish_time: # 如果当前活动的开始时间不早于上一个选中活动的结束时间
selected_activities.append((start_time, finish_time))
last_finish_time = finish_time
return selected_activities
# 调用函数
selected_activities = select_activities(activities)
print("Selected activities:", selected_activities)
rust
/*
步骤 1: 定义活动结构体
首先,我们定义一个 Activity 结构体来表示一个活动,包括它的开始时间和结束时间。
*/
#[derive(Debug, Clone)]
struct Activity {
start: i32,
end: i32,
}
/*
步骤 2: 实现活动选择函数
接下来,我们实现一个函数来解决活动选择问题。这个函数接收一个活动列表作为输入,返回一个选中活动的列表。
*/
fn activity_selection(mut activities: Vec<Activity>) -> Vec<Activity> {
// 按照活动的结束时间进行排序
activities.sort_by(|a, b| a.end.cmp(&b.end));
let mut selected_activities = Vec::new();
// 选择第一个活动
if let Some(first_activity) = activities.first() {
selected_activities.push(first_activity.clone());
let mut last_end = first_activity.end;
// 遍历其余的活动
for activity in activities.iter().skip(1) {
if activity.start >= last_end {
// 如果当前活动的开始时间晚于或等于上一个选中活动的结束时间,则选择这个活动
selected_activities.push(activity.clone());
last_end = activity.end;
}
}
}
selected_activities
}
fn activity_selection(mut activities: Vec<Activity>) -> Vec<Activity> {
// 按照活动的结束时间进行排序
activities.sort_by(|a, b| a.end.cmp(&b.end));
let mut selected_activities = Vec::new();
// 选择第一个活动
if let Some(first_activity) = activities.first() {
selected_activities.push(first_activity.clone());
let mut last_end = first_activity.end;
// 遍历其余的活动
for activity in activities.iter().skip(1) {
if activity.start >= last_end {
// 如果当前活动的开始时间晚于或等于上一个选中活动的结束时间,则选择这个活动
selected_activities.push(activity.clone());
last_end = activity.end;
}
}
}
selected_activities
}
/*
代码测试
*/
fn main() {
let activities = vec![
Activity { start: 1, end: 4 },
Activity { start: 3, end: 5 },
Activity { start: 0, end: 6 },
Activity { start: 5, end: 7 },
Activity { start: 8, end: 9 },
Activity { start: 5, end: 9 },
];
let selected_activities = activity_selection(activities);
for activity in selected_activities {
println!("Activity starts at {}, ends at {}", activity.start, activity.end);
}
}
哈夫曼树
哈夫曼树(Huffman Tree),又称最优二叉树,是一种应用广泛的用于数据压缩的树形结构。它是基于字符出现频率来构造的二叉树,其中频率高的字符离根较近,而频率低的字符离根较远,从而实现数据的有效压缩。构造哈夫曼树的过程是一种贪心算法。
构造过程
- 初始化:根据待编码的字符及其频率构造一个森林,每个字符都是一个单节点的二叉树,节点的权值为字符出现的频率。
- 构造过程:在森林中选出两个根节点的权值最小的树合并,作为一个新的二叉树的左、右子树,新二叉树的根节点权值为两个子树根节点权值之和。将选中的两棵树从森林中删除,同时将新形成的二叉树加入森林。重复上述过程,直到森林中只剩下一棵树,这棵树就是哈夫曼树。
- 结果:得到的哈夫曼树中,每个字符都对应一条从根到该字符节点的路径,路径上的左右分支分别代表编码中的0和1,从而得到每个字符的哈夫曼编码。
特点
- 哈夫曼编码是一种变长编码方法,不同字符的编码长度不同,频率高的字符编码短,频率低的字符编码长。
- 哈夫曼编码是前缀编码,任何字符的编码都不是其他字符编码的前缀,这保证了编码的唯一解码性。
应用
哈夫曼编码广泛应用于数据压缩领域,如ZIP文件压缩、JPEG图像压缩等。通过哈夫曼编码,可以有效减少存储空间或传输带宽的需求,提高数据处理效率。
rust
/*
步骤 1: 定义节点和树的结构
首先,定义哈夫曼树的节点结构,以及一个枚举来表示节点可以是叶子节点或者有两个子节点的中间节点。
*/
use std::rc::Rc;
use std::cell::RefCell;
use std::collections::BinaryHeap;
use std::cmp::Ordering;
#[derive(Debug, Clone)]
enum Node {
Leaf { character: char, frequency: usize },
Internal { frequency: usize, left: Rc<RefCell<Node>>, right: Rc<RefCell<Node>> },
}
impl Node {
fn frequency(&self) -> usize {
match *self {
Node::Leaf { frequency, .. } | Node::Internal { frequency, .. } => frequency,
}
}
}
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
self.frequency() == other.frequency()
}
}
impl Eq for Node {}
impl PartialOrd for Node {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
other.frequency().partial_cmp(&self.frequency())
}
}
impl Ord for Node {
fn cmp(&self, other: &Self) -> Ordering {
other.frequency().cmp(&self.frequency())
}
}
/*
步骤 2: 构建哈夫曼树
接下来,实现构建哈夫曼树的函数。这个函数将接受一个字符及其频率的映射,然后构建并返回哈夫曼树。
*/
fn build_huffman_tree(frequencies: &[(char, usize)]) -> Option<Rc<RefCell<Node>>> {
if frequencies.is_empty() {
return None;
}
let mut heap = BinaryHeap::new();
for &(character, frequency) in frequencies {
heap.push(Rc::new(RefCell::new(Node::Leaf { character, frequency })));
}
while heap.len() > 1 {
let left = heap.pop().unwrap();
let right = heap.pop().unwrap();
let new_node = Node::Internal {
frequency: left.borrow().frequency() + right.borrow().frequency(),
left,
right,
};
heap.push(Rc::new(RefCell::new(new_node)));
}
heap.pop()
}
/*
步骤 3: 生成哈夫曼编码
最后,我们需要从哈夫曼树中生成每个字符的编码。这需要遍历树,并在遍历过程中记录路径。
*/
fn generate_codes(node: &Rc<RefCell<Node>>, prefix: String, codes: &mut Vec<(char, String)>) {
match *node.borrow() {
Node::Leaf { character, .. } => {
codes.push((character, prefix));
},
Node::Internal { ref left, ref right, .. } => {
generate_codes(left, format!("{}0", prefix), codes);
generate_codes(right, format!("{}1", prefix), codes);
},
}
}
fn main() {
let frequencies = [('a', 45), ('b', 13), ('c', 12), ('d', 16), ('e', 9), ('f', 5)];
let tree = build_huffman_tree(&frequencies);
let mut codes = Vec::new();
generate_codes(&tree, "".to_string(), &mut codes);
for (character, code) in codes {
println!("Character: {}, Code: {}", character, code);
}
}
/*
单元测试
*/
use std::rc::Rc;
use std::cell::RefCell;
use std::collections::BinaryHeap;
use std::cmp::Ordering;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_input() {
let frequencies = [];
let tree = build_huffman_tree(&frequencies);
assert!(tree.is_none());
}
#[test]
fn test_single_element_input() {
let frequencies = [('a', 1)];
let tree = build_huffman_tree(&frequencies).expect("Tree should be created with single element");
let mut codes = Vec::new();
generate_codes(&tree, "".to_string(), &mut codes);
assert_eq!(codes.len(), 1);
assert_eq!(codes[0], ('a', "".to_string()));
}
}
最小生成树
最小生成树(Minimum Spanning Tree,MST)是在一个加权无向图中寻找一个边的子集,使得这个子集构成的树包括图中的所有顶点,并且树的所有边的权值之和最小。最小生成树在很多领域都有应用,比如网络设计、电路设计、交通网络规划等。
Prim算法(Prim's Algorithm)
从图中的任意一个顶点开始构建最小生成树。 在每一步中,选择连接树与非树顶点且权值最小的边,并将其加入到树中,直到所有顶点都被包含在树中。 时间复杂度:使用优先队列时为O(E+VlogV),其中 V 是顶点数,E 是边数。
rust
/*
定义图的数据结构:首先,我们需要定义一个适合表示图的数据结构。这里我们使用Vec<Vec<(usize, i32)>>来表示图,其中每个顶点都是一个索引,每个顶点存储一个向量,向量中的元素是(顶点索引, 边的权重)的元组。
*/
use std::collections::BinaryHeap;
use std::cmp::Ordering;
#[derive(Debug, Clone, Eq)]
struct Edge {
node: usize,
cost: i32,
}
impl PartialEq for Edge {
fn eq(&self, other: &Self) -> bool {
self.cost == other.cost
}
}
impl PartialOrd for Edge {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
other.cost.partial_cmp(&self.cost)
}
}
impl Ord for Edge {
fn cmp(&self, other: &Self) -> Ordering {
other.cost.cmp(&self.cost)
}
}
struct Graph {
adj_list: Vec<Vec<(usize, i32)>>,
}
impl Graph {
fn new(size: usize) -> Self {
Graph {
adj_list: vec![vec![]; size],
}
}
fn add_edge(&mut self, src: usize, dest: usize, cost: i32) {
self.adj_list[src].push((dest, cost));
self.adj_list[dest].push((src, cost)); // For undirected graph
}
}
/*
步骤2: 实现普里姆算法
接下来,我们实现普里姆算法来找到最小生成树:
*/
impl Graph {
fn prim_mst(&self) -> i32 {
let mut total_cost = 0;
let mut edge_heap = BinaryHeap::new();
let mut visited = vec![false; self.adj_list.len()];
// Start from the first node
visited[0] = true;
for &(node, cost) in &self.adj_list[0] {
edge_heap.push(Edge { node, cost });
}
while let Some(Edge { node, cost }) = edge_heap.pop() {
if visited[node] {
continue;
}
visited[node] = true;
total_cost += cost;
for &(next_node, next_cost) in &self.adj_list[node] {
if !visited[next_node] {
edge_heap.push(Edge { node: next_node, cost: next_cost });
}
}
}
total_cost
}
}
/*
步骤3: 添加单元测试
最后,我们为普里姆算法添加单元测试:
*/
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prim_mst() {
let mut graph = Graph::new(4);
graph.add_edge(0, 1, 10);
graph.add_edge(0, 2, 6);
graph.add_edge(0, 3, 5);
graph.add_edge(1, 3, 15);
graph.add_edge(2, 3, 4);
assert_eq!(graph.prim_mst(), 19);
}
}
Kruskal算法(Kruskal's Algorithm)
将图中的所有边按权值从小到大排序。 依次考虑每条边,如果这条边不会与已选择的边形成环,则将其加入到最小生成树中。 使用并查集(Disjoint Set Union,DSU)可以高效地检测环。 时间复杂度:O(ElogE) 或 O(ElogV)(因为需要对边进行排序)。
rust
/*
定义边和并查集的数据结构:克鲁斯卡尔算法需要对所有的边进行排序,并使用并查集(Disjoint Set Union,DSU)来检测环。
*/
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Edge {
src: usize,
dest: usize,
weight: i32,
}
struct DisjointSet {
parent: Vec<usize>,
rank: Vec<usize>,
}
impl DisjointSet {
fn new(size: usize) -> Self {
DisjointSet {
parent: (0..size).collect(),
rank: vec![0; size],
}
}
fn find(&mut self, node: usize) -> usize {
if self.parent[node] != node {
self.parent[node] = self.find(self.parent[node]);
}
self.parent[node]
}
fn union(&mut self, node1: usize, node2: usize) {
let root1 = self.find(node1);
let root2 = self.find(node2);
if self.rank[root1] < self.rank[root2] {
self.parent[root1] = root2;
} else if self.rank[root1] > self.rank[root2] {
self.parent[root2] = root1;
} else {
self.parent[root2] = root1;
self.rank[root1] += 1;
}
}
}
/*
实现克鲁斯卡尔算法:对边进行排序,并逐一检查每条边以构建最小生成树,同时使用并查集来避免环的形成。
*/
fn kruskal(edges: &mut Vec<Edge>, num_nodes: usize) -> Vec<Edge> {
edges.sort(); // Sort edges based on weight
let mut dsu = DisjointSet::new(num_nodes);
let mut mst = Vec::new();
for edge in edges.iter() {
let root1 = dsu.find(edge.src);
let root2 = dsu.find(edge.dest);
if root1 != root2 {
mst.push(edge.clone());
dsu.union(root1, root2);
}
}
mst
}
/*
添加单元测试:确保算法的正确性。
*/
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kruskal() {
let mut edges = vec![
Edge { src: 0, dest: 1, weight: 10 },
Edge { src: 0, dest: 2, weight: 6 },
Edge { src: 0, dest: 3, weight: 5 },
Edge { src: 1, dest: 3, weight: 15 },
Edge { src: 2, dest: 3, weight: 4 }
];
let mst = kruskal(&mut edges, 4);
assert_eq!(mst.len(), 3);
assert_eq!(mst.iter().map(|e| e.weight).sum::<i32>(), 19);
}
}
选择算法
[!NOTE]
在图论中,判断一个图是稠密(Dense)还是稀疏(Sparse)通常依赖于图中边的数量与顶点数量的关系。
稀疏图:如果边的数量接近顶点的数量,即 (E ≈V) 或 (E = O(V)),则图被认为是稀疏的。在实际应用中,如果边的数量少于顶点数量的两倍,即 (E < 2V),图通常也被认为是稀疏的。
稠密图:如果边的数量接近顶点数量的平方,即 (E ≈ V^2) 或 (E = O(V^2)),则图被认为是稠密的。在实际应用中,如果边的数量大于或等于顶点数量的对数倍,即 (E ≥Vlog V),图也可能被认为是较为稠密的。
快速判断方法
计算边和顶点的比率:计算 (E/V) 的值,这个比率可以提供图是稠密还是稀疏的直观感受。如果这个比率接近1或者更小,图很可能是稀疏的;如果这个比率接近顶点数 (V) 或更高,图很可能是稠密的。
观察实际应用场景:在某些情况下,图的应用背景也能提供是否稠密或稀疏的线索。例如,在社交网络中,用户(顶点)之间的连接(边)可能非常丰富,使得这类图倾向于更稠密。而在某些交通网络中,地点(顶点)之间的直接路线(边)可能相对较少,使得这类图可能更倾向于稀疏。
注意事项
- 如果图是稠密的,即边数接近顶点数的平方,通常使用普里姆算法更高效。
- 如果图是稀疏的,即边数与顶点数相近,克鲁斯卡尔算法可能更合适。