Skip to content

Commit

Permalink
2024 day 22: major optimizations
Browse files Browse the repository at this point in the history
Now runs in 30ms on xrb in fast mode.
  • Loading branch information
yut23 committed Jan 8, 2025
1 parent 1e53172 commit 8b5ac7f
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 82 deletions.
4 changes: 1 addition & 3 deletions 2024/src/day22.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ int main(int argc, char **argv) {
auto args = aoc::parse_args(argc, argv);

auto market = aoc::day22::MonkeyMarket::read(args.infile);
for (int i = 0; i < 2000; ++i) {
market.evolve(i);
}
market.evolve(2000);
std::cout << market.get_sum() << "\n";
int part_2 = market.find_best_sell_sequence();
std::cout << part_2 << "\n";
Expand Down
147 changes: 75 additions & 72 deletions 2024/src/day22.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,49 @@
#ifndef DAY22_HPP_A3Y597BB
#define DAY22_HPP_A3Y597BB

#include "lib.hpp" // for as_number, DEBUG
#include "unit_test/pretty_print.hpp" // for repr
#include "util/hash.hpp" // for make_hash

#include <algorithm> // for ranges::max_element
#include <array> // for array
#include <cstddef> // for size_t
#include <cstdint> // for int8_t, uint32_t, uint64_t
#include <functional> // for plus, hash
#include <iomanip> // for setw
#include <iostream> // for cerr, istream
#include <tuple> // for forward_as_tuple
#include <unordered_map> // for unordered_map
#include <utility> // for pair (unordered_map), piecewise_construct
#include <vector> // for vector
#include "lib.hpp" // for as_number, DEBUG
#include "util/math.hpp" // for powi

#include <algorithm> // for max_element
#include <compare> // for strong_ordering
#include <cstddef> // for size_t
#include <cstdint> // for int8_t, uint32_t, uint64_t
#include <iostream> // for cerr, istream, ostream
#include <iterator> // for distance
#include <vector> // for vector

namespace aoc::day22 {
struct ChangeSequence : public std::array<std::int8_t, 4> {};
} // namespace aoc::day22

template <>
struct std::hash<aoc::day22::ChangeSequence> {
std::size_t
operator()(const aoc::day22::ChangeSequence &changes) const noexcept {
// random number (hexdump -n8 -e '"0x" 8/1 "%02x" "ull\n"'</dev/urandom)
std::size_t seed = 0xc17a07615bebc283ull;
util::make_hash(seed, changes[0], changes[1], changes[2], changes[3]);
return seed;
}
struct ChangeSequence {
static constexpr std::uint32_t BASE = 19;
static constexpr std::uint32_t MODULUS = aoc::math::powi(BASE, 4);

std::uint32_t value{};

std::int8_t operator[](std::size_t index) const;

void add_price_change(std::int8_t new_change);

// implicit conversion to an integer index
operator std::size_t() const { return value; }
};

namespace aoc::day22 {
std::int8_t ChangeSequence::operator[](std::size_t index) const {
return static_cast<std::int8_t>((value / aoc::math::powi(BASE, 3 - index)) %
BASE) -
9;
}

ChangeSequence add_price_change(const ChangeSequence &changes,
std::int8_t new_change) {
return ChangeSequence{changes[1], changes[2], changes[3], new_change};
void ChangeSequence::add_price_change(std::int8_t new_change) {
value *= BASE;
value += static_cast<std::uint8_t>(new_change + 9);
value %= MODULUS;
}

std::ostream &operator<<(std::ostream &os, const ChangeSequence &changes) {
os << aoc::as_number(changes[0]) << ',' << aoc::as_number(changes[1]) << ','
<< aoc::as_number(changes[2]) << ',' << aoc::as_number(changes[3]);
return os;
}

class Buyer {
Expand Down Expand Up @@ -78,13 +85,27 @@ void Buyer::evolve() {
xorshift(-5);
xorshift(+11);
std::int8_t new_price = secret % 10;
curr_changes = add_price_change(curr_changes, new_price - old_price);
curr_changes.add_price_change(new_price - old_price);
}

class MonkeyMarket {
std::vector<Buyer> buyers;
std::unordered_map<ChangeSequence, std::vector<std::int8_t>>
sequence_prices;

struct PriceEntry {
// running total for each change sequence
std::uint32_t total_bananas = 0;

private:
// last monkey to encounter this sequence
std::uint32_t last_monkey_plus_1 = 0;

public:
void update(std::int8_t price, std::size_t monkey_index);

std::strong_ordering
operator<=>(const PriceEntry &other) const = default;
};
std::vector<PriceEntry> price_lookup{ChangeSequence::MODULUS};

public:
void evolve(int iter);
Expand All @@ -94,23 +115,25 @@ class MonkeyMarket {
static MonkeyMarket read(std::istream &);
};

void MonkeyMarket::evolve(int iter) {
void MonkeyMarket::PriceEntry::update(std::int8_t price,
std::size_t monkey_index) {
// this assumes we simulate the buyers individually (i.e. not interleaving
// evolve calls)
if (last_monkey_plus_1 != monkey_index + 1) {
total_bananas += price;
last_monkey_plus_1 = monkey_index + 1;
}
}

void MonkeyMarket::evolve(int num_iters) {
for (std::size_t i = 0; i < buyers.size(); ++i) {
Buyer &buyer = buyers[i];
buyer.evolve();
if (iter >= 3) {
auto it = sequence_prices.find(buyer.curr_changes);
if (it == sequence_prices.end()) {
// fill price list with -1
it = sequence_prices
.emplace(std::piecewise_construct,
std::forward_as_tuple(buyer.curr_changes),
std::forward_as_tuple(buyers.size(), -1))
.first;
it->second[i] = buyer.price();
} else if (it->second[i] == -1) {
it->second[i] = buyer.price();
}
buyer.evolve();
buyer.evolve();
for (int iter = 3; iter < num_iters; ++iter) {
buyer.evolve();
price_lookup[buyer.curr_changes].update(buyer.price(), i);
}
}
}
Expand All @@ -125,33 +148,13 @@ std::uint64_t MonkeyMarket::get_sum() const {
}

int MonkeyMarket::find_best_sell_sequence() const {
const auto sell_sequence_value = [](const auto &entry) -> int {
int total = 0;
for (int price : entry.second) {
if (price != -1) {
// cppcheck-suppress useStlAlgorithm
total += price;
}
}
return total;
};
auto it =
std::ranges::max_element(sequence_prices, {}, sell_sequence_value);
auto it = std::max_element(price_lookup.begin(), price_lookup.end());
if constexpr (aoc::DEBUG) {
std::cerr << "best sequence: "
<< pretty_print::repr<std::array<std::int8_t, 4>>(
it->first, {.char_as_number = true})
<< "\n";
std::cerr << "prices:\n";
for (int monkey = 0; const auto price : it->second) {
if (price != -1) {
std::cerr << std::setw(5) << monkey << ": "
<< aoc::as_number(price) << "\n";
}
++monkey;
}
ChangeSequence changes{static_cast<std::uint32_t>(
std::distance(price_lookup.begin(), it))};
std::cerr << "best sequence: " << changes << "\n";
}
return sell_sequence_value(*it);
return it->total_bananas;
}

MonkeyMarket MonkeyMarket::read(std::istream &is) {
Expand Down
119 changes: 119 additions & 0 deletions 2024/src/test22.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/******************************************************************************
* File: test22.hpp
*
* Author: Eric T. Johnson (yut23)
* Created: 2025-01-07
*****************************************************************************/

#include "day22.hpp" // IWYU pragma: associated

#include "unit_test/unit_test.hpp"

#include <cstddef> // for size_t
#include <source_location> // for source_location

namespace aoc::day22::test {
std::size_t test_ChangeSequence() {
unit_test::TestSuite suite("aoc::day22::ChangeSequence");

using namespace unit_test::checks;
constexpr auto check_valid = [](const ChangeSequence &changes,
std::source_location loc =
std::source_location::current()) {
check(
changes[0] >= -9 && changes[0] <= 9,
[&](auto &os) {
os << "first value out of range: "
<< aoc::as_number(changes[0]);
},
loc);
check(
changes[1] >= -9 && changes[1] <= 9,
[&](auto &os) {
os << "second value out of range: "
<< aoc::as_number(changes[1]);
},
loc);
check(
changes[2] >= -9 && changes[2] <= 9,
[&](auto &os) {
os << "third value out of range: "
<< aoc::as_number(changes[2]);
},
loc);
check(
changes[3] >= -9 && changes[3] <= 9,
[&](auto &os) {
os << "fourth value out of range: "
<< aoc::as_number(changes[3]);
},
loc);
};

constexpr auto check_values = [](const ChangeSequence &changes, int first,
int second, int third, int fourth,
std::source_location loc =
std::source_location::current()) {
check_equal(
changes[0], first,
[&](auto &os) {
os << "first value incorrect (" << changes.value << ")";
},
loc);
check_equal(
changes[1], second,
[&](auto &os) {
os << "second value incorrect (" << changes.value << ')';
},
loc);
check_equal(
changes[2], third,
[&](auto &os) {
os << "third value incorrect (" << changes.value << ')';
},
loc);
check_equal(
changes[3], fourth,
[&](auto &os) {
os << "fourth value incorrect (" << changes.value << ')';
},
loc);
check(changes.value < aoc::math::powi(changes.BASE, 4), [&](auto &os) {
os << "raw value is greater than BASE**4 (" << changes.value << ')';
});
};

suite.test("add_price_change", [&]() {
ChangeSequence changes{};
check_valid(changes);
changes.add_price_change(-1);
check_valid(changes);
changes.add_price_change(1);
check_valid(changes);
changes.add_price_change(9);
check_valid(changes);
changes.add_price_change(-9);
check_valid(changes);
check_values(changes, -1, 1, 9, -9);

changes.add_price_change(2);
check_values(changes, 1, 9, -9, 2);

changes.add_price_change(3);
check_values(changes, 9, -9, 2, 3);

changes.add_price_change(4);
check_values(changes, -9, 2, 3, 4);

changes.add_price_change(8);
check_values(changes, 2, 3, 4, 8);
});
return suite.done(), suite.num_failed();
}
} // namespace aoc::day22::test

int main() {
std::size_t num_failed = 0;
num_failed += aoc::day22::test::test_ChangeSequence();
return unit_test::fix_exit_code(num_failed);
}
13 changes: 10 additions & 3 deletions aoc_lib/src/lib.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,9 @@ class as_number {
explicit as_number(T &dest) : dest(dest) {}
};

template <class T>
as_number(const T &) -> as_number<const T>;

/**
* Advent of Code problem part number.
*/
Expand Down Expand Up @@ -518,7 +521,7 @@ struct std::hash<aoc::GenericPos<int_type>> {
// instantiate templates in an anonymous namespace, so static analyzers will
// check these functions
namespace {
[[maybe_unused]] void _lint_helper(std::istream &is) {
[[maybe_unused]] void _lint_helper(std::istream &is, std::ostream &os) {
is >> aoc::skip<char>(2);
is >> aoc::skip<int>(1);
is >> aoc::skip<std::string>(3);
Expand All @@ -528,10 +531,14 @@ namespace {
is >> aoc::expect_input<std::string>("foo");
is >> aoc::expect_input(std::string{"bar"});

char ch;
unsigned char uch;
char ch{};
unsigned char uch{};
is >> aoc::as_number(ch);
is >> aoc::as_number(uch);

os << aoc::as_number(ch);
os << aoc::as_number(uch);
os << aoc::as_number('a');
}
} // namespace

Expand Down
19 changes: 17 additions & 2 deletions aoc_lib/src/unit_test/unit_test.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -714,14 +714,16 @@ void check(auto &&condition,
}
}

void check_equal(const auto &actual, const auto &expected,
template <class T>
void check_equal(const std::type_identity_t<T> &actual, const T &expected,
const std::string &info = "", const SL loc = SL::current()) {
if (actual != expected) {
throw detail::equal_failure_helper(actual, expected, loc, info);
}
}

void check_equal(const auto &actual, const auto &expected,
template <class T>
void check_equal(const std::type_identity_t<T> &actual, const T &expected,
std::invocable<std::ostream &> auto &&info,
const SL loc = SL::current()) {
if (actual != expected) {
Expand Down Expand Up @@ -761,6 +763,19 @@ namespace {
test("banana", {{'a', 3}, {'b', 1}, {'n', 2}});
test.done();
}
{
unit_test::TestSuite suite("foo");
suite.test("asdf", []() {
using namespace unit_test::checks;
check(true);
check(true, "info");
check(true, [](auto &os) { os << "lazy info"; });
check_equal(1, 1);
check_equal("a", "a", "info");
std::vector<int> expected{1, 2, 3};
check_equal({1, 2, 3}, expected, "info");
});
}
}
} // namespace

Expand Down
Loading

0 comments on commit 8b5ac7f

Please sign in to comment.