Advent of Code - Day 3
There has been a gap as I went on a Xmas break. In Australia, we have our Summer holidays at Xmas time so tend to take longer holidays.
Day Three - Part A
Part A is pretty straightforward. The biggest issue was working out how to handle data once I transformed it. I decided to use strings, but more on this later.
The data is a list of strings representing a list of bits.
[ "011000001000", "111000110101", "101000011100", "100110010100","001111111111" ...]
To find the solution, I needed to transform the list to determine the most common bit at each position, i.e. are there more 1
or 0
at each index for the complete list. For the sample above at index 0, there are more 1
than 0
, so the result will start with 1
at index 0 and so on until the last index where there are more 0
than 1
.
The first thing I wanted to do was convert the data into a list of lists. My two options are to create a charlist
or a list
. I decided to use a list because I had more options for manipulating a list in the core library (something I realised later is untrue).
lists = Enum.map(@data, &String.split(&1, "", trim: true))
The next thing I wanted to do was create a list of values for each index.
defp lists_by_position(lists, length_of_list) do
for i <- 0..length_of_list - 1 do
Enum.map(lists, &Enum.at(&1, i))
end
end
# result - list of all bits at each index
[
["0", "1", "0", "1", "1", "0", "0", "0", "1", "0", "0", "0", "0", "0", "1",
"1", "1", "0", "0", "0", "1", "1", "0", "0", "1", "1", "1", "1", "1", "0",
"1", "0", "0", "1", "1", "1", "1", "1", "0", "0", "0", "0", "0", "0", "1",
"1", "1", "0", "1", ...],
["0", "0", "0", "0", "1", "0", "1", "1", "1", "0", "1", "0", "0", "0", "0",
"0", "1", "1", "0", "1", "1", "0", "1", "0", "1", "1", "1", "0", "0", "0",
"1", "1", "1", "1", "0", "0", "0", "1", "1", "1", "0", "0", "1", "1", "1",
"0", "1", "1", ...]
...
]
I was then able to count the most common bits for each position. I used an Enum.reduce/3
function and started by counting the 1
for each index. If the number was greater than half of the total, I added 1
to the accumulator; if not, I added 0
. Finally, because I am adding to the head
of the list, I reversed it before returning the value.
defp calc_most_common_bit(lists_to_count, length_of_pos_list) do
Enum.reduce(lists_to_count, [], fn list, acc ->
ones = Enum.count(list, &(&1 == "1"))
bit = if ones > length_of_pos_list/2, do: "1", else: "0"
[bit | acc]
end)
|> Enum.reverse()
end
# result - the most common bits at each index for the list of data
["0", "0", "1", "1", "1", "0", "1", "0", "0", "1", "1", "1"]
calc_most_common_bit
gave me the value of gamma
represented by the most common bits at each index. To calculate epsilon
(The least common bits), I reversed the gamma
values.
defp bitwise_of_gamma(gamma_list) do
Enum.reduce(gamma_list, [], fn i, acc ->
[(if i == "1", do: "0", else: "1") | acc]
end)
|> Enum.reverse
end
Now I had the gamma
and epsilon
values and needed to convert them into integer
. To do this, I needed to convert the list of strings to a charlist
; I also realised that I could have been working with a charlist
all along (Lesson learnt :)). I created a function to convert to charlist
and an integer
.
defp to_integer(list) do
list
|> List.to_charlist()
|> List.to_integer(2)
end
Bringing this all together, I have a function that calculates the power consumption of the submarine.
def day_three_a() do
lists =
Enum.map(@data, &String.split(&1, "", trim: true))
length_of_raw_list = Enum.count(List.first(lists))
lists_to_count = lists_by_position(lists, length_of_raw_list)
length_of_pos_list = Enum.count(List.first(lists_to_count))
gamma_list = calc_most_common_bit(lists_to_count, length_of_pos_list)
epsilon_list = bitwise_of_gamma(gamma_list)
gamma = to_integer(gamma_list)
epsilon = to_integer(epsilon_list)
gamma * epsilon
end
Everything worked, and I have my star!
Day Two - Part B
The second part of this problem took me way longer than it should have, I blame being on holidays, but I made a fundamentally wrong assumption upfront. Below is the solution I ended up with; I will point out my flawed assumption below.
This time I had to filter the list based on the most common bit at each position, discarding values as they failed the test provided in the problem. For the @data
I needed to look at each bit value at each index and for the oxygen generator rating
keep the numbers that have the most common bits (Defaulting to 1
if it is even) and for the CO2 scrubber rating
keep numbers that have the least common values at each index.
As I have done so far, I wanted to use recursion as I find it easier to read and debug when there are issues. I started with filtering for the most common bits for each index. I started with my exit, and I knew that my answer was as soon as I had one value.
defp filter_by_most_common(matches) when length(matches) == 1, do: matches
Now I needed to add some logic; I started by getting the most common bit at the current position I was testing. I realised that I needed an index value to track where to test the value. So I started with all the data and filtered it down to a single value.
defp filter_by_most_common(matches, _idx) when length(matches) == 1, do: matches
defp filter_by_most_common(matches, idx) do
bit =
matches
|> lists_by_position(Enum.count(List.first(matches)))
|> calc_most_common_bit(Enum.count(matches))
|> Enum.at(idx)
end
I could reuse the functions for Part A and grab the bit value based on the index. This is where I had initially made a mistake. Instead of calculating the most common bit for the filtered list, I started by passing in the result of calc_most_common_bit/2
. This gave me an incorrect answer and proved challenging to debug. After rereading the question, I realised that the test was based on the most common bit in the remaining list. I wasted over an hour debugging my logic!
Now that I had the most common bit at index = n
, I was able to get the matches and pass them back to the function.
defp filter_by_most_common(matches, _idx) when length(matches) == 1, do: matches
defp filter_by_most_common(matches, idx) do
bit =
matches
|> lists_by_position(Enum.count(List.first(matches)))
|> calc_most_common_bit(Enum.count(matches))
|> Enum.at(idx)
matches = Enum.filter(matches, &(Enum.at(&1, idx) == bit))
filter_by_most_common(matches, idx + 1)
end
Unfortunately, this did not work. After rereading the question, if the most and least common bits were equal, I needed to default to a 1
or a 0
, depending on what I calculated. I added some more logic to test the equality of the counts and used a default value for the filter.
defp filter_by_most_common(matches, _idx, _default) when length(matches) == 1, do: matches
defp filter_by_most_common(matches, idx, default) do
bit =
matches
|> lists_by_position(Enum.count(List.first(matches)))
|> calc_most_common_bit(Enum.count(matches))
|> Enum.at(idx)
match = Enum.filter(matches, &(Enum.at(&1, idx) == bit))
dont_match = Enum.filter(matches,
&(Enum.at(&1, idx) == (if bit == "1", do: "0", else: "1"))
)
matches =
cond do
Enum.count(match) == Enum.count(dont_match) ->
Enum.filter(matches, &(Enum.at(&1, idx) == default))
true -> match
end
filter_by_most_common(matches, idx + 1, default)
end
I spent a bit more time refactoring but, in the end, ended up using a bit of copy/paste coding
. I will refactor the recursion and pass in a function to provide a default filter if I get time. In the meantime, I added the following code.
defp filter_by_least_common(matches, _idx, _default) when length(matches) == 1, do: matches
defp filter_by_least_common(matches, idx, default) do
bit =
matches
|> lists_by_position(Enum.count(List.first(matches)))
|> calc_most_common_bit(Enum.count(matches))
|> bitwise_of_gamma()
|> Enum.at(idx)
match = Enum.filter(matches, &(Enum.at(&1, idx) == bit))
dont_match = Enum.filter(matches, &(Enum.at(&1, idx) == (if bit == "1", do: "0", else: "1")))
matches =
cond do
Enum.count(match) == Enum.count(dont_match) ->
Enum.filter(matches, &(Enum.at(&1, idx) == default))
true -> match
end
filter_by_least_common(matches, idx + 1, default)
end
Now that my filters were working, I created a header function and ran the code.
def day_three_b() do
lists =
Enum.map(@data, &String.split(&1, "", trim: true))
[o2_list] = filter_by_most_common(lists, 0, "1")
[co2_list] = filter_by_least_common(lists, 0, "0")
o2 = to_integer(o2_list)
co2 = to_integer(co2_list)
o2 * co2
end
Running using my data, and I get the correct answer … woohoo!