2020 Programming Advent

The example in day 17 confused the crap out of me. It would have been nice if they'd mentioned that they were recentering with each cycle.

I didn't get a very fast solution to part 2, and I'd be interested in the performance of your JS solution @Kosher Salt.

I naively used a map from vectors represented as lists of ints, which meant that I could solve both parts together and then just pick the number of dimensions.

EDIT: Got a faster solution now. Does part 2 in around 0.3s, and it can get a result for 5 dimensions in <10s.
Yeah. The re-centering part took me a moment to figure out.

I was being clever and automatically shifting the grid by 1 in each dimension (also expanding it by 2 in each direction) so that I wouldn't have to deal with negative indices. Comparing my output to the example before I started trimming it, I thought that for some reason my code was screwing up just in one dimension, which would've been strange. Looking closer at the example though I realized what they were doing.
 
There were some fairly obvious algorithmic complexity improvements to my solution, but nothing available immediately from Haskell's standard library.

My basic approach uses only pure immutable data structures.

The infinite world is a map, which in standard Haskell are backed by binary trees ordered by the key. I took those keys to be n-ary vectors. So to find whether a point is active, I just test to see if its vector is in the map.

To compute the numbers of neighbours, I translated the map in all the unit directions, and merged them with addition, starting at 1. The result is that, if you look up a vector in the map, it'll tell you the number of adjacent active points.

I then went through neighbourhood map and for each key value pair, tested whether the key is in the original. I then use this information with the count to decide whether to stay active or go inactive.

The merges are algorithmically fast, since they use a hedge union, but I only get to do that when combining the translated maps. The main update still does a full map lookup for every cell with a non-zero number of neighbours. Worse, translating the maps is horrible, because adding a vector to all the keys will completely screw up the (lexicographic) ordering, meaning that the tree has to be rebuilt.

For a second attempt, I used a trie, where you use the first component of the vector as a key to lookup another trie. You then use the second component of the vector to lookup another trie. And so on, until you've exhausted the vector. So it's a bunch of nested maps:

Code:
data Trie k a = Value a
              | Map (M.Map k (Trie k a))
              deriving (Functor, Foldable)

Translating such a trie doesn't mess with any structure, so it can be done very fast just by mapping over the keys. Next, I used a special purpose merging function so that I could combine the neighbourhood counts with the original map as a hedge merge. The solution then does absolutely no lookups. Everything is just hedge merges and mapping over the keys.

It does 4 dimensions in 0.14s, 5 dimensions in 4s and copes with 6 dimensions in 90s.
 
  • Like
Reactions: Kosher Salt
I feel like these are scaling up in difficulty pretty rapidly. Day 20 initially sounded like a pretty brutal jigsaw-solving puzzle, although it ended up taking a lot less brute force than expected. It would've been nice if they had said that edges are guaranteed to only have an exact match with the correct piece, as that completely eliminated the possibility of reaching a dead end and having to backtrack.

If you map the tile edges as a pair of codes that represent the forward and backward bitmap along that edge, then check all the tiles to see how many times that code appears, you should find that each edge code appears exactly once (for external edges, i.e. edge pieces of the jigsaw) or twice (for internal edges, i.e. two pieces that fit together). Exactly four pieces should have two external edges that form a corner, so you can just pick any of them and assemble the whole puzzle by finding the piece that has the same edge, rotating them so the edges match, and setting them into place. (Because rotations and flips are possible, each puzzle piece has exactly 8 possible orientations; rather than trying to intelligently make the exact transformation required, I just tried all 8 of them until I found the right one.)
 
I feel like these are scaling up in difficulty pretty rapidly. Day 20 initially sounded like a pretty brutal jigsaw-solving puzzle, although it ended up taking a lot less brute force than expected. It would've been nice if they had said that edges are guaranteed to only have an exact match with the correct piece, as that completely eliminated the possibility of reaching a dead end and having to backtrack.
The difficulty is all over the place and is nothing compared to the 2019AoC, when the problems were actually interesting, thought-out and well written. This is the laziest AoC shite I've seen and I've been doing them since 2017. It seems as if they completely changed the person responsible for the task - they're weak, mostly unimaginative and confusingly worded. Hell, they even go against the Eric Wastl's own "good practices" on problem descriptions.
 
Day 20 took me 102 lines of code (including whitespace, imports and type-signatures). My next longest solution is day 8 at 58 lines. It was bullshit again that they didn't specify that each tile uniquely determines its neighbours and their orientation. Without that assumption, the problem looks way harder.

Anyway, each day's setting is so badly contrived it's almost funny.

You're on a plane. It's boring, and I can't figure out any puzzles that would arise here. Umm, maybe play around with some random computer socket which we'll say has some encryption scheme you can solve for no reason.

You're on a boat. I can't think of any natural challenges here either. So, umm, oh, the elves back at the North Pole have contacted by satellite phone saying they've got a random game they want you to play.

You're on a raft. There's a...crab? The crab wants to...play a game with cards?

