Advent of Code 2021

@Cyphase it worked… so thanks for the tip :slight_smile:

1 Like

No problem. :slightly_smiling_face:

I used the same technique to solve it, as did most people it seems. Currently on IRC they’re discussing ways it might be done with a relatively closed-form mathematical solution.

Just looking at the stats from the first few days. The numbers fall off very quickly it seems.

    Both Parts    Just Part one
5     48638           2900
4     61220           3679
3     88508          23838
2    128369           4445
1    146587          16254

Day 3 had a lot of people who didn’t do part 2
My numbers show that Day 3 was the one where I did the most work to finish part 2
3 00:18:38 00:53:58
from just under 20 minutes, so it took another 35 minutes to finish it

Day 7 had me thinking there must be a simpler way to solve it than I came up with, but I brute forced it anyway as it was running through my 1000 inputs in less than a second. Part two did have a formulaic way to speed up the math, that anyone should have learned in algebra class (or combinatorics class). The sum of the numbers from 1 to n is n(n+1)/2 .

My approach was to find the minimum and maximum horizontal values as I parsed the string values to integers. (I keep them as strings, but in the future I probably should just do an array initialization with the provided values as a speedup… And I could probably find a standard library or 3rd party library to find the min and max in an array without having to code my own loop… but as I don’t currently know if they exist, it would take me longer to find out than to just code the loop myself.)

Then I coded another loop to go between the minimum and the maximum and calculate the fuel used at each step, remembering the minimum value of the fuel used to supply the answer. I actually screwed up initially because I thought the best position was being requested as the answer and not the fuel used for that position… so that wasted me a good 5 minutes while I debugged code that didn’t actually have a bug :wink:

The only change for part two was the means of calculating the fuel used, so it was pretty quick to get from a correct submission of part one to the correct submission of part two. (Although I can’t actually find out how long because the site won’t let me see my personal stats and says “This page has been temporarily disabled. Sorry!” )

Here’s my code for part two as it’s one line longer than the code for part one:

  public static long getPart2Answer(final String day07Input, final String[] day07InputLines)
  {
    final int[] crabHVals = new int[day07InputLines.length];
    int min = 100_000_000;
    int max = -1;
    for(int i=0; i<day07InputLines.length; i++)
    {
      final int crabHVal = Integer.parseInt(day07InputLines[i]);
      crabHVals[i] = crabHVal;
      if (crabHVal < min) min = crabHVal;
      if (crabHVal > max) max = crabHVal;
    }
    // System.out.format("Num crabs=%d, minHPos=%d, maxHPos=%d%n", day07InputLines.length, min, max);
    long bestMin = 100_000_000_000_000_000L;
    int bestPos = -1;
    // Brute force all values in range to find smallest fuel usage
    for (int i=min; i<=max; i++)
    {
      final long minGuess = calculateFuelUse2(crabHVals, i);
      if (minGuess < bestMin)
      {
        bestMin = minGuess;
        bestPos = i;
      }
    }
    // System.out.format("Best position=%d with fuel=%d%n", bestPos, bestMin);
    return bestMin;
  }

  private static long calculateFuelUse2(final int[] crabHVals, final int hPosition)
  {
    long fuelUsed = 0;
    for (final int crabHVal : crabHVals)
    {
      final int n = Math.abs(hPosition - crabHVal);
      fuelUsed += n*(n+1)/2;  // the sum of numbers from 1 to n is N(N+1)/2
    }
    return fuelUsed;
  }

I have a memory of taking advantage of that formula, and possibly even deriving it on the spot, in 7th grade, while we were doing our “Problem of the Week”, which I always looked forward to. In that case the problem involved, or may have consisted entirely of, summing the numbers 1-100 (= 5050). I seem to remember the teacher suggesting I may have cheated somehow because of how quickly I did it, but I think I explained what I’d done and he was fine in the end.

Though now that I think about it, I may have already known the answer for that particular case from prior experience. Too long ago to be sure. But I would have at least been able to explain how to get it.

I like that formula as an example of one someone can relatively easily derive even with no prior exposure to it, once set the task of getting an answer that would otherwise require a lot of tedious math.

“Sum of 1-10000? Ugh. Hmm, wait… 1+10000 is 10001… so is 2+9999… ooooh…”

