Skip to content

Commit

Permalink
2023 day 23: rewrite path simplification to make part 2 easier
Browse files Browse the repository at this point in the history
  • Loading branch information
yut23 committed Jan 26, 2024
1 parent 9128614 commit 4963713
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 127 deletions.
193 changes: 74 additions & 119 deletions 2023/src/day23.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,21 @@

#include "ds/grid.hpp" // for Grid
#include "lib.hpp" // for Pos, AbsDirection, Delta
#include <algorithm> // for minmax
#include <cassert> // for assert
#include <functional> // for bind_front
#include <initializer_list> // for initializer_list
#include <iostream> // for istream
#include <map> // for map, multimap
#include <map> // for map
#include <set> // for set
#include <string> // for string, getline
#include <utility> // for move
#include <vector> // for vector
// IWYU pragma: no_include <unordered_set> // no clue why it wants this

#include "graph_traversal.hpp"

namespace aoc::day23 {

struct Trail {
Pos start;
Pos end;
unsigned int length;
};

std::ostream &operator<<(std::ostream &os, const Trail &trail) {
os << "Trail from " << trail.start << " to " << trail.end
<< ", length=" << trail.length;
return os;
}

const std::map<char, AbsDirection> allowed_directions{
{'>', AbsDirection::east},
{'<', AbsDirection::west},
Expand All @@ -47,13 +35,21 @@ class TrailMap {
aoc::ds::Grid<char> grid;
std::map<Pos, std::set<Pos>> grid_path;
std::map<Pos, std::set<Pos>> grid_prev;

std::map<Pos, Trail> trails;
std::multimap<Pos, Pos> connections;
std::map<std::pair<Pos, Pos>, int> distances;

std::vector<Pos> get_grid_neighbors(const Pos &pos) const;

Pos reconstruct_trails(Pos pos, const Pos &target);
void add_edge(const Pos &from, const Pos &to, int distance = 1);
void remove_edge(const Pos &from, const Pos &to);
void collapse_node(const Pos &pos);
void simplify_trails();

std::pair<Pos, Pos> dist_key(const Pos &from, const Pos &to) const {
return std::minmax(from, to);
}
int get_distance(const Pos &from, const Pos &to) const {
return distances.at(dist_key(from, to));
}

public:
explicit TrailMap(const std::vector<std::string> &grid) : grid(grid) {}
Expand All @@ -73,9 +69,6 @@ TrailMap TrailMap::read(std::istream &is) {

std::vector<Pos> TrailMap::get_grid_neighbors(const Pos &pos) const {
std::vector<Pos> neighbors;
if (!grid.in_bounds(pos)) {
return {};
}

char terrain = grid[pos];
switch (terrain) {
Expand Down Expand Up @@ -105,12 +98,9 @@ std::vector<Pos> TrailMap::get_grid_neighbors(const Pos &pos) const {
}
auto it = grid_path.find(new_pos);
if (it != grid_path.end() && it->second.contains(pos)) {
// don't go backwards through edges we've already seen
continue;
}
// it = grid_path.find(pos);
// if (it != grid_path.end() && it->second.contains(new_pos)) {
// continue;
// }
neighbors.push_back(new_pos);
}
assert(neighbors.size() <= 2);
Expand All @@ -119,6 +109,21 @@ std::vector<Pos> TrailMap::get_grid_neighbors(const Pos &pos) const {
return neighbors;
}

void TrailMap::add_edge(const Pos &from, const Pos &to, int distance) {
grid_path[from].emplace(to);
grid_prev[to].emplace(from);
distances[dist_key(from, to)] = distance;
}

void TrailMap::remove_edge(const Pos &from, const Pos &to) {
assert(distances.erase(dist_key(from, to)) == 1);
// avoid UAF in case from and to are references into grid_path and grid_prev
auto &fwd = grid_path.at(from);
auto &rev = grid_prev.at(to);
assert(fwd.erase(to) == 1);
assert(rev.erase(from) == 1);
}

void TrailMap::parse_grid() {
assert(grid.height > 2 && grid.width > 2);
// starting point
Expand All @@ -129,130 +134,80 @@ void TrailMap::parse_grid() {

auto visit_with_parent = [this](const Pos &pos, const Pos &parent, int) {
if (parent != pos) {
grid_path[parent].emplace(pos);
grid_prev[pos].emplace(parent);
add_edge(parent, pos);
}
};
aoc::graph::dfs<false>(start,
std::bind_front(&TrailMap::get_grid_neighbors, this),
visit_with_parent);

if constexpr (aoc::DEBUG && false) {
for (const auto &[parent, children] : grid_path) {
std::cerr << parent << " -> {";
for (auto it = children.begin(); it != children.end(); ++it) {
if (it != children.begin()) {
std::cerr << ", ";
}
std::cerr << *it;
}
std::cerr << "}\n";
}
}

Pos after_target = target + Delta(0, 1);
trails.try_emplace(after_target, Trail{after_target, after_target, 0});
reconstruct_trails(start, target);
--trails.at(start).length;

if constexpr (aoc::DEBUG) {
std::cerr << "\n";
Delta delta{1, 1};
for (const auto &[_, trail] : trails) {
std::cerr << trail << ": connected to {";
auto range = connections.equal_range(trail.start);
for (auto it = range.first; it != range.second; ++it) {
if (it != range.first) {
std::cerr << ", ";
}
std::cerr << it->second + delta;
}
std::cerr << "}\n";
if (grid.in_bounds(trail.start)) {
grid[trail.start] = 'O';
}
if (grid.in_bounds(trail.end)) {
grid[trail.end] = '*';
}
}

std::cerr << "\n";
for (const auto &row : grid) {
for (const char &ch : row) {
std::cerr << ch;
}
std::cerr << "\n";
}
}
simplify_trails();
}

// reconstruct graph from path pointers
Pos TrailMap::reconstruct_trails(Pos pos, const Pos &target) {
bool inserted = trails.try_emplace(pos, Trail{pos, pos, 1}).second;
if (!inserted) {
return pos;
void TrailMap::simplify_trails() {
// iterate through all nodes and collapse those that have a single
// predecessor and successor
std::vector<Pos> nodes;
for (const auto &[key, _] : grid_path) {
nodes.push_back(key);
}
Trail &trail = trails.at(pos);
while (pos != target) {
auto path_it = grid_path.find(pos);
if (path_it == grid_path.end() || path_it->second.empty()) {
break;
}
auto prev_it = grid_prev.find(pos);
if (path_it->second.size() > 1 ||
(prev_it != grid_prev.end() && prev_it->second.size() > 1)) {
// multiple paths, recurse down each
for (const Pos &next_pos : path_it->second) {
Pos next_start = reconstruct_trails(next_pos, target);
connections.emplace(trail.start, next_start);
}
break;
for (const Pos &pos : nodes) {
const auto succ_it = grid_path.find(pos);
const auto pred_it = grid_prev.find(pos);
if (succ_it != grid_path.end() && succ_it->second.size() == 1 &&
pred_it != grid_prev.end() && pred_it->second.size() == 1) {
collapse_node(pos);
}
pos = *(path_it->second.begin());
++trail.length;
trail.end = pos;
}
if (pos == target) {
connections.emplace(trail.start, target + Delta(0, 1));
}
return trail.start;
}

void TrailMap::collapse_node(const Pos &pos) {
const Pos &before = *grid_prev.at(pos).begin();
const Pos &after = *grid_path.at(pos).begin();
int new_distance = get_distance(before, pos) + get_distance(pos, after);
// add link between before and after
add_edge(before, after, new_distance);
// remove links to pos
remove_edge(before, pos);
remove_edge(pos, after);
}

int TrailMap::part_1() const {
// find longest path in a DAG
if constexpr (aoc::DEBUG) {
std::cerr << "digraph G {\n";
for (const auto &[p1, p2] : connections) {
std::cerr << " pos_" << p1.x << "_" << p1.y << " -> pos_" << p2.x
<< "_" << p2.y << " [label=" << trails.at(p1).length
<< "];\n";
for (const auto &[from, neighbors] : grid_path) {
for (const Pos &to : neighbors) {
std::cerr << " pos_" << from.x << "_" << from.y << " -> pos_"
<< to.x << "_" << to.y
<< " [label=" << get_distance(from, to) << "];\n";
}
}
std::cerr << "}\n";
}

Pos start{1, 0};
Pos after_target{grid.width - 2, grid.height};
auto get_neighbors = [this](const Pos &pos) -> std::vector<Pos> {
const auto range = connections.equal_range(pos);
std::vector<Pos> neighbors;
for (auto it = range.first; it != range.second; ++it) {
neighbors.emplace_back(it->second);
Pos target{grid.width - 2, grid.height - 1};
const std::set<Pos> empty_set{};
auto get_neighbors = [this,
&empty_set](const Pos &pos) -> const std::set<Pos> & {
auto it = grid_path.find(pos);
if (it == grid_path.end()) {
return empty_set;
}
return neighbors;
};
auto get_distance = [this](const Pos &from, const Pos &) {
return trails.at(from).length;
return it->second;
};
auto is_target = [&after_target](const Pos &pos) {
return pos == after_target;
auto is_target = [&target](const Pos &pos) -> bool {
return pos == target;
};
const auto &[distance, path] = aoc::graph::longest_path_dag(
start, get_neighbors, get_distance, is_target);
start, get_neighbors, std::bind_front(&TrailMap::get_distance, this),
is_target);

if constexpr (aoc::DEBUG) {
std::cerr << "longest path:\n";
for (const auto &pos : path) {
std::cerr << trails.at(pos) << "\n";
std::cerr << pos << "\n";
}
}
return distance;
Expand Down
17 changes: 9 additions & 8 deletions 2023/src/util/concepts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
#ifndef CONCEPTS_HPP_0EY957BS
#define CONCEPTS_HPP_0EY957BS

#include <concepts> // for convertible_to, same_as
#include <cstddef> // for size_t
#include <functional> // for hash
#include <iostream> // for ostream
#include <iterator> // for forward_iterator
#include <ranges> // for range, range_value_t
#include <utility> // for pair
#include <concepts> // for convertible_to, same_as
#include <cstddef> // for size_t
#include <functional> // for hash
#include <iostream> // for ostream
#include <iterator> // for forward_iterator
#include <ranges> // for range, range_value_t
#include <type_traits> // for remove_reference_t
#include <utility> // for pair

namespace util::concepts {

Expand All @@ -34,7 +35,7 @@ concept Hashable =
template <typename C, typename T>
// clang-format off
concept any_iterable_collection =
std::same_as<typename C::value_type, T> &&
std::same_as<typename std::remove_reference_t<C>::value_type, T> &&
requires (C c) {
{ std::begin(c) } -> std::forward_iterator;
{ std::end(c) } -> std::forward_iterator;
Expand Down

0 comments on commit 4963713

Please sign in to comment.