🎄 Advent of Code 2'023 - Day 02
Day 02: Cube Conundrum
* Cf. aoc. d. ii xxiii
Today we’re playing a game with cubes. The game consists of a series of rounds, and each line of the input represents a game. Each game has three rounds, and each round has a series of cubes with a color and a quantity.
Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
Parsing The Input
I started the challenge by creating a new method on my Utils
class to break the
string
down in a list of vector<string>
.
static std::string read_file(const std::string& file_path) {
std::string line;
std::fstream file(file_path);
std::string input;
while(getline(file, line)) {
input += line + "\n";
}
return input;
}
Later on, I’ve also added a ;
at the end of each line, so I don’t need to add an extra
check after processing all the data.
round = round.substr(round.find(':') + 2) + ";";
Now we can use it on our solution to easily work with each isolated round. Note that
I’ve also added a possible_rounds
vector to store the possible rounds for each game, and
as we know that we should return a sum of all possible games, I’m already returning it at the
end of the function.
static int part1(string &game_records) {
vector<string> rounds = Utils::split_lines(game_records);
vector<int> possible_rounds;
return accumulate(possible_rounds.begin(), possible_rounds.end(), 0);
}
PARS I
For each line, we should determine if the game is possible given that the player has exactly 12 red cubes, 13 green cubes, and 14 blue cubes. After that, we should sum up the game round of all possible games.
My goal was to build a table where key is the color of the cube, and the value is
the max numbers that appear in the line for that given color. So I’ve created a
roud_map
to store the values of the current game.
unordered_map<string, int> round_map;
Building the round_map
The first thing I did was to remove the prefix Game *:
, I also don’t care about the round,
because as I’m iterating over it, I could access it by incrementing i
.
That done, we need to iterate over each character of the line, and do the following:
- If the current character is a
,
or a;
, we know that we have a color and a quantity, because it signifies the end of a round. We just keep track of the largestcube_quantity
and reset all the other variables. - If the current character is a digit, we know that we have a quantity.
- If the current character is a space, we can ignore it.
- If the current character is a letter, we just append it to our
cube_color
variable.
cube_quantity = "";
cube_color = "";
for (char ¤t_char: round) {
if (current_char == ',' || current_char == ';') {
round_map[cube_color] = max(round_map[cube_color], stoi(cube_quantity));
cube_color = "";
cube_quantity = "";
continue;
}
if (current_char == ' ') continue;
if (current_char >= '0' && current_char <= '9') cube_quantity += current_char;
else cube_color += current_char;
}
Back To Pars I
The last thing to do is to check the constraints of the game. If the game is possible,
we add the round to the possible_rounds
vector, and clear the round_map
for the next
game.
if (round_map["red"] <= 12 && round_map["green"] <= 13 && round_map["blue"] <= 14) {
possible_rounds.push_back(i+1);
round_map.clear();
}
The whole code looks like this:
#include <algorithm>
#include <string>
#include <numeric>
using namespace std;
class Solution_02 {
public:
static int part1(string &game_records) {
vector<string> rounds = Utils::split_lines(game_records);
vector<int> possible_rounds;
string cube_color;
string cube_quantity;
for (int i = 0; i < rounds.size(); ++i) {
unordered_map<string, int> round_map;
string round = rounds[i];
round = round.substr(round.find(':') + 2) + ";";
cube_quantity = "";
cube_color = "";
for (char ¤t_char: round) {
if (current_char == ',' || current_char == ';') {
round_map[cube_color] = max(round_map[cube_color], stoi(cube_quantity));
cube_color = "";
cube_quantity = "";
continue;
}
if (current_char == ' ') continue;
if (current_char >= '0' && current_char <= '9') cube_quantity += current_char;
else cube_color += current_char;
}
if (round_map["red"] <= 12 && round_map["green"] <= 13 && round_map["blue"] <= 14) {
possible_rounds.push_back(i+1);
round_map.clear();
}
}
return accumulate(possible_rounds.begin(), possible_rounds.end(), 0);
}
};
Quite lengthy, but it works! And with that we have our first start prize of the day! 🌟
PARS II
α
In the second part, we are asked in for the fewest number of cubes of each color that
could have been in the bag to make the game possible. Since we have the round_map
with
the max number * of cubes for each color, we already have the answer for the second part.
The tricky part is that we should multiply all the colours in each game red * green * blue
and
them sum them all up at the end to get our answer. So let’s do a little bit of refactoring.
Refactoring
There is basically two things that differ from the first part to the second part:
- The calculation based on the
round_map
values. - The constraint check.
β
We could refactor part one and abstract this specific logic in a predicate and a function.
I’ve created a calculate
method to keep the common logic between the two parts. The only
difference here is that we are receiving the changing logic as a parameter.
private:
static int calculate(
string &game_records,
const function<void(vector<int>&, unordered_map<string, int>&, int)> aggregate,
const function<bool(unordered_map<string, int>)> predicate
) {
// previous code
for (int i = 0; i < rounds.size(); ++i) {
// previous code
for (char ¤t_char: round) {
// previous code
}
// this is the only line that changed
if (predicate(round_map)) aggregate(possible_rounds, round_map, i);
}
return accumulate(possible_rounds.begin(), possible_rounds.end(), 0);
}
That looks much better! Our refactored version of part one looks like this:
static int part1(string &game_records) {
return calculate(
game_records,
[=](vector<int> &possible_rounds, unordered_map<string, int> &round_map, int index) -> bool {
possible_rounds.push_back(index + 1);
round_map.clear();
},
[=](unordered_map<string, int> round_map) -> bool {
return round_map["red"] <= 12 && round_map["green"] <= 13 && round_map["blue"] <= 14;
}
);
}
With this refactoring in place, the second part becomes more manageable. It merely requires
adjustments to the aggregate
and predicate
functions.
static int part2(string &game_records) {
return calculate(
game_records,
[=](vector<int> &possible_rounds, unordered_map<string, int> &round_map, int index) -> bool {
possible_rounds.push_back(round_map["red"] * round_map["green"] * round_map["blue"]);
round_map.clear();
},
[=](unordered_map<string, int> round_map) -> bool { return true; }
);
}
And there it is! The second star is now within reach! 🌟
Click here to access the blogpost from @fefas
α) by getting the maximum value * upper_bound
we can say that this is the minimum
number of cubes that could have been in the bag to make the game possible.
β) a predicate is a function that returns a boolean value.