Advent of Code 2021

I was expecting a huge input for Day 11, and it wasn’t huge at all. It wasn’t really a much of a challenge either, I don’t think.

I created a class to model the octopus cave, and had it able to iterate a step at a time while performing the necessary updates and tracking the stats (like the number of flashes and iterations and if the part two condition was met on any given iteration.) This felt a lot like the challenge from day 9, which I tackled the edge cases by adding a perimeter. This time I decided, for kicks, to add the nine if statements to handle all the edge cases.

final class OctopusCave
{
  private final int width;
  private final int height;
  private final int[][] octopi;
  private long numberOfFlashes = 0;
  private long iterationNumber = 0;
  private boolean didAllFlashAtOnce = false;
  
  public OctopusCave(final String[] octopusLayout)
  {
    width = octopusLayout[0].length();
    height = octopusLayout.length;
    octopi = new int[width][height];
    
    for (int y=0; y<height; y++)
    {
      for (int x=0; x<width; x++)
      {
        octopi[x][y] = octopusLayout[y].charAt(x)-'0';
      }
    }
  }

  public void iterate()
  {
    iterationNumber++;
    boolean loopAgain = true;
    boolean doIncrement = true;
    while(loopAgain)
    {
      loopAgain = false;
      for (int y=0; y<height; y++)
      {
        for (int x=0; x<width; x++)
        {
          if (doIncrement)
            if (octopi[x][y]>=0) octopi[x][y]++;

          if (octopi[x][y] > 9)
          {
            numberOfFlashes++;
            if (energizeNeighbouringOctopi(x,y))
            {
              loopAgain = true;
            }
            octopi[x][y] = -1;
          }
        }
      }
      doIncrement = false;
    }

    didAllFlashAtOnce = true;
    for (int y=0; y<height; y++)
    {
      for (int x=0; x<width; x++)
      {
        if (octopi[x][y]<0)
          octopi[x][y]=0;
        else
          didAllFlashAtOnce = false;
      }
    }
  }

  private boolean energizeNeighbouringOctopi(final int x, final int y)
  {
    boolean anotherOctopusNeedsToFlash = false;

    if ((x>0) && (y>0))
    {
      if (octopi[x-1][y-1] >= 0)
        if (++octopi[x-1][y-1] > 9)  anotherOctopusNeedsToFlash = true;
    }

    if (y>0)
    {
      if (octopi[x][y-1] >= 0)
        if (++octopi[x][y-1] > 9)  anotherOctopusNeedsToFlash = true;
    }

    if ((x<width-1) && (y>0))
    {
      if (octopi[x+1][y-1] >= 0)
        if (++octopi[x+1][y-1] > 9)  anotherOctopusNeedsToFlash = true;
    }

    if (x>0)
    {
      if (octopi[x-1][y] >= 0)
        if (++octopi[x-1][y] > 9)  anotherOctopusNeedsToFlash = true;
    }

    if (x<width-1)
    {
      if (octopi[x+1][y] >= 0)
        if (++octopi[x+1][y] > 9)  anotherOctopusNeedsToFlash = true;
    }

    if ((x>0) && (y<height-1))
    {
      if (octopi[x-1][y+1] >= 0)
        if (++octopi[x-1][y+1] > 9)  anotherOctopusNeedsToFlash = true;
    }

    if (y<height-1)
    {
      if (octopi[x][y+1] >= 0)
        if (++octopi[x][y+1] > 9)  anotherOctopusNeedsToFlash = true;
    }

    if ((x<width-1) && (y<height-1))
    {
      if (octopi[x+1][y+1] >= 0)
        if (++octopi[x+1][y+1] > 9)  anotherOctopusNeedsToFlash = true;
    }

    return anotherOctopusNeedsToFlash;
  }

  public void iterateNTimes(final int numberOfIterations)
  {
    for (int i=0; i<numberOfIterations; i++) iterate();
  }
  
  @Override
  public String toString()
  {
    final StringBuilder sb = new StringBuilder();
    for (int y=0; y<height; y++)
    {
      for (int x=0; x<width; x++)
      {
        sb.append(octopi[x][y]);
      }
      sb.append('\n');
    }
    return sb.toString();
  }

  public long getNumberOfFlashes()
  {
    return numberOfFlashes;
  }

  public boolean getDidAllFlashAtOnce()
  {
    return didAllFlashAtOnce;
  }

  public long getIterationNumber()
  {
    return iterationNumber;
  }
}

To actually get the results, the code is pretty straight forward, but I will spoiler blur it since it will give away part two:

public static long getPart1Answer(final String day11Input, final String[] day11InputLines)
{
  final OctopusCave oc = new OctopusCave(day11InputLines);
  oc.iterateNTimes(100);
  return oc.getNumberOfFlashes();
}

public static long getPart2Answer(final String day11Input, final String[] day11InputLines)
{
  final OctopusCave oc = new OctopusCave(day11InputLines);
  while (!oc.getDidAllFlashAtOnce())
  {
    oc.iterate();
  }
  return oc.getIterationNumber();
}

Okay Day 12 brought some difficulty, it took me an hour and a half for part one and then almost another hour to do part two. It’s a graph problem (or at least to me that seems the most obvious way to view it.) I tackled it by making two objects, one to do cave navigation (to find the paths to solve the challenge) and one to represent each cave and its connections to other caves.

I initially thought I had a problem with my solution for part one because of a design decision I made. I decided to make my Cave class manage lookups for all the Caves, and I also implemented three unit tests together for the three provided examples. I forgot to clear out the list of caves between each pass, so of course I didn’t get correct answers for the second and third example. Adding a “newCaveSystem()” to clear it out at the start of each pass fixed it right up.

Part two was more challenging to me, for some reason. I have an approach that I knew would work, but seemed to have a mental block about how to fully implement it. In the end, I had an extra “else” I didn’t need, and once I commented it out, everything came together. (I left that comment in my code, in case anyone is curious.)

Here’s my Cave class, I don’t think there is anything here that would be a spoiler, so not blurring:

final class Cave
{
  private static final Map<String,Cave> caveList = new HashMap<>();

  private final String caveName;
  private final Set<Cave> connectedCaves = new HashSet<>();
  
  private Cave(final String caveNameToCreate)
  {
    caveName = caveNameToCreate;
  }
  
  public void addConnectedCave(final Cave caveToConnect)
  {
    connectedCaves.add(caveToConnect);
  }

  public static Cave createOrLookupByName(final String caveNameToCreateOrLookup)
  {
    if (caveList.containsKey(caveNameToCreateOrLookup))
      return caveList.get(caveNameToCreateOrLookup);
    final Cave newCave = new Cave(caveNameToCreateOrLookup);
    caveList.put(caveNameToCreateOrLookup, newCave);
    return newCave;
  }

  public static Cave lookupByName(final String caveNameToLookup)
  {
    if (!caveList.containsKey(caveNameToLookup))
      throw new IllegalStateException("Couldn't lookup cave by name: " + caveNameToLookup);
    return caveList.get(caveNameToLookup);
  }

  public String getName()
  {
    return caveName;
  }

  public Cave[] getConnectedCaves()
  {
    final Cave[] result = connectedCaves.toArray(new Cave[0]);
    return result;
  }

  public static void newCaveSystem()
  {
    caveList.clear();
  }
}

The CaveNaviagtor class is doing most of the work. I created a findAllPaths() for part one and a findAllPaths2() which copies part one but makes the necessary change to handle the single cave that allows two visits.

final class CaveNavigator
{
  public CaveNavigator(final String[] day12InputLines)
  {
    Cave.newCaveSystem();
    for (final String caveConnectionPath : day12InputLines)
    {
      final String[] twoCaves = caveConnectionPath.split("\\-");
      if (twoCaves.length != 2)
        throw new IllegalStateException("Unable to parse cave path: " + caveConnectionPath);
      // System.out.format("Cave 1 %s connects to cave 2 %s%n", twoCaves[0], twoCaves[1]);
      final Cave cave1 = Cave.createOrLookupByName(twoCaves[0]);
      final Cave cave2 = Cave.createOrLookupByName(twoCaves[1]);
      cave1.addConnectedCave(cave2);
      cave2.addConnectedCave(cave1);
    }
    // dumpCaveList();
  }

  Set<String> findAllPaths()
  {
    final Set<String> result = new HashSet<>();

    final Deque<Cave> stack = new ArrayDeque<>();
    final Cave startCave = Cave.lookupByName("start");
    final Set<Cave> visitedCaves = new HashSet<>();

    stack.push(startCave);
    visitedCaves.add(startCave);
    seekPathFrom(startCave, stack, visitedCaves, result);
    return result;
  }

  private void seekPathFrom(final Cave currentCave, final Deque<Cave> stack, final Set<Cave> visitedCaves, final Set<String> result)
  {
    final Cave[] connectedCaves = currentCave.getConnectedCaves();
    for (final Cave connectedCave : connectedCaves)
    {
      final Set<Cave> newVisitedCavesCopy = new HashSet<>();
      newVisitedCavesCopy.addAll(visitedCaves);
      if (connectedCave.getName().equals("end"))
      {
        stack.push(connectedCave);
        final String currentPath = getCurrentPath(stack);
        result.add(currentPath);
        stack.pop();
        continue;
      }
      if (isOneVisitCave(connectedCave.getName()))
      {
        if (newVisitedCavesCopy.contains(connectedCave)) continue;
        newVisitedCavesCopy.add(connectedCave);
      }
      stack.push(connectedCave);
      seekPathFrom(connectedCave, stack, newVisitedCavesCopy, result);
      stack.pop();
    }
  }

  Set<String> findAllPaths2()
  {
    final Set<String> result = new HashSet<>();

    final Deque<Cave> stack = new ArrayDeque<>();
    final Cave startCave = Cave.lookupByName("start");
    final Set<Cave> visitedCaves = new HashSet<>();

    stack.push(startCave);
    visitedCaves.add(startCave);
    seekPathFrom2(startCave, stack, visitedCaves, result, "");
    return result;
  }

  private void seekPathFrom2(final Cave currentCave, final Deque<Cave> stack, final Set<Cave> visitedCaves, final Set<String> result, final String caveVisitedTwiceIfAny)
  {
    final Cave[] connectedCaves = currentCave.getConnectedCaves();
    for (final Cave connectedCave : connectedCaves)
    {
      final Set<Cave> newVisitedCavesCopy = new HashSet<>();
      newVisitedCavesCopy.addAll(visitedCaves);
      if (connectedCave.getName().equals("start"))  continue;
      if (connectedCave.getName().equals("end"))
      {
        stack.push(connectedCave);
        final String currentPath = getCurrentPath(stack);
        result.add(currentPath);
        stack.pop();
        continue;
      }
      if (isOneVisitCave(connectedCave.getName()))
      {
        if (caveVisitedTwiceIfAny.isEmpty())
        {
          stack.push(connectedCave);
          seekPathFrom2(connectedCave, stack, newVisitedCavesCopy, result, connectedCave.getName());
          stack.pop();
        }
        // else  // This was a bug!!
        {
          if (newVisitedCavesCopy.contains(connectedCave)) continue;
          newVisitedCavesCopy.add(connectedCave);
          stack.push(connectedCave);
          seekPathFrom2(connectedCave, stack, newVisitedCavesCopy, result, caveVisitedTwiceIfAny);
          stack.pop();
        }
      }
      else
      {
        stack.push(connectedCave);
        seekPathFrom2(connectedCave, stack, newVisitedCavesCopy, result, caveVisitedTwiceIfAny);
        stack.pop();
      }
    }
  }

  private String getCurrentPath(final Deque<Cave> stack)
  {
    final StringBuilder sb = new StringBuilder();
    final Iterator<Cave> iterator = stack.descendingIterator();
    boolean isFirst = true;
    while (iterator.hasNext())
    {
      if (!isFirst) sb.append(",");
      isFirst = false;
      final Cave cave = iterator.next();
      sb.append(cave.getName());
    }
    return sb.toString();
  }

