Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Algorithm Challenge #15

Merged
merged 8 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions custom/tkp/README.md
Original file line number Diff line number Diff line change
@@ -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.
48 changes: 48 additions & 0 deletions custom/tkp/datagen.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#define FMT_HEADER_ONLY

#include <iterator>
#include <random>
#include <ranges>
#include <set>
#include <stdexcept>
#include <vector>

#include <fmt/core.h>
#include <fmt/ranges.h>

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<int> v(2*N+1);
std::ranges::iota(v, -N);

std::vector<int> w((2*N+1)*(2*N+1));
std::set<int> 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;
}
45 changes: 45 additions & 0 deletions custom/tkp/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <bits/stdc++.h>

#include "solver.h"

auto main() -> int {
int D, P, capacity;
std::cin >> D >> P >> capacity;

std::vector<Event> deliveries(D);
std::vector<Event> 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;
}
110 changes: 110 additions & 0 deletions custom/tkp/solver.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "solver.h"

std::pair<int, std::vector<std::pair<int, int>>> solve(const std::vector<Event>& deliveries,
const std::vector<Event>& pickups, int capacity) {
const int D = (int)(deliveries.size());

// Initialize dynamic programming table.
std::vector<std::vector<int>> dp(D + 1, std::vector<int>(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<Event> 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<std::pair<int, int>> 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<std::vector<std::pair<int, int>>, int> output_route(const std::vector<Event>& deliveries, const Event& pickup, int capacity) {
std::vector<std::pair<int, int>> 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, std::set<int>, int,
int, std::vector<std::pair<int, int>>, bool>;
std::queue<queueable> q;
std::vector<int> v(D);
std::ranges::iota(v, 0);
for (int i = 0; i < D; i++) {
std::set<int> 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<std::pair<int, int>>({{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};
}
13 changes: 13 additions & 0 deletions custom/tkp/solver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include <bits/stdc++.h>

struct Event {
int x, y;
int capacity;
};

// Forward declaration.
Copy link
Owner Author

@iglesias iglesias Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • remove comment, outdated after separating initial main into main and solver.

std::pair<std::vector<std::pair<int, int>>, int> output_route(
const std::vector<Event>& deliveries, const Event& pickup, int capacity);

std::pair<int, std::vector<std::pair<int, int>>> solve(const std::vector<Event>& deliveries,
const std::vector<Event>& pickups, int capacity);
34 changes: 34 additions & 0 deletions custom/tkp/test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include <gtest/gtest.h>

#include "solver.h"

TEST(solve, NoDeliveries)
{
auto [_, route] = solve({}, {}, 3);
EXPECT_TRUE(route.empty());
}

TEST(solve, NoPickups)
{
std::vector<Event> deliveries;
deliveries.emplace_back(0, 1, 3);
auto [_, route] = solve(deliveries, {}, 3);
EXPECT_TRUE(route.empty());
}

TEST(solve, OneAndOne)
{
std::vector<Event> deliveries;
deliveries.emplace_back(0, 1, 3);
std::vector<Event> 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();
}