In high school I remember playing with VaxBASIC and coding a loop to do the math, and then being utterly shocked when it instantly gave me the answer no matter how large the sum. I was told that the compiler had that optimization because it helped game compiler benchmarks (at the time.)

1 Like

Oooh, Day 8 is a bit of a doozy. Longest time to fill the leaderboard so far, by almost double.

Yeah. I was telling my friend I “brute^2 forced it” LOL. I got done in 1h22m (after doing part one in 16m.) This is the first day this year where I hardly used any code from part one in part two. I kept feeling like there should be a more clever answer, but I knew the brute force approach was going to get me there so I just soldiered on. The second level of brute force was I knew I could code something clever, but I did copy and paste and quick edits instead for expediency.

I could have been faster on the first part, but I got tripped up on the pipe char being special to regex and forgetting to escape it. Oops. I also initially misunderstood that my case for the required digits in part one needed to match on the length of the string. Luckily my unit tests saved me multiple times even though they cost time to code.

In my approach, I have methods like isZeroDigit(), isOneDigit(), isTwoDigit() etc… and that exposed a bug in my initial approach. I was initially only considering the lit segments and not the unlit ones. Of course that meant I was getting multiple possible digits… for example 8 has all segments lit, so it can also be any other digit if you just looked at the lit segments.

Because of all my brute forcishness, I have a lot more code this time around. Here’s my class that does the majority of the hard work, it represents a single seven segment display:

final class SevenSegmentLED
{
  final char[] signalWireLetters;
  
  SevenSegmentLED(final String[] signalsToDecode)
  {
    signalWireLetters = bruteForceDecodeSignalLetters(signalsToDecode);
  }

  int getDigitFromSignals(final String signal)
  {
    final boolean[] litSegments = decodeSignalToLitSegments(signalWireLetters, signal);

    if (isZeroDigit(litSegments))   return 0;
    if (isOneDigit(litSegments))    return 1;
    if (isTwoDigit(litSegments))    return 2;
    if (isThreeDigit(litSegments))  return 3;
    if (isFourDigit(litSegments))   return 4;
    if (isFiveDigit(litSegments))   return 5;
    if (isSixDigit(litSegments))    return 6;
    if (isSevenDigit(litSegments))  return 7;
    if (isEightDigit(litSegments))  return 8;
    if (isNineDigit(litSegments))   return 9;

    throw new IllegalStateException("Couldn't get digit from signal");
  }

  private char[] bruteForceDecodeSignalLetters(final String[] signalsToDecode)
  {
    final char[] signalWireLetters = new char[7];
    
    for(int a=0; a<7; a++)
    {
      for(int b=0; b<7; b++)
      {
        if (b==a) continue;
        for(int c=0; c<7; c++)
        {
          if ((c==a) || (c==b)) continue;
          for(int d=0; d<7; d++)
          {
            if ((d==a) || (d==b) || (d==c)) continue;
            for(int e=0; e<7; e++)
            {
              if ((e==a) || (e==b) || (e==c) || (e==d)) continue;
              for(int f=0; f<7; f++)
              {
                if ((f==a) || (f==b) || (f==c) || (f==d) || (f==e)) continue;
                for(int g=0; g<7; g++)
                {
                  if ((g==a) || (g==b) || (g==c) || (g==d) || (g==e) || (g==f)) continue;
                  signalWireLetters[a]=0;
                  signalWireLetters[b]=1;
                  signalWireLetters[c]=2;
                  signalWireLetters[d]=3;
                  signalWireLetters[e]=4;
                  signalWireLetters[f]=5;
                  signalWireLetters[g]=6;
                  if (signalsDecodeCorrectly(signalWireLetters, signalsToDecode))
                  {
                    return signalWireLetters;
                  }
                }
              }
            }
          }
        }
      }
    }
    throw new IllegalStateException("Couldn't find a valid signal decode");
  }

  private boolean signalsDecodeCorrectly(final char[] signalWireLetters, final String[] signalsToDecode)
  {
    for (final String signal : signalsToDecode)
    {
      final boolean[] litSegments = decodeSignalToLitSegments(signalWireLetters, signal);
     
      if (!isValidDigit(litSegments))  return false;
    }

    return true;
  }