  private boolean isOneVisitCave(final String caveName)
  {
    return (caveName.toLowerCase() == caveName);
  }

  private void dumpCaveList()
  {
    final Deque<Cave> stack = new ArrayDeque<>();
    final Cave startCave = Cave.lookupByName("start");
    stack.push(startCave);

    final Set<Cave> visitedCaves = new HashSet<>();
    
    while (!stack.isEmpty())
    {
      final Cave currentCave = stack.pop();
      if (!visitedCaves.contains(currentCave))
      {
        visitedCaves.add(currentCave);
        System.out.println(currentCave.getName());
        final Cave[] connectedCaves = currentCave.getConnectedCaves();
        for (final Cave connectedCave : connectedCaves)
        {
          System.out.println("  - " + connectedCave.getName());
          stack.push(connectedCave);
        }
      }
    }
  }
}

The code that calls into CaveNavigator to solve each of the two parts is very straightforward. While part one is virtually instant, part two solution takes a noticeable amount of time to produce an answer, like maybe 5 or 10 seconds.

  public static long getPart1Answer(final String day12Input, final String[] day12InputLines)
  {
    final CaveNavigator cn = new CaveNavigator(day12InputLines);
    final Set<String> allPaths = cn.findAllPaths();
    return allPaths.size();
  }

  public static long getPart2Answer(final String day12Input, final String[] day12InputLines)
  {
    final CaveNavigator cn = new CaveNavigator(day12InputLines);
    final Set<String> allPaths = cn.findAllPaths2();
    return allPaths.size();
  }

Day 13 is folding transparent paper. Mark the paper with dots at the supplied list of locations, then make the horizontal or vertical folds at the supplied locations, then overlapping dots will spell out a code for part 2. This seemed daunting at first, until I realized that the folds are actually very straight forward. The only trick you need to know is to remember that the folds dictate the new size of the paper (it will get smaller in width or height with each fold.)

I made a FoldingTransparentPaper class:

final class FoldingTransparentPaper
{
  private final int originalWidth;
  private final int originalHeight;
  private final char[][] paper;
  private int foldedWidth;
  private int foldedHeight;

  public FoldingTransparentPaper(final int width, final int height)
  {
    originalWidth = width;
    originalHeight = height;
    paper = new char[originalWidth][originalHeight];
    foldedWidth = width;
    foldedHeight = height;
  }

  public void addDotsAtLocations(final String[] dotLocations)
  {
    for (final String dotLocation : dotLocations)
    {
      final String parts[] = dotLocation.split(",");
      if (parts.length != 2) throw new IllegalStateException("Couldn't parse dot location: " + dotLocation);
      final int x = Integer.parseInt(parts[0]);
      final int y = Integer.parseInt(parts[1]);
      paper[x][y] = '#';
    }
  }

  public void makeAFold(final String foldInstruction)
  {
    if (!foldInstruction.startsWith("fold along "))
      throw new IllegalStateException("Invalid fold instruction: " + foldInstruction);
    final int foldLine = Integer.parseInt(foldInstruction.substring(13));
    if (foldInstruction.charAt(11)=='y')
      doYFold(foldLine);
    else if (foldInstruction.charAt(11)=='x')
      doXFold(foldLine);
    else
      throw new IllegalStateException("Couldn't get fold direction: " + foldInstruction);
  }

  public void makeFolds(final String[] foldInstructions)
  {
    for (final String foldInstruction : foldInstructions)
    {
      makeAFold(foldInstruction);
    }
  }

  private void doXFold(final int foldLine)
  {
    int xLeft=foldLine-1;
    int xRight=foldLine+1;
    while (xLeft >= 0)
    {
      for (int y=0; y<foldedHeight; y++)
      {
        if (paper[xLeft][y] != '#')
        {
          paper[xLeft][y] = paper[xRight][y];
        }
      }
      xLeft--;
      xRight++;
      if (xRight>=foldedWidth) break;
    }
    foldedWidth=foldLine;
  }

  private void doYFold(final int foldLine)
  {
    int yUp=foldLine-1;
    int yDown=foldLine+1;
    while (yUp >= 0)
    {
      for (int x=0; x<foldedWidth; x++)
      {
        if (paper[x][yUp] != '#')
        {
          paper[x][yUp] = paper[x][yDown];
        }
      }
      yUp--;
      yDown++;
      if (yDown>=foldedHeight) break;
    }
    foldedHeight=foldLine;
  }

  int getNumberOfDots()
  {
    int result = 0;
    for (int x=0; x<foldedWidth; x++)
    {
      for (int y=0; y<foldedHeight; y++)
      {
        if (paper[x][y] == '#')  result++;
      }
    }
    return result;
  }

  @Override
  public String toString()
  {
    final StringBuilder sb = new StringBuilder();
    for (int y=0; y<foldedHeight; y++)
    {
      for (int x=0; x<foldedWidth; x++)
      {
        if (paper[x][y]=='#')
          sb.append('#');
        else
          sb.append('.');
      }
      sb.append('\n');
    }
    return sb.toString();
  }
}

For these problems I start with a preset bit of template code. It assumes each part will return a long integer. For part 2 I had to change this to a String. There is nothing here that is a spoiler, so I won’t blur it.

public static long getPart1Answer(final String[] dotLocations, final String[] foldInstructions, final int width, final int height)
{
  final FoldingTransparentPaper ftp = new FoldingTransparentPaper(width, height);
  ftp.addDotsAtLocations(dotLocations);
  ftp.makeAFold(foldInstructions[0]);
  return ftp.getNumberOfDots();
}

public static String getPart2Answer(final String[] dotLocations, final String[] foldInstructions, final int width, final int height)
{
  final FoldingTransparentPaper ftp = new FoldingTransparentPaper(width, height);
  ftp.addDotsAtLocations(dotLocations);
  ftp.makeFolds(foldInstructions);
  return ftp.toString();
}

This reminded of an IntCode problem. Part one had you run the code, but the output was basically junk for day 1 (You needed to count the number of painted squares for part 1), but you change the starting square and got some value in the painting you would enter for part 2.

I couldn’t get day 11 done. The amount of time I wasted as a result made it so I didn’t have time to even think about day 12. I’m fairly certain I seen a problem like day 12 before however.

So day 14… has the obvious way to solve it, and the right way. I coded both because I guessed wrong about the risk of the problem exploding to infinity because so far in this years challenges there haven’t really been any of the challenges that did. (I guess that might technically be a spoiler, but I think he gives plenty of foreshadowing in the challenge text.)

Anyway, my thought process was to wrap the processing in a class that I called PolymerizationTool. It handled parsing the rules, taking the base polymer and then applying the rules and counting the results. This is a spoiler for part one, but won’t be much use for part two:

final class PolymerizationTool
{
  record PolymerizationRule(String pair, String result) {}
  
  private final Map<String, PolymerizationRule> polymerizationRules = new HashMap<>();
  private String currentPolymer;
  
  PolymerizationTool(final String startingPolymer, final String[] inputPolymerizationRules)
  {
    currentPolymer = startingPolymer;
    for (final String polymerizationRule : inputPolymerizationRules)
    {
      final String[] parts = polymerizationRule.split(" -> ");
      if (parts.length != 2)  throw new IllegalStateException("Couldn't parse polymerization rule: " + polymerizationRule);
      final PolymerizationRule pr = new PolymerizationRule(parts[0], parts[1]);
      polymerizationRules.put(parts[0], pr);
    }
  }

  public void processNPolymerizationSteps(final int numberOfStepsToProcess)
  {
    for (int i=0; i<numberOfStepsToProcess; i++)  processOnePolymerizationStep();
  }

  void processOnePolymerizationStep()
  {
    final StringBuilder newPolymerization = new StringBuilder();
    for (int i=0; i<currentPolymer.length()-1; i++)
    {
      final String currentPair = currentPolymer.substring(i, i+2);
      newPolymerization.append(currentPair.charAt(0));
      if (polymerizationRules.containsKey(currentPair))
      {
        newPolymerization.append(polymerizationRules.get(currentPair).result);
      }
    }
    newPolymerization.append(currentPolymer.charAt(currentPolymer.length()-1));
    currentPolymer = newPolymerization.toString();
  }

  public String getCurrentPolymer()
  {
    return currentPolymer;
  }

  public long[] getElementCounts()
  {
    final long[] elementCounts = new long[26];
    for (int i=0; i<currentPolymer.length(); i++)
    {
      elementCounts[currentPolymer.charAt(i)-'A']++;
    }

    int numDiffElements = 0;
    for (int i=0; i<26; i++) if (elementCounts[i] != 0)  numDiffElements++;
    
    final long[] result = new long[numDiffElements];
    int j = 0;
    for (int i=0; i<26; i++) if (elementCounts[i] != 0)  result[j++] = elementCounts[i];
    Arrays.sort(result);

    return result;
  }
}

For part two, a new approach to the problem was necessary. I copied PolymerizationTool to PolymerizationTool2 and then made the necessary changes. I tested, and this version still worked for part one, but also worked for part two.

final class PolymerizationTool2
{
  record PolymerizationRule(String pair, char result) {}
  record PolymerizationCount(String pair, long count) {}
  
  private final Map<String, PolymerizationRule> polymerizationRules = new HashMap<>();
  private Map<String, PolymerizationCount> polymerizationCounts = new HashMap<>();
  final long[] elementCounts = new long[26];
  
  PolymerizationTool2(final String startingPolymer, final String[] inputPolymerizationRules)
  {
    for (int i=0; i<startingPolymer.length(); i++)
    {
      elementCounts[startingPolymer.charAt(i)-'A']++;
    }

    for (int i=0; i<startingPolymer.length()-1; i++)
    {
      final String currentPair = startingPolymer.substring(i, i+2);
      updatePolymerizationCount(polymerizationCounts, currentPair, 1);
    }
    
    for (final String polymerizationRule : inputPolymerizationRules)
    {
      final String[] parts = polymerizationRule.split(" -> ");
      if (parts.length != 2)  throw new IllegalStateException("Couldn't parse polymerization rule: " + polymerizationRule);
      final PolymerizationRule pr = new PolymerizationRule(parts[0], parts[1].charAt(0));
      polymerizationRules.put(parts[0], pr);
    }
  }

  private void updatePolymerizationCount(final Map<String, PolymerizationCount> polymerizationCounts, final String polymerPair, final long increment)
  {
    long count = increment;
    if (polymerizationCounts.containsKey(polymerPair))
    {
      count = polymerizationCounts.get(polymerPair).count+increment;
    }
    final PolymerizationCount newPolymerizationCount = new PolymerizationCount(polymerPair, count);
    polymerizationCounts.put(polymerPair, newPolymerizationCount);
  }

  public void processNPolymerizationSteps(final int numberOfStepsToProcess)
  {
    for (int i=0; i<numberOfStepsToProcess; i++)  processOnePolymerizationStep();
  }

  void processOnePolymerizationStep()
  {
    final Map<String, PolymerizationCount> newPolymerizationCounts = new HashMap<>();

    for (final Entry<String, PolymerizationRule> entry: polymerizationRules.entrySet())
    {
      final String polymerPair = entry.getKey();
      if (polymerizationCounts.containsKey(polymerPair))
      {
        final long numberOfPolymerPair = polymerizationCounts.get(polymerPair).count;
        final char addedMiddleElement = entry.getValue().result;
        final String newPairOne = "" + polymerPair.charAt(0) + addedMiddleElement;
        final String newPairTwo = "" + addedMiddleElement + polymerPair.charAt(1); 
        updatePolymerizationCount(newPolymerizationCounts, newPairOne, numberOfPolymerPair);
        updatePolymerizationCount(newPolymerizationCounts, newPairTwo, numberOfPolymerPair);
        elementCounts[addedMiddleElement-'A'] += numberOfPolymerPair;
      }
    }
    polymerizationCounts = newPolymerizationCounts;
  }

