Skip to content

Commit e7b5c16

Browse files
committed
refactor(day154-156): deepen pathfinding lessons with tests and study guide
1 parent 3086ec8 commit e7b5c16

4 files changed

Lines changed: 291 additions & 49 deletions

File tree

src/day154_to_day156/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Days 154-156: Pathfinding and Graph Distances (Deep Version)
2+
3+
## Learning Goals
4+
- Day 154: Dijkstra with predecessor tracking, so learners see both distance and actual route.
5+
- Day 155: BFS on grid with full shortest-path reconstruction, not just distance.
6+
- Day 156: Floyd-Warshall with path rebuilding for all-pairs reasoning.
7+
8+
## Why This Matters
9+
- Dijkstra appears in route planners, dependency optimization, and weighted workflow engines.
10+
- Grid BFS is the cleanest introduction to shortest path on unweighted graphs.
11+
- Floyd-Warshall is a great fit when node count is small but many pair queries are needed.
12+
13+
## Common Mistakes
14+
- Forgetting to ignore stale entries in Dijkstra's heap.
15+
- Returning a BFS distance without preserving parent pointers.
16+
- Updating Floyd-Warshall distances without updating the `next` matrix for path replay.
17+
18+
## Practice Exercises
19+
1. Easy: Add walls with different terrain costs and switch BFS to Dijkstra.
20+
2. Medium: Return every reachable cell and its parent from the grid search.
21+
3. Hard: Detect negative cycles in Floyd-Warshall and expose them in the API.
22+
23+
## Suggested Homework
24+
- Build a tiny maze solver that prints the recovered path.
25+
- Compare BFS vs Dijkstra on weighted and unweighted maps.
26+
- Add a CLI that loads an adjacency matrix from JSON and prints shortest routes.

src/day154_to_day156/bfs_grid.rs

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,109 @@
11
use std::collections::VecDeque;
22

33
pub fn run() {
4-
println!("\nLesson: BFS on grid");
4+
println!("\nDay 155 - BFS on grid: shortest route reconstruction");
55

66
let grid = vec![
7-
vec![0, 0, 1],
8-
vec![1, 0, 0],
9-
vec![1, 0, 0],
7+
vec![0, 0, 1, 0],
8+
vec![1, 0, 1, 0],
9+
vec![1, 0, 0, 0],
10+
vec![1, 1, 1, 0],
1011
];
11-
println!("distance={:?}", bfs((0, 0), (2, 2), &grid));
12+
13+
let route = shortest_path((0, 0), (3, 3), &grid);
14+
println!("route={route:?}");
1215
}
1316

