From 0bd849d4098ba06901dd00b36b8d4cbbf1a2baf0 Mon Sep 17 00:00:00 2001 From: Natty Date: Sat, 15 Jul 2023 20:09:47 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + CMakeLists.txt | 12 ++ README | 29 +++++ main.cpp | 37 ++++++ renderwindow.cpp | 302 +++++++++++++++++++++++++++++++++++++++++++++++ renderwindow.h | 90 ++++++++++++++ 6 files changed, 473 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README create mode 100644 main.cpp create mode 100644 renderwindow.cpp create mode 100644 renderwindow.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc827fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +cmake-build-* +build +.idea \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..644e40d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.25) +project(tsp) + +set(CMAKE_CXX_STANDARD 23) + +find_package(Qt6 REQUIRED COMPONENTS Core Widgets) +find_package(SDL2 COMPONENTS SDL2 REQUIRED) + +set(CMAKE_AUTOMOC ON) + +add_executable(tsp main.cpp renderwindow.cpp renderwindow.h) +target_link_libraries(tsp Qt6::Core Qt6::Widgets SDL2::SDL2) \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..f8590da --- /dev/null +++ b/README @@ -0,0 +1,29 @@ +# tsp + +An implementation of a Traveling Salesman Problem (TSP) solver in C++. + +## Dependencies + +* Qt 6 +* SDL2 +* CMake 3.25+ + +## Build + +```bash +mkdir build && cd build +cmake .. +make +``` + +## Run + +```bash +./tsp +``` + +## License + +Consider the content of this repository licensed under the Unlicense, but +note that the dependencies may be licensed differently, and that some parts +of the code have been adapted from other sources (marked via comments). \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..41625be --- /dev/null +++ b/main.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include + +#include "renderwindow.h" + +#include +#include +#include +#include + + +int main(int argc, char *argv[]) +{ + QApplication application(argc, argv); + + SDL_InitSubSystem(SDL_INIT_VIDEO); + + if((SDL_Init(SDL_INIT_VIDEO) == -1)) { + std::cerr << "Error initializing SDL: " << SDL_GetError() << std::endl; + return EXIT_FAILURE; + } + + auto window = std::make_shared(); + window->show(); + + int res = QApplication::exec(); + + if (res != 0) { + std::cerr << "QApplication::exec() returned " << res << std::endl; + } + + SDL_Quit(); + + return EXIT_SUCCESS; +} diff --git a/renderwindow.cpp b/renderwindow.cpp new file mode 100644 index 0000000..bcf6136 --- /dev/null +++ b/renderwindow.cpp @@ -0,0 +1,302 @@ +// +// Created by natty on 15.7.23. +// + +#include "renderwindow.h" + +#include +#include +#include +#include +#include +#include + + +std::unique_ptr strategyFromName(const std::string& name) { + if (name == "Anna") { + return std::make_unique(); + } else { + throw std::runtime_error("Unknown strategy"); + } +} + +RenderWindow::RenderWindow() : window(nullptr), renderer(nullptr) { + this->mainWindowWidget = new QWidget(); + this->layout = new QVBoxLayout(this->mainWindowWidget); + this->renderArea = new QWidget(); + this->layout->addWidget(this->renderArea); + this->controlsLayout = new QHBoxLayout(); + this->layout->addLayout(this->controlsLayout); + + auto* strategyLabel = new QLabel("Strategy:"); + this->controlsLayout->addWidget(strategyLabel); + + this->strategySelector = new QComboBox(); + this->strategySelector->addItem("Anna"); + QObject::connect(this->strategySelector, &QComboBox::currentIndexChanged, this, &RenderWindow::resetState); + this->controlsLayout->addWidget(this->strategySelector); + + this->controlsLayout->addStretch(); + + this->distanceLabel = new QLabel("Distance: 0"); + this->controlsLayout->addWidget(this->distanceLabel); + + this->controlsLayout->addStretch(); + + auto *resetButton = new QPushButton("Reset"); + QObject::connect(resetButton, &QPushButton::clicked, this, &RenderWindow::resetState); + this->controlsLayout->addWidget(resetButton); + + auto *hardResetButton = new QPushButton("Reset (new vertices)"); + QObject::connect(hardResetButton, &QPushButton::clicked, this, &RenderWindow::hardResetState); + this->controlsLayout->addWidget(hardResetButton); + + auto* solveButton = new QPushButton("Solve"); + QObject::connect(solveButton, &QPushButton::clicked, this, &RenderWindow::solve); + this->controlsLayout->addWidget(solveButton); + + this->playButton = new QPushButton("Play"); + this->playButton->setCheckable(true); + QObject::connect(this->playButton, &QPushButton::toggled, this, &RenderWindow::togglePlay); + this->controlsLayout->addWidget(this->playButton); + + auto* stepButton = new QPushButton("Step"); + QObject::connect(stepButton, &QPushButton::clicked, this, &RenderWindow::step); + this->controlsLayout->addWidget(stepButton); + + this->setCentralWidget(this->mainWindowWidget); + + this->animTimer = new QTimer(this); + QObject::connect(this->animTimer, &QTimer::timeout, this, &RenderWindow::step); + + this->renderArea->setUpdatesEnabled(false); + this->renderArea->setFixedSize(800, 800); + + SDL_SetHint(SDL_HINT_VIDEO_WINDOW_SHARE_PIXEL_FORMAT, "1"); + + this->window = SDL_CreateWindowFrom(reinterpret_cast(this->renderArea->winId())); + + if (this->window == nullptr) { + std::cerr << "Error creating SDL window: " << SDL_GetError() << std::endl; + throw std::runtime_error("Error creating SDL window"); + } + + this->renderer = SDL_CreateRenderer(this->window, -1, SDL_RENDERER_ACCELERATED); + + if (this->renderer == nullptr) { + std::cerr << "Error creating SDL renderer: " << SDL_GetError() << std::endl; + throw std::runtime_error("Error creating SDL renderer"); + } + + this->hardResetState(); +} + +void RenderWindow::hardResetState() +{ + this->state.vertices = {}; + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dist(100.0, 700.0); + + for (size_t i = 0; i < 100; i++) { + this->state.vertices.push_back({ + .x = dist(gen), + .y = dist(gen) + }); + } + + this->resetState(); +} + +void RenderWindow::resetState() { + this->animTimer->stop(); + + this->strategy = strategyFromName(this->strategySelector->currentText().toStdString()); + + this->state.edges = {}; + + this->render(); +} + +void RenderWindow::paintEvent(QPaintEvent* event) +{ + QMainWindow::paintEvent(event); + this->render(); +} + +void RenderWindow::render() { + SDL_SetRenderDrawColor(this->renderer, 0, 0, 0, 255); + SDL_RenderClear(this->renderer); + + SDL_SetRenderDrawColor(this->renderer, 255, 255, 255, 255); + + for (auto& vertex : this->state.vertices) { + SDL_Rect rect = { + static_cast(vertex.x - 3), + static_cast(vertex.y - 3), + 6, + 6 + }; + + SDL_RenderFillRect(this->renderer, &rect); + } + + double dist = 0.0; + + for (auto& edge : this->state.edges) { + SDL_SetRenderDrawColor(this->renderer, 255, 255, 0, 255); + + dist += std::hypot( + this->state.vertices[edge.from].x - this->state.vertices[edge.to].x, + this->state.vertices[edge.from].y - this->state.vertices[edge.to].y + ); + + SDL_RenderDrawLine( + this->renderer, + static_cast(this->state.vertices[edge.from].x), + static_cast(this->state.vertices[edge.from].y), + static_cast(this->state.vertices[edge.to].x), + static_cast(this->state.vertices[edge.to].y) + ); + } + + this->distanceLabel->setText(QString("Distance: %1").arg(dist)); + + SDL_RenderPresent(this->renderer); +} + +void RenderWindow::step() { + if (this->strategy->step(this->state)) { + this->animTimer->stop(); + this->playButton->setChecked(false); + } + this->render(); +} + +void RenderWindow::solve() { + if (this->state.edges.size() == this->state.vertices.size()) { + this->resetState(); + } + + while (!this->strategy->step(this->state)) {} + + this->render(); +} + +RenderWindow::~RenderWindow() { + if (this->renderer != nullptr) { + SDL_DestroyRenderer(this->renderer); + this->renderer = nullptr; + } + + if (this->window != nullptr) { + SDL_DestroyWindow(this->window); + this->window = nullptr; + } +} + +void RenderWindow::togglePlay(bool play) +{ + if (play) { + if (this->state.edges.size() == this->state.vertices.size()) { + this->resetState(); + } + + this->animTimer->start(50); + } else { + this->animTimer->stop(); + } +} + +bool AnnaStrategy::step(State& state) { + if (!this->init) { + this->init = true; + this->unvisited = {}; + for (size_t i = 0; i < state.vertices.size(); i++) { + this->unvisited.push_back(i); + } + std::shuffle(this->unvisited.begin(), this->unvisited.end(), std::mt19937(std::random_device()())); + } + + if (state.vertices.size() < 2) { + return true; + } + + if (state.vertices.size() == 2) { + state.edges = {{ + .from = 0, + .to = 1 + }}; + return true; + } + + if (this->unvisited.empty()) + return true; + + if (state.vertices.size() - this->unvisited.size() == 0) { + size_t a, b, c; + a = this->unvisited.back(); + this->unvisited.pop_back(); + b = this->unvisited.back(); + this->unvisited.pop_back(); + c = this->unvisited.back(); + this->unvisited.pop_back(); + + state.edges = {{ + .from = a, + .to = b + }, { + .from = b, + .to = c + }, { + .from = c, + .to = a + }}; + + return false; + } + + size_t i = this->unvisited.back(); + this->unvisited.pop_back(); + + const auto& vertex = state.vertices[i]; + + auto& closest_edge = *std::min_element(state.edges.begin(), state.edges.end(), [&state, &vertex](const Edge& a, const Edge& b) { + return a.distanceOf(state, vertex) < b.distanceOf(state, vertex); + }); + + size_t old_to = closest_edge.to; + closest_edge.to = i; + state.edges.push_back({ + .from = i, + .to = old_to + }); + + return false; +} + +double Edge::distanceOf(const State& state, const Vertex& vertex) const { + // Adapted from https://stackoverflow.com/a/1501725 + auto x1 = state.vertices[this->from].x; + auto y1 = state.vertices[this->from].y; + auto x2 = state.vertices[this->to].x; + auto y2 = state.vertices[this->to].y; + + auto dx = x2 - x1; + auto dy = y2 - y1; + + auto l2 = dx * dx + dy * dy; + + constexpr auto epsilon = 0.0001; + + if (l2 < epsilon) + return std::hypot(vertex.x - x1, vertex.y - y1); + + const double t = std::max(0.0, std::min(1.0, ((vertex.x - x1) * dx + (vertex.y - y1) * dy) / l2)); + + const double projection_x = x1 + t * dx; + const double projection_y = y1 + t * dy; + + return std::hypot(vertex.x - projection_x, vertex.y - projection_y); +} \ No newline at end of file diff --git a/renderwindow.h b/renderwindow.h new file mode 100644 index 0000000..b5358cd --- /dev/null +++ b/renderwindow.h @@ -0,0 +1,90 @@ +// +// Created by natty on 15.7.23. +// + +#ifndef TSP_RENDERWINDOW_H +#define TSP_RENDERWINDOW_H + + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +struct State; + +struct Vertex { + double x; + double y; +}; + +struct Edge { + size_t from; + size_t to; + + double distanceOf(const State& state, const Vertex& vertex) const; +}; + +struct State { + std::vector vertices; + std::vector edges; +}; + +class Strategy { + public: + virtual ~Strategy() = default; + virtual bool step(State& state) = 0; +}; + +class AnnaStrategy : public Strategy { + public: + bool step(State& state) override; + private: + bool init; + std::vector unvisited; +}; + +class RenderWindow : public QMainWindow { + Q_OBJECT + + public: + RenderWindow(); + ~RenderWindow() override; + + void paintEvent(QPaintEvent* event) override; + + public slots: + void render(); + void step(); + void solve(); + + private: + QVBoxLayout *layout; + QHBoxLayout *controlsLayout; + QWidget *renderArea; + QWidget *mainWindowWidget; + QLabel *distanceLabel; + QPushButton *playButton; + QComboBox *strategySelector; + QTimer *animTimer; + SDL_Window *window{}; + SDL_Renderer* renderer{}; + + State state; + + std::unique_ptr strategy; + + private slots: + void resetState(); + void hardResetState(); + void togglePlay(bool play); +}; + +#endif //TSP_RENDERWINDOW_H