Introducing Crunchy Data Warehouse: A next-generation Postgres-native data warehouse. Crunchy Data Warehouse Learn more
Tutorial Instructions
This puzzle was overall very straightforward. The tech we used this time:
For this challenge, the goal was to put packets in order in which each pair consisted of deeply nested arrays like so:
[1,1,3,1,1]
[1,1,5,1,1]
[[1],[2,3,4]]
[[1],4]
[9]
[[8,7,6]]
[[4,4],4,4]
[[4,4],4,4,4]
[7,7,7,7]
[7,7,7]
[]
[3]
[[[]]]
[[]]
[1,[2,[3,[4,[5,6,7]]]],8,9]
[1,[2,[3,[4,[5,6,0]]]],8,9]
There are specific rules for determining the correct order based on the numeric values, how to handle arrays compared to non-arrays, etc. While I suspect a lot of solutions jumped to trying to parse the input as actual arrays or json objects and then evaluate them, I immediately saw this as a bit-by-bit type of challenge.
First, I verified some assumptions and made observations about the data:
10
. So if we can change that 10 to something else, things get a lot simpler as we only need to parse a single character at a time.We can slurp the pairs of packets to compare into a table, with each pair going on a single row, thanks to nextval()
for the line number, lag()
to grab the previous line, and a special WHERE
clause to only grab the correct line so we have two entries:
DROP SEQUENCE if exists aoc;
CREATE SEQUENCE aoc;
DROP TABLE if exists signal;
CREATE UNLOGGED TABLE signal(id int, r text, l text);
WITH x AS (SELECT line as l, lag(line) over() AS r FROM aoc_day13)
,y AS (SELECT nextval('aoc') AS id, * FROM x WHERE length(l) >1 AND length(r) > 1)
INSERT INTO signal SELECT * FROM y;
SELECT * FROM signal LIMIT 10;
id | r | l
----+-----------------------------+-----------------------------
2 | [1,1,5,1,1] | [1,1,3,1,1]
3 | [[1],4] | [[1],[2,3,4]]
4 | [[8,7,6]] | [9]
5 | [[4,4],4,4,4] | [[4,4],4,4]
6 | [7,7,7] | [7,7,7,7]
7 | [3] | []
8 | [[]] | [[[]]]
9 | [1,[2,[3,[4,[5,6,0]]]],8,9] | [1,[2,[3,[4,[5,6,7]]]],8,9]
(8 rows)
Now we need a function that walks through both sides, left and right, and compares them character by character:
CREATE or replace FUNCTION signal_sifter1()
returns int language plpgsql as $$
DECLARE
myrec record; lpos int; rpos int; myindex int = 0;
lval int; rval int; in_order bool; correctq int[];
BEGIN
FOR myrec in SELECT * FROM signal LOOP
rpos = 0; lpos = 0; myindex = myindex + 1;
myrec.l = regexp_replace(myrec.l, '10', ':', 'g');
myrec.r = regexp_replace(myrec.r, '10', ':', 'g');
LOOP
lpos = lpos + 1; rpos = rpos + 1;
IF rpos > length(myrec.r) THEN in_order = false; EXIT; END IF;
IF lpos > length(myrec.l) THEN in_order = true; EXIT; END IF;
SELECT INTO lval ascii( substr(myrec.l,lpos,1) );
SELECT INTO rval ascii( substr(myrec.r,rpos,1) );
IF lval = rval THEN CONTINUE; END IF;
IF lval BETWEEN 48 and 58 AND rval BETWEEN 48 and 58 THEN
in_order = CASE WHEN lval > rval THEN false ELSE true END;
EXIT;
END IF;
IF lval = 93 THEN in_order=true; EXIT; END IF;
IF rval = 93 THEN in_order=false; EXIT; END IF;
IF rval = 91 AND lval BETWEEN 48 and 58 THEN
myrec.l = overlay(myrec.l placing '['||chr(lval)||']' from lpos for 1);
CONTINUE;
END IF;
IF lval = 91 AND rval BETWEEN 48 and 58 THEN
myrec.r = overlay(myrec.r placing '['||chr(rval)||']' from rpos for 1);
CONTINUE;
END IF;
RAISE EXCEPTION 'Cannot handle this case!';
END LOOP;
IF in_order THEN correctq = correctq || myindex; END IF;
END LOOP;
lval = 0; rval = 0;
LOOP
lval = lval + 1;
IF correctq[lval] IS NULL THEN EXIT; END IF;
rval = rval + correctq[lval];
END LOOP;
RETURN rval;
END;
$$;
Running it produces the correct answer:
SELECT signal_sifter1();
signal_sifter1
----------------
4863
Part Two called for sorting all the packets, then adding in a custom pair of items, and see where they fall in the sorted outout. Rather than worrying about actually sorting the entire list, we can simply see how many packets fall before or after each of the custom pairs:
CREATE or replace FUNCTION signal_sifter2()
returns int language plpgsql as $$
DECLARE
myrec record; lpos int; rpos int;
lval int; rval int; in_order bool;
oldstring text; newstring text;
before int = 1; after int = 2;
BEGIN
FOR myrec in SELECT * FROM signal LOOP
myrec.l = regexp_replace(myrec.l, '10', ':', 'g');
myrec.r = regexp_replace(myrec.r, '10', ':', 'g');
FOR oldstring IN values (myrec.l), (myrec.r) LOOP
FOR newstring IN values ('[[2]]'), ('[[6]]') LOOP
rpos = 0; lpos = 0;
LOOP
lpos = lpos + 1; rpos = rpos + 1;
IF lpos > length(oldstring) THEN in_order = true; EXIT; END IF;
IF lpos > length(newstring) THEN in_order = true; EXIT; END IF;
SELECT INTO lval ascii( substr(oldstring,lpos,1) );
SELECT INTO rval ascii( substr(newstring,rpos,1) );
IF lval = rval THEN CONTINUE; END IF;
IF lval BETWEEN 48 and 58 AND rval BETWEEN 48 and 58 THEN
in_order = CASE when lval > rval THEN false ELSE true END;
EXIT;
END IF;
IF lval = 93 THEN in_order=true; EXIT; END IF;
IF rval = 93 THEN in_order=false; EXIT; END IF;
IF rval = 91 AND lval BETWEEN 48 and 58 THEN
oldstring = overlay(oldstring placing '['||chr(lval)||']' from lpos for 1);
CONTINUE;
END IF;
IF lval = 91 AND rval BETWEEN 48 and 58 THEN
newstring = overlay(newstring placing '['||chr(rval)||']' from rpos for 1);
CONTINUE;
END IF;
RAISE EXCEPTION 'Cannot handle this case!';
END LOOP;
IF in_order THEN
IF newstring ~ '2' THEN before = before + 1; ELSE after = after + 1; END IF;
END IF;
END LOOP;
END LOOP;
END LOOP;
RETURN before * after;
END;
$$;
Once again, running it gives us the correct answer quickly:
SELECT signal_sifter2();
signal_sifter2
----------------
21942
(1 row)
Loading terminal...
Loading terminal...