  public long[] getElementCounts()
  {
    int numDiffElements = 0;
    for (int i=0; i<26; i++) if (elementCounts[i] != 0)  numDiffElements++;
    
    final long[] result = new long[numDiffElements];
    int j = 0;
    for (int i=0; i<26; i++) if (elementCounts[i] != 0)  result[j++] = elementCounts[i];
    Arrays.sort(result);

    return result;
  }
}

The part that gets the two answers is, as usual, very simple:

  public static long getPart1Answer(final String day14Input, final String[] day14InputLines)
  {
    final PolymerizationTool pt = new PolymerizationTool(day14Input, day14InputLines);
    pt.processNPolymerizationSteps(10);

    long[] elementCounts = pt.getElementCounts();  // returned sorted

    return elementCounts[elementCounts.length-1] - elementCounts[0];
  }

I will spoiler blur part two because it could give something away (although I suspect the challenge text foreshadowed it sufficiently):

  public static long getPart2Answer(final String day14Input, final String[] day14InputLines)
  {
    final PolymerizationTool2 pt2 = new PolymerizationTool2(day14Input, day14InputLines);
    pt2.processNPolymerizationSteps(40);

    long[] elementCounts = pt2.getElementCounts();  // returned sorted

    return elementCounts[elementCounts.length-1] - elementCounts[0];
  }

Hi. It’s me. From way way back in the pack.

Just solved Day 8 after two days working on it off and on. In hindsight I was being too clever. I should have used brute-force, I would have been done days ago. Well I had fun anyway. And my solution runs in 6 milliseconds (instead of what? 5 seconds? wow!).

Maybe after the dust settles I’ll write a brute force solution and see how it really compares. Sometimes it’s better to let the computer do the work!

My brain usually visualizes the brute force approach automatically, and so I usually start with coding it. At least I am not stuck doing nothing (analysis paralysis) and I still get insight into the problem even if the brute force is throw away. Frequently, with challenges that are designed for you to use a clever approach, it will become obvious in part two that brute force will take forever. This year, so far, there have only been one or two of those that brute force won’t apply for part two.

Day 15 was a challenge for me. Graph type algorithms are not something I have a lot of experience with. I created three different versions of solvers before I got to one that made sense to me, and ran in reasonable time and of course got the correct answers. What finally worked for me was realizing I could do a flood fill type algorithm where I only used moves to the right or down. Then that gave the correct answers for the sample input and the first part of the challenge input but not for the second part of the challenge input. I then realized that I had calculated a value that could be the best but that there might be better. I added an optimization pass and that got m all the way.

I created a class called RiskResolver to do the majority of the work:

final class RiskResolver
{
  @org.eclipse.jdt.annotation.NonNullByDefault({})
  record MapLocation(int x, int y) {}

  private final int[][] riskMap;
  private final int[][] riskFill;
  private final int width;
  private final int height;
  private final int tileWidth;
  private final int tileHeight;

  public RiskResolver(final String[] inputRiskMap)
  {
    width = inputRiskMap[0].length();
    height = inputRiskMap.length;
    tileWidth=width;
    tileHeight=height;
    riskMap  = new int[width][height];
    riskFill = new int[width][height];
    for (int y=0; y<height; y++)
    {
      for (int x=0; x<width; x++)
      {
        riskMap[x][y] = inputRiskMap[y].charAt(x)-'0';
      }
    }
  }

  public RiskResolver(final String[] inputRiskMap, int sizeMultiplier)
  {
    tileWidth = inputRiskMap[0].length();
    tileHeight = inputRiskMap.length;
    width = inputRiskMap[0].length() * sizeMultiplier;
    height = inputRiskMap.length * sizeMultiplier;
    riskMap  = new int[width][height];
    riskFill = new int[width][height];
    for (int y=0; y<tileHeight; y++)
    {
      for (int x=0; x<tileWidth; x++)
      {
        riskMap[x][y] = inputRiskMap[y].charAt(x)-'0';
      }
    }
    
    for (int i=1; i<sizeMultiplier; i++)
    {
      for (int y=0; y<tileHeight; y++)
      {
        for (int x=0; x<tileWidth; x++)
        {
          final int tileX = tileWidth*i + x;
          final int tileY = y;
          int tileValue = riskMap[x][y] + i;
          if (tileValue > 9) tileValue -= 9;
          riskMap[tileX][tileY] = tileValue;
        }
      }
    }

    for (int i=1; i<sizeMultiplier; i++)
    {
      for (int j=0; j<sizeMultiplier; j++)
      {
        for (int y=0; y<tileHeight; y++)
        {
          for (int x=0; x<tileWidth; x++)
          {
            final int tileX = tileWidth*j + x;
            final int tileY = tileHeight*i + y;
            int tileValue = riskMap[tileX][tileY-tileHeight] + 1;
            if (tileValue > 9) tileValue -= 9;
            riskMap[tileX][tileY] = tileValue;
          }
        }
      }
    }
  }
 
  long getLowestRiskPath()
  {
    fillMap();
    optimizeMap();
    return riskFill[width-1][height-1];
  }

  private void fillMap()
  {
    riskFill[0][0] = 0;
    riskFill[0][1] = riskMap[0][1];
    riskFill[1][0] = riskMap[1][0];
    riskFill[1][1] = riskMap[1][1] + Math.min(riskFill[0][1], riskFill[1][0]);

    int l=2;
    while (l<width)
    {
      riskFill[l][0] = riskFill[l-1][0] + riskMap[l][0];
      riskFill[0][l] = riskFill[0][l-1] + riskMap[0][l];
      for (int i=1; i<=l; i++)
      {
        riskFill[i][l] = riskMap[i][l] + Math.min(riskFill[i-1][l], riskFill[i][l-1]);
        riskFill[l][i] = riskMap[l][i] + Math.min(riskFill[l-1][i], riskFill[l][i-1]);
      }
      l++;
    }
  }

  private void optimizeMap()
  {
    boolean madeChange = true;
    while (madeChange)
    {
      madeChange = false;
      for (int y=0; y<height; y++)
      {
        for (int x=0; x<width; x++)
        {
          int min=riskFill[x][y];
          final Set<MapLocation> neighbours = getNeightbours(x,y);
          for (final MapLocation neighbour : neighbours)
          {
            if (riskFill[neighbour.x][neighbour.y] + riskMap[x][y] < min)
            {
              riskFill[x][y] = riskFill[neighbour.x][neighbour.y] + riskMap[x][y];
              min = riskFill[x][y];
              madeChange = true;
            }
          }
        }
      }
    }
  }

  private Set<MapLocation> getNeightbours(final int x, final int y)
  {
    final Set<MapLocation> neighbours = new HashSet<>();
    if (x>0) neighbours.add(new MapLocation(x-1, y));
    if (x<width-1) neighbours.add(new MapLocation(x+1, y));
    if (y>0) neighbours.add(new MapLocation(x, y-1));
    if (y<height-1) neighbours.add(new MapLocation(x, y+1));

    return neighbours;
  }
}

The two parts just call into the RiskResolver class, in the second part I call a different constructor to handle the changes to the map:

public static long getPart1Answer(final String day15Input, final String[] day15InputLines)
{
  final RiskResolver rr = new RiskResolver(day15InputLines);
    
  return rr.getLowestRiskPath();
}

public static long getPart2Answer(final String day15Input, final String[] day15InputLines)
{
  final RiskResolver rr = new RiskResolver(day15InputLines, 5);
    
  return rr.getLowestRiskPath();
}

Day 16 is a binary protocol that implements a mathematical expression parser. You need to be able to convert hex input to binary and read from it in groups of bits at a time. To that end, I created a HexToBitStringTranslator class. There are various ways to tackle this, but for me the simplest way was to convert the text hex string into a text binary string. The only tricky bit is that you will need a way to get subsections that you can treat individually.

final class HexToBitStringTranslator
{
  private final String bitString;
  private int currentBit = 0;
  private int bitsLeft;
  
  HexToBitStringTranslator(final String hexInput)
  {
    bitString = convertHexToBitString(hexInput);
    bitsLeft = bitString.length();
  }

  private HexToBitStringTranslator(final String inputBitString, final int startOffset, final int length)
  {
    bitString = inputBitString.substring(startOffset, startOffset+length);
    bitsLeft = length;
  }

  int getNextNBits(final int numberOfBitsToGet)
  {
    if (bitsLeft < numberOfBitsToGet)
    {
      throw new IllegalStateException(String.format("Request to get %d bits when only %d bits are left", numberOfBitsToGet, bitsLeft));
    }
    // add a zero sign bit just in case
    final String nextBits = "0" + bitString.substring(currentBit, currentBit+numberOfBitsToGet);
    final int result = Integer.parseInt(nextBits, 2);
    currentBit += numberOfBitsToGet;
    bitsLeft -= numberOfBitsToGet;
    return result;
  }

  private String convertHexToBitString(final String hexInput)
  {
    final StringBuilder sb = new StringBuilder();
    for (final char c: hexInput.toCharArray())
    {
      switch (c)
      {
        case '0': sb.append("0000"); break;
        case '1': sb.append("0001"); break;
        case '2': sb.append("0010"); break;
        case '3': sb.append("0011"); break;
        case '4': sb.append("0100"); break;
        case '5': sb.append("0101"); break;
        case '6': sb.append("0110"); break;
        case '7': sb.append("0111"); break;
        case '8': sb.append("1000"); break;
        case '9': sb.append("1001"); break;
        case 'A': sb.append("1010"); break;
        case 'B': sb.append("1011"); break;
        case 'C': sb.append("1100"); break;
        case 'D': sb.append("1101"); break;
        case 'E': sb.append("1110"); break;
        case 'F': sb.append("1111"); break;
        default:  throw new IllegalStateException("Invalid hex digit: " + c);
      }
    }
    return sb.toString();
  }

  public int getBitsLeft()
  {
    return bitsLeft;
  }

  public HexToBitStringTranslator getSubPacket(final int lengthToGet)
  {
    final HexToBitStringTranslator result = new HexToBitStringTranslator(bitString, currentBit, lengthToGet);
    currentBit += lengthToGet;
    bitsLeft -= lengthToGet;
    return result;
  }
}

Now that you can get groups of bits as integers as needed, for part one I created a parser to get the sums of the verison numbers. The challenge calls the input BITS so I created the class BITSDecoder. The best approach for the parsing it to use recursion, so you need to make sure you can pass in various different instances of HexToBitStringTranslator, and HexToBitStringTranslator needed to have a way to create them.

final class BITSDecoder
{
  private final HexToBitStringTranslator h2bst;

  public BITSDecoder(final HexToBitStringTranslator h2bst)
  {
    this.h2bst = h2bst;
  }
  
  long decodeAllInstructionsAndSumVersions()
  {
    return decodeAllInstructionsAndSumVersions(h2bst);
  }

  private long decodeAllInstructionsAndSumVersions(final HexToBitStringTranslator h2bst)
  {
    long versionSum = 0;
    while (h2bst.getBitsLeft() > 7)
    {
      versionSum += parsePacket(h2bst);
    }
    return versionSum;
  }

  private long parsePacket(final HexToBitStringTranslator h2bst)
  {
    long verSum = h2bst.getNextNBits(3);
    final int type = h2bst.getNextNBits(3);
    switch (type)
    {
      case 4:   parseLiteralValue(h2bst); break;
      default:  verSum += parseOperator(h2bst); break;
    }
    return verSum;
  }

  private void parseLiteralValue(final HexToBitStringTranslator h2bst)
  {
    boolean more = true;
    while (more)
    {
      more = h2bst.getNextNBits(1) == 1;
      h2bst.getNextNBits(4);
    }
  }

