Skip to content

Commit 0974356

Browse files
feat: add Ant Colony Optimization algorithm for TSP (#1016)
1 parent a468046 commit 0974356

File tree

3 files changed

+392
-1
lines changed

3 files changed

+392
-1
lines changed

DIRECTORY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@
179179
* [Ramer Douglas Peucker](https://github.com/TheAlgorithms/Rust/blob/master/src/geometry/ramer_douglas_peucker.rs)
180180
* [Segment](https://github.com/TheAlgorithms/Rust/blob/master/src/geometry/segment.rs)
181181
* Graph
182-
* [Astar](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/astar.rs)
182+
* [Ant Colony Optimization](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/ant_colony_optimization.rs)
183+
* [A*](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/astar.rs)
183184
* [Bellman-Ford](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/bellman_ford.rs)
184185
* [Bipartite Matching](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/bipartite_matching.rs)
185186
* [Breadth First Search](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/breadth_first_search.rs)
Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
//! Ant Colony Optimization (ACO) algorithm for solving the Travelling Salesman Problem (TSP).
2+
//!
3+
//! The Travelling Salesman Problem asks: "Given a list of cities and the distances between
4+
//! each pair of cities, what is the shortest possible route that visits each city exactly
5+
//! once and returns to the origin city?"
6+
//!
7+
//! The ACO algorithm uses artificial ants that build solutions iteratively. Each ant constructs
8+
//! a tour by probabilistically choosing the next city based on pheromone trails and heuristic
9+
//! information (distance). After all ants complete their tours, pheromone trails are updated,
10+
//! with stronger pheromones deposited on shorter routes. Over multiple iterations, this process
11+
//! converges toward finding good solutions to the TSP.
12+
//!
13+
//! # References
14+
//! - [Ant Colony Optimization Algorithms](https://en.wikipedia.org/wiki/Ant_colony_optimization_algorithms)
15+
//! - [Travelling Salesman Problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem)
16+
17+
use rand::Rng;
18+
use std::collections::HashSet;
19+
20+
/// Represents a 2D city with coordinates
21+
#[derive(Debug, Clone, Copy, PartialEq)]
22+
struct City {
23+
x: f64,
24+
y: f64,
25+
}
26+
27+
impl City {
28+
/// Calculate Euclidean distance to another city
29+
fn distance_to(&self, other: &City) -> f64 {
30+
let dx = self.x - other.x;
31+
let dy = self.y - other.y;
32+
(dx * dx + dy * dy).sqrt()
33+
}
34+
}
35+
36+
/// Ant Colony Optimization solver for the Travelling Salesman Problem
37+
struct AntColonyOptimization {
38+
cities: Vec<City>,
39+
pheromones: Vec<Vec<f64>>,
40+
num_ants: usize,
41+
num_iterations: usize,
42+
evaporation_rate: f64,
43+
pheromone_influence: f64,
44+
distance_influence: f64,
45+
pheromone_constant: f64,
46+
}
47+
48+
impl AntColonyOptimization {
49+
/// Create a new ACO solver with the given cities and parameters
50+
fn new(
51+
cities: Vec<City>,
52+
num_ants: usize,
53+
num_iterations: usize,
54+
evaporation_rate: f64,
55+
pheromone_influence: f64,
56+
distance_influence: f64,
57+
pheromone_constant: f64,
58+
) -> Self {
59+
let n = cities.len();
60+
let pheromones = vec![vec![1.0; n]; n];
61+
Self {
62+
cities,
63+
pheromones,
64+
num_ants,
65+
num_iterations,
66+
evaporation_rate,
67+
pheromone_influence,
68+
distance_influence,
69+
pheromone_constant,
70+
}
71+
}
72+
73+
/// Run the ACO algorithm and return the best solution found
74+
fn solve(&mut self) -> Option<(Vec<usize>, f64)> {
75+
if self.cities.is_empty() {
76+
return None;
77+
}
78+
79+
let mut best_route: Vec<usize> = Vec::new();
80+
let mut best_distance = f64::INFINITY;
81+
82+
for _ in 0..self.num_iterations {
83+
let routes = self.construct_solutions();
84+
85+
for route in &routes {
86+
let distance = self.calculate_route_distance(route);
87+
if distance < best_distance {
88+
best_distance = distance;
89+
best_route.clone_from(route);
90+
}
91+
}
92+
93+
self.update_pheromones(&routes);
94+
}
95+
96+
if best_route.is_empty() {
97+
None
98+
} else {
99+
Some((best_route, best_distance))
100+
}
101+
}
102+
103+
/// Construct solutions for all ants in one iteration
104+
fn construct_solutions(&self) -> Vec<Vec<usize>> {
105+
(0..self.num_ants)
106+
.map(|_| self.construct_ant_solution())
107+
.collect()
108+
}
109+
110+
/// Construct a solution for a single ant
111+
fn construct_ant_solution(&self) -> Vec<usize> {
112+
let n = self.cities.len();
113+
let mut route = Vec::with_capacity(n + 1);
114+
let mut unvisited: HashSet<usize> = (0..n).collect();
115+
116+
// Start at city 0
117+
let mut current = 0;
118+
route.push(current);
119+
unvisited.remove(&current);
120+
121+
// Visit remaining cities
122+
while !unvisited.is_empty() {
123+
current = self.select_next_city(current, &unvisited);
124+
route.push(current);
125+
unvisited.remove(&current);
126+
}
127+
128+
// Return to starting city
129+
route.push(0);
130+
route
131+
}
132+
133+
/// Select the next city to visit based on pheromone and distance
134+
fn select_next_city(&self, current: usize, unvisited: &HashSet<usize>) -> usize {
135+
let probabilities: Vec<(usize, f64)> = unvisited
136+
.iter()
137+
.map(|&city| {
138+
let pheromone = self.pheromones[current][city];
139+
let distance = self.cities[current].distance_to(&self.cities[city]);
140+
let heuristic = 1.0 / distance;
141+
142+
let probability = pheromone.powf(self.pheromone_influence)
143+
* heuristic.powf(self.distance_influence);
144+
145+
(city, probability)
146+
})
147+
.collect();
148+
149+
// Roulette wheel selection
150+
let total: f64 = probabilities.iter().map(|(_, p)| p).sum();
151+
let mut rng = rand::rng();
152+
let mut random_value = rng.random::<f64>() * total;
153+
154+
for (city, prob) in probabilities {
155+
random_value -= prob;
156+
if random_value <= 0.0 {
157+
return city;
158+
}
159+
}
160+
161+
// Fallback to last city if rounding errors occur
162+
*unvisited.iter().next().unwrap()
163+
}
164+
165+
/// Calculate the total distance of a route
166+
fn calculate_route_distance(&self, route: &[usize]) -> f64 {
167+
route
168+
.windows(2)
169+
.map(|pair| self.cities[pair[0]].distance_to(&self.cities[pair[1]]))
170+
.sum()
171+
}
172+
173+
/// Update pheromone trails based on ant solutions
174+
fn update_pheromones(&mut self, routes: &[Vec<usize>]) {
175+
let n = self.cities.len();
176+
177+
// Evaporate pheromones
178+
for i in 0..n {
179+
for j in 0..n {
180+
self.pheromones[i][j] *= self.evaporation_rate;
181+
}
182+
}
183+
184+
// Deposit new pheromones
185+
for route in routes {
186+
let distance = self.calculate_route_distance(route);
187+
let deposit = self.pheromone_constant / distance;
188+
189+
for pair in route.windows(2) {
190+
let (i, j) = (pair[0], pair[1]);
191+
self.pheromones[i][j] += deposit;
192+
self.pheromones[j][i] += deposit; // Symmetric for undirected graph
193+
}
194+
}
195+
}
196+
}
197+
198+
/// Solve the Travelling Salesman Problem using Ant Colony Optimization.
199+
///
200+
/// Given a list of cities (as (x, y) coordinates), finds a near-optimal route
201+
/// that visits each city exactly once and returns to the starting city.
202+
///
203+
/// # Arguments
204+
///
205+
/// * `cities` - Vector of (x, y) coordinate tuples representing city locations
206+
/// * `num_ants` - Number of ants per iteration (default: 10)
207+
/// * `num_iterations` - Number of iterations to run (default: 20)
208+
/// * `evaporation_rate` - Pheromone evaporation rate 0.0-1.0 (default: 0.7)
209+
/// * `alpha` - Influence of pheromone on decision making (default: 1.0)
210+
/// * `beta` - Influence of distance on decision making (default: 5.0)
211+
/// * `q` - Pheromone deposit constant (default: 10.0)
212+
///
213+
/// # Returns
214+
///
215+
/// `Some((route, distance))` where route is a vector of city indices and distance
216+
/// is the total route length, or `None` if the cities list is empty.
217+
///
218+
/// # Example
219+
///
220+
/// ```
221+
/// use the_algorithms_rust::graph::ant_colony_optimization;
222+
///
223+
/// let cities = vec![
224+
/// (0.0, 0.0),
225+
/// (0.0, 5.0),
226+
/// (3.0, 8.0),
227+
/// (8.0, 10.0),
228+
/// ];
229+
///
230+
/// let result = ant_colony_optimization(cities, 10, 20, 0.7, 1.0, 5.0, 10.0);
231+
/// if let Some((route, distance)) = result {
232+
/// println!("Best route: {:?}", route);
233+
/// println!("Distance: {}", distance);
234+
/// }
235+
/// ```
236+
pub fn ant_colony_optimization(
237+
cities: Vec<(f64, f64)>,
238+
num_ants: usize,
239+
num_iterations: usize,
240+
evaporation_rate: f64,
241+
alpha: f64,
242+
beta: f64,
243+
q: f64,
244+
) -> Option<(Vec<usize>, f64)> {
245+
if cities.is_empty() {
246+
return None;
247+
}
248+
249+
let city_structs: Vec<City> = cities.into_iter().map(|(x, y)| City { x, y }).collect();
250+
251+
let mut aco = AntColonyOptimization::new(
252+
city_structs,
253+
num_ants,
254+
num_iterations,
255+
evaporation_rate,
256+
alpha,
257+
beta,
258+
q,
259+
);
260+
261+
aco.solve()
262+
}
263+
264+
#[cfg(test)]
265+
mod tests {
266+
use super::*;
267+
268+
#[test]
269+
fn test_city_distance() {
270+
let city1 = City { x: 0.0, y: 0.0 };
271+
let city2 = City { x: 3.0, y: 4.0 };
272+
assert!((city1.distance_to(&city2) - 5.0).abs() < 1e-10);
273+
}
274+
275+
#[test]
276+
fn test_city_distance_negative() {
277+
let city1 = City { x: 0.0, y: 0.0 };
278+
let city2 = City { x: -3.0, y: -4.0 };
279+
assert!((city1.distance_to(&city2) - 5.0).abs() < 1e-10);
280+
}
281+
282+
#[test]
283+
fn test_aco_simple() {
284+
let cities = vec![(0.0, 0.0), (2.0, 2.0)];
285+
286+
let result = ant_colony_optimization(cities, 5, 5, 0.7, 1.0, 5.0, 10.0);
287+
288+
assert!(result.is_some());
289+
let (route, distance) = result.unwrap();
290+
291+
// Expected route: [0, 1, 0]
292+
assert_eq!(route, vec![0, 1, 0]);
293+
294+
// Expected distance: 2 * sqrt(8) ≈ 5.656854
295+
let expected_distance = 2.0 * (8.0_f64).sqrt();
296+
assert!((distance - expected_distance).abs() < 0.001);
297+
}
298+
299+
#[test]
300+
fn test_aco_larger_problem() {
301+
let cities = vec![
302+
(0.0, 0.0),
303+
(0.0, 5.0),
304+
(3.0, 8.0),
305+
(8.0, 10.0),
306+
(12.0, 8.0),
307+
(12.0, 4.0),
308+
(8.0, 0.0),
309+
(6.0, 2.0),
310+
];
311+
312+
let result = ant_colony_optimization(cities.clone(), 10, 20, 0.7, 1.0, 5.0, 10.0);
313+
314+
assert!(result.is_some());
315+
let (route, distance) = result.unwrap();
316+
317+
// Verify the route visits all cities
318+
assert_eq!(route.len(), cities.len() + 1);
319+
assert_eq!(route.first(), Some(&0));
320+
assert_eq!(route.last(), Some(&0));
321+
322+
// Verify all cities are visited exactly once (except start/end)
323+
let mut visited = std::collections::HashSet::new();
324+
for &city in &route[0..route.len() - 1] {
325+
assert!(visited.insert(city), "City {city} visited multiple times");
326+
}
327+
assert_eq!(visited.len(), cities.len());
328+
329+
// Distance should be reasonable (not infinity)
330+
assert!(distance > 0.0);
331+
assert!(distance < f64::INFINITY);
332+
}
333+
334+
#[test]
335+
fn test_aco_empty_cities() {
336+
let cities: Vec<(f64, f64)> = Vec::new();
337+
let result = ant_colony_optimization(cities, 10, 20, 0.7, 1.0, 5.0, 10.0);
338+
assert!(result.is_none());
339+
}
340+
341+
#[test]
342+
fn test_aco_single_city() {
343+
let cities = vec![(0.0, 0.0)];
344+
let result = ant_colony_optimization(cities, 10, 20, 0.7, 1.0, 5.0, 10.0);
345+
346+
assert!(result.is_some());
347+
let (route, distance) = result.unwrap();
348+
assert_eq!(route, vec![0, 0]);
349+
assert!((distance - 0.0).abs() < 1e-10);
350+
}
351+
352+
#[test]
353+
fn test_default_parameters() {
354+
let cities = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)];
355+
let result = ant_colony_optimization(cities, 10, 20, 0.7, 1.0, 5.0, 10.0);
356+
assert!(result.is_some());
357+
}
358+
359+
#[test]
360+
fn test_zero_ants() {
361+
// Test with zero ants - should return None as no solutions are constructed
362+
let cities = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)];
363+
let result = ant_colony_optimization(cities, 0, 20, 0.7, 1.0, 5.0, 10.0);
364+
assert!(result.is_none());
365+
}
366+
367+
#[test]
368+
fn test_zero_iterations() {
369+
// Test with zero iterations - should return None as no solutions are found
370+
let cities = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)];
371+
let result = ant_colony_optimization(cities, 10, 0, 0.7, 1.0, 5.0, 10.0);
372+
assert!(result.is_none());
373+
}
374+
375+
#[test]
376+
fn test_extreme_parameters() {
377+
// Test with extreme beta value and many iterations to potentially trigger
378+
// the rounding fallback in select_next_city
379+
let cities = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)];
380+
// Very high beta makes distance dominate, low alpha reduces pheromone influence
381+
// This creates extreme probability distributions that may trigger rounding edge cases
382+
let result = ant_colony_optimization(cities, 50, 100, 0.5, 0.1, 100.0, 10.0);
383+
assert!(result.is_some());
384+
let (route, _) = result.unwrap();
385+
// Should still produce valid route
386+
assert_eq!(route.len(), 6); // 5 cities + return to start
387+
}
388+
}

0 commit comments

Comments
 (0)