Module:User:Cscott/Advent Of Code 2023/Day 20

return (function local builders = {} local function register(name, f) builders[name] = f end register('llpeg.lpegrex', function return require Module:User:Cscott/lpegrex end)

register('advent.compat', function return require Module:User:Cscott/compat end)

register('bignum', function(myrequire) local compat = myrequire('advent.compat')

-- poor man's bignum library local RADIX = 1000 -- power of 10 to make printout easier

local BigNum = {} BigNum.__index = BigNum function BigNum:new(n) return setmetatable( {n or 0}, self):normalize end function BigNum:__tostring local result = {} local first = true for i=#self,1,-1 do   local n = self[i] if n ~= 0 or not first then local s = tostring(n) if first then first = false else while #s < 3 do s = '0' .. s end end table.insert(result, s)   end end if #result == 0 then return "0" end return table.concat(result) end function BigNum:normalize local i = 1 while self[i] ~= nil do   if self[i] >= 1000 then local carry = math.floor(self[i] / 1000) self[i] = self[i] % 1000 self[i+1] = (self[i+1] or 0) + carry end i = i + 1 end return self end function BigNum:copy local r = BigNum:new for i=1,#self do   r[i] = self[i] end return r end function BigNum.__add(a, b) if type(a) == 'number' then a,b = b,a end local r = a:copy if type(b) == 'number' then r[1] = (r[1] or 0) + b else for i=1,#b do     r[i] = (r[i] or 0) + b[i] end end return r:normalize end function BigNum.__mul(a, b) if type(a) == 'number' then a,b = b,a end local r = BigNum:new if type(b) == 'number' then for i=1,#a do     r[i] = a[i] * b    end return r:normalize end for i=1,#a do   for j=1,#b do      local prod = a[i] * b[j] r[i+j-1] = (r[i+j-1] or 0) + prod end r:normalize end return r end

return BigNum

end)

register('util', function(myrequire) local function read_wiki_input(func)   return function (frame, ...)      if type(frame)=='string' then        frame = { args = { frame, ... } }      end      local title = mw.title.new(frame.args[1])      local source = title:getContent      if source == nil then        error("Can't find title " .. tostring(title))     end      source = source:gsub("^%s*]*>", "", 1)      source = source:gsub("]*>%s*$", "", 1)      return func(source)    end end

return { read_wiki_input = read_wiki_input, }

end)

register('day20', function(myrequire) -- DAY 20 --

local lpegrex = myrequire('llpeg.lpegrex') local compat = myrequire('advent.compat') local BigNum = myrequire('bignum') local read_wiki_input = myrequire('util').read_wiki_input

-- PARSING --

local patt = lpegrex.compile([[ Puzzle <-| nl* Module (nl Module)* nl* Module <-| Type? Name `->` {:dest: Dest :} Dest <-| {%w+} SKIP (`,` {%w+} SKIP)*

Type <-- {:type: `%` -> '%%' / `&` -> '&' :} Name <-- {:name: %w+ :} SKIP

nl <-- %nl SKIP <- [ ]* NAME_SUFFIX <- [_%w]+ ]])

local Module = {} Module.__index = Module

local FlipFlop = setmetatable({}, Module) FlipFlop.__index = FlipFlop

local Conj = setmetatable({}, Module) Conj.__index = Conj

local Broadcaster = setmetatable({}, Module) Broadcaster.__index = Broadcaster

function parse(source) --print(inspect(source)) local ast, errlabel, pos = patt:match(source) if not ast then local lineno, colno, line = lpegrex.calcline(source, pos) local colhelp = string.rep(' ', colno-1)..'^' error('syntax error: '..lineno..':'..colno..': '..errlabel..           '\n'..line..'\n'..colhelp) end --print('Parsed with success!') --print(inspect(ast))

-- turn the lists into maps local modules = {} for _,v in ipairs(ast) do    modules[v.name] = v     if v.type == '&' then setmetatable(v, Conj) v.inputs = {} v.inputList = {} elseif v.type == '%' then setmetatable(v, FlipFlop) v.state = false -- initially off elseif v.name == 'broadcaster' then setmetatable(v, Broadcaster) end end -- link up inputs of conjunction modules for _,v in ipairs(ast) do    for _,dest in ipairs(v.dest) do       local m = modules[dest] if m ~= nil and m.type == '&' then m.inputs[v.name] = false -- remember a low pulse for each input table.insert(m.inputList, v.name) end end end return modules end

-- PART 1 --

local LList = {} LList.__index = LList function LList:new return setmetatable({}, self) end function LList:pullFromHead local node = self.first self.first = self.first.next if self.first == nil then self.last = nil end return node.val end function LList:addToTail(val) if self.last == nil then self.last = { val = val } self.first = self.last else self.last.next = { val=val } self.last = self.last.next end end function LList:isEmpty return self.first == nil end

function Module:deliverAll(which, sendPulse) for _,dest in ipairs(self.dest) do   sendPulse(self.name, dest, which) end end function Module:attribs return string.format('label="%s%s" ', self.type or "", self.name) end

function Broadcaster:deliver(source, which, sendPulse) self:deliverAll(which, sendPulse) end function Broadcaster:attribs return Module.attribs(self) .. "shape=trapezium" end

function FlipFlop:deliver(source, which, sendPulse) if which == false then self.state = not self.state self:deliverAll(self.state, sendPulse) end end function FlipFlop:attribs return Module.attribs(self) .. "shape=box" end function FlipFlop:collectState(accum) if self.state then table.insert(accum, "1") else table.insert(accum, "0") end end

function Conj:deliver(source, which, sendPulse) self.inputs[source] = which local sawLow = false for _,v in pairs(self.inputs) do   if v == false then sawLow = true ; break end end self:deliverAll(sawLow, sendPulse) end function Conj:attribs return Module.attribs(self) .. "shape=ellipse" end function Conj:collectState(accum) for _,v in ipairs(self.inputList) do   if self.inputs[v] then table.insert(accum, "1") else table.insert(accum, "0") end end end

function pressButton(modules, watch, watchLevel) local low, high, rx, queue = 0,0,{},LList:new -- pressing the button sends a low pulse to the broadcaster, who sends -- a low pulse to each destination local function sendPulse(source, dest, which) if which then high = high + 1 else low = low + 1 end --if dest == watch and which == watchLevel then rx = rx + 1 end queue:addToTail({source=source, dest=dest, which=which}) end sendPulse(nil, 'broadcaster', false) -- button press to broadcaster local step = 1 while not queue:isEmpty do   local event = queue:pullFromHead local m = modules[event.dest] if m ~= nil then m:deliver(event.source, event.which, sendPulse) end if watch ~= nil and modules.tj.inputs[watch] == watchLevel then table.insert(rx, step) end step = step + 1 end return low, high, rx end

function part1(source) local modules = parse(source)

local lowSum, highSum = 0, 0 for i=1,1000 do   local low,high = pressButton(modules) lowSum = lowSum + low highSum = highSum + high end return lowSum * highSum end

-- PART 2 --

function dump_xdot(modules) print("digraph {") print(" {") for _,v in pairs(modules) do   local attribs = v:attribs print(string.format('%s [%s]', v.name, attribs)) end print("rx [shape=doubleoctagon]") print(" }") for _,v in pairs(modules) do   for _,d in ipairs(v.dest) do      print(string.format("%s -> %s ;", v.name, d)) end end print("}") end

-- disjoint pieces of the graph found by looking at xdot output local module_sets = { -- after 3932 presses matches state after 1 press -- after 3931 presses the output goes high { "kk", "fc", "xb", "lc", "bh", "pv", "vv", "sm", "hh", "bf", "qm", "fb", "dr", "nx" }, -- period 3918 presses matches state after 1 press -- after 3917 presses the output goes high { "sk", "pm", "xg", "mb", "mt", "st", "zc", "tb", "lg", "gd", "sr", "zv", "gv", "lq" }, -- after 3944 presses matches state after 1 press -- after 3943 presses output goes high { "vt", "nf", "dl", "sv", "ht", "ch", "xf", "zf", "cz", "zm", "hm", "hl", "pn", "kx" }, -- after 4058 presses matches state after 1 press -- after 4057 presses the state goes high { "xc", "jd", "gh", "vd", "dc", "gb", "qq", "ts", "sg", "mh", "pb", "rv", "nh", "rs" }, }

function collectState(modules, module_set) local accum = {} for _,v in ipairs(module_set) do   modules[v]:collectState(accum) end return table.concat(accum) end

function find_cycle(modules, module_set) local i = 1 local seen = {} local high = {} local output = module_set[1] seen[collectState(modules, module_set)] = { afterButton=0, {} } --print('Before', collectState(modules, module_set)) while true do   local _,_,rx = pressButton(modules, output, true) local s = collectState(modules, module_set) --print('After',i,s) if seen[s] ~= nil then --print(string.format("Found cycle after %d presses, matches after %d", i, seen[s].afterButton)) --       Conveniently enough, the desired output goes high one cycle before        the cycle loops over      -- for _,v in pairs(seen) do       if #(v[1]) > 0 then assert(v.afterButton == (i-1)) end end return seen[s].afterButton, i - seen[s].afterButton end seen[s] = { afterButton=i, rx } i = i + 1 end end

function part2(source) local totalCycleLength = BigNum:new(1) for i=1,4 do   -- parse from scratch as a hack to reset the module state local modules = parse(source) local start,cycle = find_cycle(modules, module_sets[i]) -- each cycle loops to the point after the first button press, -- oddly enough. assert(start == 1) totalCycleLength = totalCycleLength * cycle end -- we should add one step, to get to the initial cycle start point -- (remember, start == 1), but then subtract one step, because the -- event we're interested in, when our output goes high, happens one -- step before the cycle loops over. return totalCycleLength + (1 - 1) end

-- CLI ] ]-- local source = io.input("day20.input"):read("*a") --dump_xdot(parse(source)) print('Sum:', part1(source)) print('Fewest button presses:', part2(source)) --[ [ END CLI --

return { part1 = read_wiki_input(part1), part2 = read_wiki_input(part2), }

end)

local modules = {} modules['bit32'] = require('bit32') modules['string'] = require('string') modules['strict'] = {} modules['table'] = require('table') local function myrequire(name) if modules[name] == nil then modules[name] = true modules[name] = (builders[name])(myrequire) end return modules[name] end return myrequire('day20') end)