  private long parseOperator(final HexToBitStringTranslator h2bst)
  {
    int subTypeIndicator = h2bst.getNextNBits(1);
    if (subTypeIndicator == 0)
    {
      final int lengthToGet = h2bst.getNextNBits(15);
      return parseSubPacket(h2bst.getSubPacket(lengthToGet));
    }
    else
    {
      final int numSubPackets = h2bst.getNextNBits(11);
      long sum = 0;
      for (int i=0; i<numSubPackets; i++)
      {
        sum += parsePacket(h2bst);
      }
      return sum;
    }
  }

  private long parseSubPacket(final HexToBitStringTranslator subPacket)
  {
    return decodeAllInstructionsAndSumVersions(subPacket);
  }
}

For part two, since the version numbers weren’t relevant any more, I copied my part one BITSDecoder into BITSDecoder2, and changed it to suit the part two task:

final class BITSDecoder2
{
  private final HexToBitStringTranslator h2bst;

  public BITSDecoder2(final HexToBitStringTranslator h2bst)
  {
    this.h2bst = h2bst;
  }
  
  long decodeAndCalulate()
  {
    return decodeAllInstructionsAndCalculateResult(h2bst);
  }

  private long decodeAllInstructionsAndCalculateResult(final HexToBitStringTranslator h2bst)
  {
    final long result = parsePacket(h2bst);
    return result;
  }

  private long parsePacket(final HexToBitStringTranslator h2bst)
  {
    h2bst.getNextNBits(3);
    final int type = h2bst.getNextNBits(3);
    switch (type)
    {
      case 4:   return parseLiteralValue(h2bst);
      case 0:   return sum(h2bst);
      case 1:   return product(h2bst);
      case 2:   return minimum(h2bst);
      case 3:   return maximum(h2bst);
      case 5:   return greaterThan(h2bst);
      case 6:   return lessThan(h2bst);
      case 7:   return equalTo(h2bst);
      default:  throw new IllegalStateException("Unknown type: " + type);
    }
  }

  private long sum(final HexToBitStringTranslator h2bst)
  {
    long sum=0;
    for (final Long value: parseOperator(h2bst))
    {
      sum += value;
    }
    return sum;
  }

  private long product(final HexToBitStringTranslator h2bst)
  {
    long product=1;
    for (final Long value: parseOperator(h2bst))
    {
      product *= value;
    }
    return product;
  }

  private long minimum(final HexToBitStringTranslator h2bst)
  {
    long minimum=Long.MAX_VALUE;
    for (final Long value: parseOperator(h2bst))
    {
      if (value < minimum) minimum = value;
    }
    return minimum;
  }

  private long maximum(final HexToBitStringTranslator h2bst)
  {
    long maximum=Long.MIN_VALUE;
    for (final Long value: parseOperator(h2bst))
    {
      if (value > maximum) maximum = value;
    }
    return maximum;
  }

  private long greaterThan(final HexToBitStringTranslator h2bst)
  {
    final List<Long> values = parseOperator(h2bst);
    if (values.size() != 2)  throw new IllegalStateException("Not precisely two values in greaterThan");

    if (values.get(0) > values.get(1))
    {
      return 1;
    }
    return 0;
  }

  private long lessThan(final HexToBitStringTranslator h2bst)
  {
    final List<Long> values = parseOperator(h2bst);
    if (values.size() != 2)  throw new IllegalStateException("Not precisely two values in lessThan");
    if (values.get(0) < values.get(1))
    {
      return 1;
    }
    return 0;
  }

  private long equalTo(final HexToBitStringTranslator h2bst)
  {
    final List<Long> values = parseOperator(h2bst);
    if (values.size() != 2)  throw new IllegalStateException("Not precisely two values in equalTo");
    if (values.get(0).equals(values.get(1)))
    {
      return 1;
    }
    return 0;
  }

  private long parseLiteralValue(final HexToBitStringTranslator h2bst)
  {
    long result = 0;
    boolean more = true;
    while (more)
    {
      more = h2bst.getNextNBits(1) == 1;
      int digit = h2bst.getNextNBits(4);
      result = result*16 + digit;
    }
    return result;
  }

  private List<Long> parseOperator(final HexToBitStringTranslator h2bst)
  {
    final List<Long> values = new ArrayList<>();
    int subTypeIndicator = h2bst.getNextNBits(1);
    if (subTypeIndicator == 0)
    {
      final int lengthToGet = h2bst.getNextNBits(15);
      final List<Long> subValues = parseSubPacket(h2bst.getSubPacket(lengthToGet));
      values.addAll(subValues);
    }
    else
    {
      final int numSubPackets = h2bst.getNextNBits(11);
      for(int i=0; i<numSubPackets; i++)
      {
        final long subValue = parsePacket(h2bst);
        values.add(subValue);
      }
    }
    return values;
  }

  private List<Long> parseSubPacket(final HexToBitStringTranslator subPacket)
  {
    final List<Long> values = new ArrayList<>();
    while (subPacket.getBitsLeft() > 6)
    {
      final long value = parsePacket(subPacket);
      values.add(value);
    }
    return values;
  }
}

and finally the two parts need to call the two BITSDecoder classes to get the answers:

public static long getPart1Answer(final String day16Input)
{
  final HexToBitStringTranslator h2bst = new HexToBitStringTranslator(day16Input);
  final BITSDecoder bd = new BITSDecoder(h2bst);
  return bd.decodeAllInstructionsAndSumVersions();
}

public static long getPart2Answer(final String day16Input)
{
  final HexToBitStringTranslator h2bst = new HexToBitStringTranslator(day16Input);
  final BITSDecoder2 bd2 = new BITSDecoder2(h2bst);
  return bd2.decodeAndCalulate();
}

Day 17 is all about physics of projectiles and optimizing a trajectory. I started off with records (so basically read only classes with no logic) to represent a point and a box and a velocity. I then created a ProjectileOptimizer class to actually do all the work. Rather than try anything fancy, I just brute forced some reasonable values for initial velocities, from 0 to 200 for part one (because we’re looking for a positive upward value, and -200 to 200 for part 2 because anything that works is considered valid.)

final class ProjectileOptimizer
{
  record Velocity(int xVector, int yVector) {}
  
  private final Point startPosition;
  private final Box targetArea;
  private int maximumHeight = Integer.MIN_VALUE;

  public ProjectileOptimizer(final Point startPosition, final Box targetArea)
  {
    this.startPosition = startPosition;
    this.targetArea    = targetArea;
  }

  public void optimizeTrajectoryForMaximumHeight()
  {
    for (int xVector = 0; xVector < 100; xVector++)
    {
      for (int yVector = 0; yVector < 200; yVector++)
      {
        Point projectile = startPosition;
        Velocity velocity = new Velocity(xVector, yVector);
        boolean reachedTarget = false;
        int maxProjectileHeight = 0;
        while (!reachedTarget)
        {
          if (!projectileCanStillReachTargetArea(projectile, velocity)) break;
          projectile = updateProjectile(projectile, velocity);
          velocity = updateVelocity(velocity);
          if (projectile.y() > maxProjectileHeight)  maxProjectileHeight = projectile.y(); 
          if (projectileReachedTargetArea(projectile))
          {
            reachedTarget = true;
            break;
          }
        }
        if (reachedTarget)
        {
          if (maxProjectileHeight > maximumHeight)  maximumHeight = maxProjectileHeight;
        }
      }
    }
  }
 
  public int findNumberOfPossibleTrajectoriesThatReachTarget()
  {
    int result = 0;

    for (int xVector = -200; xVector < 200; xVector++)
    {
      for (int yVector = -200; yVector < 200; yVector++)
      {
        Point projectile = startPosition;
        Velocity velocity = new Velocity(xVector, yVector);
        boolean reachedTarget = false;
        int maxProjectileHeight = 0;
        while (!reachedTarget)
        {
          if (!projectileCanStillReachTargetArea(projectile, velocity)) break;
          projectile = updateProjectile(projectile, velocity);
          velocity = updateVelocity(velocity);
          if (projectile.y() > maxProjectileHeight)  maxProjectileHeight = projectile.y(); 
          if (projectileReachedTargetArea(projectile))
          {
            reachedTarget = true;
            break;
          }
        }
        if (reachedTarget)
        {
          result++;
        }
      }
    }
    return result;
  }
  
  private boolean projectileCanStillReachTargetArea(final Point projectile, final Velocity velocity)
  {
    if (projectile.y() < Math.min(targetArea.y1(), targetArea.y2())) return false;
    return true;
  }

  private Point updateProjectile(final Point projectile, final Velocity velocity)
  {
    int px = projectile.x();
    int py = projectile.y();
    px += velocity.xVector;
    py += velocity.yVector;
    return new Point(px,py);
  }

  private Velocity updateVelocity(final Velocity velocity)
  {
    int vx=0;
    int vy=velocity.yVector-1;
    if (velocity.xVector > 0)
    {
      vx = velocity.xVector-1;
    }
    else if (velocity.xVector < 0)
    {
      vx = velocity.xVector+1;
    }

    return new Velocity(vx, vy);
  }

  private boolean projectileReachedTargetArea(final Point projectile)
  {
    if ( (projectile.y() >= Math.min(targetArea.y1(), targetArea.y2()))  && 
         (projectile.y() <= Math.max(targetArea.y1(), targetArea.y2()))  && 
         (projectile.x() >= Math.min(targetArea.x1(), targetArea.x2()))  && 
         (projectile.x() <= Math.max(targetArea.x1(), targetArea.x2()))   )
    {
      return true;
    }
    return false;
  }

  public long getMaximumHeight()
  {
    return maximumHeight;
  }
}

To actually get the required answers, the solvers just call the necessary method(s) on the ProjectileOptimizer :

record Point(int x, int y) {}
record Box(int x1, int y1, int x2, int y2) {}
  
public static long getPart1Answer(
    final int startX,
    final int startY,
    final int targetX1,
    final int targetY1,
    final int targetX2,
    final int targetY2
  )
{
  final Point startPosition = new Point(startX, startY);
  final Box   targetArea    = new Box(targetX1, targetY1, targetX2, targetY2);
  final ProjectileOptimizer popt = new ProjectileOptimizer(startPosition, targetArea);
  popt.optimizeTrajectoryForMaximumHeight();
  return popt.getMaximumHeight();
}

public static long getPart2Answer(
    final int startX,
    final int startY,
    final int targetX1,
    final int targetY1,
    final int targetX2,
    final int targetY2
  )
{
  final Point startPosition = new Point(startX, startY);
  final Box   targetArea    = new Box(targetX1, targetY1, targetX2, targetY2);
  final ProjectileOptimizer popt = new ProjectileOptimizer(startPosition, targetArea);
  return popt.findNumberOfPossibleTrajectoriesThatReachTarget();
}

Analysis paralysis - perfect description. I’m stuck on Day 10 pt 2. I know it’s a tree search with backtracking, but I have a mental block coding these. I know, Day 10 is a distant memory for you!

Thanks for the graph warning. I hate those as much as I hate trees! But I’m going to force myself, even if it takes a long time. This year has really been great for expanding my knowledge. Fun, too!

Ever get into the middle of a situation and wonder if you’ve made the right choices to end up there? That was day 18 for me! I typically code immutable data structures/classes whenever I can. The problem presented in day 18 is probably best addressed by a mutable binary tree of some design. I didn’t do that, and didn’t really realize I wished I had, until after I was like 3 hours in. (It took me just over five hours for part one, and then another two minutes for part two.)

I created a class to represent the snailfish numbers in the problem, and another class to wrap the math needed to be done on them. (Although, in the end, I’m unsure I really needed the second class, and I’m unsure the boundaries are clean between them. I was too fried after the 5 hours to consider any further refactoring… maybe later when I’ve slept.)

I’m not even really sure I want to share this code, as it’s not something I’m the most proud of… but it does indeed solve the present problem(s)… it’s just a LOT more code that I wish it were.

