Skip to main content

vrp_gpu/
construct.rs

1//! Initial constructive heuristics (e.g. Nearest Neighbor, Clarke-Wright Savings).
2
3use crate::instance::SolomonInstance;
4use crate::solution::{Route, Solution};
5
6/// Builds an initial feasible CVRP solution using a greedy Nearest Neighbor heuristic.
7///
8/// Starting from the depot (node 0), the algorithm repeatedly selects the closest
9/// unvisited customer that fits within the current vehicle's remaining capacity.
10/// When no feasible insertion exists, the current route is closed (return to depot)
11/// and a new vehicle route is started.
12///
13/// # Panics
14///
15/// Panics on an invalid instance, an individual demand exceeding capacity, or
16/// when the greedy construction exhausts the fleet. Fleet exhaustion does not
17/// prove that the instance is infeasible: another construction may succeed.
18pub fn nearest_neighbor(instance: &SolomonInstance) -> Solution {
19    instance.validate().expect("invalid CVRP instance");
20    assert!(
21        instance
22            .demands
23            .iter()
24            .all(|&demand| demand <= instance.vehicle.capacity),
25        "customer demand exceeds vehicle capacity"
26    );
27    let n = instance.num_nodes;
28    if n <= 1 {
29        return Solution::empty();
30    }
31
32    let mut visited = vec![false; n];
33    visited[0] = true; // depot is not a customer
34
35    let mut routes = Vec::new();
36    let mut remaining_customers = n - 1;
37
38    while remaining_customers > 0 {
39        assert!(
40            routes.len() < instance.vehicle.num_vehicles,
41            "greedy construction exhausted the vehicle fleet"
42        );
43        let mut route_nodes = Vec::new();
44        let mut current_node = 0usize;
45        let mut route_demand = 0.0f32;
46
47        loop {
48            // Find the nearest unvisited customer that fits in remaining capacity
49            let mut best_node = None;
50            let mut best_dist = f32::INFINITY;
51
52            #[allow(clippy::needless_range_loop)]
53            for candidate in 1..n {
54                if visited[candidate] {
55                    continue;
56                }
57                let demand = instance.demand(candidate);
58                if route_demand + demand > instance.vehicle.capacity {
59                    continue;
60                }
61                let dist = instance.distance(current_node, candidate);
62                if dist < best_dist {
63                    best_dist = dist;
64                    best_node = Some(candidate);
65                }
66            }
67
68            match best_node {
69                Some(node) => {
70                    visited[node] = true;
71                    route_demand += instance.demand(node);
72                    route_nodes.push(node);
73                    current_node = node;
74                    remaining_customers -= 1;
75                }
76                None => break, // no feasible insertion, close route
77            }
78        }
79
80        assert!(
81            !route_nodes.is_empty(),
82            "greedy construction made no progress"
83        );
84        routes.push(Route::from_nodes(route_nodes));
85    }
86
87    Solution::new(routes)
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::instance::{VehicleConfig, compute_distance_matrix};
94
95    #[test]
96    #[should_panic(expected = "customer demand exceeds vehicle capacity")]
97    fn test_nearest_neighbor_rejects_oversized_demand() {
98        let mut instance = create_test_instance();
99        instance.demands[1] = instance.vehicle.capacity + 1.0;
100        nearest_neighbor(&instance);
101    }
102
103    #[test]
104    #[should_panic(expected = "exhausted the vehicle fleet")]
105    fn test_nearest_neighbor_rejects_fleet_exhaustion() {
106        let mut instance = create_test_instance();
107        instance.vehicle.num_vehicles = 1;
108        nearest_neighbor(&instance);
109    }
110
111    #[test]
112    #[should_panic(expected = "invalid CVRP instance")]
113    fn test_nearest_neighbor_rejects_nonfinite_demand() {
114        let mut instance = create_test_instance();
115        instance.demands[1] = f32::NAN;
116        nearest_neighbor(&instance);
117    }
118
119    #[test]
120    fn test_nearest_neighbor_capacity_uses_same_sum_as_solution() {
121        let mut instance = create_test_instance();
122        instance.vehicle.capacity = 0.6;
123        instance.demands = vec![0.0, 0.1, 0.2, 0.3, 0.0];
124        assert!(nearest_neighbor(&instance).is_feasible(&instance));
125    }
126
127    fn create_test_instance() -> SolomonInstance {
128        // Depot at (0, 0)
129        // Customer 1 at (1, 0), demand 10
130        // Customer 2 at (2, 0), demand 10
131        // Customer 3 at (3, 0), demand 10
132        // Customer 4 at (0, 5), demand 10
133        // Vehicle capacity: 25 (forces at least 2 routes)
134        let xs = vec![0.0, 1.0, 2.0, 3.0, 0.0];
135        let ys = vec![0.0, 0.0, 0.0, 0.0, 5.0];
136        let demands = vec![0.0, 10.0, 10.0, 10.0, 10.0];
137        let ready_times = vec![0.0; 5];
138        let due_times = vec![1000.0; 5];
139        let service_times = vec![0.0; 5];
140        let distance_matrix = compute_distance_matrix(&xs, &ys);
141
142        SolomonInstance {
143            name: "TestNN".into(),
144            vehicle: VehicleConfig {
145                num_vehicles: 3,
146                capacity: 25.0,
147            },
148            num_nodes: 5,
149            xs,
150            ys,
151            demands,
152            ready_times,
153            due_times,
154            service_times,
155            distance_matrix,
156        }
157    }
158
159    #[test]
160    fn test_nearest_neighbor_produces_feasible_solution() {
161        let instance = create_test_instance();
162        let solution = nearest_neighbor(&instance);
163
164        assert!(
165            solution.is_feasible(&instance),
166            "Nearest neighbor must produce a feasible solution"
167        );
168    }
169
170    #[test]
171    fn test_nearest_neighbor_visits_all_customers() {
172        let instance = create_test_instance();
173        let solution = nearest_neighbor(&instance);
174
175        assert_eq!(
176            solution.total_customers_visited(),
177            instance.num_nodes - 1,
178            "All customers must be visited"
179        );
180    }
181
182    #[test]
183    fn test_nearest_neighbor_respects_capacity() {
184        let instance = create_test_instance();
185        let solution = nearest_neighbor(&instance);
186
187        for (i, route) in solution.routes.iter().enumerate() {
188            assert!(
189                route.is_capacity_feasible(&instance),
190                "Route {i} exceeds vehicle capacity: demand {} > capacity {}",
191                route.total_demand(&instance),
192                instance.vehicle.capacity,
193            );
194        }
195    }
196
197    #[test]
198    fn test_nearest_neighbor_splits_routes_on_capacity() {
199        let instance = create_test_instance();
200        let solution = nearest_neighbor(&instance);
201
202        // 4 customers x 10 demand each = 40 total, capacity 25 per vehicle
203        // Must use at least 2 routes
204        assert!(
205            solution.routes.len() >= 2,
206            "Expected at least 2 routes due to capacity constraint, got {}",
207            solution.routes.len(),
208        );
209    }
210
211    #[test]
212    fn test_nearest_neighbor_single_customer() {
213        let xs = vec![0.0, 5.0];
214        let ys = vec![0.0, 0.0];
215        let demands = vec![0.0, 10.0];
216        let distance_matrix = compute_distance_matrix(&xs, &ys);
217
218        let instance = SolomonInstance {
219            name: "Single".into(),
220            vehicle: VehicleConfig {
221                num_vehicles: 1,
222                capacity: 100.0,
223            },
224            num_nodes: 2,
225            xs,
226            ys,
227            demands,
228            ready_times: vec![0.0; 2],
229            due_times: vec![1000.0; 2],
230            service_times: vec![0.0; 2],
231            distance_matrix,
232        };
233
234        let solution = nearest_neighbor(&instance);
235        assert!(solution.is_feasible(&instance));
236        assert_eq!(solution.routes.len(), 1);
237        assert_eq!(solution.routes[0].nodes, vec![1]);
238    }
239
240    #[test]
241    fn test_nearest_neighbor_empty_instance() {
242        let xs = vec![0.0];
243        let ys = vec![0.0];
244        let demands = vec![0.0];
245        let distance_matrix = compute_distance_matrix(&xs, &ys);
246
247        let instance = SolomonInstance {
248            name: "DepotOnly".into(),
249            vehicle: VehicleConfig {
250                num_vehicles: 1,
251                capacity: 100.0,
252            },
253            num_nodes: 1,
254            xs,
255            ys,
256            demands,
257            ready_times: vec![0.0],
258            due_times: vec![1000.0],
259            service_times: vec![0.0],
260            distance_matrix,
261        };
262
263        let solution = nearest_neighbor(&instance);
264        assert!(solution.routes.is_empty());
265    }
266}