Just did some simple recursion with memoization like most people did. Also I looked at all the relevant paths and also found that relevant steps are [svr => fft], [fft => dac], [dac => out].
part 2:
fn bytesToU24(bytes: [3]u8) u24 {
return @bitCast(bytes);
}
fn u24ToBytes(val: u24) [3]u8 {
return @bitCast(val);
}
const svr = bytesToU24("svr".*);
const fft = bytesToU24("fft".*);
const dac = bytesToU24("dac".*);
const you = bytesToU24("you".*);
const out = bytesToU24("out".*);
const NodeData = struct {
parents: ArrayList(u24),
pub const empty: NodeData = .{
.parents = .empty,
};
};
fn solution(r: *Reader, _: Io, gpa: Allocator) !u64 {
var node_data: NodeDataSet = .empty;
defer {
var it = node_data.valueIterator();
while (it.next()) |value| {
value.parents.deinit(gpa);
}
node_data.deinit(gpa);
}
while (try r.takeDelimiter('\n')) |line| {
var it = mem.tokenizeAny(u8, line, ": ");
const node = bytesToU24(it.next().?[0..3].*);
{
const gop = try node_data.getOrPut(gpa, node);
switch (gop.found_existing) {
true => {},
false => gop.value_ptr.* = .empty,
}
}
while (it.next()) |bytes| {
const outflow = bytesToU24(bytes[0..3].*);
const gop = try node_data.getOrPut(gpa, outflow);
switch (gop.found_existing) {
true => {
if (mem.findScalar(u24, gop.value_ptr.parents.items, outflow) == null) {
try gop.value_ptr.parents.append(gpa, node);
}
},
false => {
gop.value_ptr.* = .empty;
try gop.value_ptr.parents.append(gpa, node);
},
}
}
}
var seen_set: SeenSet = .empty;
defer seen_set.deinit(gpa);
const svr_fft = try pathToWrapped(gpa, &seen_set, &node_data, svr, fft);
const fft_dac = try pathToWrapped(gpa, &seen_set, &node_data, fft, dac);
const dac_out = try pathToWrapped(gpa, &seen_set, &node_data, dac, out);
std.log.info("Cached {d} lookups", .{cache});
cache = 0;
return svr_fft * fft_dac * dac_out;
}
const NodeDataSet = std.AutoHashMapUnmanaged(u24, NodeData);
const SeenSet = std.AutoArrayHashMapUnmanaged(u24, u64);
fn pathToWrapped(gpa: Allocator, seen_set: *SeenSet, data_set: *NodeDataSet, src: u24, dest: u24) !u64 {
seen_set.clearRetainingCapacity();
return pathTo(gpa, seen_set, data_set, src, dest);
}
var cache: u64 = 0;
fn pathTo(gpa: Allocator, seen_set: *SeenSet, data_set: *NodeDataSet, src: u24, dest: u24) !u64 {
assert(dest != src);
{
const seen_gop = try seen_set.getOrPut(gpa, dest);
if (seen_gop.found_existing) {
cache += 1;
return seen_gop.value_ptr.*;
}
seen_gop.value_ptr.* = 0;
}
const data = data_set.get(dest) orelse fatal("Invalid node {x}", .{dest});
var seen: u64 = 0; // Only update seen _after_ we recurse to ignore cycles
for (data.parents.items) |parent| {
if (parent == src) {
seen += 1;
continue;
}
seen += try pathTo(gpa, seen_set, data_set, src, parent);
}
const seen_gop = try seen_set.getOrPut(gpa, dest);
assert(seen_gop.found_existing);
seen_gop.value_ptr.* = seen;
return seen;
}