You're at a hotel. They're retiling the floor. Surely that's relevant to you?
 
  • Like
Reactions: Kosher Salt
I don't feel like figuring out the "clever" solution for day 23 part 2, so fuck it, I'm just gonna let it run until it finishes. It should only take a few hours or so, I hope. edit: based on the progress so far, I'd say roughly 10 hours.
 
Last edited:
I don't feel like figuring out the "clever" solution for day 23 part 2, so fuck it, I'm just gonna let it run until it finishes. It should only take a few hours or so, I hope. edit: based on the progress so far, I'd say roughly 10 hours.
I'm genuinely curious as to what your non-clever solution is. I've literally took the solution for the first part, changed a few constants, recompiled and had the answer in a few seconds.
 
I'm genuinely curious as to what your non-clever solution is. I've literally took the solution for the first part, changed a few constants, recompiled and had the answer in a few seconds.
I'm on yesterday's challenge, moving cups around in a million-cup circle ten million times.
 
I'm on yesterday's challenge, moving cups around in a million-cup circle ten million times.
Yes, this I understand, but how did you manage to make it so slow? I mean, I can't think of a solution that would be convoluted enough. I'm not being facetious or spiteful - the only brute force solution I can think of (make an array of all the cups and perform deletions, searches and insertions in O(n)) is actually harder to implement than slapping an associative container and copypasta some code on top.
 
the only brute force solution I can think of (make an array of all the cups and perform deletions, searches and insertions in O(n)) is actually harder to implement than slapping an associative container and copypasta some code on top
Well, it wasn't really hard, just slow...
 
I'm genuinely curious as to what your non-clever solution is. I've literally took the solution for the first part, changed a few constants, recompiled and had the answer in a few seconds.
In Haskell, the shortest solution I can think of to part 1 is to use lists, where it's dead easy to match off the next 4 elements, split at the next destination, and append the results together. It definitely doesn't scale to part 2. I was back to unboxed mutable vectors (where an element v at index i means that the cup numbered i is immediately followed by the cup numbered v).

I'm curious what you propose to copypasta?
 
