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

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

register('pqueue', function(myrequire) -- Priority Queue implemented in lua, based on a binary heap. Copyright (C) 2017 Lucas de Morais Siqueira  License: zlib  This software is provided 'as-is', without any express or implied  warranty. In no event will the authors be held liable for any damages  arising from the use of this software.  Permission is granted to anyone to use this software for any purpose,  including commercial applications, and to alter it and redistribute it  freely, subject to the following restrictions:  1. The origin of this software must not be misrepresented; you must not     claim that you wrote the original software. If you use this software     in a product, an acknowledgement in the product documentation would be     appreciated but is not required.  2. Altered source versions must be plainly marked as such, and must not be     misrepresented as being the original software.  3. This notice may not be removed or altered from any source distribution. -- -- modified by xxopxe@gmail.com

local floor = math.floor

local PriorityQueue = {} PriorityQueue.__index = PriorityQueue

setmetatable( PriorityQueue,  {    __call = function       local new = {}      setmetatable(new, PriorityQueue)      new:initialize      return new    end  } )

function PriorityQueue:initialize -- Initialization.    Example:        PriorityQueue = require "priority_queue"        pq = PriorityQueue    -- self.heap_val = {} self.heap_pri = {} self.current_size = 0 end

function PriorityQueue:empty return self.current_size == 0 end

function PriorityQueue:size return self.current_size end

function PriorityQueue:swim -- Swim up on the tree and fix the order heap property. local heap_val = self.heap_val local heap_pri = self.heap_pri local floor = floor local i = self.current_size

while floor(i / 2) > 0 do   local half = floor(i / 2) if heap_pri[i] < heap_pri[half] then heap_val[i], heap_val[half] = heap_val[half], heap_val[i] heap_pri[i], heap_pri[half] = heap_pri[half], heap_pri[i] end i = half end end

function PriorityQueue:put(v, p) -- Put an item on the queue.    Args:        v: the item to be stored        p(number): the priority of the item    -- -- self.current_size = self.current_size + 1 self.heap_val[self.current_size] = v self.heap_pri[self.current_size] = p  self:swim end

function PriorityQueue:sink -- Sink down on the tree and fix the order heap property. local size = self.current_size local heap_val = self.heap_val local heap_pri = self.heap_pri local i = 1

while (i * 2) <= size do   local mc = self:min_child(i) if heap_pri[i] > heap_pri[mc] then heap_val[i], heap_val[mc] = heap_val[mc], heap_val[i] heap_pri[i], heap_pri[mc] = heap_pri[mc], heap_pri[i] end i = mc end end

function PriorityQueue:min_child(i) if (i * 2) + 1 > self.current_size then return i * 2 else if self.heap_pri[i * 2] < self.heap_pri[i * 2 + 1] then return i * 2 else return i * 2 + 1 end end end

function PriorityQueue:pop -- Remove and return the top priority item local heap_val = self.heap_val local heap_pri = self.heap_pri local retval, retprio = heap_val[1], heap_pri[1] heap_val[1], heap_pri[1] = heap_val[self.current_size], heap_pri[self.current_size] heap_val[self.current_size], heap_pri[self.current_size] = nil, nil self.current_size = self.current_size - 1 self:sink return retval, retprio end

function PriorityQueue:peek -- return the top priority item return self.heap_val[1], self.heap_pri[1] end

return PriorityQueue

end)

