Initial commit

This commit is contained in:
Natty 2023-07-15 20:09:47 +02:00
commit 0bd849d409
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
6 changed files with 473 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
cmake-build-*
build
.idea

12
CMakeLists.txt Normal file
View File

@ -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)

29
README Normal file
View File

@ -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).

37
main.cpp Normal file
View File

@ -0,0 +1,37 @@
#include <iostream>
#include <algorithm>
#include <vector>
#include <format>
#include "renderwindow.h"
#include <QApplication>
#include <QWidget>
#include <SDL2/SDL.h>
#include <qtimer.h>
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<RenderWindow>();
window->show();
int res = QApplication::exec();
if (res != 0) {
std::cerr << "QApplication::exec() returned " << res << std::endl;
}
SDL_Quit();
return EXIT_SUCCESS;
}

302
renderwindow.cpp Normal file
View File

@ -0,0 +1,302 @@
//
// Created by natty on 15.7.23.
//
#include "renderwindow.h"
#include <iostream>
#include <QPushButton>
#include <random>
#include <QComboBox>
#include <QLabel>
#include <QTimer>
std::unique_ptr<Strategy> strategyFromName(const std::string& name) {
if (name == "Anna") {
return std::make_unique<AnnaStrategy>();
} 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<const void*>(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<double> 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<int>(vertex.x - 3),
static_cast<int>(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<int>(this->state.vertices[edge.from].x),
static_cast<int>(this->state.vertices[edge.from].y),
static_cast<int>(this->state.vertices[edge.to].x),
static_cast<int>(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);
}

90
renderwindow.h Normal file
View File

@ -0,0 +1,90 @@
//
// Created by natty on 15.7.23.
//
#ifndef TSP_RENDERWINDOW_H
#define TSP_RENDERWINDOW_H
#include <QApplication>
#include <qmainwindow.h>
#include <qwidget.h>
#include <SDL2/SDL.h>
#include <QHBoxLayout>
#include <unordered_set>
#include <random>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
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<Vertex> vertices;
std::vector<Edge> 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<size_t> 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> strategy;
private slots:
void resetState();
void hardResetState();
void togglePlay(bool play);
};
#endif //TSP_RENDERWINDOW_H