  private boolean[] decodeSignalToLitSegments(final char[] signalWireLetters, final String signal)
  {
    final boolean[] litSegments = new boolean[7];
    for (final char c : signal.toCharArray())
    {
      switch (c)
      {
        case 'a':
          litSegments[signalWireLetters[0]] = true;
          break;
        case 'b':
          litSegments[signalWireLetters[1]] = true;
          break;
        case 'c':
          litSegments[signalWireLetters[2]] = true;
          break;
        case 'd':
          litSegments[signalWireLetters[3]] = true;
          break;
        case 'e':
          litSegments[signalWireLetters[4]] = true;
          break;
        case 'f':
          litSegments[signalWireLetters[5]] = true;
          break;
        case 'g':
          litSegments[signalWireLetters[6]] = true;
          break;
        default:
          throw new IllegalStateException("Invalid letter: " + c);
      }
    }
    return litSegments;
  }

  private boolean isValidDigit(final boolean[] litSegments)
  {
    if (isZeroDigit(litSegments))   return true;
    if (isOneDigit(litSegments))    return true;
    if (isTwoDigit(litSegments))    return true;
    if (isThreeDigit(litSegments))  return true;
    if (isFourDigit(litSegments))   return true;
    if (isFiveDigit(litSegments))   return true;
    if (isSixDigit(litSegments))    return true;
    if (isSevenDigit(litSegments))  return true;
    if (isEightDigit(litSegments))  return true;
    if (isNineDigit(litSegments))   return true;

    return false;
  }

  private boolean isZeroDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[0])  &&
         (litSegments[1])  &&
         (litSegments[2])  &&
         (litSegments[4])  &&
         (litSegments[5])  &&
         (litSegments[6])  &&
         (!litSegments[3])
       ) return true;
    return false;
  }

  private boolean isOneDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[2])  &&
         (litSegments[5])  &&
         (!litSegments[0]) &&
         (!litSegments[1]) &&
         (!litSegments[3]) &&
         (!litSegments[4]) &&
         (!litSegments[6])
       ) return true;
    return false;
  }

  private boolean isTwoDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[0]) &&
         (litSegments[2]) &&
         (litSegments[3]) &&
         (litSegments[4]) &&
         (litSegments[6]) &&
         (!litSegments[1]) &&
         (!litSegments[5])
       ) return true;
    return false;
  }

  private boolean isThreeDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[0])  &&
         (litSegments[2])  &&
         (litSegments[3])  &&
         (litSegments[5])  &&
         (litSegments[6])  &&
         (!litSegments[1]) &&
         (!litSegments[4])
       ) return true;
    return false;
  }

  private boolean isFourDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[1])  &&
         (litSegments[2])  &&
         (litSegments[3])  &&
         (litSegments[5])  &&
         (!litSegments[0]) &&
         (!litSegments[4]) &&
         (!litSegments[6])
       ) return true;
    return false;
  }

  private boolean isFiveDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[0])  &&
         (litSegments[1])  &&
         (litSegments[3])  &&
         (litSegments[5])  &&
         (litSegments[6])  &&
         (!litSegments[2]) &&
         (!litSegments[4])
       ) return true;
    return false;
  }

  private boolean isSixDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[0])  &&
         (litSegments[1])  &&
         (litSegments[3])  &&
         (litSegments[4])  &&
         (litSegments[5])  &&
         (litSegments[6])  &&
         (!litSegments[2]) 
       ) return true;
    return false;
  }

  private boolean isSevenDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[0])  &&
         (litSegments[2])  &&
         (litSegments[5])  &&
         (!litSegments[1]) &&
         (!litSegments[3]) &&
         (!litSegments[4]) &&
         (!litSegments[6])
       ) return true;
    return false;
  }

  private boolean isEightDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[0]) &&
         (litSegments[1]) &&
         (litSegments[2]) &&
         (litSegments[3]) &&
         (litSegments[4]) &&
         (litSegments[5]) &&
         (litSegments[6])
       ) return true;
    return false;
  }

  private boolean isNineDigit(final boolean[] litSegments)
  {
    if ( 
         (litSegments[0])  &&
         (litSegments[1])  &&
         (litSegments[2])  &&
         (litSegments[3])  &&
         (litSegments[5])  &&
         (litSegments[6])  &&
         (!litSegments[4])
       ) return true;
    return false;
  }
}