This is the SnailFishNumber class. The one odd/tricky thing I did was to manage the explode and split operations by converting the snailfish number back to text with the modifications, and then reparsing it back into a new immutable snailfish number:

final class SnailFishNumber
{
  public static final SnailFishNumber ZERO = new SnailFishNumber("[0]");

  private final SnailFishNumberElement theNumber;

  private SnailFishNumber(final SnailFishNumberElement theNumber)
  {
    this.theNumber = theNumber;
  }
  
  public SnailFishNumber(final String textualSnailFishNumber)
  {
    final Deque<SnailFishNumberElementBuilder> stack = new ArrayDeque<>();
    
    SnailFishNumberElementBuilder currentElementBuilder = new SnailFishNumberElementBuilder();
    SnailFishNumberElement currentElement = SnailFishNumberElement.INVALID;

    for (final char c : textualSnailFishNumber.toCharArray())
    {
      switch(c)
      {
        case '[':
        {
          stack.push(currentElementBuilder);
          currentElementBuilder = new SnailFishNumberElementBuilder();
        }
        break;
        
        case ']':
        {
          currentElement = currentElementBuilder.build();
          currentElementBuilder = stack.pop();
          currentElementBuilder.processElement(currentElement);
        }
        break;

        case ',':
        {
          currentElementBuilder.processComma();
        }
        break;

        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        {
          currentElementBuilder.processDigit(c);
        }
        break;
        
        default:
          throw new IllegalStateException("Don't know how to process char: " + c);
      }
    }
    theNumber = currentElement;
  }

  static SnailFishNumber doAddition(final SnailFishNumber leftSide, final SnailFishNumber rightSide)
  {
    final SnailFishNumberElementBuilder builder = new SnailFishNumberElementBuilder();
    builder.processElement(leftSide.theNumber);
    builder.processComma();
    builder.processElement(rightSide.theNumber);
    return new SnailFishNumber(builder.build());
  }

  public long getMagnitude()
  {
    return getMagnitude(theNumber);
  }

  private long getMagnitude(final SnailFishNumberElement currentElement)
  {
    if (currentElement.isRegularNumber)
    {
      return currentElement.regularNumber;
    }
    final long leftSide = getMagnitude(currentElement.leftSideElement);
    final long rightSide = getMagnitude(currentElement.rightSideElement);
    return 3*leftSide + 2*rightSide;
  }

  public SnailFishNumber doExplode(final ExplodeComponents explodeComponents)
  {
    final StringBuilder sb = new StringBuilder();
    writeIntoDoExplodeStringBuilder(sb, theNumber, explodeComponents);
    return new SnailFishNumber(sb.toString());
  }

  public SnailFishNumber doSplit(final SnailFishNumberElement splitElement)
  {
    final StringBuilder sb = new StringBuilder();
    writeIntoDoSplitStringBuilder(sb, theNumber, splitElement);
    return new SnailFishNumber(sb.toString());
  }

  private void writeIntoDoExplodeStringBuilder(final StringBuilder sb, final SnailFishNumberElement snailFishNumberElement, final ExplodeComponents explodeComponents)
  {
    if (snailFishNumberElement.isRegularNumber)
    {
      if (snailFishNumberElement == explodeComponents.leftSide)
      {
        sb.append(snailFishNumberElement.regularNumber + explodeComponents.explodeElement.leftSideElement.regularNumber);
      }
      else if (snailFishNumberElement == explodeComponents.rightSide)
      {
        sb.append(snailFishNumberElement.regularNumber + explodeComponents.explodeElement.rightSideElement.regularNumber);
      }
      else
        sb.append(snailFishNumberElement.regularNumber);
    }
    else
    {
      if (snailFishNumberElement == explodeComponents.explodeElement)
      {
        sb.append("0");
      }
      else
      {
        sb.append("[");
        final StringBuilder nestedLeftSB = new StringBuilder();
        writeIntoDoExplodeStringBuilder(nestedLeftSB, snailFishNumberElement.leftSideElement, explodeComponents);
        sb.append(nestedLeftSB);
        sb.append(",");
        final StringBuilder nestedRightSB = new StringBuilder();
        writeIntoDoExplodeStringBuilder(nestedRightSB, snailFishNumberElement.rightSideElement, explodeComponents);
        sb.append(nestedRightSB);
        sb.append("]");
      }
    }
  }

  private void writeIntoDoSplitStringBuilder(final StringBuilder sb, final SnailFishNumberElement snailFishNumberElement, final SnailFishNumberElement splitElement)
  {
    if (snailFishNumberElement.isRegularNumber)
    {
      if (snailFishNumberElement == splitElement)
      {
        sb.append('[');
        final int leftSideVal = snailFishNumberElement.regularNumber / 2;
        final int rightSideVal = snailFishNumberElement.regularNumber - leftSideVal;
        sb.append(leftSideVal);
        sb.append(',');
        sb.append(rightSideVal);
        sb.append(']');
      }
      else
        sb.append(snailFishNumberElement.regularNumber);
    }
    else
    {
      sb.append("[");
      final StringBuilder nestedLeftSB = new StringBuilder();
      writeIntoDoSplitStringBuilder(nestedLeftSB, snailFishNumberElement.leftSideElement, splitElement);
      sb.append(nestedLeftSB);
      sb.append(",");
      final StringBuilder nestedRightSB = new StringBuilder();
      writeIntoDoSplitStringBuilder(nestedRightSB, snailFishNumberElement.rightSideElement, splitElement);
      sb.append(nestedRightSB);
      sb.append("]");
    }
  }

  boolean shouldExplode()
  {
    final Deque<SnailFishNumberElement> stack = new ArrayDeque<>();
    SnailFishNumberElement explodeElement = getExplodeElement(stack, theNumber, 1);
    if (explodeElement != SnailFishNumberElement.INVALID)
    {
      return true;
    }
    return false;
  }

  public SnailFishNumberElement getSplitElement()
  {
    final Deque<SnailFishNumberElement> stack = new ArrayDeque<>();
    SnailFishNumberElement splitElement = getSplitElement(stack, theNumber);
    if (splitElement != SnailFishNumberElement.INVALID)
    {
      return splitElement;
    }
    return SnailFishNumberElement.INVALID;
  }

  record ExplodeComponents(SnailFishNumberElement leftSide, SnailFishNumberElement explodeElement, SnailFishNumberElement rightSide) {};

  ExplodeComponents getExplodeComponents()
  {
    SnailFishNumberElement leftSide       = SnailFishNumberElement.INVALID;
    SnailFishNumberElement explodeElement = SnailFishNumberElement.INVALID;
    SnailFishNumberElement rightSide      = SnailFishNumberElement.INVALID;
    
    final Deque<SnailFishNumberElement> stack = new ArrayDeque<>();
    explodeElement = getExplodeElement(stack, theNumber, 1);
    if (explodeElement != SnailFishNumberElement.INVALID)
    {
      explodeElement = stack.peek();
      leftSide  = getLeftRegularNumberBefore(explodeElement);
      rightSide = getRightRegularNumberAfter(explodeElement);
    }

    return new ExplodeComponents(leftSide, explodeElement, rightSide);
  }

  private SnailFishNumberElement getLeftRegularNumberBefore(final SnailFishNumberElement explodeElement)
  {
    final Deque<SnailFishNumberElement> stack = new ArrayDeque<>();
    SnailFishNumberElement result = SnailFishNumberElement.INVALID;
    SnailFishNumberElement currentElement = theNumber;
    while (currentElement != explodeElement)
    {
      if (currentElement.isRegularNumber)
      {
        result = currentElement;
        currentElement = stack.pop();
      }
      else
      {
        stack.push(currentElement.rightSideElement);
        currentElement = currentElement.leftSideElement;
      }
    }
    return result;
  }

  private SnailFishNumberElement getRightRegularNumberAfter(SnailFishNumberElement explodeElement)
  {
    final Deque<SnailFishNumberElement> stack = new ArrayDeque<>();
    SnailFishNumberElement currentElement = theNumber;
    while (currentElement != explodeElement)
    {
      if (currentElement.isRegularNumber)
      {
        currentElement = stack.pop();
      }
      else
      {
        stack.push(currentElement.rightSideElement);
        currentElement = currentElement.leftSideElement;
      }
    }

    while (!stack.isEmpty())
    {
      currentElement = stack.pop();
      if (currentElement.isRegularNumber)
      {
        return currentElement;
      }
      else
      {
        stack.push(currentElement.rightSideElement);
        stack.push(currentElement.leftSideElement);
      }
    }
    return SnailFishNumberElement.INVALID;
  }

  private SnailFishNumberElement getExplodeElement(final Deque<SnailFishNumberElement> stack, final SnailFishNumberElement currentElement, final int currentDepth)
  {
    if (currentElement.isRegularNumber)
    {
      if (currentDepth > 5) return currentElement;
      return SnailFishNumberElement.INVALID;
    }
    stack.push(currentElement);
    final SnailFishNumberElement leftSide = getExplodeElement(stack, currentElement.leftSideElement, currentDepth+1);
    if (leftSide != SnailFishNumberElement.INVALID) return leftSide;
    final SnailFishNumberElement rightSide = getExplodeElement(stack, currentElement.rightSideElement, currentDepth+1);
    if (rightSide != SnailFishNumberElement.INVALID) return rightSide;
    stack.pop();
    return SnailFishNumberElement.INVALID;
  }
  
  private SnailFishNumberElement getSplitElement(final Deque<SnailFishNumberElement> stack, final SnailFishNumberElement currentElement)
  {
    if (currentElement.isRegularNumber)
    {
      if (currentElement.regularNumber > 9) return currentElement;
      return SnailFishNumberElement.INVALID;
    }
    stack.push(currentElement);
    final SnailFishNumberElement leftSide = getSplitElement(stack, currentElement.leftSideElement);
    if (leftSide != SnailFishNumberElement.INVALID) return leftSide;
    final SnailFishNumberElement rightSide = getSplitElement(stack, currentElement.rightSideElement);
    if (rightSide != SnailFishNumberElement.INVALID) return rightSide;
    stack.pop();
    return SnailFishNumberElement.INVALID;
  }


  @Override
  public String toString()
  {
    return theNumber.toString();
  }

  
  static final class SnailFishNumberElement
  {
    static final SnailFishNumberElement INVALID = new SnailFishNumberElement(-1);

    final boolean isRegularNumber;
    final int regularNumber;
    final SnailFishNumberElement leftSideElement; 
    final SnailFishNumberElement rightSideElement;
    
    private SnailFishNumberElement(final int regularNumber)
    {
      isRegularNumber = true;
      this.regularNumber = regularNumber;
      leftSideElement = INVALID;
      rightSideElement = INVALID;
    }

    private SnailFishNumberElement(final SnailFishNumberElement leftSide, final SnailFishNumberElement rightSide)
    {
      isRegularNumber = false;
      this.regularNumber = -1;
      leftSideElement = leftSide;
      rightSideElement = rightSide;
    }

    static SnailFishNumberElement makeRegularNumber(final int regularNumberToMake)
    {
      return new SnailFishNumberElement(regularNumberToMake);
    }

    static SnailFishNumberElement makeSnailFishNumber(final SnailFishNumberElement leftSide, final SnailFishNumberElement rightSide)
    {
      return new SnailFishNumberElement(leftSide, rightSide);
    }
   
    @Override
    public String toString()
    {
      final StringBuilder sb = new StringBuilder();
      writeIntoStringBuilder(sb, this);
      return sb.toString();
    }