register('day14', function(myrequire) -- DAY 14 -- local l = myrequire('llpeg') local PriorityQueue = myrequire('pqueue')

-- PARSING -- local Spot = {} Spot.__index = Spot function Spot:new(args) return setmetatable(args, self) end function Spot:isEmpty return not(self.round or self.cube) end function Spot:__tostring if self.round then return "O" end if self.cube then return "#" end return "." end

local nl = l.P"\n"

function make_spot(s) if s == "#" then return Spot:new{cube=true} end if s == "O" then return Spot:new{round=true} end return Spot:new{} end

local patt = l.P{  "Graph", Graph = l.Ct( l.V"Row" * (nl^1 * l.V"Row")^0 * nl^0) * -1, Row = l.Ct( l.V"Spot"^1 ), Spot = l.S"#O." / make_spot, }

local Graph = {} Graph.__index = Graph

function parse(source) --print(inspect(source)) local ast, errlabel, pos = patt:match(source) if not ast then error(string.format("Error at pos %s label '%s'", pos, errlabel)) end --print('Parsed with success!') --print(inspect(ast)) return Graph:new(ast) end

-- Part 1 --

function Graph:new(data) return setmetatable({ data=data }, self) end

function Graph:at(row,col,default) return (self.data[row] or {})[col] or default end

function Graph:set(row,col,val) if self.data == nil then self.data = {} end if self.data[row] == nil then self.data[row] = {} end self.data[row][col] = val end

function Graph:rowN return #(self.data) end

function Graph:colN return #(self.data[1]) end

function Graph:print for r,row in ipairs(self.data) do     for c,val in ipairs(row) do         if val == nil then val = " " end io.write(tostring(val)) end io.write("\n") end end

function Graph:link for r=1,self:rowN do     for c=1,self:colN do         local sp = self:at(r,c) sp.r, sp.c = r,c if r > 1 then sp.n = self:at(r-1,c) end if c > 1 then sp.w = self:at(r,c-1) end if r < self:rowN then sp.s = self:at(r+1,c) end if c < self:colN then sp.e = self:at(r,c+1) end --print(string.format("%d,%d %s %s %s %s", sp.r,sp.c,sp.n,sp.e,sp.s,sp.w)) end end end

function Graph:numberRocks local rocks = {} local num = 1 for r=1,self:rowN do     for c=1,self:colN do         local sp = self:at(r,c) if sp.round then sp.id = num num = num + 1 table.insert(rocks, sp) end end end return rocks end

-- the cube rocks partition each row or column, and after each -- roll all the rocks are going to pile up at each. so we just -- need to know for a given rock its (wlog) col, compare that to -- the closest cube in the row, and move it adjacent to the cube. -- if each cube keeps track of how many rocks have moved beside it, -- we can space them out appropriately. This makes rocks move -- through each other! ie doesn't actually respect the rock identity, -- but the results are correct.

-- HOWEVER, this way also works, and is "fast enough" function Graph:roll(rocks, dir, keyFunc) local Q = PriorityQueue for _,sp in ipairs(rocks) do     Q:put(sp, keyFunc(sp)) end -- now start rolling ! while not Q:empty do     local sp = Q:pop local neigh = sp[dir] if neigh ~= nil and neigh:isEmpty then -- can we roll here? local id = sp.id        sp.round = false sp.id = nil neigh.round = true neigh.id = id        rocks[id] = neigh Q:put(neigh, keyFunc(neigh)) end end end

function Graph:debug_cycle(rocks) print("Roll north:") self:roll(rocks, "n", function(sp) return sp.r end) self:print print

print("Roll west:") self:roll(rocks, "w", function(sp) return sp.c end) self:print print

print("Roll south:") self:roll(rocks, "s", function(sp) return -sp.r end) self:print print

print("Roll east:") self:roll(rocks, "e", function(sp) return -sp.c end) end

function Graph:cycle_one(rocks) self:roll(rocks, "n", function(sp) return sp.r end) self:roll(rocks, "w", function(sp) return sp.c end) self:roll(rocks, "s", function(sp) return -sp.r end) self:roll(rocks, "e", function(sp) return -sp.c end) end

function Graph:cycle(rocks, n)  local seen = {} local i=1 while i <= n do     local h = self:hash if seen[h] == nil then seen[h] = i        self:cycle_one(rocks) i = i + 1 else -- short cut! local s = seen[h] -- after s steps we're at a cycle point -- we'll be back here/there in (i-s) more steps -- so skip ahead as many (i-s) steps as we can local cycles = math.floor((n - i) / (i - s)) -- print(i,s,cycles) i = i + cycles*(i-s) while i<=n do           self:cycle_one(rocks) i = i + 1 end return end end end

function Graph:hash local t = {} for r=1,self:rowN do     for c=1,self:colN do         local sp = self:at(r,c) if sp.cube then -- skip, cubes never move else table.insert(t, tostring(sp)) end end end return table.concat(t) end

function Graph:score local sum = 0 for r=1,self:rowN do     local row_score = 1 + self:rowN - r      for c=1,self:colN do         local sp = self:at(r,c) if sp.round then sum = sum + row_score end end end return sum end

function part1(source) local graph = parse(source) graph:link --graph:print --print local rocks = graph:numberRocks graph:roll(rocks, "n", function(sp) return sp.r end) --graph:print return graph:score end

function part2(source) local graph = parse(source) graph:link --graph:print --print local rocks = graph:numberRocks graph:cycle(rocks, 1000000000) --graph:print return graph:score end

-- CLI ] ]-- local source = io.input("day14.input"):read("a") print('Sum:', part1(source)) print('Sum:', part2(source)) --[ [ END CLI --

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, tonumber(frame.args[2])) 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('day14') end)