O Advent of Code é um Calendário do Advento desenvolvido por Eric Wastl composto por 25 pequenos exercícios de programação que vão sendo disponibilizados, um a um, entre 1º de dezembro e o Natal de cada ano.
Meu objetivo com o Advent of R é resolver todos os problemas do Advent of Code 2021 em R e documentar o processo através desta série de posts. Todo dia entre 01/12/2021 e 25/12/2021 eu vou tentar resolver o novo problema, documentar a minha solução aqui no blog e subir os meus scripts completos para um repositório público no GitHub.
A minha esperança é que, com essa série, mais pessoas pratiquem seus conhecimentos de R resolvendo exercícios divertidos e desafiadores! Ao final da jornada vamos todos ter afiado nossas habilidades de R e, quem sabe, divulgado essa linguagem incrível para mais pessoas. Boas festas e bom código!
Diagnóstico Binário (A)
Na primeira parte do terceiro dia do AoC somos apresentados aos diagnósticos do submarino. Cada linha é composta por um número binário e precisamos carclular, a partir deles, os índices gama e épsilon.
# 00100
# 11110
# 10110
# 10111
# 10101
# 01111
# 00111
# 11100
# 10000
# 11001
# 00010
# 01010
Cada bit do fator gama é igual ao valor mais comum do bit correspondente na entrada, enquanto o épsilon funciona ao contrário. No exemplo acima, o primeiro bit mais comum é 1 e o segundo é 0, então o índice gama começará com 10… e o índice épsilon começará com 01…
O meu código quebra os bits da entrada com tidyr::separate()
e calcula o valor
mais frequente com names(sort(-table(.x)))[1]
(a moda estatística). É
importante lembrar que épsilon é o oposto, então eu troquei todos os bits de
gama com stringr::str_replace_all()
. A resposta final é a multiplicação
de gama por épsilon na base decimal.
"data-raw/03a_binary_diagnostic.txt" |>
readr::read_table(col_names = "reading") |>
tidyr::separate(reading, paste0("B", 0:12), "") |>
dplyr::select(-B0) |>
dplyr::summarise_all(~names(sort(-table(.x)))[1]) |>
tidyr::unite("gamma", dplyr::everything(), sep = "") |>
dplyr::mutate(
epsilon = gamma |>
stringr::str_replace_all("0", "!") |>
stringr::str_replace_all("1", "0") |>
stringr::str_replace_all("!", "1") |>
strtoi(base = 2),
gamma = strtoi(gamma, base = 2),
output = gamma * epsilon
) |>
dplyr::pull(output)
Diagnóstico Binário (B)
O segundo item desse dia foi o mais difícil de todos, ainda mais considerando que eu tento resolver tudo em apenas uma pipeline. Usando os mesmos dados, precisamos obter a taxa de O\(_2\) e de CO\(_2\) do submarino, sendo que as regras são as seguintes:
Jogue fora os número que não atendem ao critério daquele gás.
Se restar apenas 1 número, essa é a taxa daquele gás.
Caso contrário, repita o processo com o próximo bit.
E quais são os critérios?
Para o oxigênio, determinamos o valor mais comum para o bit atual e jogamos fora todos os números que diferem, nessa posição, desse valor. Se 0 e 1 forem igualmente comuns, manter apenas os números com 1 no bit considerado.
Para gás carbônico, determinamos o valor menos comum para o bit atual e jogamos fora todos os números que diferem, nessa posição, desse valor. Se 0 e 1 forem igualmente comuns, manter apenas os números com 0 no bit considerado.
O primeiro passo da minha solução foi criar uma função que calcula a anti-moda
de um vetor. Ela difere da função usada no item anterior somente pelo sinal de
subtração, mas isso garante a ela uma propriedade importante: se 0 e 1 empatarem
na contagem, ela retorna o valor que vem antes na ordem alfabética, ou seja, 0.
Dessa forma a função antimode()
realiza exatamente a operação que precisamos
para determinar a taxa de gás carbônico.
antimode <- function(x) names(sort(table(x)))[1]
A função abaixo é uma versão recursiva do cálculo das taxas dos gases. A coluna
current
é só um atalho para deixar o filtro mais enxuto, pois ela não passa da
do bit atual. O op()
, porém, é a chave que nos permite usar a mesma função
para calcular O\(_2\) e CO\(_2\); por padrão a função filtra os valores iguais à
anti-moda, mas, com co2 = FALSE
, ela filtra os valores diferentes da
anti-moda, atendendo ao critério do oxigênio (incluindo o desempate)!
A última linha chama a função de novo para o próximo bit, resolvendo o cálculo.
gas <- function(df, co2 = TRUE, bit = 1) {
# Condição de parada
if (bit > 12 || nrow(df) == 1) return(df)
# Escolher o operador apropriado
if (co2) op <- `==` else op <- `!=`
# Filtrar usando antimode() e fazer a recursão
df |>
dplyr::mutate(current = .data[[names(df)[bit]]]) |>
dplyr::filter(op(current, antimode(current))) |>
dplyr::select(-current) |>
find_rating(co2 = co2, bit = bit + 1)
}
Só nos resta aplicar essa função na lista de números. Para tentar manter o fim
do código em uma pipeline só (já que não foi possível com o resto), eu usei
rep_len(list(df), 2)
para duplicar a base e poder aplicar gas()
e
gas(co2 = FALSE)
em uma linha só com purrr::map2_dfr()
. O final do código
deixa cada taxa em uma linha, junta os seu bits, as converte para decimal e
multiplica seus valores. Essa é a saída.
"data-raw/03b_binary_diagnostic.txt" |>
readr::read_table(col_names = "reading") |>
tidyr::separate(reading, paste0("B", 0:12), "") |>
dplyr::select(-B0) |>
list() |>
rep_len(2) |>
purrr::map2_dfr(list(gas, \(df) gas(df, FALSE)), ~.y(.x)) |>
tidyr::unite("reading", dplyr::everything(), sep = "") |>
dplyr::mutate(reading = strtoi(reading, base = 2)) |>
dplyr::summarise(output = prod(reading)) |>
dplyr::pull(output)