    private void writeIntoStringBuilder(final StringBuilder sb, final SnailFishNumberElement snailFishNumberElement)
    {
      if (snailFishNumberElement.isRegularNumber)
      {
        sb.append(snailFishNumberElement.regularNumber);
      }
      else
      {
        sb.append("[");
        final StringBuilder nestedLeftSB = new StringBuilder();
        writeIntoStringBuilder(nestedLeftSB, snailFishNumberElement.leftSideElement);
        sb.append(nestedLeftSB);
        sb.append(",");
        final StringBuilder nestedRightSB = new StringBuilder();
        writeIntoStringBuilder(nestedRightSB, snailFishNumberElement.rightSideElement);
        sb.append(nestedRightSB);
        sb.append("]");
      }
    }
  }

  static final class SnailFishNumberElementBuilder
  {
    private int leftSide = -1;
    private int rightSide = -1;
    private boolean wasComma = false;
    private SnailFishNumberElement leftSideElement = SnailFishNumberElement.INVALID;
    private SnailFishNumberElement rightSideElement = SnailFishNumberElement.INVALID;

    SnailFishNumberElementBuilder()
    {
    }

    public void processElement(final SnailFishNumberElement e)
    {
      if (wasComma)
      {
        rightSideElement = e;
      }
      else
      {
        leftSideElement = e;
      }
    }

    public void processDigit(final char c)
    {
      if (wasComma)
      {
        if (rightSide < 0)
          rightSide = c - '0';
        else
          rightSide = rightSide*10 + (c - '0');
      }
      else
      {
        if (leftSide < 0)
          leftSide = c - '0';
        else
          leftSide = leftSide * 10 + (c - '0'); 
      }
    }

    public void processComma()
    {
      wasComma = true;
    }

    public SnailFishNumberElement build()
    {
      if (!wasComma)
      {
        return SnailFishNumberElement.makeRegularNumber(leftSide);
      }
      if (leftSideElement != SnailFishNumberElement.INVALID)
      {
        if (rightSideElement != SnailFishNumberElement.INVALID)
        {
          return SnailFishNumberElement.makeSnailFishNumber(leftSideElement, rightSideElement);
        }
        return SnailFishNumberElement.makeSnailFishNumber(leftSideElement, SnailFishNumberElement.makeRegularNumber(rightSide));
      }
      if (rightSideElement != SnailFishNumberElement.INVALID)
      {
        return SnailFishNumberElement.makeSnailFishNumber(SnailFishNumberElement.makeRegularNumber(leftSide), rightSideElement);
      }
      return SnailFishNumberElement.makeSnailFishNumber(SnailFishNumberElement.makeRegularNumber(leftSide), SnailFishNumberElement.makeRegularNumber(rightSide));
    }
  }
}

The SnailFishMath class really just helps make the part one and part two methods read a little more clealy. I’m not actually sure much of it is any sort of a spoiler, but there might be one thing that isn’t completely obvious, so I will blur it.

final class SnailFishMath
{
  public static SnailFishNumber add(final SnailFishNumber leftSide, final SnailFishNumber rightSide)
  {
    if (leftSide  == SnailFishNumber.ZERO)  return rightSide;
    if (rightSide == SnailFishNumber.ZERO)  return leftSide;

    final SnailFishNumber unreducedAddition = SnailFishNumber.doAddition(leftSide, rightSide);
    return doReduction(unreducedAddition);
  }

  static SnailFishNumber doReduction(final SnailFishNumber unreducedSnailFishNumber)
  {
    SnailFishNumber reductionResult = unreducedSnailFishNumber;

    boolean madeAChange = true;
    while (madeAChange)
    {
      madeAChange = false;
      if (shouldExplode(reductionResult))
      {
        madeAChange = true;
        final ExplodeComponents explodeComponents = reductionResult.getExplodeComponents();
        reductionResult = reductionResult.doExplode(explodeComponents);
        continue;
      }
      final SnailFishNumberElement splitElement = reductionResult.getSplitElement();
      if (splitElement != SnailFishNumberElement.INVALID)
      {
        madeAChange = true;
        reductionResult = reductionResult.doSplit(splitElement);
        continue;
      }
    }

    return reductionResult;
  }

  static boolean shouldExplode(final SnailFishNumber snailFishNumberToCheck)
  {
    return snailFishNumberToCheck.shouldExplode();
  }

  public static long getMagnitude(final SnailFishNumber snailFishNumberToGetMagnitudeOf)
  {
    return snailFishNumberToGetMagnitudeOf.getMagnitude();
  }
}

The solvers for each part should be pretty obvious, but the handling of part two has one trick:

public static long getPart1Answer(final String day18Input, final String[] day18InputLines)
{
  SnailFishNumber currentSum = SnailFishNumber.ZERO;
  for (final String textualSnailFishNumber :  day18InputLines)
  {
    final SnailFishNumber sfn = new SnailFishNumber(textualSnailFishNumber);
    currentSum = SnailFishMath.add(currentSum, sfn);
  }
  return SnailFishMath.getMagnitude(currentSum);
}

public static long getPart2Answer(final String day18Input, final String[] day18InputLines)
{
  long maxMagnitude = -1;
  for (int i=0; i<day18InputLines.length; i++)
  {
    for (int j=0; j<day18InputLines.length; j++)
    {
      if (i==j) continue;
      final SnailFishNumber sfn1 = new SnailFishNumber(day18InputLines[i]);
      final SnailFishNumber sfn2 = new SnailFishNumber(day18InputLines[j]);
      final SnailFishNumber sum = SnailFishMath.add(sfn1, sfn2);
      final long magnitude = SnailFishMath.getMagnitude(sum);
      if (magnitude > maxMagnitude)  maxMagnitude = magnitude;
    }
  }
  return maxMagnitude;
}

I still don’t have working code for Day 19. I know an approach that should work, but life has gotten in the way of getting it debugged. I think what you need to do is use 3D Manhatten Distatance for each beacon at each scanner. Since the distance between beacons won’t change no matter which scanner sees them (even though they each may not see all) then you can use identical distances to start to hone in on which sets of beacons pairs of scanners have in common. Then you can use the pinned scanners (initially only scanner 0) to translate other scanners into the same space and pin them down too. Repeat until done.

One of the things that got in the way of getting this debugged was driving to another city to get computer parts and then assembling a new computer and installing and configuring it so that I can use it to continue with the challenges. I’m not concerned about missing one since I figured out an approach to solve it that I feel I could implement (with available time.)

Coincidentally, 3D geometry is some of the types of coding I enjoy least… so I’m not terribly motivated to work on the problem anyway. I knew I was never going to be on the leaderboard… :man_shrugging:

Day 20 would be very straightforward, if only Eric wasn’t deliberately making it a tricky situation. The trick is to consider what pixel value should occupy the infinite portion of the image.

Further consider that the problem only asks for even numbered iterations of the picture, because if the infinite background was on when he asked for the count of pixels, the answer would be infinite too.

To solve the problem I made three classes: PixelImage and PixelImageWriter and ImageEnhancer. None of them have significant code.

Here’s PixelImage:

final class PixelImage
{
  private final int width;
  private final int height;
  private final String[] pixelLines;
  private final boolean infiniteBackgroundPixel;

  public PixelImage(final String[] pixelLines)
  {
    this(pixelLines, false);
  }

  public PixelImage(final String[] pixelLines, final boolean infiniteBackgroundPixel)
  {
    width = pixelLines[0].length();
    height = pixelLines.length;
    this.pixelLines = pixelLines;
    this.infiniteBackgroundPixel = infiniteBackgroundPixel;
  }

  boolean isPixelLit(int x, int y)
  {
    if ((x<0) || (x>=width))  return infiniteBackgroundPixel;
    if ((y<0) || (y>=height))  return infiniteBackgroundPixel;
    return pixelLines[y].charAt(x) == '#';
  }
  
  int getClusterValue(final int x, final int y)
  {
    int result = 0;
    if (isPixelLit(x-1, y-1))  result+=256;
    if (isPixelLit(x  , y-1))  result+=128;
    if (isPixelLit(x+1, y-1))  result+= 64;
    if (isPixelLit(x-1, y  ))  result+= 32;
    if (isPixelLit(x  , y  ))  result+= 16;
    if (isPixelLit(x+1, y  ))  result+=  8;
    if (isPixelLit(x-1, y+1))  result+=  4;
    if (isPixelLit(x  , y+1))  result+=  2;
    if (isPixelLit(x+1, y+1))  result+=  1;
    return result;
  }

  public int getWidth()
  {
    return width;
  }

  public int getHeight()
  {
    return height;
  }

  public long getNumberOfLitPixels()
  {
    int count = 0;
    for (int y=0; y<height; y++)
    {
      for (int x=0; x<width; x++)
      {
        if (isPixelLit(x, y)) count++;
      }
    }

    return count;
  }

  public boolean getInfinitBackgroundPixel()
  {
    return infiniteBackgroundPixel;
  }
}

Here’s PixelImageWriter, spoiler blurred for one parameter name:

final class PixelImageWriter
{
  final int width;
  final int height;
  final char[][] pixels;
  public PixelImageWriter(final int width, final int height)
  {
    this.width  = width;
    this.height = height;
    pixels = new char[width][height];
  }

  public void putPixel(final int x, final int y, final boolean isPixelLit)
  {
    if (isPixelLit)
      pixels[x][y] = '#';
    else
      pixels[x][y] = '.';
  }
  
  PixelImage getPixelImage(final boolean infiniteBackgroundPixel)
  {
    final String[] pixelStrings = new String[height];
    for (int y=0; y<height; y++)
    {
      final StringBuilder sb = new StringBuilder();
      for (int x=0; x<width; x++)
      {
        sb.append(pixels[x][y]);
      }
      pixelStrings[y] = sb.toString();
    }
    return new PixelImage(pixelStrings, infiniteBackgroundPixel);
  }
}

And here’s ImageEnhancer:

final class ImageEnhancer
{
  private final String imageEnhancementLookup;
  
  ImageEnhancer(final String imageEnhancementLookup)
  {
    this.imageEnhancementLookup = imageEnhancementLookup;
  }

  PixelImage enhanceImage(final PixelImage inputImage)
  {
    final PixelImageWriter piw = new PixelImageWriter(inputImage.getWidth()+6, inputImage.getHeight()+6);
    for (int y=-3; y<inputImage.getHeight()+3; y++)
    {
      for (int x=-3; x<inputImage.getWidth()+3; x++)
      {
        piw.putPixel(x+3, y+3, imageEnhancementLookup.charAt(inputImage.getClusterValue(x, y)) == '#');
      }
    }
    
    final boolean infinitBackgroundPixel;
    if (inputImage.getInfinitBackgroundPixel())
    {
      infinitBackgroundPixel = getVal(511);
    }
    else
    {
      infinitBackgroundPixel = getVal(0);
    }
    return piw.getPixelImage(infinitBackgroundPixel);
  }

  boolean getVal(int lookupPos)
  {
    return imageEnhancementLookup.charAt(lookupPos) == '#';
  }
}

And finally, here’s the two methods to get the answers for the two parts, nothing special here to blur:

public static long getPart1Answer(final String day20Input, final String[] day20InputLines)
{
  final ImageEnhancer ie = new ImageEnhancer(day20Input);
  final PixelImage pi = new PixelImage(day20InputLines);
    
  final PixelImage pi2 = ie.enhanceImage(pi);
  final PixelImage pi3 = ie.enhanceImage(pi2);

  return pi3.getNumberOfLitPixels();
}

public static long getPart2Answer(final String day20Input, final String[] day20InputLines)
{
  final ImageEnhancer ie = new ImageEnhancer(day20Input);
  PixelImage pi = new PixelImage(day20InputLines);
  for (int i=0; i<50; i++)
  {
    pi = ie.enhanceImage(pi);
  }
  return pi.getNumberOfLitPixels();
}

Day 21 has us play quantum dice games… WTF!! :wink: I actually cracked 1000 with part one, coming in at 740 or so. I just straight out simulated the game, and it was so little code, I didn’t even code a new class, I just included it into the part one solution finder. For part two, I came up with the two tables I needed by working on paper. Three dice throws is a row where each one “splits” three ways, is 27 possible outcomes. There will only be one case with three ones (a sum of three), but there are three ways to get a sum of four, six ways to get a sum of five, seven ways to get a sum of six, six ways to get a sum of seven, three ways to get a sum of eight and finally one way to get a sum of nine.

