Jump to content

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

From Wikipedia, the free encyclopedia
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*<syntaxhighlight[^>]*>", "", 1)
      source = source:gsub("</syntaxhighlight[^>]*>%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)()