1use crate::instance::SolomonInstance;
4use crate::solution::{Route, Solution};
5
6pub 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; 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 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, }
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 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 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}