Then the solvers for each of the two parts were smaller:

  public static long getPart1Answer(final String day08Input, final String[] day08InputLines)
  {
    final Pattern p = Pattern.compile("[\\w\\s]+ \\| ([\\w\\s]+)");
    int count = 0;
    for (final String s: day08InputLines)
    {
      final Matcher m = p.matcher(s);
      if (!m.matches())  throw new IllegalStateException("Couldn't parse pattern: " + s);
      final String[] parts = m.group(1).split(" ");
      if (parts.length != 4)  throw new IllegalStateException("Didn't get 4 parts after the | " + m.group(1));
      for (final String part: parts)
      {
        switch(part.length())
        {
          case 2:
          case 3:
          case 4:
          case 7:
            count++;
            break;
          default:  // ignore
        }
      }
    }
    return count;
  }

  public static long getPart2Answer(final String day08Input, final String[] day08InputLines)
  {
    long sum = 0;
    final Pattern p = Pattern.compile("([\\w\\s]+) \\| ([\\w\\s]+)");
    for (final String s: day08InputLines)
    {
      final Matcher m = p.matcher(s);
      if (!m.matches())  throw new IllegalStateException("Couldn't parse pattern: " + s);
      final String[] leftParts = m.group(1).split(" ");
      if (leftParts.length != 10)  throw new IllegalStateException("Didn't get 10 parts before the | " + m.group(1));
      final String[] rightParts = m.group(2).split(" ");
      if (rightParts.length != 4)  throw new IllegalStateException("Didn't get 4 parts after the | " + m.group(1));
      final SevenSegmentLED ssLED = new SevenSegmentLED(leftParts);
      int fourDigits = 0;
      for (final String digitSignals: rightParts)
      {
        fourDigits *= 10;
        fourDigits += ssLED.getDigitFromSignals(digitSignals);
      }
      sum += fourDigits;
    }
    return sum;
  }

I’m so way behind you guys, but I work slooooow.

Finished Day 4 part 1 last night, but part two made me refactor the whole dang thing!

I had a 3am epiphany that what I needed was a general bingo solver, so I did that. It’s one thing to find the first board, quite another thing to find the last board. The big realization was 1. it’s possible that a single ball could make more than one bingo, and 2. I don’t need to keep track of the winning boards and their balls - just the solutions. Never enjoyed refactoring so much. And my new solution is so much more elegant!

As usual, it will be up on GitHub as soon as I get a chance. (Uploaded. On to Day 5.)

The whole experience was very satisfying and reminded me why I love AoC time!

Poor Lisa.

I love it when I can make a “one-liner” solution for a day

export const part1 = async d => {
	return d.split('\n')
		.map(e => e.split(' | ')[1].split(' ').filter(e => e.length == 2 || e.length == 4 || e.length == 3 || e.length == 7))
		.reduce((p, v) => v.length + p, 0);
};

I think last year I did that for like most of the first 8 days

My part 2 is a mess however.

Anyway, in case you want to peek at my code: GitHub - miquelfire/aoc2021: Code for Advent of Code 2021

1 Like

Java has these chained lambda expressions too, and they can be pretty readable, but I find them hellish to debug because you can’t set breakpoints where you want. This is the majority reason why I avoid them unless they’re very small and unlikely to be buggy.

This year I’m using a Racket library called “threading” which emulates a similar Haskell feature. It introduces a macro ~> which allows you to thread modifications instead of nesting functions.

So instead of

