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.
- 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:
- 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.
- 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.
- 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.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.