Add solution for "Day 24: Crossed Wires", part 2

This commit is contained in:
Stefan Müller 2025-07-08 20:56:39 +02:00
parent d9fdb22bab
commit 4d58746c6d
6 changed files with 218 additions and 7 deletions

View File

@ -186,6 +186,14 @@ The general problem of finding a maximum clique in a graph is *NP hard*, but the
Thus the algorithm starts with the maximum degree as target clique number, which must be an upper bound if the graph is connected and itself not complete. It will loop over all `t`-vertices and try to find a clique of that size, which includes the vertex, by checking all combinations of the vertex' neighbors of that size. If unsuccessful, the target number is decreased and another iteration started. However, it turns out that the clique number is indeed equal to the maximum degree, so only one loop is required.
### Day 24: Crossed Wires
:mag_right: Puzzle: <https://adventofcode.com/2024/day/24>, :white_check_mark: Solver: [`CrossedWires.cpp`](src/CrossedWires.cpp)
For part 1, many of the output wire values can be determined on the fly, in particular by making use of the fact that `OR` and `AND` logic gates can be evaluated if a single input is `true` or `false`, respectively. The gates that cannot be evaluated immediately are stored and processed iteratively at the end.
The solver then validates all logic gates against parts of the expected structure of a full adder, comprised out of a certain combination of five logic gates per resulting digit and their linking wires, to find the 8 swapped output wires.
## Thanks
* [Alexander Brouwer](https://github.com/Bromvlieg) for getting the project set up with CMake.

View File

@ -17,6 +17,8 @@
#include <bitset>
#include <forward_list>
#include <memory>
#include <set>
#include <unordered_map>
#include <aoc/common/LogicGate.hpp>
@ -33,13 +35,31 @@ class CrossedWires
virtual void finish() override;
private:
static constexpr char getInputWireDelimiter();
static constexpr char getInput1WiresChar();
static constexpr char getOutputWiresChar();
bool isProcessingInputWires_;
std::unordered_map<std::string, bool> wires_;
std::forward_list<LogicGate> unevaluatedGates_;
// Uses input wire + gate kind as keys.
std::unordered_map<std::string, std::shared_ptr<LogicGate>> gatesByInput_;
// Uses output wire as keys.
std::unordered_map<std::string, std::shared_ptr<LogicGate>> gatesByOutput_;
std::forward_list<std::shared_ptr<LogicGate>> unevaluatedGates_;
std::bitset<64> z_;
std::set<std::string> swappedOutputWires_;
void processInputWire(const std::string& line);
void processLogicGate(const std::string& line);
bool tryEvaluateLogicGate(const LogicGate& gate);
void setOutputWire(const std::string& wire, const bool value);
void validateFullAdderWires();
// Returns a tuple containing a bool indicating whether validation was successful, the name of the output wire from
// the AND gate, and the name of the wire from the XOR gate.
std::tuple<bool, std::string, std::string> validateAndXorGatePair(const std::string& inputWire);
// Returns a tuple containing a bool indicating whether validation was successful, the name of the output wire from
// the AND gate, and the name of the wire from the XOR gate.
std::tuple<bool, std::string, std::string> validateAndXorGatePair(const std::string& inputWire1,
const std::string& inputWire2);
// Returns a tuple containing a bool indicating whether validation was successful, and the name of the output wire
// from the OR gate.
std::pair<bool, std::string> validateOrGate(const std::string& inputWire1, const std::string& inputWire2);
void swapOutputWires(std::string& outputWire1, std::string& outputWire2);
};

View File

@ -22,11 +22,13 @@ class LogicGate
{
public:
enum class Kind { And, Or, Xor, Unknown };
static std::string getKindString(const Kind kind);
std::string inputWire1;
std::string inputWire2;
std::string outputWire;
Kind kind{ Kind::Unknown };
std::string getKindString() const;
};
std::istream& operator>>(std::istream& is, LogicGate& logicGate);

View File