The major trick for part two is to realize you will see the same outcomes occur many times over and so you can pass the number of times into a recursive call.

Here’s the solution for part one along with the part two call to the DiceGame class:

public static long getPart1Answer(final int player1StartingLocation, final int player2StartingLocation)
{
  int player1Score = 0;
  int player2Score = 0;
  int diceVal = 1;
  int player1Location = player1StartingLocation;
  int player2Location = player2StartingLocation;
  int numberOfDiceRolls = 0;
  while (player1Score < 1000 && player2Score < 1000)
  {
    final int player1Move = diceVal*3+3;
    numberOfDiceRolls += 3;
    diceVal += 3;
    player1Location = ((player1Location-1) + player1Move) % 10 + 1;
    player1Score += player1Location;
    if (player1Score >= 1000) break;

    final int player2Move = diceVal*3+3;
    numberOfDiceRolls += 3;
    diceVal += 3;
    player2Location = ((player2Location-1) + player2Move) % 10 + 1;
    player2Score += player2Location;
  }

  return Math.min(player1Score, player2Score) * numberOfDiceRolls;
}

public static long getPart2Answer(final int player1StartingLocation, final int player2StartingLocation)
{
  final DiceGame dg = new DiceGame(21);
    
  return dg.playAGame(player1StartingLocation, player2StartingLocation);
}

And here’s the part two class DiceGame:

final class DiceGame
{
  private long numberOfPlayer1Wins;
  private long numberOfPlayer2Wins;
  private final int winningScore;
  
  public DiceGame( final int winningScore)
  {
    this.winningScore = winningScore;
  }

  long playAGame(final int player1StartingLocation, final int player2StartingLocation)
  {
    numberOfPlayer1Wins = 0;
    numberOfPlayer2Wins = 0;
    playARound(player1StartingLocation, player2StartingLocation, 0, 0, 1, 1);
    return Math.max(numberOfPlayer1Wins, numberOfPlayer2Wins);
  }

  private static final int[] EACH_DICE_ROLL_COUNT   = {1, 3, 6, 7, 6, 3, 1};
  private static final int[] EACH_DICE_ROLL_ADVANCE = {3, 4, 5, 6, 7, 8, 9};

  void playARound(
      final int player1Position,
      final int player2Position,
      final int player1Score,
      final int player2Score,
      final int whichPlayersTurn,
      long numberOfGamesThatReachThisSituation
    )
  {
    if (whichPlayersTurn == 1)
    {
      for (int i=0; i<EACH_DICE_ROLL_COUNT.length; i++)
      {
        final int p1NewPos = (player1Position-1 + EACH_DICE_ROLL_ADVANCE[i]) % 10 + 1;
        final int p1NewScore = player1Score + p1NewPos;
        if (p1NewScore >= winningScore)
          numberOfPlayer1Wins += numberOfGamesThatReachThisSituation * EACH_DICE_ROLL_COUNT[i];
        else playARound(p1NewPos, player2Position, p1NewScore, player2Score, 2, numberOfGamesThatReachThisSituation * EACH_DICE_ROLL_COUNT[i]);
      }
    }
    else
    {
      for (int i=0; i<EACH_DICE_ROLL_COUNT.length; i++)
      {
        final int p2NewPos = (player2Position-1 + EACH_DICE_ROLL_ADVANCE[i]) % 10 + 1;
        final int p2NewScore = player2Score + p2NewPos;
        if (p2NewScore >= winningScore)
          numberOfPlayer2Wins += numberOfGamesThatReachThisSituation * EACH_DICE_ROLL_COUNT[i];
        else playARound(player1Position, p2NewPos, player1Score, p2NewScore, 1, numberOfGamesThatReachThisSituation * EACH_DICE_ROLL_COUNT[i]);
      }
    }
  }
}

Day 22 is not possible to brute force in general. You would need a lot of RAM to represent “cube pixels” even as bits. Let’s assume the range was from -100K to +100K in each dimension, then you’d need 200k200K200K/8 = 1,000,000,000,000,000 bytes which is 1TB of RAM. I suppose you might have that much memory, but then you’d need years of compute to process all the “pixel bits” individually.

Part one you can brute force because it’s only -50 to +50 in each dimension, which is 100100100 = 1M if you use a boolean per “pixel”. I did part one by brute forcing it for expediency.

I know how to approach part two, but I am not even slightly interested in actually coding it. (Have I mentioned before that 3D challenges are completely uninteresting to me.) In any case the approach is to take any two intersecting cubiods and break them down into smaller non-intersecting cuboids. In general that means 64 smaller cubiods will result from breaking down an intersection. (You have x1 and x2 for cuboid one and x1 and x2 for cuboid two, meaning you have 4 values for x, 4 for y and 4 for z. 444 = 64.)

Anyway, for what it’s worth, here’s my brute force classes CuboidInstruction and Cuboid50. They’re practically derivative, so I don’t think they offer any spoilers really.

final class CuboidInstruction
{
  final int x1;
  final int x2;
  final int y1;
  final int y2;
  final int z1;
  final int z2;
  boolean isOn;

  // example: on x=-20..26,y=-36..17,z=-47..7
  private static final Pattern CUBIOD_INSTRUCTION_PATTERN =
      Pattern.compile("(on|off) x=([-]?\\d+)[.][.]([-]?\\d+),y=([-]?\\d+)[.][.]([-]?\\d+),z=([-]?\\d+)[.][.]([-]?\\d+)");
  public CuboidInstruction(final String instruction)
  {
    final Matcher m = CUBIOD_INSTRUCTION_PATTERN.matcher(instruction);
    if (!m.matches()) throw new IllegalStateException("Couldn't parse instruction: " + instruction);
    isOn=m.group(1).equals("on");
    
    final int xt1=Integer.parseInt(m.group(2));
    final int xt2=Integer.parseInt(m.group(3));
    final int yt1=Integer.parseInt(m.group(4));
    final int yt2=Integer.parseInt(m.group(5));
    final int zt1=Integer.parseInt(m.group(6));
    final int zt2=Integer.parseInt(m.group(7));
    
    if (xt1>xt2)
    {
      x1=xt2;
      x2=xt1;
    }
    else
    {
      x1=xt1;
      x2=xt2;
    }

    if (yt1>yt2)
    {
      y1=yt2;
      y2=yt1;
    }
    else
    {
      y1=yt1;
      y2=yt2;
    }

    if (zt1>zt2)
    {
      z1=zt2;
      z2=zt1;
    }
    else
    {
      z1=zt1;
      z2=zt2;
    }
  }
  
  @Override
  public String toString()
  {
    final StringBuilder sb = new StringBuilder();
    if (isOn)
      sb.append("[on  ");
    else
      sb.append("[off ");
    sb.append(String.format("%6d", x1));
    sb.append("..");
    sb.append(String.format("%6d", x2));
    sb.append(", ");
    sb.append(String.format("%6d", y1));
    sb.append("..");
    sb.append(String.format("%6d", y2));
    sb.append(", ");
    sb.append(String.format("%6d", z1));
    sb.append("..");
    sb.append(String.format("%6d", z2));
    sb.append("]");
    return sb.toString();
  }

  public record Range(int v1, int v2) {}

  public Range getXRange()
  {
    return new Range(x1, x2);
  }

  public Range getYRange()
  {
    return new Range(y1, y2);
  }

  public Range getZRange()
  {
    return new Range(z1, z2);
  }
}
final class Cuboid50
{
  final boolean cuboidRepresentation[][][] = new boolean [101][101][101];

  public void applyInstruction(final CuboidInstruction cuboidInstruction)
  {
    if (cuboidInstruction.x1<-50 || cuboidInstruction.x2>50) return;
    if (cuboidInstruction.y1<-50 || cuboidInstruction.y2>50) return;
    if (cuboidInstruction.z1<-50 || cuboidInstruction.z2>50) return;

    for (int x=cuboidInstruction.x1; x<=cuboidInstruction.x2; x++)
    {
      for (int y=cuboidInstruction.y1; y<=cuboidInstruction.y2; y++)
      {
        for (int z=cuboidInstruction.z1; z<=cuboidInstruction.z2; z++)
        {
          cuboidRepresentation[x+50][y+50][z+50] = cuboidInstruction.isOn;
        }
      }
    }
  }

  public long getNumberOfActive()
  {
    long count = 0;
    for (int x=-50; x<=50; x++)
    {
      for (int y=-50; y<=50; y++)
      {
        for (int z=-50; z<=50; z++)
        {
          if (cuboidRepresentation[x+50][y+50][z+50])  count++;
        }
      }
    }
    return count;
  }
}

So I am angry with AoC. Day 23 wasn’t really a programming problem. I mean you could program it, but it was probably just as easy to do by hand… and apparently that is what the folks getting top leaderboard positions did. Accordingly I have no code for day 23, and no desire to even write any. I’m also angry about day 24, but I will make a separate post about it.

I’m angry about Day 24 because it was impossible to solve with code. You cannot brute force a 14 digit number, no matter how good your code is you’re not writing any. I did write the parser anyway, because I thought maybe Eric was going to be nice enough to restrict the range so it would be found quickly. He did not and so I wasted an hour on that.

I then thought maybe it was supposed to be a threading exercise, so I added parallel threads to my code. That didn’t improve things enough to have bothered. (It MIGHT have finished in a couple of days.)

I then wrote code to convert the input “program” into [Java] code, thinking maybe that would run fast enough if multi-threaded. It did run somewhat faster, but not fast enough to complete in reasonable time.

I then spent time simplifying the generated code, removing stuff that wasn’t really doing useful processing. As I was doing that, I started to see a trend in the input program. There are 14 input digits, and there was 14 sections in the program that were mostly identical, but with different constants.

Having then run out of ideas, I peeked at the Reddit thread and saw someone suggest the code was using the z register to implement a stack. Looking over the code, I started to see what they meant, and then manually transcoded the code into a set of 7 rules. I then used those rules to get the first 5 digits, and then let my now fast bruteforcer do the rest. That got me part one. Part two just meant revisiting the working out of the first 5 digits, and then some quick tweaks to my brute forcer and bingo part two done.

To be clear, I didn’t need to do any brute forcing, I just thought my code would be marginally faster at getting the results than my working it out by hand.

Although my parser is completely pointless, I will share it here. It also includes the code I used to translate the input program into Java code. I could also include my brute forcer, but it would include my working out of my supplied inputs, and wouldn’t really be useful without it, so I don’t think I should.

final class MonadProgram
{
  private final String[] monadProgramInstructions;
  private long w;
  private long x;
  private long y;
  private long z;
  private String[] programInput = new String[0];
  private int inputPointer;
  
  public MonadProgram(String[] monadProgramInstructions)
  {
    this.monadProgramInstructions = monadProgramInstructions;
  }

  public void translateProgramToJavaCode()
  {
    System.out.println("long w=0;");
    System.out.println("long x=0;");
    System.out.println("long y=0;");
    System.out.println("long z=0;");
    int programCounter = 0;
    int inputNumber = 0;
    while (programCounter < monadProgramInstructions.length)
    {
      final String instruction = monadProgramInstructions[programCounter++];
      final String[] instructionParts = instruction.split(" ");
      switch (instructionParts[0])
      {
        case "inp": System.out.println(instructionParts[1] + " = input[" + inputNumber++ + "];"); continue;
        case "add": System.out.println(instructionParts[1] + " += " + instructionParts[2] + ";"); continue;
        case "mul": System.out.println(instructionParts[1] + " *= " + instructionParts[2] + ";"); continue;
        case "div": System.out.println(instructionParts[1] + " /= " + instructionParts[2] + ";"); continue;
        case "mod": System.out.println(instructionParts[1] + " %= " + instructionParts[2] + ";"); continue;
        case "eql": System.out.println(instructionParts[1] + " = (" + instructionParts[1] + "==" + instructionParts[2] + ")?1:0;"); continue;
        default:  throw new IllegalStateException("Unknown instruction: " + instructionParts[0]);
      }
    }
  }

