DEV Community

Cover image for Cube Conundrum
Robert Mion
Robert Mion

Posted on

Cube Conundrum

Advent of Code 2023 Day 2

Part 1

Surgically splitting a string

From a string like this:

Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Enter fullscreen mode Exit fullscreen mode

I need to construct this data:

id: 2
set:
  blue: 1
  green: 2
set:
  green: 3
  blue: 4
  red: 1
set:
  green: 1
  blue: 1
Enter fullscreen mode Exit fullscreen mode

Thankfully:

  • A colon separates the id
  • Semicolons separate the sets
  • Commas separate the cube types and amounts

Sounds like an overly-complicated series of split() and conditions will help me complete this task.

Let's give it a try!

split()s

split(':') gets me:

[
  'Game 2',
  '1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue'
]
Enter fullscreen mode Exit fullscreen mode

split(';') gets me:

[
  'Game 2',
  [
    '1 blue, 2 green',
    '3 green, 4 blue, 1 red',
    '1 green, 1 blue'
  ]
]
Enter fullscreen mode Exit fullscreen mode

split(',') gets me:

[
  'Game 2',
  [
    ['1 blue', '2 green'],
    ['3 green', '4 blue', '1 red'],
    ['1 green', '1 blue']
  ]
]
Enter fullscreen mode Exit fullscreen mode

That should be all the split()s needed, aside from the one in the next step.

Using reduce() to convert arrays into dictionaries

I want to turn this:

['3 green', '4 blue', '1 red']
Enter fullscreen mode Exit fullscreen mode

Into this:

{ 'green': 3, 'blue': 4, 'red': 1 }
Enter fullscreen mode Exit fullscreen mode

I can use reduce() to build a dictionary from scratch:

game.reduce((dict, cube) => {
  const [amount, type] = cube.split(' ')
  dict[type] = +amount
  return dict
}, {})
Enter fullscreen mode Exit fullscreen mode

It's probably time to put this code in a text editor and check my work!

Checkpoint 1: was surgery successful?

With a few edits, my JavaScript for parsing the input into ids and set dictionaries looks like this:

input.split('\n').reduce((idSums, game) => {
  let [id, sets] = game.split(':')
  id = +id.match(/\d+/)
  sets = sets
    .split(';')
    .map(el => el.split(', '))
    .map(el => el.reduce((dict, set) => {
            const [amount, type] = set.trim().split(' ')
            dict[type] = +amount
            return dict
          }, {})
        )
  // To-do: check sets for possible cube totals
}, 0)
Enter fullscreen mode Exit fullscreen mode

Examining each set for possibilities

The current criteria for possible match is:

red: 12,
green: 13,
blue: 14
Enter fullscreen mode Exit fullscreen mode

My thought process for constructing an algorithm:

  • Each game consists of a series of sets
  • I must check each set for cube amounts less than or equal to each color's respective total
  • Any sets that exceed the total should flag the whole set as invalid
  • Any games with a single invalid set should not count toward the sum of ids

For example, Game 3's sets are:

8 green, 6 blue, 20 red
5 blue, 4 red, 13 green
5 green, 1 red
Enter fullscreen mode Exit fullscreen mode

Checking each one results in:

valid, valid, invalid
valid, valid, valid
valid, valid
Enter fullscreen mode Exit fullscreen mode

Making each set:

invalid
valid
valid
Enter fullscreen mode Exit fullscreen mode

Making the game invalid due to the single invalid cube amount.

Of course, valid will be true and invalid will be false.

And each check will compare the set dictionary with the totals corresponding value.

Checkpoint 2: was my thinking valid?

Yes, it was! Although, my algorithm got a bit more complicated:

input.split('\n').reduce((idSums, game) => {
  let [id, sets] = game.split(':')
  id = +id.match(/\d+/)
  sets = sets
    .split(';')
    .map(set => set.split(', ')
          .reduce((dict, set) => {
            const [amount, type] = set.trim().split(' ')
            dict[type] = +amount
            return dict
          }, {})
        )
    .reduce((flags, cubes) => {
      for (let cube in cubes) {
        flags.push(cubes[cube] <= totals[cube] ? true : false)
      }
      return flags
    }, [])
  return idSums += sets.every(el => el == true) ? id : 0
}, 0)
Enter fullscreen mode Exit fullscreen mode

Correct, or not?

My algorithm generates the correct answer for the example input.

And for my puzzle input!!!

Woohoo!

Part 2

First, a hypothesis

  • The game ids and sets can't change: they're in the input
  • Part 1 had fairly low values as criteria
  • No digits in my puzzle input are over 20

My hunch:

  • Part 2 will exponentially increase the criteria values
  • And ask the solver to see which game would last the longest if each set subtracted from the new values

Let's see!

Confusion, then full understanding and confidence

  • Well, I was way off in my hunch
  • Minimum amount?
  • How is the author determining this?

...

Ohhhh, it's just the largest number across all sets of each cube color!

That seems fairly straightforward to calculate.

My approach

  • Instead of accumulating boolean values as I visit each cube...
  • I'll compare each amount to a running tally of the current highest value per color
  • By the time I visit the last cube in the last set of each game, I should have the winning numbers for all three colors!

My JavaScript algorithm

input.split('\n').reduce((powerSums, game) => {
  let [, sets] = game.split(':')
  powers = sets
    .split(';')
    .map(set => set.split(', ')
          .reduce((dict, set) => {
            const [amount, type] = set.trim().split(' ')
            dict[type] = +amount
            return dict
          }, {})
        )
    .reduce((tallies, cubes) => {
      for (let color in cubes) {
        tallies[color] = tallies[color] >= cubes[color] ? tallies[color] : cubes[color]
      }
      return tallies
    }, { red: 0, green: 0, blue: 0 })
  return powerSums += Object.values(powers)
    .reduce((power, min) => power *= min)
}, 0)
Enter fullscreen mode Exit fullscreen mode

Gosh, isn't reduce() so versatile?

I just used it to keep track of my cube color tallies...without needing a separate variable!

Checking, submitting and celebrating

It generated the correct answer for the example input.

And for my puzzle input!

I love these earlier Days because there are rarely many intentionally hidden 'gotcha' edge cases in the puzzle input.

Get it to work no the example, and odds are it will work on my puzzle input.

Not the case come Days...well...7 and up.

Bring on Day 3!

Top comments (0)