@ -15,10 +15,13 @@
#include <aoc/CrossedWires.hpp>
#include <format>
#include <numeric>
#include <sstream>
CrossedWires::CrossedWires(const int inputFileNameSuffix)
: Solver(inputFileNameSuffix), isProcessingInputWires_{ true }, wires_{}, unevaluatedGates_{}, z_{ 0 }
: Solver(inputFileNameSuffix), isProcessingInputWires_{ true }, wires_{}, gatesByInput_{}, gatesByOutput_{},
unevaluatedGates_{}, z_{ 0 }, swappedOutputWires_{}
{
}
@ -55,9 +58,16 @@ void CrossedWires::finish()
{
while (!unevaluatedGates_.empty())
{
unevaluatedGates_.remove_if([this](const LogicGate& gate) { return tryEvaluateLogicGate(gate); });
unevaluatedGates_.remove_if([this](auto gate) { return tryEvaluateLogicGate(*gate); });
}
part1 = z_.to_ullong();
validateFullAdderWires();
if (!swappedOutputWires_.empty())
{
part2 = std::accumulate(++swappedOutputWires_.begin(), swappedOutputWires_.end(), *swappedOutputWires_.begin(),
[](const std::string& acc, const std::string& x) { return acc + "," + x; });
}
}
constexpr char CrossedWires::getInputWireDelimiter()
@ -65,6 +75,11 @@ constexpr char CrossedWires::getInputWireDelimiter()
return ':';
}
constexpr char CrossedWires::getInput1WiresChar()
{
return 'x';
}
constexpr char CrossedWires::getOutputWiresChar()
{
return 'z';
@ -84,10 +99,15 @@ void CrossedWires::processInputWire(const std::string& line)
void CrossedWires::processLogicGate(const std::string& line)
{
std::istringstream stream{ line };
LogicGate gate;
stream >> gate;
auto gate = std::make_shared<LogicGate>();
stream >> *gate;
if (!tryEvaluateLogicGate(gate))
// Uses different keys for the two maps to guarantee unique keys.
gatesByInput_.insert({ gate->inputWire1 + gate->getKindString(), gate });
gatesByInput_.insert({ gate->inputWire2 + gate->getKindString(), gate });
gatesByOutput_.insert({ gate->outputWire, gate });
if (!tryEvaluateLogicGate(*gate))
{
unevaluatedGates_.push_front(gate);
}
@ -158,3 +178,143 @@ void CrossedWires::setOutputWire(const std::string& wire, const bool value)
}
wires_.insert({ wire, value });
}
void CrossedWires::validateFullAdderWires()
{
// Except for the first digit "00", one full adder for a single digit is comprised of the same configuration of five
// logic gates, chained together by the "carry" from one digit to the next. For example, these are the logic gates
// for the second digit "01".
// x01 AND y01 -> carryFromDigits
// x01 XOR y01 -> digitsXor
// digitsXor AND carry00 -> carryFromCarry
// digitsXor XOR carry00 -> z01
// carryFromDigits OR carryFromCarry -> carry01
// The following codes validates parts of these in order to find swapped output wires.
uint64_t i{ 0 };
std::string carryWire;
bool success{ true };
while (success)
{
std::string inputDigitWire{ std::format("{0}{1:0>2}", getInput1WiresChar(), i) };
std::string expResultDigitWire{ std::format("{0}{1:0>2}", getOutputWiresChar(), i) };
std::string resultDigitWire, carryFromDigitsWire, digitsXorWire, carryFromCarryWire;
if (i == 0)
{
// Checks "x00 AND y00 -> carry" and "x00 XOR y00 -> z00" gate.
std::tie(success, carryWire, resultDigitWire) = validateAndXorGatePair(inputDigitWire);
if (success && expResultDigitWire != resultDigitWire)
{
swapOutputWires(expResultDigitWire, resultDigitWire);
}
}
else
{
// Checks "x** AND y**" and "x** XOR y**" gate pair.
std::tie(success, carryFromDigitsWire, digitsXorWire) = validateAndXorGatePair(inputDigitWire);
if (!success)
{
break;
}
// Checks the AND and XOR gate pair with the digits XOR gate output and carry as inputs.
std::tie(success, carryFromCarryWire, resultDigitWire) = validateAndXorGatePair(digitsXorWire, carryWire);
if (!success)
{
std::tie(success, carryFromCarryWire, resultDigitWire) =
validateAndXorGatePair(carryFromDigitsWire, carryWire);
if (success)
{
swapOutputWires(carryFromDigitsWire, digitsXorWire);
}
}
if (success && expResultDigitWire != resultDigitWire)
{
swapOutputWires(expResultDigitWire, resultDigitWire);
if (carryFromDigitsWire == resultDigitWire)
{
carryFromDigitsWire = expResultDigitWire;
}
if (carryFromCarryWire == resultDigitWire)
{
carryFromCarryWire = expResultDigitWire;
}
}
// Checks the OR gate with the two carry wires as inputs.
std::tie(success, carryWire) = validateOrGate(carryFromCarryWire, carryFromDigitsWire);
}
++i;
}
}
std::tuple<bool, std::string, std::string> CrossedWires::validateAndXorGatePair(const std::string& inputWire)
{
// Checks "in AND XXX" gate.
auto itAnd = gatesByInput_.find(inputWire + LogicGate::getKindString(LogicGate::Kind::And));
if (itAnd == gatesByInput_.end())
{
// AND gate is missing.
return std::make_tuple(false, "", "");
}
// Checks "in XOR XXX" gate.
auto itXor = gatesByInput_.find(inputWire + LogicGate::getKindString(LogicGate::Kind::Xor));
if (itXor == gatesByInput_.end())
{
// XOR gate is missing.
return std::make_tuple(false, "", "");
}
return std::make_tuple(true, itAnd->second->outputWire, itXor->second->outputWire);
}
std::tuple<bool, std::string, std::string> CrossedWires::validateAndXorGatePair(const std::string& inputWire1,
const std::string& inputWire2)
{
// Checks "in1 AND in2" gate.
auto itAnd = gatesByInput_.find(inputWire1 + LogicGate::getKindString(LogicGate::Kind::And));
if (itAnd == gatesByInput_.end() ||
!(itAnd->second->inputWire1 == inputWire2 || itAnd->second->inputWire2 == inputWire2))
{
// AND gate is missing.
return std::make_tuple(false, "", "");
}
// Checks "in1 XOR in2" gate.
auto itXor = gatesByInput_.find(inputWire1 + LogicGate::getKindString(LogicGate::Kind::Xor));
if (itXor == gatesByInput_.end() ||
!(itXor->second->inputWire1 == inputWire2 || itXor->second->inputWire2 == inputWire2))
{
// XOR gate is missing.
return std::make_tuple(false, "", "");
}
return std::make_tuple(true, itAnd->second->outputWire, itXor->second->outputWire);
}
std::pair<bool, std::string> CrossedWires::validateOrGate(const std::string& inputWire1, const std::string& inputWire2)
{
// Checks "in1 OR in2 -> carry" gate.
auto itOr = gatesByInput_.find(inputWire1 + LogicGate::getKindString(LogicGate::Kind::Or));
if (itOr == gatesByInput_.end() ||
!(itOr->second->inputWire1 == inputWire2 || itOr->second->inputWire2 == inputWire2))
{
// OR gate is missing.
return std::make_pair(false, "");
}
return std::make_pair(true, itOr->second->outputWire);
}
void CrossedWires::swapOutputWires(std::string& outputWire1, std::string& outputWire2)
{
swappedOutputWires_.insert(outputWire1);
swappedOutputWires_.insert(outputWire2);
auto& gate1 = gatesByOutput_[outputWire1];
gate1->outputWire = outputWire2;
auto& gate2 = gatesByOutput_[outputWire2];
gate2->outputWire = outputWire1;
std::swap(gate1, gate2);
std::swap(outputWire1, outputWire2);
}

View File

@ -15,6 +15,26 @@
#include <aoc/common/LogicGate.hpp>
std::string LogicGate::getKindString(const Kind kind)
{
switch (kind)
{
case Kind::And :
return "AND";
case Kind::Or :
return "OR";
case Kind::Xor :
return "XOR";
case Kind::Unknown :
return "unknown";
}
}
std::string LogicGate::getKindString() const
{
return getKindString(kind);
}
std::istream& operator>>(std::istream& is, LogicGate& logicGate)
{
std::string token;
@ -26,6 +46,7 @@ std::istream& operator>>(std::istream& is, LogicGate::Kind& kind)
{
std::string s;
is >> s;
// Checks only the first char.
switch (s[0])
{
case 'A' :

View File

@ -420,7 +420,7 @@ TEST_CASE("[CrossedWiresTests]")
TestContext test;
SECTION("FullData")
{
test.runFull(std::make_unique<CrossedWires>(), 48508229772400, "");
test.runFull(std::make_unique<CrossedWires>(), 48508229772400, "cqr,ncd,nfj,qnw,vkg,z15,z20,z37");
}
SECTION("ExampleData")
{