14-
fn bfs(start: (usize, usize), target: (usize, usize), grid: &[Vec<i32>]) -> Option<i32> {
15-
let n = grid.len();
16-
let m = grid[0].len();
17-
let mut dist = vec![vec![-1; m]; n];
18-
let mut q = VecDeque::new();
17+
pub fn shortest_path(
18+
start: (usize, usize),
19+
target: (usize, usize),
20+
grid: &[Vec<i32>],
21+
) -> Option<Vec<(usize, usize)>> {
22+
if grid.is_empty() || grid[0].is_empty() {
23+
return None;
24+
}
25+
if grid[start.0][start.1] == 1 || grid[target.0][target.1] == 1 {
26+
return None;
27+
}
28+
29+
let rows = grid.len();
30+
let cols = grid[0].len();
31+
let mut queue = VecDeque::new();
32+
let mut visited = vec![vec![false; cols]; rows];
33+
let mut prev = vec![vec![None; cols]; rows];
34+
35+
visited[start.0][start.1] = true;
36+
queue.push_back(start);
37+
38+
let directions = [(1_i32, 0_i32), (-1, 0), (0, 1), (0, -1)];
1939

20-
q.push_back(start);
21-
dist[start.0][start.1] = 0;
40+
while let Some((x, y)) = queue.pop_front() {
41+
if (x, y) == target {
42+
return Some(reconstruct_path(start, target, &prev));
43+
}
2244

23-
let dirs = [(1,0),(-1,0),(0,1),(0,-1)];
24-
while let Some((x,y)) = q.pop_front() {
25-
if (x,y) == target { return Some(dist[x][y]); }
26-
for (dx,dy) in dirs {
45+
for (dx, dy) in directions {
2746
let nx = x as i32 + dx;
2847
let ny = y as i32 + dy;
29-
if nx < 0 || ny < 0 || nx >= n as i32 || ny >= m as i32 { continue; }
30-
let (nx,ny) = (nx as usize, ny as usize);
31-
if grid[nx][ny] == 1 || dist[nx][ny] != -1 { continue; }
32-
dist[nx][ny] = dist[x][y] + 1;
33-
q.push_back((nx,ny));
48+
if nx < 0 || ny < 0 || nx >= rows as i32 || ny >= cols as i32 {
49+
continue;
50+
}
51+
52+
let (nx, ny) = (nx as usize, ny as usize);
53+
if grid[nx][ny] == 1 || visited[nx][ny] {
54+
continue;
55+
}
56+
57+
visited[nx][ny] = true;
58+
prev[nx][ny] = Some((x, y));
59+
queue.push_back((nx, ny));
3460
}
3561
}
62+
3663
None
3764
}
65+
66+
fn reconstruct_path(
67+
start: (usize, usize),
68+
target: (usize, usize),
69+
prev: &[Vec<Option<(usize, usize)>>],
70+
) -> Vec<(usize, usize)> {
71+
let mut path = Vec::new();
72+
let mut current = Some(target);
73+
74+
while let Some(node) = current {
75+
path.push(node);
76+
if node == start {
77+
break;
78+
}
79+
current = prev[node.0][node.1];
80+
}
81+
82+
path.reverse();
83+
path
84+
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use super::shortest_path;
89+
90+
#[test]
91+
fn reconstructs_shortest_grid_path() {
92+
let grid = vec![
93+
vec![0, 0, 1],
94+
vec![1, 0, 0],
95+
vec![1, 0, 0],
96+
];
97+
98+
let path = shortest_path((0, 0), (2, 2), &grid).expect("path exists");
99+
assert_eq!(path.first(), Some(&(0, 0)));
100+
assert_eq!(path.last(), Some(&(2, 2)));
101+
assert_eq!(path.len(), 5);
102+
}
103+
104+
#[test]
105+
fn returns_none_for_blocked_target() {
106+
let grid = vec![vec![0, 1], vec![1, 1]];
107+
assert_eq!(shortest_path((0, 0), (1, 1), &grid), None);
108+
}
109+
}
Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,101 @@
11
use std::cmp::Reverse;
22
use std::collections::BinaryHeap;
33

4+
const INF: i64 = i64::MAX / 4;
5+
46
pub fn run() {
5-
println!("\nLesson: Dijkstra shortest path");
7+
println!("\nDay 154 - Dijkstra: shortest path + path reconstruction");
68

7-
let g = vec![
9+
let graph = vec![
810
vec![(1usize, 2i64), (2, 5)],
911
vec![(2, 1), (3, 4)],
10-
vec![(3, 1)],
12+
vec![(3, 1), (4, 7)],
13+
vec![(4, 3)],
1114
vec![],
1215
];
1316

14-
let d = dijkstra(0, &g);
15-
println!("dist={:?}", d);
17+
let result = dijkstra_with_prev(0, &graph);
18+
println!("distances={:?}", result.distances);
19+
println!("path 0 -> 4 = {:?}", result.path_to(4));
20+
}
21+
22+
#[derive(Debug, Clone, PartialEq, Eq)]
23+
pub struct DijkstraResult {
24+
pub distances: Vec<i64>,
25+
pub previous: Vec<Option<usize>>,
1626
}
1727

18-
fn dijkstra(src: usize, g: &[Vec<(usize, i64)>]) -> Vec<i64> {
19-
let n = g.len();
20-
let mut dist = vec![i64::MAX / 4; n];
21-
dist[src] = 0;
28+
impl DijkstraResult {
29+
pub fn path_to(&self, target: usize) -> Option<Vec<usize>> {
30+
if self.distances.get(target).copied()? >= INF {
31+
return None;
32+
}
33+
34+
let mut path = Vec::new();
35+
let mut current = Some(target);
36+
while let Some(node) = current {
37+
path.push(node);
38+
current = self.previous[node];
39+
}
40+
path.reverse();
41+
Some(path)
42+
}
43+
}
44+
45+
pub fn dijkstra_with_prev(src: usize, graph: &[Vec<(usize, i64)>]) -> DijkstraResult {
46+
let n = graph.len();
47+
let mut distances = vec![INF; n];
48+
let mut previous = vec![None; n];
2249
let mut pq = BinaryHeap::new();
23-
pq.push((Reverse(0i64), src));
24-
25-
while let Some((Reverse(d), u)) = pq.pop() {
26-
if d != dist[u] { continue; }
27-
for &(v, w) in &g[u] {
28-
let nd = d + w;
29-
if nd < dist[v] {
30-
dist[v] = nd;
31-
pq.push((Reverse(nd), v));
50+
51+
distances[src] = 0;
52+
pq.push((Reverse(0_i64), src));
53+
54+
while let Some((Reverse(current_dist), u)) = pq.pop() {
55+
if current_dist != distances[u] {
56+
continue;
57+
}
58+
59+
for &(v, weight) in &graph[u] {
60+
let next_dist = current_dist + weight;
61+
if next_dist < distances[v] {
62+
distances[v] = next_dist;
63+
previous[v] = Some(u);
64+
pq.push((Reverse(next_dist), v));
3265
}
3366
}
3467
}
3568

36-
dist
69+
DijkstraResult {
70+
distances,
71+
previous,
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::{dijkstra_with_prev, INF};
78+
79+
#[test]
80+
fn computes_shortest_distance_and_path() {
81+
let graph = vec![
82+
vec![(1usize, 2i64), (2, 5)],
83+
vec![(2, 1), (3, 4)],
84+
vec![(3, 1)],
85+
vec![],
86+
];
87+
88+
let result = dijkstra_with_prev(0, &graph);
89+
assert_eq!(result.distances, vec![0, 2, 3, 4]);
90+
assert_eq!(result.path_to(3), Some(vec![0, 1, 2, 3]));
91+
}
92+
93+
#[test]
94+
fn unreachable_node_has_no_path() {
95+
let graph = vec![vec![(1usize, 3i64)], vec![], vec![]];
96+
let result = dijkstra_with_prev(0, &graph);
97+
98+
assert_eq!(result.distances[2], INF);
99+
assert_eq!(result.path_to(2), None);
100+
}
37101
}
Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,101 @@
1+
const INF: i64 = 1_000_000_000;
2+
13
pub fn run() {
2-
println!("\nLesson: Floyd-Warshall all-pairs shortest path");
4+
println!("\nDay 156 - Floyd-Warshall: all-pairs shortest path");
35

4-
let inf = 1_000_000;
5-
let mut d = vec![
6-
vec![0, 3, inf],
7-
vec![2, 0, 4],
8-
vec![inf, 1, 0],
6+
let matrix = vec![
7+
vec![0, 3, INF, 7],
8+
vec![8, 0, 2, INF],
9+
vec![5, INF, 0, 1],
10+
vec![2, INF, INF, 0],
911
];
1012

11-
let n = d.len();
13+
let result = floyd_warshall(matrix);
14+
println!("dist matrix={:?}", result.distances);
15+
println!("path 0 -> 3 = {:?}", result.path(0, 3));
16+
}
17+
18+
#[derive(Debug, Clone, PartialEq, Eq)]
19+
pub struct FloydWarshallResult {
20+
pub distances: Vec<Vec<i64>>,
21+
next: Vec<Vec<Option<usize>>>,
22+
}
23+
24+
impl FloydWarshallResult {
25+
pub fn path(&self, start: usize, end: usize) -> Option<Vec<usize>> {
26+
self.next.get(start)?.get(end)?.as_ref()?;
27+
28+
let mut path = vec![start];
29+
let mut current = start;
30+
31+
while current != end {
32+
current = self.next[current][end]?;
33+
path.push(current);
34+
}
35+
36+
Some(path)
37+
}
38+
}
39+
40+
pub fn floyd_warshall(mut distances: Vec<Vec<i64>>) -> FloydWarshallResult {
41+
let n = distances.len();
42+
let mut next = vec![vec![None; n]; n];
43+
44+
for i in 0..n {
45+
for j in 0..n {
46+
if i != j && distances[i][j] < INF {
47+
next[i][j] = Some(j);
48+
}
49+
}
50+
}
51+
1252
for k in 0..n {
1353
for i in 0..n {
1454
for j in 0..n {
15-
d[i][j] = d[i][j].min(d[i][k] + d[k][j]);
55+
if distances[i][k] >= INF || distances[k][j] >= INF {
56+
continue;
57+
}
58+
59+
let through_k = distances[i][k] + distances[k][j];
60+
if through_k < distances[i][j] {
61+
distances[i][j] = through_k;
62+
next[i][j] = next[i][k];
63+
}
1664
}
1765
}
1866
}
1967

20-
println!("dist matrix={:?}", d);
68+
FloydWarshallResult { distances, next }
69+
}
70+
71+
#[cfg(test)]
72+
mod tests {
73+
use super::{floyd_warshall, INF};
74+
75+
#[test]
76+
fn computes_all_pairs_distances() {
77+
let matrix = vec![
78+
vec![0, 3, INF, 7],
79+
vec![8, 0, 2, INF],
80+
vec![5, INF, 0, 1],
81+
vec![2, INF, INF, 0],
82+
];
83+
84+
let result = floyd_warshall(matrix);
85+
assert_eq!(result.distances[0][3], 6);
86+
assert_eq!(result.distances[3][1], 5);
87+
}
88+
89+
#[test]
90+
fn reconstructs_path() {
91+
let matrix = vec![
92+
vec![0, 3, INF, 7],
93+
vec![8, 0, 2, INF],
94+
vec![5, INF, 0, 1],
95+
vec![2, INF, INF, 0],
96+
];
97+
98+
let result = floyd_warshall(matrix);
99+
assert_eq!(result.path(0, 3), Some(vec![0, 1, 2, 3]));
100+
}
21101
}

0 commit comments

Comments
 (0)