Jump to content

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

From Wikipedia, the free encyclopedia
return (function()
local builders = {}
local function register(name, f)
  builders[name] = f
end
register('llpeg', function() return require [[Module:User:Cscott/llpeg]] end)

register('day8', function(myrequire)
--[[ DAY 8 ]]--
local l = myrequire('llpeg')
--local inspect = require 'inspect'

--[[ PARSING ]]--

local SKIP = l.P" " ^ 0
local nl = l.P"\n"
function tok(s) return l.P(s) * SKIP end

function record(graph, name, node)
   node.name = name
   graph[name] = node
   if graph._nodes == nil then graph._nodes = {} end
   table.insert(graph._nodes, name)
   return graph
end

local patt = l.P{
   l.Ct( l.V"Instructions" * nl^1 * l.V"Graph" * -1),
   Instructions = l.Cg( l.Ct( l.V"Dir" ^ 1 ), "instrs" ),
   Graph = l.Cg( l.Ct("") * ( (l.V("GraphNode") % record)^1 ), "graph" ),
   GraphNode = l.V"Location" * tok("=") * l.V"Node" * nl^0,
   Node = l.Ct( tok"(" * l.Cg( l.V"Location", "L" ) * tok"," *
               l.Cg( l.V"Location", "R" ) * tok")" ),
   Dir = l.C( l.S"LR" ) * SKIP,
   Location = l.C( l.R("AZ","09") ^ 1 ) * SKIP,
}

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))
   ast.nodes = ast.graph._nodes
   ast.graph._nodes = nil
   return ast
end

--[[ PART 1 ]]--

function part1(source)
   local input = parse(source)
   local steps = 0
   local location = "AAA"
   local i = 1
   while location ~= "ZZZ" do
      steps = steps + 1
      local dir = input.instrs[i]
      location = input.graph[location][dir]
      i = i + 1
      if i > #input.instrs then i = 1 end
   end
   return steps
end

--[[ Part 2 ]]--

function find_cycle(input, location)
   local start = location
   local stops = {}
   local steps = 0
   local i = 1
   local seen = {}
   local path = {}
   repeat
      seen[location .. "-" .. i] = steps
      table.insert(path, location)
      local node = input.graph[location]
      if node.stop then table.insert(stops, steps) end
      local dir = input.instrs[i]
      --table.insert(path, dir)
      location = node[dir]
      steps = steps + 1
      i = i + 1
      if i > #input.instrs then i = 1 end
   until seen[location.."-"..i] ~= nil
   table.insert(path,location)

   -- after 'inital' steps we start the cycle
   local initial = seen[location.."-"..i]
   -- after 'initial + n*cyclelength' steps we are at the same place
   local cyclelength = steps - initial
   -- path[s+1] is where we are at after step `s` (silly 1-based language)
   assert(path[1+initial] == path[1+initial+cyclelength])
   -- subtract the initial portion from the stops
   -- after 'n*cyclelength + stop' steps we are at a stop; note that the
   -- 'initial' portion is already included in the 'stop' amount
   assert(string.sub(path[1+stops[1]], -1) == "Z")
   return cyclelength, stops
end

function gcd(a,b) -- good ol' euclid
   while a ~= b do
      if a > b then
         a = a - b
      else
         b = b - a
      end
   end
   return a
end

function lcm(nums)
   local partial = {}
   for i=1,#nums do
      partial[i] = nums[i]
   end
   local prod = 1
   for i=1,#nums do
      -- remove common factors of i from the rest of the list
      for j=i+1,#nums do
         partial[j] = math.floor(partial[j] / gcd(partial[i], partial[j]))
      end
      prod = prod * partial[i]
   end
   return prod
end

function part2(source)
   local input = parse(source)
   -- post process!
   local location = {}
   for _,name in ipairs(input.nodes) do
      local node = input.graph[name]
      local lastchar = string.sub(name, -1)
      if lastchar == "A" then
         node.start = true
         table.insert(location, name) -- collect starting locations
      elseif lastchar == "Z" then
         node.stop = true
      end
   end
   -- print(inspect(input))
   -- find cycles for each starting location
   local nums = {}
   for i=1,#location do
      local cyclelen,stops = find_cycle(input, location[i])
      -- luckily there's only one stop per cycle!
      assert(#stops == 1) -- not guaranteed!
      -- luckily the stop position is exactly the same as the cyclelength
      assert(cyclelen == stops[1]) -- not guaranteed!
      -- solve simultaneous equations:
      --   a*cycle1 + stop1 = b*cycle2 + stop2 = c*cycle3 + stop3 = ...
      -- because we're so lucky, this is just
      --   a*cycle1 = b*cycle2 = c*cycle3 = ..
      -- which is just the least common multiple of cycle1..etc
      -- if we were *really* lucky these would all be primes, but alas.
      --print(cyclelen,inspect(stops))
      table.insert(nums, cyclelen)
   end
   return lcm(nums)
end

-- CLI version
--[[
local source = io.input("day8.input"):read("a")
print(part1(source))

print(part2(source))
]]--

return {
   part1 = function(frame)
      local s = mw.title.new(frame.args[1]):getContent()
      return part1(s)
   end,
   part2 = function(frame)
      local s = mw.title.new(frame.args[1]):getContent()
      return part2(s)
   end,
}

end)

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