Advent of Code 2020

OMG!! I ranked in the top 1000 for day 19:

It was an interesting challenge, which I solved in a very interesting (and potentially unusual) way.

The part 2 twist is really twisted, too. He pretty much tells you you’ll have to do something specific to the problem as the general approach would require way too much work.

The presumed logical approach to dealing with this problem would be a state machine… you have states you enter into and leave via rule numbers. And with some back tracking (read recursion) you could probably get that approach to work for part 1. I thought of that before deciding my approach, and that is basically what regex does for you… it builds a state machine with backtracking support. So I figure I would have my code translate the rules into a regex string and let the regex pattern matcher do all the work. That worked great for part 1, here’s a sample of the output:
Number of rules: 130
regexStr: (a((b(a((a(ba|ab)|b(bb|aa))b|(a(aa|b(a|b))|bba)a)|b((bab|(a(a|b)|ba)a)b|(baa|(aa|ba)b)a)) … very truncated …
Number of messages: 458

Part 2 throws in a serious twist thou… the expressions can now be self-referential… My approach for this was kind of ugly, but it allowed me to trap out the changes needed without having to rewrite all my code:

  private void _buildRecursive(final StringBuilder sb, final int ruleNumber)
  {
    if (inRule2Mode)
    {
      if (ruleNumber == 8)
      {
        handlePart2Rule8(sb);
        return;
      }
      if (ruleNumber == 11)
      {
        handlePart2Rule11(sb);
        return;
      }
    }

then all I had to do was figure out what changes to make to handle the two new rules. For one, it would match inputs like a, aa, aaa, aaaa, aaaaa, and so on, so a simple (a+) matches one more more a’s. For the second one, that was harder. Here you want to match patters where there are the same number of a’s as b’s like: ab aabb aaabbb aaaabbbb and so on. General regex is not well equipped for this, you can say a+b+ but that would match a different number of a’s and b’s. (I did try to cheat with this approach, but I generated a wrong (too high) number, and also got the warning about how it interestingly matched someone else’s answer.)

The only way I could think to handle this in regex was to make pretty long set of specific rules specifying the numbers. So I generated a regex containing (ab|a{2}b{2}|a{3}b{3}|a{4}b{4}|a{5}b{5}| … and had to decide how high to go. I stopped at 20, which was apparently enough for this problem.

Here’s my main code:

final RuleWrangler rw = new RuleWrangler();

final RuleReader rr = new FileRuleReader(FILE);
int numberOfRules = 0;
while (rr.isMoreInput())
{
  final String rule = rr.getNextRule();
  numberOfRules++;
  rw.addRule(rule);
}
System.out.println("Number of rules: " + numberOfRules);
final Pattern p1 = rw.getMasterPattern(0);

final MessageReader mr = new FileMessageReader(FILE);
int numberOfMessages = 0;
int numberOfValidMessages = 0;
while (mr.isMoreInput())
{
  final String message = mr.getNextMessage();
  numberOfMessages++;
  final Matcher m = p1.matcher(message);
  if (m.matches()) numberOfValidMessages++;
}
System.out.println("Number of messages: " + numberOfMessages);
System.out.println("Part 1: " + numberOfValidMessages);

rw.toggleToPart2Mode();
final Pattern p2 = rw.getMasterPattern(0);

final MessageReader mr2 = new FileMessageReader(FILE);
int numberOfValidMessages2 = 0;
while (mr2.isMoreInput())
{
  final String message = mr2.getNextMessage();
  final Matcher m = p2.matcher(message);
  if (m.matches()) numberOfValidMessages2++;
}
System.out.println("Number of messages: " + numberOfMessages);
System.out.println("Part 2: " + numberOfValidMessages2);
1 Like

I ended up writing it as a recursive generator.

The generator yields possible remainder strings from applying the given rule to the given string. Character rules yield the string with that character removed from the front, which is the only way a string can get shorter. As soon as the top-level call returns an empty string, we know that the rule matched in some way, and we short-circuit the rest of the search.

Checking for the empty string prohibits leftover characters; and if we reach the empty string while there are still character rules to apply, we won’t find those characters at the beginning of the string, so it gets filtered out.

I was able to use my exact Part 1 code for Part 2, aside from adding the two changed rules. Here’s Part 2 with only superficial cleanup. The parsing code is not included.

def part2(data, first_rule=0):
    rules, msgs = data

    rules[8] = [(42,), (42, 8)]
    rules[11] = [(42, 31), (42, 11, 31)]

    def func(rule_num, msg):
        # a branch here is something like (42,) or (42, 11, 31) or 'a'
        for branch in rules[rule_num]:
            if isinstance(branch, str):  # it's a character
                if msg.startswith(branch):
                    # the only place characters are consumed from the string
                    yield msg[len(branch) :]
            else:  # it's a sequence of rule numbers
                possibles = {msg}
                for subrule_num in branch:
                    possibles = {x for ps in possibles for x in func(subrule_num, ps)}
                yield from possibles

    return sum(1 for m in msgs if any(s == "" for s in func(first_rule, m)))

(Apparently new users such as myself can’t add more than 3 replies to the same topic.)

@Leo As Chief TWiT, can you make a private TWiT leaderboard?

Yowza Day 20 was a lot of work and I had some insidious bugs while getting it done. I don’t know if there is much implementation spoilers on this one, it’s just a lot of work. I guess I did use a trick to save some effort on the map matching/assembly.

What I did was convert the map edges to binary. I stored resulting 10 bit numbers in a HashMap so I could find the map with that specific edge quickly. There should only be a matching set of two edges with the “correct” values, and the corners are easier to find this way also. When building my map, I tried each of the four identified corners in each of the 8 possible orientations for loctation 0,0 and the solved for the rest of the map with that. The bug I had, and that took me a long time to figure out, is that the map pieces that abut each other don’t have the same binary value, you need to flip the bits end for end on one because they fit together when they’re mirrored. After I got that bug squashed, the map was solved in seconds and I could start on the next phase of hunting sea monsters.

I also had a bug in my monster hunter. It was finding them, but wasn’t breaking out of the loop properly when it did, so it reported none found :frowning: I must have been getting pretty tired by then, but I finally got a clue and decided to input his provided example as a unit test and I needed the debugger to see my stupidity.

Nearly 7 and a half hours and I came in 2000th or so… pity those poor people still struggling on this long one.

Here’s my main:

final TileMatcher tm = new TileMatcher(12,12);
final TileReader tr = new TileFileReader(FILE);
int numberOfTiles = 0;
while (tr.isMoreInput())
{
  final Tile t = tr.getNextTile();
  tm.addTile(t);
  numberOfTiles++;
}
System.out.println("Number of tiles: " + numberOfTiles);
final Set<Integer> corners = tm.findCornerTiles();
long part1 = 1;
for (final int tileNumber : corners)
{
  part1 *= tileNumber;
}
System.out.println("Part 1: " + part1);

tm.buildMap();
final MonsterMap mm = new MonsterMap(12*8, 12*8);
tm.populateMonsterMap(mm);
mm.findAndHighlightSeaMonsters();
mm.dumpMap();
final int part2 = mm.countChar('#');
System.out.println("Part 2: " + part2);

Yeah I’m staring and staring at Day 7. It looks like I’ll need an arbitrary-arity tree with backtracking search. But then to a man with a hammer everything looks like a nail.

I’m digging into graph algorithms and data structures to see if they’re a better fit. I have a feeling some of these nodes might loop.

I’m not worried about the regex - that I’ve got a decent grasp of that ever since reading Jeffrey Friedl’s Mastering Regular Expressions. As seems to often be the case, choosing the right data structure is more than halfway there.

According to this blog post

The puzzles aren’t made with the assumption that you’ve taken a data structures course or even any computer science course. You are expected to know how to do a little coding.

Yeah right. He also says:

The two [data structures] that you need to know to tackle most, if not all, of the puzzles are:

  • Arrays (some programming languages, such as Python, call them lists )
  • Hash tables (these go by many other names — for example, they’re called hashes in Ruby, objects in JavaScript, dictionaries in Python and Swift, and Maps in Kotlin.)

If you’re familiar with these, you should be able to solve most of the puzzles without any help.

Hmmm. I don’t think dictionaries/hashes are gonna get the job done. But I’m in way over my head at this point. Fortunately I have a week off. There’s a chance I’ll get to day 8 before I have to go back to work!

Day 21… and it took me a bit to even understand the problem. Then it took me an even longer time to understand the example answer. Then it took me even longer to figure out how the answer was produced.

Spoiler: it’s a process of elimination, so you’ll need a loop along the lines of:

somethingWasEliminated=true;
while(somethingWasEliminated)
{
  somethingWasEliminated=false;
...
  if (condition)  somethingWasEliminated=true;
}

Here’s a good portion of the method that does the part 1 work, the comments explain the process a bit, I think:

  public int resolveAllergens()
  {
    boolean madeAnEntry = true;
    // keep making passes until we have eliminated all we can
    while (madeAnEntry)
    {
      madeAnEntry = false;
      for (final Allergen a : Allergen.getAllAllergens())
      {
        // Find all foods that list this allergen
        final Set<FoodItem> foodItemsWithAllergen = allergenToFoodItemMapping.get(a);
        // find any ingredient(s) in common with all those foods
        final Set<Ingredient> ingredientsInCommon = getIngredientsInCommon(foodItemsWithAllergen);
        // One of the ingredient in common MUST be the allergen
        // See if any of the ingredients are already known to be another allergen
        final Set<Ingredient> ingredientsNotEliminated = new HashSet<>();
        for (final Ingredient i : ingredientsInCommon)
        {
          if (!mappingOfIngredientToAllergen.containsKey(i))
          {
            ingredientsNotEliminated.add(i);
          }
        }
        if (ingredientsNotEliminated.size() == 1)
        {
          // this must be the allergen
          for (final Ingredient i: ingredientsNotEliminated)
          {
            mappingOfIngredientToAllergen.put(i, a);
          }
          madeAnEntry = true;
        }
      }
    }
    
    final Set<Ingredient> ingredientsNotEliminated = new HashSet<>();
    for (final FoodItem fi: foodItemList)
    {
      for (final Ingredient i : fi.getIngredientList())
      {
        if (!mappingOfIngredientToAllergen.containsKey(i))
        {
          ingredientsNotEliminated.add(i);
        }
      }
    }

Part 2 wasn’t much of a challenge for me, because I collected the necessary data to answer part 1, so just had to restructure and sort that data to generate the answer for part 2.

Here’s my main method:

final Food food = new Food();
final FoodItemReader fir = new FoodItemReader(FILE);
while (fir.isMoreInput())
{
  final FoodItem fi = fir.getNextFoodItem();
  food.addItem(fi);
}
System.out.println("Number of food items: " + food.getNumberOfFoodItems());
System.out.println("Number of unique ingredients: " + Ingredient.getNumberOfUniqueIngredients());
System.out.println("Number of unique allergens: " + Allergen.getNumberOfUniqueAllergens());
final int part1 = food.resolveAllergens();
System.out.println("Part 1: " + part1);
final String part2 = food.getDangerousIngredients();
System.out.println("Part 2: " + part2);

sigh I almost ranked 49 or 50 for Day 22 Part 2, except for the most ridiculously stupidly infuriating bug to have; I somehow used the wrong comparison operator, even though I knew exactly what the problem was asking for, because dumb mistakes are a thing. :sob:

I would have been 2-3 ranks below geohot.

1 Like

For Day 22, part 1 is mostly just brute force coding. If you already had “Decks” ready to do the types of things needed, or if your language makes it easy to manage lists of numbers, then part 1 should be easy.

Here’s the key code for part 1:

  public Hand playToEnd()
  {
    while (hand1.getNumberOfCards()>0 && hand2.getNumberOfCards()>0)
    {
      final int p1c = hand1.getTopCard();
      final int p2c = hand2.getTopCard();
      if (p1c > p2c)
      {
        hand1.putOnBottom(p1c);
        hand1.putOnBottom(p2c);
      }
      else
      {
        hand2.putOnBottom(p2c);
        hand2.putOnBottom(p1c);
      }
    }

    return returnWinner();
  }

Part 2 is a little more tricky. Part of it is just understanding precisely what is being asked. You should pay particular attention to the previous hand detection because the small example (unit test) given does not contain an example of this. My first attempted part 2 answer misunderstood what was expected for this previous hand detection. Also, the previous hand detection will be the slowest part of the code, so you should find a good approach or be prepared to wait, my input went for over 25000 games (of multiple rounds, all of which ended up in the previous hand detector.)

Here’s my code for part 2:

  private static int _recurse(final Hand hand1, final Hand hand2, final Set<String> previousHands)
  {
    while (hand1.getNumberOfCards()>0 && hand2.getNumberOfCards()>0)
    {
      if (isPreviousHand(previousHands, hand1, hand2))
      {
        return 1;
      }
      
      final int p1c = hand1.getTopCard();
      final int p2c = hand2.getTopCard();
      int p1nc = hand1.getNumberOfCards();
      int p2nc = hand2.getNumberOfCards();
      
      if ((p1nc>=p1c) && (p2nc>=p2c))
      {
        final int roundWinner = makeHandAndRecurse(p1c, p2c, hand1, hand2);
        doWinner(roundWinner, hand1, hand2, p1c, p2c);
      }
      else if (p1c > p2c)
      {
        doWinner(1, hand1, hand2, p1c, p2c);
      }
      else
      {
        doWinner(2, hand1, hand2, p1c, p2c);
      }
    }
    if (hand1.getNumberOfCards() == 0)  return 2;
    return 1;
  }

  private static int makeHandAndRecurse(final int p1nc, final int p2nc, final Hand hand1in, final Hand hand2in)
  {
    final Set<String> previousHands = new HashSet<>();
    final Hand newHand1 = hand1in.newSubHand(p1nc);
    final Hand newHand2 = hand2in.newSubHand(p2nc);
    return _recurse(newHand1, newHand2, previousHands);
  }

And of course some main code:

// I chose to just copy the supplied input into String arrays in my main code
final Hand hand1 = new Hand(HAND1);
final Hand hand2 = new Hand(HAND2);
final Game game = new Game(hand1,hand2);
final Hand winningHand = game.playToEnd();
final long part1 = winningHand.calculateHandSum();
System.out.println("Part 1: " + part1);

final Hand hand1b = new Hand(HAND1);
final Hand hand2b = new Hand(HAND2);
final Game game2 = new Game(hand1b,hand2b);
final Hand winningHand2 = game2.recursivePlayToEnd();
final long part2 = winningHand2.calculateHandSum();
winningHand2.dump();
System.out.println("Part 2: " + part2);

Day 23… getting near the end. The trick with this one is to not implement the solution they way you’re going to instinctively want to do so. If you read the instructions in the right way, he kind of gives you a hint on an approach to take. Especially when he says to start at the next number after 1. He’s hinting that you need an arrangement that lets you easily connect the numbers so you can find them, but can also easily change what they connect to.

I initially did it the “wrong way” for part 1, and it worked find (after a little debugging to eliminate a one off.) Although this won’t help you for part 2, here is the “wrong way” to do part 1:

  public void playRounds(final int numberOfRounds)
  {
    final int[] pickedup = new int[3];
    for (int i=0; i<numberOfRounds; i++)
    {
      pickedup[0]=cups[(currentCupPos+1)%cups.length];
      pickedup[1]=cups[(currentCupPos+2)%cups.length];
      pickedup[2]=cups[(currentCupPos+3)%cups.length];
      
      int destination=cups[currentCupPos]-1;
      if (destination <1)  destination = cups.length;
      while(destination==pickedup[0] || destination==pickedup[1] || destination==pickedup[2])
      {
        destination--;
        if (destination <1)  destination = cups.length;
      }
      int destLocation=-1;
      int j=(currentCupPos+4)%cups.length;
      while (destLocation<0)
      {
        if (cups[j] == destination)
          destLocation=(j+1)%cups.length;
        else
          j=(j+1)%cups.length;
      }
      int to=(currentCupPos+1)%cups.length;
      int from=(currentCupPos+4)%cups.length;
      while (from != destLocation)
      {
        cups[to]=cups[from];
        to=(to+1)%cups.length;
        from=(from+1)%cups.length;
      }
      cups[(to)%cups.length]=pickedup[0];
      cups[(to+1)%cups.length]=pickedup[1];
      cups[(to+2)%cups.length]=pickedup[2];
      currentCupPos=(currentCupPos+1)%cups.length;
    }
  }

This won’t work for part 2 because it involves too much linear searching and moving of data for that many rounds. You need a design that can eliminate most of that overhead. You could use a linked list easily, except that finding a specific element in the linked list is still going to be slow. Instead I used a HashMap and the key is a cup and the value is the next cup (the cup beside it.) This way you can pull out three cups and rejoin the loop, and then quickly find the destination to hook them back in. Each iteration you’re only updating a couple of pointers and doing a 5 or so hash table lookups.

Here’s the code for part 2, and interestingly it’s shorter and simpler (in a way) that the code above for part 1:

  public void playRounds2(final int numberOfRounds)
  {
    final int[] pickedup = new int[3];
    for(int i=0; i<cups.length-1; i++)
    {
      nextNumber.put(cups[i], cups[i+1]);
    }
    prevValue = cups[cups.length-1];
    nextNumber.put(prevValue, cups[0]);

    for (int i=0; i<numberOfRounds; i++)
    {
      if (i%1_000_000==0)System.out.format("Round #%,d%n",i);
      int nextPrev = nextNumber.get(prevValue);
      pickedup[0]=nextNumber.get(nextPrev);
      pickedup[1]=nextNumber.get(pickedup[0]);
      pickedup[2]=nextNumber.get(pickedup[1]);
      
      int destination=nextNumber.get(prevValue)-1;
      nextNumber.put(nextNumber.get(prevValue), nextNumber.get(pickedup[2]));
      prevValue = nextPrev;
      
      if (destination <1)  destination = cups.length;
      while(destination==pickedup[0] || destination==pickedup[1] || destination==pickedup[2])
      {
        destination--;
        if (destination <1)  destination = cups.length;
      }
      int hold=nextNumber.get(destination);
      nextNumber.put(destination, pickedup[0]);
      nextNumber.put(pickedup[2], hold);
    }
  }

Using this code, getting the part 2 answer is just a couple of lines of code. Here’s the main:

  // cups is a hard coded int[] with the supplied input
  public static void main(final String[] args)
  {
    final CupGame game = new CupGame(cups);
    game.playRounds(100);
    final String part1 = game.collectResult();
    System.out.println("Part 1: " + part1);

    final int[] newCups = makeNewCups(cups, 1_000_000);
    final CupGame game2 = new CupGame(newCups);
    game2.playRounds2(10_000_000);
    final long part2 = game2.collectPart2Result();
    System.out.println("Part 2: " + part2);
  }

  private static int[] makeNewCups(final int[] firstCups, final int numberOfCups)
  {
    final int[] newCups = new int[numberOfCups];
    for (int i=0; i<firstCups.length; i++)
    {
      newCups[i]=firstCups[i];
    }
    for (int i=firstCups.length; i<numberOfCups; i++)
    {
      newCups[i]=i+1;
    }
    return newCups;
  }

You might be thinking of a linked list as a linked list of instances of a Node() class, e.g. a singly linked list or doubly linked list; but what you did is absolutely an implementation of a linked list. An intermediate solution would be to have a more traditional linked list, e.g. with a Node() class, and then have a mapping from value to each Node() instance. But there’s no need for that if you can just have a value → next_value mapping, which is what you did. It’s more powerful than a “pure” linked list, but only works in that form if the node values are unique and unchanging, which they are in this case.

Since our values are [1, 1000000] for this problem, you can also easily pre-allocate an array/list for extra performance, and use that for the mapping. Doing that, my solution runs on the sample input (389125467) in ~1.876s ~0.4s in PyPy.

EDIT: Fixed the non-optimal way I was building the mapping list and cut the runtime by 75%.

Following this suggestion, I implemented a new faster approach:

  // Using an array named nextNumberArray of cups.length+1 (not using the zeroth element)
  public void playRounds3(final int numberOfRounds)
  {
    final int[] pickedup = new int[3];
    for(int i=0; i<cups.length-1; i++)
    {
      nextNumberArray[cups[i]] = cups[i+1];
    }
    prevValue = cups[cups.length-1];
    nextNumberArray[prevValue] = cups[0];

    for (int i=0; i<numberOfRounds; i++)
    {
      if (i%1_000_000==0)System.out.format("Round #%,d%n",i);
      int nextPrev = nextNumberArray[prevValue];
      pickedup[0]=nextNumberArray[nextPrev];
      pickedup[1]=nextNumberArray[pickedup[0]];
      pickedup[2]=nextNumberArray[pickedup[1]];
      
      int destination=nextNumberArray[prevValue]-1;
      nextNumberArray[nextNumberArray[prevValue]] = nextNumberArray[pickedup[2]];
      prevValue = nextPrev;
      
      if (destination <1)  destination = cups.length;
      while(destination==pickedup[0] || destination==pickedup[1] || destination==pickedup[2])
      {
        destination--;
        if (destination <1)  destination = cups.length;
      }
      int hold=nextNumberArray[destination];
      nextNumberArray[destination]= pickedup[0];
      nextNumberArray[pickedup[2]] = hold;
    }
  }

Penultimate day… Day 24. Game of life on hex tiles. I spent way too much time debugging a stupid bug… my reader was skipping the first input line and adding a blank line. This gave me the correct number for part 1 but made part 2 not work right. (A blank line was interpreted as a black tile at 0,0 :frowning: )

The trick here is how to represent hex tiles. This is “common” in hex tile based games, so you may need to learn the co-ordinate system. There are two general ways to orient the tiles, one with the points facing north/south and one with them facing east/west. The tiles in the problem set are point facing north/south. Accordingly when navigating east/west along tiles, x needs to increase or decrease by 2. For the other four directions, x and y will increase or decrease by 1. This site may be useful Hexagonal Grids

Here’s the tile path calculation code:

  public static TileAddress convertTilePathToAddress(final String tilePath)
  {
    int x = 0;
    int y = 0;
    int i = 0;
    while (i < tilePath.length())
    {
      final char c1 = tilePath.charAt(i++);
      if ((c1 == 'n') || (c1 == 's'))
      {
        final char c2 = tilePath.charAt(i++);
        if (c1 == 'n')
        {
          y--;
          if (c2 == 'w')
          {
            x--;
          }
          else if (c2 == 'e')
          {
            x++;
          }
          else throw new IllegalStateException();
        }
        else if (c1 == 's')
        {
          y++;
          if (c2 == 'w')
          {
            x--;
          }
          else if (c2 == 'e')
          {
            x++;
          }
          else throw new IllegalStateException();
        }
        else throw new IllegalStateException();
      }
      else
      {
        if (c1 == 'w')
        {
          x -= 2;
        }
        else if (c1 == 'e')
        {
          x += 2;
        }
        else throw new IllegalStateException();
      }
    }
    return new TileAddress(x, y);
  }

For part 2, it’s just the normal game of life semantics. There are a number of ways to represent the “infinite” tiles (that aren’t actually infinite, of course, as as you’ve done a game of line of an infinite space previously in this years problems (day 11) there is not much new to add here.

Here’s my main code:

final TilePathReader tpr = new TilePathReader(FILE);
final TileTracker tt = new TileTracker();
while(tpr.isMoreInput())
{
  final String tilePath = tpr.getNextTilePath();
  final TileAddress ta = TileAddress.convertTilePathToAddress(tilePath);
  tt.flipTileAtAddress(ta);
}
final int part1 = tt.getNumberOfBlackTiles();
System.out.println("Part 1: " + part1);

tt.flipTilesForDays(100);
final int part2 = tt.getNumberOfBlackTiles();
System.out.println("Part 2: " + part2);

Yay, all 25 days done, and all 50 stars collected. Merry Christmas Everyone!

Some more modular math… The RFID cards doing Diffie-Hellman-Merkle https://en.wikipedia.org/wiki/Diffie–Hellman_key_exchange

Here’s my main:

  private static final long v1 = __INPUT#1_not_shown__;
  private static final long v2 = __INPUT#2_not_shown__;
  
  public static void main(final String[] args)
  {
    final int loopSize1 = LoopFinder.findLoop(1, 7, 20201227, v1);
    System.out.println(loopSize1);
    final int loopSize2 = LoopFinder.findLoop(1, 7, 20201227, v2);
    System.out.println(loopSize2);
    final long key1 = LoopFinder.findKey(1, v1, loopSize2, 20201227);
    System.out.println(key1);
    final long key2 = LoopFinder.findKey(1, v2, loopSize1, 20201227);
    System.out.println(key2);
  }
1 Like

So so impressed @PHolder - thanks for taking us along with you this year, Paul!!!

1 Like