Last edited:
I'm curious what you propose to copypasta?
Doing three removals/insertions by hand instead of doing the Right Thing and splicing out/in the relevant part of a linked list (however it's being implemented with - I used an associative array). Because the removals and insertions are always of size 3 I was being lazy. In the general case it could be done in O(1) even if the part to be cut out was in the orders of O(n).

As an aside: this year's AoC left me irritated with its lameness. I'm thinking about setting, from time to time, small weekend-ish programming contest on onlinejudge.org for fellow Farmers. @cecograph, @Kosher Salt would you be interested in participating? If we have at least three people, I think I might set up a dedicated thread on the forums and therefore maybe grab a few more people.
 
Doing three removals/insertions by hand instead of doing the Right Thing and splicing out/in the relevant part of a linked list (however it's being implemented with - I used an associative array). Because the removals and insertions are always of size 3 I was being lazy. In the general case it could be done in O(1) even if the part to be cut out was in the orders of O(n).
You definitely don't just want a linked list, because then lookup of the destination cup is O(n). That's what I tried first in Haskell, and it wasn't going to work for part 2. I guessed @Kosher Salt had something similar.

How did you guys solve 18 and 19? I allow myself parser combinators in these puzzles, because there are some in the standard library.

As an aside: this year's AoC left me irritated with its lameness. I'm thinking about setting, from time to time, small weekend-ish programming contest on onlinejudge.org for fellow Farmers. @cecograph, @Kosher Salt would you be interested in participating? If we have at least three people, I think I might set up a dedicated thread on the forums and therefore maybe grab a few more people.
I'm potentially interested. I don't know much about that site though.
 
You definitely don't just want a linked list, because then lookup of the destination cup is O(n). That's what I tried first in Haskell, and it wasn't going to work for part 2. I guessed @Kosher Salt had something similar.
I'm using hashmap to implement a linked list, so I get expected O(1) per lookup. And even without the implementation-provided hashmap I can create a true linked list myself while storing pointers to cups using an array and have a guaranteed O(1) lookup.
I'm potentially interested. I don't know much about that site though.
Check out https://onlinejudge.org/. Word of warning though: the supported languages are C, C++, Java, Pascal and Python.

How did you guys solve 18 and 19? I allow myself parser combinators in these puzzles, because there are some in the standard library.
18: Simple recursive-descent parser: recurse on '(', return subexpression value on ')'.
19: Treat rule dependency as a graph, traverse and substitute accordingly. Push final rule into an implementation-provided regular expression library.
 
I'm using hashmap to implement a linked list, so I get expected O(1) per lookup.
That's not a linked list. A (singly-) linked list is just a head consisting of an element and a pointer to the rest of the linked list. They have specific computational properties, one of which is element lookup being O(n). So it's not the right data structure here. If you shove every node into a hashmap, that's a different data structure.

And even without the implementation-provided hashmap I can create a true linked list myself while storing pointers to cups using an array and have a guaranteed O(1) lookup.
The cups are numbered contiguously from 1 to 1,000,000, so there's no reason to think of hashmaps at all. Just use a 1d array, where indices are cup numbers, and each element is the number of the next cup.

Check out https://onlinejudge.org/. Word of warning though: the supported languages are C, C++, Java, Pascal and Python.
Ah. I'll have to pass.

18: Simple recursive-descent parser: recurse on '(', return subexpression value on ')'.
19: Treat rule dependency as a graph, traverse and substitute accordingly. Push final rule into an implementation-provided regular expression library.
For 18, did you write the parser from scratch, or use a library? I'd be interested to read your solution if you're happy to share. Here's my Haskell (without blank lines and type signatures):

Code:
import Control.Applicative
import qualified Text.ParserCombinators.ReadP as P

token = (<* P.skipSpaces)
readInt = token (P.readS_to_P reads)
parseAtom expr atom =
  P.between (token . P.char $ '(') (token . P.char $ ')') expr <|> atom
parseAdd = token (P.char '+') *> pure (+)
parseMult = token (P.char '*') *> pure (*)
parseExpr1 = P.chainl1 (parseAtom parseExpr1 readInt) (parseAdd <|> parseMult)
parseExpr2 = P.chainl1 (parseAtom parseExpr2 parseAddExpr) parseMult
  where parseAddExpr = P.chainl1 (parseAtom parseExpr2 readInt) parseAdd

part1 = sum . fmap (fst . head . P.readP_to_S (parseExpr1 <* P.eof)) . lines
        <$> readFile "18.input"
part2 = sum . fmap (fst . head . P.readP_to_S (parseExpr2 <* P.eof)) . lines
        <$> readFile "18.input"
 
Last edited:
How did you guys solve 18 and 19? I allow myself parser combinators in these puzzles, because there are some in the standard library.
For day 18 I just wrote a simple parser. First it replaced parenthetical subexpressions with their value, starting from the innermost (so that there were no internal parentheses; this was done recursively but because the subexpressions did not contain subexpressions themselves, the recursion only went 1 level deep). When the expression contains no more parentheses, it replaces the first sum or product with its value, and repeats until it has reduced the expression to a single value. I used regular expressions for all of the replacement matching.

Day 19 I transformed all of the rules into regular expressions. I wrote a function that would return the regular expression for rule n, which are cached and created on demand. Calling that function and requesting the expression for rule 0 will recursively get or create the regular expressions for all of the rules it depends on, and finally create the expression for rule 0 and return it. I then added ^ and $ symbols so it would only match a complete string, and counted how many of the strings were matched.
Check out https://onlinejudge.org/. Word of warning though: the supported languages are C, C++, Java, Pascal and Python.
I really haven't touched C++ since college.
 
  • Like
Reactions: cecograph
That's not a linked list. A (singly-) linked list is just a head consisting of an element and a pointer to the rest of the linked list. They have specific computational properties, one of which is element lookup being O(n). So it's not the right data structure here. If you shove every node into a hashmap, that's a different data structure.
We're in a potatoe/potatoh mode. Pedantically speaking, I did not implement a linked list according to this definition. Logically speaking, I did.
The cups are numbered contiguously from 1 to 1,000,000, so there's no reason to think of hashmaps at all. Just use a 1d array, where indices are cup numbers, and each element is the number of the next cup.
...and this is precisely what I did, except I kept the hashmap for some retarded reason (probably thought it would come in handy in part B and forgot to simplify it later). And yes, I would call such an array storing the number of next element a "linked list". Might be just a case of a mental shortcut I use when describing solutions.

For 18, did you write the parser from scratch, or use a library? I'd be interested to read your solution if you're happy to share. Here's my Haskell (without blank lines and type signatures):
Written from scratch, solution for part B:
Code:
long solve(const std::string &s, unsigned &idx)
{
    std::vector <long> num;
    char op = 0;
    while (s[idx] != ')') {
        if (s[idx] == ' ') {
            ++idx;
        } else if (s[idx] == '(') {
            ++idx;
            long tmp = solve(s, idx);

            if (op == '+')
                num.back() += tmp;
            else
                num.push_back(tmp);
        } else if (isdigit(s[idx])) {
            long tmp = 0;
            while (isdigit(s[idx])) {
                tmp = tmp * 10 + s[idx] - '0';
                ++idx;
            }

            if (op == '+')
                num.back() += tmp;
            else
                num.push_back(tmp);
        } else {
            assert(s[idx] == '+' || s[idx] == '*');
            op = s[idx];
            ++idx;
        }
    }
    ++idx;

    long result = 1;
    for (auto n : num)
        result *= n;
    return result;
}
Note: external to this function, I've appended a single ')' symbol to the input string to avoid complicating the end condition for the loop.

[EDIT]
Only now I've spotted how buggy this solution is and how weak the AoC's testcases were. At the risk of repeating myself I'm going to remark once again, that this year's AoC was a total shitshow and an embarassment.
 
Last edited:
  • Like
Reactions: cecograph
Back