diff --git a/CHANGELOG.md b/CHANGELOG.md index 16481e666..00bd15719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Internals +- Apply heuristics to partial solutions provided in input (#977) - LOG_LS flag to generate debug info on the internal solving process (#1124) ### Changed diff --git a/src/algorithms/heuristics/heuristics.cpp b/src/algorithms/heuristics/heuristics.cpp index 5f603660c..03433c898 100644 --- a/src/algorithms/heuristics/heuristics.cpp +++ b/src/algorithms/heuristics/heuristics.cpp @@ -9,12 +9,30 @@ All rights reserved (see LICENSE). #include #include +#include #include "algorithms/heuristics/heuristics.h" #include "utils/helpers.h" namespace vroom::heuristics { +template +std::set get_unassigned(const std::vector& routes, + const Iter jobs_begin, + const Iter jobs_end) { + std::unordered_set assigned; + std::ranges::for_each(routes, [&](const auto& r) { + std::ranges::copy(r.route, std::inserter(assigned, assigned.end())); + }); + std::set unassigned; + std::copy_if(jobs_begin, + jobs_end, + std::inserter(unassigned, unassigned.begin()), + [&](const auto j) { return !assigned.contains(j); }); + + return unassigned; +} + template Eval basic(const Input& input, std::vector& routes, @@ -25,19 +43,11 @@ Eval basic(const Input& input, INIT init, double lambda, SORT sort) { - assert(std::all_of(routes.cbegin(), routes.cend(), [](const auto& r) { - return r.empty(); - })); - - Eval sol_eval; - - // Consider all jobs as unassigned at first. - std::set unassigned; - std::copy(jobs_begin, - jobs_end, - std::inserter(unassigned, unassigned.begin())); + auto unassigned = get_unassigned(routes, jobs_begin, jobs_end); - // Perform heuristic ordering of the vehicles on a copy. + // Perform heuristic ordering of the vehicles on a copy. Ordering is + // based on vehicles description only so do not account for initial + // routes if any. const auto nb_vehicles = std::distance(vehicles_begin, vehicles_end); std::vector vehicles_ranks; vehicles_ranks.reserve(nb_vehicles); @@ -70,14 +80,15 @@ Eval basic(const Input& input, // regrets[v][j] holds the min cost for reaching job j in an empty // route across all remaining vehicles **after** vehicle at rank v - // in vehicles_ranks. + // in vehicles_ranks. Regrets are only computed for available + // vehicles and unassigned jobs, but are based on empty routes + // evaluations so do not account for initial routes if any. std::vector> regrets(nb_vehicles, std::vector(input.jobs.size())); // Use own cost for last vehicle regret values. - auto& last_regrets = regrets.back(); for (const auto j : unassigned) { - last_regrets[j] = evals[j][vehicles_ranks.back()].cost; + regrets.back()[j] = evals[j][vehicles_ranks.back()].cost; } for (Index rev_v = 0; rev_v < nb_vehicles - 1; ++rev_v) { @@ -89,15 +100,24 @@ Eval basic(const Input& input, } } + Eval sol_eval; + for (Index v = 0; v < nb_vehicles; ++v) { auto v_rank = vehicles_ranks[v]; auto& current_r = routes[v_rank]; const auto& vehicle = input.vehicles[v_rank]; + // Route eval without fixed cost. Eval current_route_eval; + if (!current_r.empty()) { + current_route_eval = + utils::route_eval_for_vehicle(input, v_rank, current_r.route); + assert(vehicle.fixed_cost() <= current_route_eval.cost); + current_route_eval.cost -= vehicle.fixed_cost(); + } - if (init != INIT::NONE) { + if (current_r.empty() && init != INIT::NONE) { // Initialize current route with the "best" valid job. bool init_ok = false; @@ -123,20 +143,20 @@ Eval basic(const Input& input, bool try_validity = false; if (init == INIT::HIGHER_AMOUNT) { - try_validity |= (higher_amount < current_job.pickup || - higher_amount < current_job.delivery); + try_validity = (higher_amount < current_job.pickup || + higher_amount < current_job.delivery); } if (init == INIT::EARLIEST_DEADLINE) { Duration current_deadline = is_pickup ? input.jobs[job_rank + 1].tws.back().end : current_job.tws.back().end; - try_validity |= (current_deadline < earliest_deadline); + try_validity = (current_deadline < earliest_deadline); } if (init == INIT::FURTHEST) { - try_validity |= (furthest_cost < evals[job_rank][v_rank].cost); + try_validity = (furthest_cost < evals[job_rank][v_rank].cost); } if (init == INIT::NEAREST) { - try_validity |= (evals[job_rank][v_rank].cost < nearest_cost); + try_validity = (evals[job_rank][v_rank].cost < nearest_cost); } if (!try_validity) { @@ -432,17 +452,7 @@ Eval dynamic_vehicle_choice(const Input& input, INIT init, double lambda, SORT sort) { - assert(std::all_of(routes.cbegin(), routes.cend(), [](const auto& r) { - return r.empty(); - })); - - Eval sol_eval; - - // Consider all jobs as unassigned at first. - std::set unassigned; - std::copy(jobs_begin, - jobs_end, - std::inserter(unassigned, unassigned.begin())); + auto unassigned = get_unassigned(routes, jobs_begin, jobs_end); // Work on a copy of the vehicles ranks from which we erase values // each time a route is completed. @@ -453,11 +463,14 @@ Eval dynamic_vehicle_choice(const Input& input, const auto& evals = input.jobs_vehicles_evals(); + Eval sol_eval; + while (!vehicles_ranks.empty() && !unassigned.empty()) { // For any unassigned job at j, jobs_min_costs[j] // (resp. jobs_second_min_costs[j]) holds the min cost // (resp. second min cost) of picking the job in an empty route - // for any remaining vehicle. + // for any remaining vehicle. Evaluation are based on empty routes + // so do not account for initial routes if any. std::vector jobs_min_costs(input.jobs.size(), input.get_cost_upper_bound()); std::vector jobs_second_min_costs(input.jobs.size(), @@ -475,8 +488,9 @@ Eval dynamic_vehicle_choice(const Input& input, } } - // Pick vehicle that has the biggest number of compatible jobs - // closest to him than to any other different vehicle. + // Pick vehicle that has the biggest number of compatible + // unassigned jobs closest to him than to any other different + // vehicle still available. std::vector closest_jobs_count(input.vehicles.size(), 0); for (const auto job_rank : unassigned) { for (const auto v_rank : vehicles_ranks) { @@ -521,9 +535,12 @@ Eval dynamic_vehicle_choice(const Input& input, vehicles_ranks.erase(chosen_vehicle); } - // Once current vehicle is decided, regrets[j] holds the min cost - // of picking the job in an empty route for other remaining - // vehicles. + // Once current vehicle is decided, then for any unassigned job at + // j, regrets[j] holds the min cost of picking the job in an empty + // route for other remaining vehicles. Regrets are only computed + // for available vehicles and unassigned jobs, but are based on + // empty routes evaluations so do not account for initial routes + // if any. std::vector regrets(input.jobs.size(), input.get_cost_upper_bound()); for (const auto job_rank : unassigned) { if (jobs_min_costs[job_rank] < evals[job_rank][v_rank].cost) { @@ -536,9 +553,16 @@ Eval dynamic_vehicle_choice(const Input& input, const auto& vehicle = input.vehicles[v_rank]; auto& current_r = routes[v_rank]; + // Route eval without fixed cost. Eval current_route_eval; + if (!current_r.empty()) { + current_route_eval = + utils::route_eval_for_vehicle(input, v_rank, current_r.route); + assert(vehicle.fixed_cost() <= current_route_eval.cost); + current_route_eval.cost -= vehicle.fixed_cost(); + } - if (init != INIT::NONE) { + if (current_r.empty() && init != INIT::NONE) { // Initialize current route with the "best" valid job that is // closest for current vehicle than to any other remaining // vehicle. @@ -568,20 +592,20 @@ Eval dynamic_vehicle_choice(const Input& input, bool try_validity = false; if (init == INIT::HIGHER_AMOUNT) { - try_validity |= (higher_amount < current_job.pickup || - higher_amount < current_job.delivery); + try_validity = (higher_amount < current_job.pickup || + higher_amount < current_job.delivery); } if (init == INIT::EARLIEST_DEADLINE) { Duration current_deadline = is_pickup ? input.jobs[job_rank + 1].tws.back().end : current_job.tws.back().end; - try_validity |= (current_deadline < earliest_deadline); + try_validity = (current_deadline < earliest_deadline); } if (init == INIT::FURTHEST) { - try_validity |= (furthest_cost < evals[job_rank][v_rank].cost); + try_validity = (furthest_cost < evals[job_rank][v_rank].cost); } if (init == INIT::NEAREST) { - try_validity |= (evals[job_rank][v_rank].cost < nearest_cost); + try_validity = (evals[job_rank][v_rank].cost < nearest_cost); } if (!try_validity) { @@ -865,149 +889,148 @@ Eval dynamic_vehicle_choice(const Input& input, return sol_eval; } -template -void initial_routes(const Input& input, std::vector& routes) { - assert(std::all_of(routes.cbegin(), routes.cend(), [](const auto& r) { - return r.empty(); - })); - - for (Index v = 0; v < input.vehicles.size(); ++v) { - const auto& vehicle = input.vehicles[v]; - auto& current_r = routes[v]; - - // Startup load is the sum of deliveries for (single) jobs. - Amount single_jobs_deliveries(input.zero_amount()); - for (const auto& step : vehicle.steps) { - if (step.type == STEP_TYPE::JOB) { - assert(step.job_type.has_value()); - - if (step.job_type.value() == JOB_TYPE::SINGLE) { - single_jobs_deliveries += input.jobs[step.rank].delivery; - } - } - } - if (!(single_jobs_deliveries <= vehicle.capacity)) { - throw InputException( - std::format("Route over capacity for vehicle {}.", vehicle.id)); - } +template void set_route(const Input& input, Route& route) { + assert(route.empty()); + const auto& vehicle = input.vehicles[route.vehicle_rank]; - // Track load and travel time during the route for validity. - Amount current_load = single_jobs_deliveries; - Eval eval_sum; - std::optional previous_index; - if (vehicle.has_start()) { - previous_index = vehicle.start.value().index(); - } + // Startup load is the sum of deliveries for (single) jobs. + Amount single_jobs_deliveries(input.zero_amount()); + for (const auto& step : vehicle.steps) { + if (step.type == STEP_TYPE::JOB) { + assert(step.job_type.has_value()); - std::vector job_ranks; - job_ranks.reserve(vehicle.steps.size()); - std::unordered_set expected_delivery_ranks; - for (const auto& step : vehicle.steps) { - if (step.type != STEP_TYPE::JOB) { - continue; + if (step.job_type.value() == JOB_TYPE::SINGLE) { + single_jobs_deliveries += input.jobs[step.rank].delivery; } + } + } + if (!(single_jobs_deliveries <= vehicle.capacity)) { + throw InputException( + std::format("Route over capacity for vehicle {}.", vehicle.id)); + } - const auto job_rank = step.rank; - const auto& job = input.jobs[job_rank]; - job_ranks.push_back(job_rank); - - if (!input.vehicle_ok_with_job(v, job_rank)) { - throw InputException( - std::format("Missing skill or step out of reach for vehicle {} and " - "job {}.", - vehicle.id, - job.id)); - } + // Track load and travel time during the route for validity. + Amount current_load = single_jobs_deliveries; + Eval eval_sum; + std::optional previous_index; + if (vehicle.has_start()) { + previous_index = vehicle.start.value().index(); + } - // Update current travel time. - if (previous_index.has_value()) { - eval_sum += vehicle.eval(previous_index.value(), job.index()); - } - previous_index = job.index(); + std::vector job_ranks; + job_ranks.reserve(vehicle.steps.size()); + std::unordered_set expected_delivery_ranks; + for (const auto& step : vehicle.steps) { + if (step.type != STEP_TYPE::JOB) { + continue; + } - // Handle load. - assert(step.job_type.has_value()); - switch (step.job_type.value()) { - case JOB_TYPE::SINGLE: { - current_load += job.pickup; - current_load -= job.delivery; - break; - } - case JOB_TYPE::PICKUP: { - expected_delivery_ranks.insert(job_rank + 1); + const auto job_rank = step.rank; + const auto& job = input.jobs[job_rank]; + job_ranks.push_back(job_rank); - current_load += job.pickup; - break; - } - case JOB_TYPE::DELIVERY: { - auto search = expected_delivery_ranks.find(job_rank); - if (search == expected_delivery_ranks.end()) { - throw InputException( - std::format("Invalid shipment in route for vehicle {}.", - vehicle.id)); - } - expected_delivery_ranks.erase(search); + if (!input.vehicle_ok_with_job(route.vehicle_rank, job_rank)) { + throw InputException( + std::format("Missing skill or step out of reach for vehicle {} and " + "job {}.", + vehicle.id, + job.id)); + } - current_load -= job.delivery; - break; - } - } + // Update current travel time. + if (previous_index.has_value()) { + eval_sum += vehicle.eval(previous_index.value(), job.index()); + } + previous_index = job.index(); + + // Handle load. + assert(step.job_type.has_value()); + switch (step.job_type.value()) { + case JOB_TYPE::SINGLE: { + current_load += job.pickup; + current_load -= job.delivery; + break; + } + case JOB_TYPE::PICKUP: { + expected_delivery_ranks.insert(job_rank + 1); - // Check validity after this step wrt capacity. - if (!(current_load <= vehicle.capacity)) { + current_load += job.pickup; + break; + } + case JOB_TYPE::DELIVERY: { + auto search = expected_delivery_ranks.find(job_rank); + if (search == expected_delivery_ranks.end()) { throw InputException( - std::format("Route over capacity for vehicle {}.", vehicle.id)); + std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); } - } + expected_delivery_ranks.erase(search); - if (vehicle.has_end() && !job_ranks.empty()) { - // Update with last route leg. - assert(previous_index.has_value()); - eval_sum += - vehicle.eval(previous_index.value(), vehicle.end.value().index()); - } - if (!vehicle.ok_for_travel_time(eval_sum.duration)) { - throw InputException( - std::format("Route over max_travel_time for vehicle {}.", vehicle.id)); + current_load -= job.delivery; + break; } - if (!vehicle.ok_for_distance(eval_sum.distance)) { - throw InputException( - std::format("Route over max_distance for vehicle {}.", vehicle.id)); + default: + assert(false); } - if (vehicle.max_tasks < job_ranks.size()) { + // Check validity after this step wrt capacity. + if (!(current_load <= vehicle.capacity)) { throw InputException( - std::format("Too many tasks for vehicle {}.", vehicle.id)); + std::format("Route over capacity for vehicle {}.", vehicle.id)); } + } - if (!expected_delivery_ranks.empty()) { - throw InputException( - std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); - } + if (vehicle.has_end() && !job_ranks.empty()) { + // Update with last route leg. + assert(previous_index.has_value()); + eval_sum += + vehicle.eval(previous_index.value(), vehicle.end.value().index()); + } + if (!vehicle.ok_for_travel_time(eval_sum.duration)) { + throw InputException( + std::format("Route over max_travel_time for vehicle {}.", vehicle.id)); + } + if (!vehicle.ok_for_distance(eval_sum.distance)) { + throw InputException( + std::format("Route over max_distance for vehicle {}.", vehicle.id)); + } - // Now route is OK with regard to capacity, max_travel_time, - // max_tasks, precedence and skills constraints. - if (!job_ranks.empty()) { - if (!current_r.is_valid_addition_for_tw(input, - single_jobs_deliveries, - job_ranks.begin(), - job_ranks.end(), - 0, - 0)) { - throw InputException( - std::format("Infeasible route for vehicle {}.", vehicle.id)); - } + if (vehicle.max_tasks < job_ranks.size()) { + throw InputException( + std::format("Too many tasks for vehicle {}.", vehicle.id)); + } + + if (!expected_delivery_ranks.empty()) { + throw InputException( + std::format("Invalid shipment in route for vehicle {}.", vehicle.id)); + } - current_r.replace(input, - single_jobs_deliveries, - job_ranks.begin(), - job_ranks.end(), - 0, - 0); + // Now route is OK with regard to capacity, max_travel_time, + // max_tasks, precedence and skills constraints. + if (!job_ranks.empty()) { + if (!route.is_valid_addition_for_tw(input, + single_jobs_deliveries, + job_ranks.begin(), + job_ranks.end(), + 0, + 0)) { + throw InputException( + std::format("Infeasible route for vehicle {}.", vehicle.id)); } + + route.replace(input, + single_jobs_deliveries, + job_ranks.begin(), + job_ranks.end(), + 0, + 0); } } +template +void set_initial_routes(const Input& input, std::vector& routes) { + std::ranges::for_each(routes, [&](auto& r) { set_route(input, r); }); +} + using RawSolution = std::vector; using TWSolution = std::vector; @@ -1032,7 +1055,7 @@ dynamic_vehicle_choice(const Input& input, double lambda, SORT sort); -template void initial_routes(const Input& input, RawSolution& routes); +template void set_initial_routes(const Input& input, RawSolution& routes); template Eval basic(const Input& input, TWSolution& routes, @@ -1055,6 +1078,6 @@ dynamic_vehicle_choice(const Input& input, double lambda, SORT sort); -template void initial_routes(const Input& input, TWSolution& routes); +template void set_initial_routes(const Input& input, TWSolution& routes); } // namespace vroom::heuristics diff --git a/src/algorithms/heuristics/heuristics.h b/src/algorithms/heuristics/heuristics.h index f234fa693..68e94a5f1 100644 --- a/src/algorithms/heuristics/heuristics.h +++ b/src/algorithms/heuristics/heuristics.h @@ -41,7 +41,7 @@ Eval dynamic_vehicle_choice(const Input& input, // Populate routes with user-defined vehicle steps. template -void initial_routes(const Input& input, std::vector& routes); +void set_initial_routes(const Input& input, std::vector& routes); } // namespace vroom::heuristics diff --git a/src/problems/vrp.h b/src/problems/vrp.h index 956519e35..c8d0d3ae6 100644 --- a/src/problems/vrp.h +++ b/src/problems/vrp.h @@ -53,15 +53,20 @@ class VRP { nb_searches = std::min(nb_searches, static_cast(parameters.size())); - // Build empty solutions to be filled by heuristics. - std::vector empty_sol; - empty_sol.reserve(_input.vehicles.size()); + // Build initial solution to be filled by heuristics. Solution is + // empty at first but populated with input data if provided. + std::vector init_sol; + init_sol.reserve(_input.vehicles.size()); for (Index v = 0; v < _input.vehicles.size(); ++v) { - empty_sol.emplace_back(_input, v, _input.zero_amount().size()); + init_sol.emplace_back(_input, v, _input.zero_amount().size()); } - std::vector> solutions(nb_searches, empty_sol); + if (_input.has_initial_routes()) { + heuristics::set_initial_routes(_input, init_sol); + } + + std::vector> solutions(nb_searches, init_sol); #ifdef LOG_LS std::vector ls_dumps; @@ -97,9 +102,6 @@ class VRP { Eval h_eval; switch (p.heuristic) { - case HEURISTIC::INIT_ROUTES: - heuristics::initial_routes(_input, solutions[rank]); - break; case HEURISTIC::BASIC: h_eval = heuristics::basic(_input, solutions[rank], @@ -125,18 +127,14 @@ class VRP { break; } - if (!_input.has_homogeneous_costs() && - p.heuristic != HEURISTIC::INIT_ROUTES && h_param.empty() && + if (!_input.has_homogeneous_costs() && h_param.empty() && p.sort == SORT::AVAILABILITY) { // Worth trying another vehicle ordering scheme in // heuristic. - std::vector other_sol = empty_sol; + std::vector other_sol = init_sol; Eval h_other_eval; switch (p.heuristic) { - case HEURISTIC::INIT_ROUTES: - assert(false); - break; case HEURISTIC::BASIC: h_other_eval = heuristics::basic(_input, other_sol, diff --git a/src/structures/typedefs.h b/src/structures/typedefs.h index abce63dc3..ea9b647be 100644 --- a/src/structures/typedefs.h +++ b/src/structures/typedefs.h @@ -114,7 +114,7 @@ enum class JOB_TYPE { SINGLE, PICKUP, DELIVERY }; enum class STEP_TYPE { START, JOB, BREAK, END }; // Heuristic options. -enum class HEURISTIC { BASIC, DYNAMIC, INIT_ROUTES }; +enum class HEURISTIC { BASIC, DYNAMIC }; enum class INIT { NONE, HIGHER_AMOUNT, NEAREST, FURTHEST, EARLIEST_DEADLINE }; enum class SORT { AVAILABILITY, COST }; @@ -130,15 +130,6 @@ struct HeuristicParameters { SORT sort = SORT::AVAILABILITY) : heuristic(heuristic), init(init), regret_coeff(regret_coeff), sort(sort) { } - - // Only makes sense for user-defined initial routes. - constexpr HeuristicParameters(HEURISTIC heuristic) - : heuristic(heuristic), - init(INIT::NONE), - regret_coeff(0), - sort(SORT::AVAILABILITY) { - assert(heuristic == HEURISTIC::INIT_ROUTES); - } }; // Possible violations. diff --git a/src/structures/vroom/input/input.cpp b/src/structures/vroom/input/input.cpp index dd3267c8a..cec6bcb10 100644 --- a/src/structures/vroom/input/input.cpp +++ b/src/structures/vroom/input/input.cpp @@ -433,6 +433,10 @@ bool Input::has_homogeneous_costs() const { return _homogeneous_costs; } +bool Input::has_initial_routes() const { + return _has_initial_routes; +} + bool Input::vehicle_ok_with_vehicle(Index v1_index, Index v2_index) const { return _vehicle_to_vehicle_compatibility[v1_index][v2_index]; } @@ -1133,13 +1137,8 @@ Solution Input::solve(unsigned nb_searches, } // Solve. - const std::vector h_init_routes(1, - HEURISTIC::INIT_ROUTES); - auto sol = instance->solve(nb_searches, - depth, - nb_thread, - solve_time, - _has_initial_routes ? h_init_routes : h_param); + auto sol = + instance->solve(nb_searches, depth, nb_thread, solve_time, h_param); // Update timing info. sol.summary.computing_times.loading = loading.count(); diff --git a/src/structures/vroom/input/input.h b/src/structures/vroom/input/input.h index ae2519f64..fd320bd36 100644 --- a/src/structures/vroom/input/input.h +++ b/src/structures/vroom/input/input.h @@ -173,6 +173,8 @@ class Input { bool has_homogeneous_costs() const; + bool has_initial_routes() const; + bool vehicle_ok_with_job(size_t v_index, size_t j_index) const { return static_cast(_vehicle_to_job_compatibility[v_index][j_index]); } diff --git a/src/utils/output_json.cpp b/src/utils/output_json.cpp index 0f2bee396..e417b5841 100644 --- a/src/utils/output_json.cpp +++ b/src/utils/output_json.cpp @@ -492,9 +492,6 @@ rapidjson::Value to_json(const ls::log::Dump& dump, case DYNAMIC: heuristic = "DYNAMIC"; break; - case INIT_ROUTES: - heuristic = "INIT_ROUTES"; - break; default: assert(false); }