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*<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)()

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.