Skip to content

Commit 405ccd1

Browse files
committed
Merge branch 'revert/recreate-complexity'
2 parents 67f2cb4 + c044809 commit 405ccd1

File tree

5 files changed

+165
-104
lines changed

5 files changed

+165
-104
lines changed

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
- Refactor heuristics to reduce code duplication (#1181)
1919
- Refactor `Matrix` template class (#1089)
2020
- Refactor to use `std::format` whenever possible (#1081)
21-
- Reduce complexity for recreation process (#1155)
2221
- Refactor `SolutionIndicators` (#1169)
2322
- Remove amount consistency checks in `parse` in favor of upstream checks in `Input` (#1086)
2423
- Reduce code duplication in routing wrappers (#1184)

src/algorithms/local_search/local_search.cpp

Lines changed: 161 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -161,98 +161,180 @@ void LocalSearch<Route,
161161
SwapStar,
162162
RouteSplit,
163163
PriorityReplace,
164-
TSPFix>::recreate(const std::vector<Index>& routes
164+
TSPFix>::try_job_additions(const std::vector<Index>& routes,
165+
double regret_coeff
165166
#ifdef LOG_LS
166-
,
167-
bool log_addition_step
167+
,
168+
bool log_addition_step
168169
#endif
169170
) {
170-
std::vector<Index> ordered_jobs(_sol_state.unassigned.begin(),
171-
_sol_state.unassigned.end());
172-
std::ranges::sort(ordered_jobs, [this](const Index lhs, const Index rhs) {
173-
return _input.jobs[lhs] < _input.jobs[rhs];
174-
});
171+
bool job_added;
175172

176-
std::unordered_set<Index> modified_vehicles;
173+
std::vector<std::vector<RouteInsertion>> route_job_insertions;
174+
route_job_insertions.reserve(routes.size());
177175

178-
for (const auto j : ordered_jobs) {
179-
const auto& current_job = _input.jobs[j];
180-
if (current_job.type == JOB_TYPE::DELIVERY) {
181-
continue;
176+
for (std::size_t i = 0; i < routes.size(); ++i) {
177+
route_job_insertions.emplace_back(_input.jobs.size(),
178+
RouteInsertion(_input.get_amount_size()));
179+
180+
const auto v = routes[i];
181+
const auto fixed_cost =
182+
_sol[v].empty() ? _input.vehicles[v].fixed_cost() : 0;
183+
184+
for (const auto j : _sol_state.unassigned) {
185+
if (const auto& current_job = _input.jobs[j];
186+
current_job.type == JOB_TYPE::DELIVERY) {
187+
continue;
188+
}
189+
route_job_insertions[i][j] =
190+
compute_best_insertion(_input, _sol_state, j, v, _sol[v]);
191+
192+
if (route_job_insertions[i][j].eval != NO_EVAL) {
193+
route_job_insertions[i][j].eval.cost += fixed_cost;
194+
}
182195
}
196+
}
197+
198+
std::unordered_set<Index> modified_vehicles;
183199

200+
do {
201+
Priority best_priority = 0;
184202
RouteInsertion best_insertion(_input.get_amount_size());
185-
Index best_v = 0;
203+
double best_cost = std::numeric_limits<double>::max();
204+
Index best_job_rank = 0;
205+
Index best_route = 0;
206+
std::size_t best_route_idx = 0;
186207

187-
for (const auto v : routes) {
188-
if (const unsigned added_tasks =
189-
(current_job.type == JOB_TYPE::PICKUP) ? 2 : 1;
190-
_sol[v].size() + added_tasks > _input.vehicles[v].max_tasks) {
208+
for (const auto j : _sol_state.unassigned) {
209+
const auto& current_job = _input.jobs[j];
210+
if (current_job.type == JOB_TYPE::DELIVERY) {
191211
continue;
192212
}
193213

194-
auto current_best_insertion =
195-
compute_best_insertion(_input, _sol_state, j, v, _sol[v]);
214+
const auto job_priority = current_job.priority;
196215

197-
if (current_best_insertion.eval != NO_EVAL && _sol[v].empty()) {
198-
current_best_insertion.eval.cost += _input.vehicles[v].fixed_cost();
216+
if (job_priority < best_priority) {
217+
// Insert higher priority jobs first.
218+
continue;
199219
}
200220

201-
if (current_best_insertion.eval < best_insertion.eval) {
202-
best_insertion = current_best_insertion;
203-
best_v = v;
221+
auto smallest = _input.get_cost_upper_bound();
222+
auto second_smallest = _input.get_cost_upper_bound();
223+
std::size_t smallest_idx = std::numeric_limits<std::size_t>::max();
224+
225+
for (std::size_t i = 0; i < routes.size(); ++i) {
226+
if (route_job_insertions[i][j].eval.cost < smallest) {
227+
smallest_idx = i;
228+
second_smallest = smallest;
229+
smallest = route_job_insertions[i][j].eval.cost;
230+
} else if (route_job_insertions[i][j].eval.cost < second_smallest) {
231+
second_smallest = route_job_insertions[i][j].eval.cost;
232+
}
204233
}
205-
}
206234

207-
if (best_insertion.eval == NO_EVAL) {
208-
// Current job cannot be inserted.
209-
continue;
210-
}
235+
// Find best route for current job based on cost of addition and
236+
// regret cost of not adding.
237+
for (std::size_t i = 0; i < routes.size(); ++i) {
238+
if (route_job_insertions[i][j].eval == NO_EVAL) {
239+
continue;
240+
}
211241

212-
// Found a valid insertion spot for current job.
213-
_sol_state.unassigned.erase(j);
242+
const auto& current_r = _sol[routes[i]];
243+
const auto& vehicle = _input.vehicles[routes[i]];
214244

215-
if (current_job.type == JOB_TYPE::SINGLE) {
216-
_sol[best_v].add(_input, j, best_insertion.single_rank);
217-
} else {
218-
assert(current_job.type == JOB_TYPE::PICKUP);
245+
if (const bool is_pickup = (_input.jobs[j].type == JOB_TYPE::PICKUP);
246+
current_r.size() + (is_pickup ? 2 : 1) > vehicle.max_tasks) {
247+
continue;
248+
}
219249

220-
std::vector<Index> modified_with_pd;
221-
modified_with_pd.reserve(best_insertion.delivery_rank -
222-
best_insertion.pickup_rank + 2);
223-
modified_with_pd.push_back(j);
224-
225-
std::copy(_sol[best_v].route.begin() + best_insertion.pickup_rank,
226-
_sol[best_v].route.begin() + best_insertion.delivery_rank,
227-
std::back_inserter(modified_with_pd));
228-
modified_with_pd.push_back(j + 1);
229-
230-
_sol[best_v].replace(_input,
231-
best_insertion.delivery,
232-
modified_with_pd.begin(),
233-
modified_with_pd.end(),
234-
best_insertion.pickup_rank,
235-
best_insertion.delivery_rank);
236-
237-
assert(_sol_state.unassigned.find(j + 1) != _sol_state.unassigned.end());
238-
_sol_state.unassigned.erase(j + 1);
250+
const auto regret_cost =
251+
(i == smallest_idx) ? second_smallest : smallest;
252+
253+
const double current_cost =
254+
static_cast<double>(route_job_insertions[i][j].eval.cost) -
255+
regret_coeff * static_cast<double>(regret_cost);
256+
257+
if ((job_priority > best_priority) ||
258+
(job_priority == best_priority && current_cost < best_cost)) {
259+
best_priority = job_priority;
260+
best_job_rank = j;
261+
best_route = routes[i];
262+
best_insertion = route_job_insertions[i][j];
263+
best_cost = current_cost;
264+
best_route_idx = i;
265+
}
266+
}
239267
}
240268

241-
// Update best route data, required for consistency.
242-
modified_vehicles.insert(best_v);
243-
_sol_state.update_route_eval(_sol[best_v].route, best_v);
244-
_sol_state.set_insertion_ranks(_sol[best_v], best_v);
269+
job_added = (best_cost < std::numeric_limits<double>::max());
270+
271+
if (job_added) {
272+
_sol_state.unassigned.erase(best_job_rank);
273+
274+
if (const auto& best_job = _input.jobs[best_job_rank];
275+
best_job.type == JOB_TYPE::SINGLE) {
276+
_sol[best_route].add(_input, best_job_rank, best_insertion.single_rank);
277+
} else {
278+
assert(best_job.type == JOB_TYPE::PICKUP);
279+
280+
std::vector<Index> modified_with_pd;
281+
modified_with_pd.reserve(best_insertion.delivery_rank -
282+
best_insertion.pickup_rank + 2);
283+
modified_with_pd.push_back(best_job_rank);
284+
285+
std::copy(_sol[best_route].route.begin() + best_insertion.pickup_rank,
286+
_sol[best_route].route.begin() + best_insertion.delivery_rank,
287+
std::back_inserter(modified_with_pd));
288+
modified_with_pd.push_back(best_job_rank + 1);
289+
290+
_sol[best_route].replace(_input,
291+
best_insertion.delivery,
292+
modified_with_pd.begin(),
293+
modified_with_pd.end(),
294+
best_insertion.pickup_rank,
295+
best_insertion.delivery_rank);
296+
297+
assert(_sol_state.unassigned.find(best_job_rank + 1) !=
298+
_sol_state.unassigned.end());
299+
_sol_state.unassigned.erase(best_job_rank + 1);
300+
}
301+
302+
// Update best_route data required for consistency.
303+
modified_vehicles.insert(best_route);
304+
_sol_state.update_route_eval(_sol[best_route].route, best_route);
305+
_sol_state.set_insertion_ranks(_sol[best_route], best_route);
306+
307+
const auto fixed_cost =
308+
_sol[best_route].empty() ? _input.vehicles[best_route].fixed_cost() : 0;
309+
310+
for (const auto j : _sol_state.unassigned) {
311+
if (const auto& current_job = _input.jobs[j];
312+
current_job.type == JOB_TYPE::DELIVERY) {
313+
continue;
314+
}
315+
route_job_insertions[best_route_idx][j] =
316+
compute_best_insertion(_input,
317+
_sol_state,
318+
j,
319+
best_route,
320+
_sol[best_route]);
321+
322+
if (route_job_insertions[best_route_idx][j].eval != NO_EVAL) {
323+
route_job_insertions[best_route_idx][j].eval.cost += fixed_cost;
324+
}
325+
}
245326

246327
#ifdef LOG_LS
247-
if (log_addition_step) {
248-
steps.emplace_back(utils::now(),
249-
log::EVENT::JOB_ADDITION,
250-
OperatorName::MAX,
251-
utils::SolutionIndicators(_input, _sol),
252-
std::nullopt);
253-
}
328+
if (log_addition_step) {
329+
steps.emplace_back(utils::now(),
330+
log::EVENT::JOB_ADDITION,
331+
OperatorName::MAX,
332+
utils::SolutionIndicators(_input, _sol),
333+
std::nullopt);
334+
}
254335
#endif
255-
}
336+
}
337+
} while (job_added);
256338

257339
for (const auto v : modified_vehicles) {
258340
_sol_state.update_route_bbox(_sol[v].route, v);
@@ -1805,14 +1887,16 @@ void LocalSearch<Route,
18051887

18061888
for (auto v_rank : update_candidates) {
18071889
// Only this update (and update_route_eval done above) are
1808-
// actually required for consistency inside recreate step.
1890+
// actually required for consistency inside try_job_additions.
18091891
_sol_state.set_insertion_ranks(_sol[v_rank], v_rank);
18101892
}
18111893

1812-
recreate(best_ops[best_source][best_target]->addition_candidates()
1894+
try_job_additions(best_ops[best_source][best_target]
1895+
->addition_candidates(),
1896+
0
18131897
#ifdef LOG_LS
1814-
,
1815-
true
1898+
,
1899+
true
18161900
#endif
18171901
);
18181902

@@ -1859,8 +1943,8 @@ void LocalSearch<Route,
18591943
for (auto req_u : best_ops[v][v]->required_unassigned()) {
18601944
if (!_sol_state.unassigned.contains(req_u)) {
18611945
// This move should be invalidated because a required
1862-
// unassigned job has been added by recreate step in the
1863-
// meantime.
1946+
// unassigned job has been added by try_job_additions in
1947+
// the meantime.
18641948
invalidate_move = true;
18651949
break;
18661950
}
@@ -2003,11 +2087,12 @@ void LocalSearch<Route,
20032087
_sol_state.set_insertion_ranks(_sol[v], v);
20042088
}
20052089

2006-
// Recreate solution
2007-
recreate(_all_routes);
2090+
// Refill jobs.
2091+
constexpr double refill_regret = 1.5;
2092+
try_job_additions(_all_routes, refill_regret);
20082093

20092094
// Update everything except what has already been updated in
2010-
// recreate step.
2095+
// try_job_additions.
20112096
for (std::size_t v = 0; v < _sol.size(); ++v) {
20122097
_sol_state.update_costs(_sol[v].route, v);
20132098
_sol_state.update_skills(_sol[v].route, v);

src/algorithms/local_search/local_search.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ class LocalSearch {
6767
std::vector<log::Step> steps;
6868
#endif
6969

70-
void recreate(const std::vector<Index>& routes
70+
void try_job_additions(const std::vector<Index>& routes,
71+
double regret_coeff
7172
#ifdef LOG_LS
72-
,
73-
bool log_addition_step = false
73+
,
74+
bool log_addition_step = false
7475
#endif
7576
);
7677

src/structures/vroom/job.cpp

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,8 @@ All rights reserved (see LICENSE).
1010
#include "structures/vroom/job.h"
1111
#include "utils/helpers.h"
1212

13-
#include <numeric>
14-
1513
namespace vroom {
1614

17-
inline Duration get_tw_length(const std::vector<TimeWindow>& tws) {
18-
return std::accumulate(tws.begin(),
19-
tws.end(),
20-
0,
21-
[](auto sum, const auto& tw) {
22-
return sum + tw.length;
23-
});
24-
};
25-
2615
Job::Job(Id id,
2716
const Location& location,
2817
UserDuration setup,
@@ -43,7 +32,6 @@ Job::Job(Id id,
4332
skills(std::move(skills)),
4433
priority(priority),
4534
tws(tws),
46-
tw_length(get_tw_length(tws)),
4735
description(std::move(description)) {
4836
utils::check_tws(tws, id, "job");
4937
utils::check_priority(priority, id, "job");
@@ -69,7 +57,6 @@ Job::Job(Id id,
6957
skills(std::move(skills)),
7058
priority(priority),
7159
tws(tws),
72-
tw_length(get_tw_length(tws)),
7360
description(std::move(description)) {
7461
assert(type == JOB_TYPE::PICKUP || type == JOB_TYPE::DELIVERY);
7562
utils::check_tws(tws, id, "job");

src/structures/vroom/job.h

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ struct Job {
3030
const Skills skills;
3131
const Priority priority;
3232
const std::vector<TimeWindow> tws;
33-
const Duration tw_length;
3433
const std::string description;
3534

3635
// Constructor for regular one-stop job (JOB_TYPE::SINGLE).
@@ -65,16 +64,6 @@ struct Job {
6564
}
6665

6766
bool is_valid_start(Duration time) const;
68-
69-
friend bool operator<(const Job& lhs, const Job& rhs) {
70-
// Sort by:
71-
// - decreasing priority
72-
// - increasing TW length
73-
// - decreasing delivery amount
74-
// - decreasing pickup amount
75-
return std::tie(rhs.priority, lhs.tw_length, rhs.delivery, rhs.pickup) <
76-
std::tie(lhs.priority, rhs.tw_length, lhs.delivery, lhs.pickup);
77-
}
7867
};
7968

8069
} // namespace vroom

0 commit comments

Comments
 (0)