(map string->number (string-split (first (string-split day4data "\n\n")) ",")

I can type

(~> day4data  ; convert to list of numbers
                        (string-split _ "\n\n")
                        (first _)
                        (string-split _ ",")
                        (map string->number _))

The _represents the output of the preceding element.

I think this is clearer. It is for me, at least.

Part two of day 9 had me brute force a flood fill for the first time in my programming career. I’m sure it’s possible to do it more efficiently than I did, as I was definitely iterating over the same spots too many times, but, for expediency, I did what I knew would work rather than getting fancier.

I represented the given data in a class I called HeightMap. I thought it would make my life easier if a grew the map by a unit in every direction, so that I could work inside the perimeter without worrying about special casing for the edges. (Not sure that really paid off, but neither do I think it really hurt any.) It didn’t start out storing the locations for part one, because of course the part one answer didn’t call for them. When I realized I would need them for part two, I had to quickly refactor my part one to start using a Point record and store all the Points from part one to use in part two. The HeightMap class has all the logic to find the answers, but I ended up including a sort() in the method for part two.

final class HeightMap
{
  final int[][] heights;
  final int width;
  final int height;
  HeightMap(final String[] mapHeights)
  {
    width  = mapHeights[0].length();
    height = mapHeights.length;
    heights = new int[width+2][height+2];

    for (int x=0; x<=width+1; x++)
    {
      for (int y=0; y<=height+1; y++)
      {
        heights[x][y] = -1;
      }
    }

    for (int x=1; x<=width; x++)
    {
      for (int y=1; y<=height; y++)
      {
        heights[x][y] = mapHeights[y-1].charAt(x-1)-'0';
      }
    }
  }

  int getRiskLevel()
  {
    int totalRiskLevel = 0;
    final Point[] allLowPoints = getAllLowPoints();
    for (final Point p : allLowPoints)
    {
      totalRiskLevel += 1+heights[p.x][p.y];
    }
    return totalRiskLevel;
  }

  @org.eclipse.jdt.annotation.NonNullByDefault({})
  record Point(int x, int y) {}
  
  Point[] getAllLowPoints()
  {
    final List<Point> lowPoints = new ArrayList<>();
    for (int x=1; x<=width; x++)
    {
      for (int y=1; y<=height; y++)
      {
        if (isLowPoint(new Point(x,y)))
        {
          lowPoints.add(new Point(x,y));
        }
      }
    }
    return lowPoints.toArray(new Point[lowPoints.size()]);
  }
  
  int[] getSizesOfAllBasins()
  {
    final List<Integer> basinSizes = new ArrayList<>();
    for (final Point p : getAllLowPoints())
    {
      final int basinSize = getBasinSize(p);
      basinSizes.add(basinSize);
    }
    final int[] result = new int[basinSizes.size()];
    for (int i=0; i<basinSizes.size(); i++) result[i] = basinSizes.get(i);
    return result;
  }

  private int getBasinSize(final Point startingPoint)
  {
    final Set<Point> pointsInBasin = new HashSet<>();
    pointsInBasin.add(startingPoint);

    growBasin(pointsInBasin);
    
    return pointsInBasin.size();
  }

  private void growBasin(final Set<Point> pointsInBasin)
  {
    final Set<Point> pointsToAdd = new HashSet<>();
    for (final Point p : pointsInBasin)
    {
      final Point[] adjacentPoints = getAdjacentPoints(p);
      for (final Point adjacentPoint : adjacentPoints)
      {
        if (!pointsInBasin.contains(adjacentPoint))
        {
          if (heights[adjacentPoint.x][adjacentPoint.y] < 9)
          {
            pointsToAdd.add(adjacentPoint);
          }
        }
      }
    }
    if (!pointsToAdd.isEmpty())
    {
      pointsInBasin.addAll(pointsToAdd);
      growBasin(pointsInBasin);  // recursively check the newly added points
                                 // (inefficient because it will recheck already checked points too)
    }
  }

  private boolean isLowPoint(final Point point)
  {
    final Point[] adjacentPoints = getAdjacentPoints(point);
    final int c = heights[point.x][point.y];
    boolean allGreater = true;
    for (final Point otherPoint : adjacentPoints)
    {
      if (heights[otherPoint.x][otherPoint.y] <= c)
      {
        allGreater = false;
        break;
      }
    }
    return allGreater;
  }

  private Point[] getAdjacentPoints(final Point p)
  {
    final Set<Point> adjacentPoints = new HashSet<>();
    int t = heights[p.x][p.y-1];
    if (t >=0) adjacentPoints.add(new Point(p.x, p.y-1));
    int l = heights[p.x-1][p.y];
    if (l >=0) adjacentPoints.add(new Point(p.x-1, p.y));
    int r = heights[p.x+1][p.y];
    if (r >=0) adjacentPoints.add(new Point(p.x+1, p.y));
    int b = heights[p.x][p.y+1];
    if (b >=0) adjacentPoints.add(new Point(p.x, p.y+1));
    return adjacentPoints.toArray(new Point[adjacentPoints.size()]);
  }

  @Override
  public String toString()
  {
    final StringBuilder sb = new StringBuilder();
    for (int y=0; y<=height+1; y++)
    {
      for (int x=0; x<=width+1; x++)
      {
        if (heights[x][y] < 0)
          sb.append('X');
        else
          sb.append((char)('0'+heights[x][y]));
      }
      sb.append('\n');
    }
    return sb.toString();
  }
}

Here’s the part one and part two code, including the aforementioned sort(). I don’t think there is anything here to bother spoiler blurring:

public static long getPart1Answer(final String day09Input, final String[] day09InputLines)
{
  final HeightMap hm = new HeightMap(day09InputLines);
  return hm.getRiskLevel();
}

public static long getPart2Answer(final String day09Input, final String[] day09InputLines)
{
  final HeightMap hm = new HeightMap(day09InputLines);
  final int[] basinSizes = hm.getSizesOfAllBasins();
  Arrays.sort(basinSizes);
  return (long)basinSizes[basinSizes.length-1] *
         (long)basinSizes[basinSizes.length-2] *
         (long)basinSizes[basinSizes.length-3];
}

OK you old timers, what the heck is going on with Day 6? Part 1 was worryingly trivial. Then comes part two. First the test input is the same as the problem - both 256 days! Typo?

Then, of course, the problem won’t run in any normal memory. I double the available memory each time. Not sure if it will ever run. What am I missing?

EDIT: @PHolder I see your post on day 6. Not looking at the spoilers, but I gather I’ll have to try to solve this without representing each fish. Hmmm.

I got it! RLE! I still think there’s a typo in the problem but what the heck.

OMG the run time went from 1.5 seconds to 1 millisecond. What an optimization! On to Day 7.

Yup 256 days. There is no way you can simulate each fish as the final answer is very very large, like quadrillions or more.

You confused me at first, but after thinking on it some, yes, Run Length Encoding is one way to look at the approach I used :wink:

Day 10 tests whether you know how to handle and validate nesting of things. I believe the most common way to do this is with a stack of some sort. Assuming you know this, then it’s not terribly challenging to solve day 10. Part one has you finding improperly nested items and part two has you filter out the improperly nested ones to find the incomplete ones and calculate how to complete them. You do this by popping the stack of open braces to figure out what the corresponding close brace would be.

I built a more general class to handle the task, and in theory it could work with any pairs of open and close characters, not just the 4 braces used by the problem. (I just can’t not be general when it costs so little, but of course that means I’ll never be on the leader boards.)

Here is my NestingChecker class:

final class NestingChecker
{
  private final Deque<Character> stack = new ArrayDeque<>();
  private final String openChars;
  private final String closeChars;

  NestingChecker(final String openChars, final String closeChars)
  {
    this.openChars = openChars;
    this.closeChars = closeChars;
  }
  
  @org.eclipse.jdt.annotation.NonNullByDefault({})
  record CheckResult(boolean isProperlyNested, char errorChar) {}
  
  CheckResult checkNestingOf(final String stringToCheck)
  {
    stack.clear();
    for (final char c : stringToCheck.toCharArray())
    {
      if (isOpenChar(c))
      {
        stack.push(c);
      }
      else if (isCloseChar(c))
      {
        if (stack.isEmpty()) return new CheckResult(false, c);
        final char openC = stack.pop();
        if (!charsCorrespond(openC, c)) return new CheckResult(false, c);
      }
      else
      {
        // ignore non-open and non-close chars?
        System.out.println("Non-open and non-close char detected: " + c);
      }
    }
    return new CheckResult(true, ' ');
  }

  private boolean charsCorrespond(final char openChar, final char closeChar)
  {
    for (int i=0; i<openChars.length(); i++)
    {
      if (openChars.charAt(i) == openChar)
      {
        if (closeChars.charAt(i) == closeChar) return true;
      }
    }
    return false;
  }

  private boolean isOpenChar(final char c)
  {
    if (openChars.contains(""+c)) return true; 
    return false;
  }

  private boolean isCloseChar(final char c)
  {
    if (closeChars.contains(""+c)) return true; 
    return false;
  }

  // Really should have just returned the necessary characters and moved the problem math out
  // to the calling code, but hindsight is 20/20 as they say...  that's just a small refactor away
  long getScoreOfNestingCompletion(final String input)
  {
    final CheckResult cr = checkNestingOf(input);
    if (!cr.isProperlyNested) throw new IllegalStateException("Can't do nesting completion on improperly nested input: " + input);

    long sum=0;
    while (!stack.isEmpty())
    {
      final char openChar = stack.pop();
      final char neededCloseChar = getNeededCloseChar(openChar);
      sum*=5;
      switch (neededCloseChar)
      {
        case ')': sum+=1; break;
        case ']': sum+=2; break;
        case '}': sum+=3; break;
        case '>': sum+=4; break;
        default:
          break;
      }
    }
    
    return sum;
  }

  private char getNeededCloseChar(final char openChar)
  {
    for (int i=0; i<openChars.length(); i++)
    {
      if (openChars.charAt(i) == openChar)  return closeChars.charAt(i); 
    }
    throw new IllegalStateException("Can't getNeededCloseChar: " + openChar);
  }
}

The two solver methods are pretty straight forward, not sure there is anything interesting enough to be a spoiler here:

  public static long getPart1Answer(final String day10Input, final String[] day10InputLines)
  {
    final NestingChecker nc = new NestingChecker("([{<", ")]}>");
    long sum = 0;
    for (final String input : day10InputLines)
    {
      final CheckResult cr = nc.checkNestingOf(input);
      if (!cr.isProperlyNested())
      {
        switch(cr.errorChar())
        {
          case ')': sum +=3; break;
          case ']': sum +=57; break;
          case '}': sum +=1197; break;
          case '>': sum +=25137; break;
          default:
            break;
        }
      }
    }
    return sum;
  }

  public static long getPart2Answer(final String day10Input, final String[] day10InputLines)
  {
    final NestingChecker nc = new NestingChecker("([{<", ")]}>");
    final List<Long> unsortedResults = new ArrayList<>();
    for (final String input : day10InputLines)
    {
      final CheckResult cr = nc.checkNestingOf(input);
      if (cr.isProperlyNested())
      {
        final long itemResult = nc.getScoreOfNestingCompletion(input);
        unsortedResults.add(itemResult);
      }
    }
    final Long[] sortedResults = unsortedResults.toArray(new Long[0]);
    Arrays.sort(sortedResults);
    return sortedResults[sortedResults.length/2];
  }

One thing to note is that my “isProperlyNested” result is a bit of a lie. It’s just indicating that it’s not an invalid nesting (for part 1) not that it’s actually fully validly nested, as that was never called for. One more line at the end of the method in the checker class would handle that, where one would need to make sure the stack is empty to get an indication of proper nesting.

This is my first year with advent of code and I’m really enjoying it, the only bump in the road for me was “Octopus Bingo” on Saturday - which was just anoying to test when you get something slightly wrong.

I did the first 2 in awk and the rest in python + was thinking of switching it up to golang.

You guys did this last year - does this feel like the same level of difficulty as last year? or have they upped the complexity as more people jump on board?

Honestly it seems a little lighter this year. I keep waiting for that one question that requires very special knowledge. (Last year that was one that required you to recognize and apply the Chinese Remainder Theorem.) Past years I’ve found part two brought enough of a twist to require significant effort. A couple so far this year have had part two so easy that I submit it literally in under two minutes after finishing part one.

On the other hand, maybe I am just used to how this works and getting better at anticipating how it will go. In any case, it will definitely get harder sooner than later… but enjoy the ride! :slight_smile:

Yeah I can’t decide either. I’ve definitely gone farther than ever before - just solved Day 7 in a half hour, faster than most Day 1s. Some of that is getting used to the format, and I did spend the past few months practicing for it.

But I bet Eric took some of the complaints about the Chinese Remainder Theorem to heart. At least I hope so! We’ll see in a few days! (No INTCODE interpreters yet, either.)

However, I’m having a lot of fun so far, so I’m not complaining!! Off to Day 8 - I’m catching up!

Incidentally Eric says he saves the hardest problems for weekends. Maybe this is like The NY Times Crossword and Monday is easy and the week gets progressively harder? This Monday’s was certainly simple.

UPDATE: I take it back. Just skimmed through Day 8 Part 1 - OMG!