  public void runWithInput(final String[] programInput)
  {
    w=0;
    x=0;
    y=0;
    z=0;
    inputPointer = 0;
    this.programInput = programInput;
    int programCounter = 0;
    while (programCounter < monadProgramInstructions.length)
    {
      executeInstruction(monadProgramInstructions[programCounter++]);
    }
  }

  private void executeInstruction(final String instruction)
  {
    final String[] instructionParts = instruction.split(" ");
    switch (instructionParts[0])
    {
      case "inp": doInp(instructionParts[1]); return;
      case "add": doAdd(instructionParts[1], instructionParts[2]); return;
      case "mul": doMul(instructionParts[1], instructionParts[2]); return;
      case "div": doDiv(instructionParts[1], instructionParts[2]); return;
      case "mod": doMod(instructionParts[1], instructionParts[2]); return;
      case "eql": doEql(instructionParts[1], instructionParts[2]); return;
      default:  throw new IllegalStateException("Unknown instruction: " + instructionParts[0]);
    }
  }

  private void doInp(final String register)
  {
    if (inputPointer >= programInput.length)  throw new IllegalStateException("Out of input");
    final String input = programInput[inputPointer++];
    final long val = Long.parseLong(input);
    switch (register)
    {
      case "w": w = val; return;
      case "x": x = val; return;
      case "y": y = val; return;
      case "z": z = val; return;
      default:  throw new IllegalStateException("Invalid register: " + register);
    }
  }

  private void doAdd(final String register1, final String register2)
  {
    switch (register1)
    {
      case "w": w += switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "x": x += switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "y": y += switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "z": z += switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      default:  throw new IllegalStateException("Invalid register: " + register1);
    }
  }

  private void doMul(final String register1, final String register2)
  {
    switch (register1)
    {
      case "w": w *= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "x": x *= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "y": y *= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "z": z *= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      default:  throw new IllegalStateException("Invalid register: " + register1);
    }
  }

  private void doDiv(final String register1, final String register2)
  {
    switch (register1)
    {
      case "w": w /= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "x": x /= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "y": y /= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "z": z /= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      default:  throw new IllegalStateException("Invalid register: " + register1);
    }
  }

  private void doMod(final String register1, final String register2)
  {
    switch (register1)
    {
      case "w": w %= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "x": x %= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "y": y %= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      case "z": z %= switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);}; return;
      default:  throw new IllegalStateException("Invalid register: " + register1);
    }
  }

  private void doEql(final String register1, final String register2)
  {
    final long compareTo = switch(register2) {case "w" -> w; case "x" -> x; case "y" -> y; case "z" ->z; default -> Long.parseLong(register2);};
    switch (register1)
    {
      case "w": w = (w == compareTo)?1:0; return;
      case "x": x = (x == compareTo)?1:0; return;
      case "y": y = (y == compareTo)?1:0; return;
      case "z": z = (z == compareTo)?1:0; return;
      default:  throw new IllegalStateException("Invalid register: " + register1);
    }
  }

  long getW()
  {
    return w;
  }

  long getX()
  {
    return x;
  }

  long getY()
  {
    return y;
  }

  long getZ()
  {
    return z;
  }
}

This is the threading runner I put in front of the above class to allow me to run parallel copies:

final class MonadProgramRunner implements Runnable
{
  private final long start;
  private final long end;
  private long result = -1;
  final MonadProgram mp;

  MonadProgramRunner(final long start, final long end, final String[] inputProgram)
  {
    this.start = start;
    this.end   = end;
    mp = new MonadProgram(inputProgram);
  }

  @Override
  public void run()
  {
    result = -1;
    for (long possibleSN = end; possibleSN>start; possibleSN--)
    {
      final String[] programInput = convertLongToProgramInput(possibleSN);
      if (programInput.length == 0) continue;
      if (possibleSN % 444_444 == 0) System.out.println(possibleSN);  // provide some infrequent progress indication
      mp.runWithInput(programInput);
      if (mp.getZ() == 0)
      {
        result = possibleSN;
        System.out.println("Found result: " + result);
        break;
      }
    }
    if (result == -1)  System.out.println("Thread ends without result");
  }

  private static String[] convertLongToProgramInput(final long numberToConvert)
  {
    final String possibleSNStr = String.format("%d", numberToConvert);
    final String[] result = new String[14];
    for(int i=0; i<14; i++)
    {
      final char c = possibleSNStr.charAt(i);
      if (c == '0') return new String[0];
      result[i] = "" + c;
    }
    return result;
  }


  boolean isResult()
  {
    return result != -1;
  }
  
  long getResult()
  {
    return result;
  }
}

This is the brute forcing code that scheduled the above class. There is commented code and other messiness because I never actually used this code the way I normally would. You can see the code commented out that I used to produce the Java code translation, for example. And you can see the code is currently in the state to brute force part two with some part one code commented out:

  public static long getPart1Answer(final String[] day24InputLines)
  {
    //final MonadProgram mp = new MonadProgram(day24InputLines);
    //mp.translateProgramToJavaCode();
    return findLargestValidSN();
  }
  
  public static long getPart1AnswerSlowBruteForce(final String[] day24InputLines)
  {
    final ExecutorService executorService = Executors.newFixedThreadPool(30);
    final List<Future<MonadProgramRunner>> futures = new ArrayList<>();
    for (long possibleSN = 99999999999999L; possibleSN>11111111111111L; possibleSN-=10_000_000_000L)
    {
      final long start = possibleSN-10_000_000_000L;
      final long end   = possibleSN;
      final MonadProgramRunner mpr = new MonadProgramRunner(start, end, day24InputLines);
      final Future<MonadProgramRunner> future = executorService.submit(mpr);
      futures.add(future);
    }

    long bestResult = -1;
    for (final Future<MonadProgramRunner> future: futures)
    {
      final MonadProgramRunner mpr;
      try
      {
        mpr = future.get();
      }
      catch (final InterruptedException | ExecutionException e)
      {
        throw new IllegalStateException("Couldn't get future: " + e);
      }

      if (mpr.isResult())
      {
        final long result = mpr.getResult();
        if (result > bestResult)
        {
          System.out.println("Updating bestResult: " + result);
          bestResult = result;
        }
      }
    }
    executorService.shutdown();
    return bestResult;
  }

  private static String[] convertLongToProgramInput(final long numberToConvert)
  {
    final String possibleSNStr = String.format("%d", numberToConvert);
    final String[] result = new String[14];
    for(int i=0; i<14; i++)
    {
      final char c = possibleSNStr.charAt(i);
      if (c == '0') return new String[0];
      result[i] = "" + c;
    }
    return result;
  }

  static long findLargestValidSN()
  {
    long result = -1;
    final int[] input = new int[14];
    //for (int i=0; i<14; i++) input[i] =9;
    for (int i=0; i<14; i++) input[i] =1;
    input[0]=5;  // This would actually be the first five digits of the answer, I changed them all to 5 here so as to not
    input[1]=5;  // give anything away about my input
    input[2]=5;
    input[3]=5;
    input[4]=5;
    
    while (result < 0)
    {
      if (isValidSN(input)) result = Long.parseLong(String.format("%d%d%d%d%d%d%d%d%d%d%d%d%d%d",
          input[0], input[1], input[2], input[3], input[4], input[5], input[6], input[7],
          input[8], input[9], input[10], input[11], input[12], input[13]));
      /*
      input[13]--;
      if (input[13]==0)
      {
        input[12]--;
        input[13]=9;
      }
      if (input[12]==0)
      {
        input[11]--;
        input[12]=9;
      }
      if (input[11]==0)
      {
        input[10]--;
        input[11]=9;
      }
      if (input[10]==0)
      {
        input[9]--;
        input[10]=9;
      }
      if (input[9]==0)
      {
        input[8]--;
        input[9]=9;
      }
      if (input[8]==0)
      {
        input[7]--;
        input[8]=9;
      }
      if (input[7]==0)
      {
        input[6]--;
        input[7]=9;
      }
      if (input[6]==0)
      {
        input[5]--;
        input[6]=9;
      }
      if (input[5]==0)
      {
        input[4]--;
        input[5]=9;
        System.out.println("digit 4 wrap");
      }
      if (input[4]==0)
      {
        input[3]--;
        input[4]=9;
        System.out.println("digit 3 wrap");
      }
      if (input[3]==0)
      {
        input[2]--;
        input[3]=9;
        System.out.println("digit 2 wrap");
      }
      if (input[2]==0)
      {
        input[1]--;
        input[2]=9;
        System.out.println("digit 1 wrap");
      }
      if (input[1]==0)
      {
        input[0]--;
        input[1]=9;
        System.out.println("digit 0 wrap");
      }
      if (input[0]==0)  throw new IllegalStateException("Ran out of digits");
      */
      
      input[13]++;
      if (input[13]==10)
      {
        input[12]++;
        input[13]=1;
      }
      if (input[12]==10)
      {
        input[11]++;
        input[12]=1;
      }
      if (input[11]==10)
      {
        input[10]++;
        input[11]=1;
      }
      if (input[10]==10)
      {
        input[9]++;
        input[10]=1;
      }
      if (input[9]==10)
      {
        input[8]++;
        input[9]=1;
      }
      if (input[8]==10)
      {
        input[7]++;
        input[8]=1;
      }
      if (input[7]==10)
      {
        input[6]++;
        input[7]=1;
      }
      if (input[6]==10)
      {
        input[5]++;
        input[6]=1;
      }
      if (input[5]==10)
      {
        input[4]++;
        input[5]=1;
        System.out.println("digit 4 wrap");
      }
      if (input[4]==10)
      {
        input[3]++;
        input[4]=1;
        System.out.println("digit 3 wrap");
      }
      if (input[3]==10)
      {
        input[2]++;
        input[3]=1;
        System.out.println("digit 2 wrap");
      }
      if (input[2]==10)
      {
        input[1]++;
        input[2]=1;
        System.out.println("digit 1 wrap");
      }
      if (input[1]==10)
      {
        input[0]++;
        input[1]=1;
        System.out.println("digit 0 wrap");
      }
      if (input[0]==10)  throw new IllegalStateException("Ran out of digits");

    }
    return result;
  }

This is the reduced generated Java code that I commented as I worked through it… This should give you a massive hint how it was done, so I will spoiler blur it:

  static boolean isValidSN(final int[] input)
  {
    long w=0;
    long x=0;
    long y=0;
    long z=0;

    z += input[0]+7;  // push input0+7

    z *= 26;
    z += input[1]+8;  // push input1+8

    z *= 26;
    z += input[2]+10;  // push input2+10

    w = input[3];
    x = z % 26 - 2;  //  does input3 == input2+8
    z /= 26;         // pop
    x = (x==w)?1:0;
    x = (x==0)?1:0;
    y = 25 * x + 1;
    z *= y;
    y = (w+4)*x;     
    z += y;           

    w = input[4];
    x = z % 26 - 10;  // does input4 == input1-2
    z /= 26;          // pop
    x = (x==w)?1:0;
    x = (x==0)?1:0;
    y = 25 * x + 1;
    z *= y;
    y = (w+4)*x;
    z += y;           

    z *= 26;
    z += input[5]+6;  // push input5+6

   \\ rest of the code resulting from my personal input is elided

You are a talented and patient coder @PHolder - I salute you!!

(Waving from way back in Day 9!)

Well, I hope you’re still having fun, because there are some pretty rough days later on. But it’s not really the end result now is it… it’s the journey… so keep plugging onward and polishing your skills and learning!