diff --git a/custom/tkp/README.md b/custom/tkp/README.md new file mode 100644 index 0000000..c835603 --- /dev/null +++ b/custom/tkp/README.md @@ -0,0 +1,36 @@ +# Approach + +Dynamic programming to compute the deliveries. Then, search for a route going through the computed deliveries (respecting the problem restrictions, e.g. start and end in depot at origin) and one pickup, iterating through all pickups and keeping the shortest route. + +# Usage instructions + +Requirements: +- C++ compiler with C++23 (developed with gcc 13.2.1 20230821 on Arch). +- The data generator (dagaten.cpp) uses [fmtlib](https://github.com/fmtlib/fmt). +- gtest for building and running the test. + +Compile: + +`g++ -std=c++23 -o datagen datagen.cpp` +`g++ -std=c++23 -o main main.cpp solver.cpp` + +Run: + +`./datagen | ./main` + +Note that the data generation may include a delivery or pickup co-located with the depot. +The solver is robust enough to handle it. +Using the hardcoded values in datagen.cpp, expect 4-5 deliveries (increasing the hardcoded Q in datagen.cpp will increase them, as expected - recall to recompile datagen if doing so). + +Unit tests: + +`g++ -std=c++23 -o test test.cpp solver.cpp -lgtest` + + +# Extensions + +- Leverage that the vehicle’s used capacity is dynamic. Then, simultaneously optimize the deliveries together with the pickup to choose. This could be done adding an additional dimension to the memoization table for the pickups as well as whether a pickup has been chosen and if affirmative the cost it involves in the route. +- After selecting deliveries, build the route and incorporate pickup greedily. +- Further test coverage. +- Refactor usage to CMakeLists.txt. +- Other TODO comments in the code. diff --git a/custom/tkp/datagen.cpp b/custom/tkp/datagen.cpp new file mode 100644 index 0000000..8b3abf9 --- /dev/null +++ b/custom/tkp/datagen.cpp @@ -0,0 +1,48 @@ +#define FMT_HEADER_ONLY + +#include +#include +#include +#include +#include +#include + +#include +#include + +auto main(int argc, char* argv[]) -> int { + int N = 3; + if (argc > 1) { + try { + N = std::abs(std::stoi(argv[1])); + } catch (std::invalid_argument const&) { + N = 3; + } + } + + std::vector v(2*N+1); + std::ranges::iota(v, -N); + + std::vector w((2*N+1)*(2*N+1)); + std::set idxs; + std::ranges::iota(w, 0); + int const D = 7, P = 4, Q = 9; // TODO randomize. + + if (D+P > (int)w.size()) { + throw std::invalid_argument("Not enough space (f N) for the specified D and P."); + } + + std::ranges::sample(w, std::inserter(idxs, idxs.end()), D+P, + std::mt19937 {std::random_device{}()}); + // TODO consider adding shuffle so that pickups are everywhere. + fmt::println("{} {} {}", D, P, Q); + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> capacity_distribution(1, 3); // TODO magic numbers + for (int i{0}; auto const& item : std::views::cartesian_product(v, v)) { + int const q = capacity_distribution(gen); + if (idxs.contains(i++)) fmt::println("{} {}", item, q); + } + + return 0; +} diff --git a/custom/tkp/main.cpp b/custom/tkp/main.cpp new file mode 100644 index 0000000..2dce548 --- /dev/null +++ b/custom/tkp/main.cpp @@ -0,0 +1,45 @@ +#include + +#include "solver.h" + +auto main() -> int { + int D, P, capacity; + std::cin >> D >> P >> capacity; + + std::vector deliveries(D); + std::vector pickups(P); + + for (int i = 0; i < D; ++i) { + char lparen, rparen, sep[2]; + std::cin >> lparen >> deliveries[i].x >> sep >> deliveries[i].y >> rparen >> deliveries[i].capacity; + } + + std::cout << "Vehicle's capacity: " << capacity << '\n'; + std::cout << "Deliveries (x, y, capacity):\n"; + for (auto delivery : deliveries) + std::cout << " " << std::setw(3) << delivery.x << ' ' + << std::setw(3) << delivery.y << ' ' + << std::setw(3) << delivery.capacity << '\n'; + + for (int i = 0; i < P; ++i) { + char lparen, rparen, sep[2]; + std::cin >> lparen >> pickups[i].x >> sep >> pickups[i].y >> rparen >> pickups[i].capacity; + } + + std::cout << "Pickups:\n"; + for (auto pickup : pickups) + std::cout << " " << std::setw(3) << pickup.x << ' ' + << std::setw(3) << pickup.y << ' ' + << std::setw(3) << pickup.capacity << '\n'; + + auto [max_deliveries, route] = solve(deliveries, pickups, capacity); + + std::cout << "Maximum number of deliveries: " << max_deliveries << std::endl; + std::cout << "Route: "; + for (auto loc : route) { + std::cout << "(" << loc.first << ", " << loc.second << ") "; + } + std::cout << std::endl; + + return 0; +} diff --git a/custom/tkp/solver.cpp b/custom/tkp/solver.cpp new file mode 100644 index 0000000..492a930 --- /dev/null +++ b/custom/tkp/solver.cpp @@ -0,0 +1,110 @@ +#include "solver.h" + +std::pair>> solve(const std::vector& deliveries, + const std::vector& pickups, int capacity) { + const int D = (int)(deliveries.size()); + + // Initialize dynamic programming table. + std::vector> dp(D + 1, std::vector(capacity + 1, 0)); + + // Fill the table. + for (int i = 1; i <= D; ++i) { + for (int c = 1; c <= capacity; ++c) { + // Check if current capacity allows delivery. + if (deliveries[i - 1].capacity <= c) { + // Consider visiting delivery. + dp[i][c] = std::max(dp[i - 1][c], dp[i - 1][c - deliveries[i - 1].capacity] + 1); + } else { + // No delivery possible, maintain previous value. + dp[i][c] = dp[i - 1][c]; + } + } + } + + // Reconstruct. + std::vector picked_deliveries; + int i = D, c = capacity; + while (i > 0 && c > 0) { + if (dp[i][c] != dp[i - 1][c]) { + picked_deliveries.push_back(deliveries[i - 1]); + c -= deliveries[i - 1].capacity; + } + --i; + } + + // Calculate route. + std::vector> best_route; + int best_cost = 100000; + const int P = (int)(pickups.size()); + for (int p = 0; p < P; p++) { + auto [route, cost] = output_route(picked_deliveries, pickups[p], capacity); + if(cost < best_cost) { + best_route = route; + best_cost = cost; + } + } + + return {dp[D][capacity], best_route}; +} + +std::pair>, int> output_route(const std::vector& deliveries, const Event& pickup, int capacity) { + std::vector> best_route; + int best_cost = 1000000; + const int D = (int)(deliveries.size()); + const int DC = std::accumulate(deliveries.cbegin(), deliveries.cend(), 0, [](int c, const Event& e){ return c + e.capacity; }); + // Description queueable: + // magic index, deliveries to go, cost, capacity, route, picked-up + // [0,D-1] -> delivery + // -1 -> depot + // D -> pickup + using queueable = std::tuple, int, + int, std::vector>, bool>; + std::queue q; + std::vector v(D); + std::ranges::iota(v, 0); + for (int i = 0; i < D; i++) { + std::set togo(v.cbegin(), v.cend()); + (void)togo.erase(i); + q.emplace(i, togo, std::abs(deliveries[i].x) + std::abs(deliveries[i].y), + capacity-DC+deliveries[i].capacity, + std::vector>({{deliveries[i].x, deliveries[i].y}}), + false); + } + while(!q.empty()) { + auto [i, togo, cost, cap, route, picked] = q.front(); + q.pop(); + if (togo.empty() and picked and i == -1 and best_cost > cost) { + best_route = route; + best_cost = cost; + } else if (togo.empty() and picked and i != -1) { + Event e = i < D ? deliveries[i] : pickup; + route.push_back({0,0}); + q.emplace(-1, togo, cost + std::abs(e.x) + std::abs(e.y), + capacity-pickup.capacity, + route, + picked); + } else if (togo.empty() and not picked) { + route.push_back({pickup.x, pickup.y}); + q.emplace(D, togo, cost + std::abs(pickup.x) + std::abs(pickup.y), + capacity-pickup.capacity, + route, + not picked); + } else if (not togo.empty()) { + for (const int j : togo) { + auto ntogo = togo; + auto nroute = route; + nroute.push_back({deliveries[j].x, deliveries[j].y}); + (void)ntogo.erase(j); + q.emplace(j, ntogo, + cost + std::abs(deliveries[j].x-deliveries[i].x) + std::abs(deliveries[j].y-deliveries[i].y), + cap+deliveries[j].capacity, + nroute, + picked); + } + //TODO extension, handle picked in this branch, ATM it finds + //a reasonably short route with the pickup at the end. + } + } + best_route.insert(best_route.begin(), {0, 0}); + return {best_route, best_cost}; +} diff --git a/custom/tkp/solver.h b/custom/tkp/solver.h new file mode 100644 index 0000000..92e2f5d --- /dev/null +++ b/custom/tkp/solver.h @@ -0,0 +1,12 @@ +#include + +struct Event { + int x, y; + int capacity; +}; + +std::pair>, int> output_route( + const std::vector& deliveries, const Event& pickup, int capacity); + +std::pair>> solve(const std::vector& deliveries, + const std::vector& pickups, int capacity); diff --git a/custom/tkp/test.cpp b/custom/tkp/test.cpp new file mode 100644 index 0000000..036e1cb --- /dev/null +++ b/custom/tkp/test.cpp @@ -0,0 +1,34 @@ +#include + +#include "solver.h" + +TEST(solve, NoDeliveries) +{ + auto [_, route] = solve({}, {}, 3); + EXPECT_TRUE(route.empty()); +} + +TEST(solve, NoPickups) +{ + std::vector deliveries; + deliveries.emplace_back(0, 1, 3); + auto [_, route] = solve(deliveries, {}, 3); + EXPECT_TRUE(route.empty()); +} + +TEST(solve, OneAndOne) +{ + std::vector deliveries; + deliveries.emplace_back(0, 1, 3); + std::vector pickups; + pickups.emplace_back(-1, -1, 1); + const auto [n, route] = solve(deliveries, pickups, 3); + EXPECT_EQ(n, 1); + EXPECT_EQ(route.size(), 4); +} + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}