Caio Lente

Advent of R

· Caio Lente

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 foi resolver todos os problemas do Advent of Code 2021 em R e documentar o processo. Todo dia entre 01/12/2021 e 25/12/2021 eu vou resolvi o novo problema, documentei a minha solução e subi os meus scripts completos para um repositório público no GitHub.

Abaixo, cada seção representa um item de um dia do advent. Cada exercício foi individualmente resolvido no blog da Curso-R no dia correto, então todos começam com a minha opinião daquele exercício. Boas festas e bom código!

Varredura de Sonar (A)

A parte 1 do primeiro exercício do AoC envolve ler uma lista de números e ver quantas vezes os valores aumentam em relação ao anterior. Em linguagem matemática, precisamos avaliar quantas vezes xi>xi1 .

Por exemplo, suponha a seguinte lista:

# 199
# 200
# 208
# 210
# 200
# 207
# 240
# 269
# 260
# 263

Nesse caso, precisamos comparar cada número com o da linha anterior e verificar se ele representa que a série aumentou, diminuiu ou manteve-se constante.

# 199 (NA)
# 200 (aumentou)
# 208 (aumentou)
# 210 (aumentou)
# 200 (diminuiu)
# 207 (aumentou)
# 240 (aumentou)
# 269 (aumentou)
# 260 (diminuiu)
# 263 (aumentou)

Tendo isso, podemos concluir que houve 7 aumentos na série exemplo e essa seria a resposta do problema.

O meu código para resolver o exercício ficou bem enxuto. Bastou ler a lista de número do arquivo disponibilizado como uma tabela e comparar seus valores com o seu dplyr::lag(); depois disso um dplyr::summarise() contou o número de TRUEs ignorando NAs.

"data-raw/01a_sonar_sweep.txt" |>
  readr::read_table(col_names = "depth") |>
  dplyr::mutate(
    prev_depth = dplyr::lag(depth),
    is_deeper = depth > prev_depth
  ) |>
  dplyr::summarise(n_deeper = sum(is_deeper, na.rm = TRUE)) |>
  dplyr::pull(n_deeper)
#> [1] 1228

Varredura de Sonar (B)

A segunda parte, entretanto, aumenta (com o perdão do trocadilho) a dificuldade. Dessa vez precisamos somar uma janela de 3 valores e comparar com a próxima janela, ou seja, verificar quantas vezes k=ii+2xkk=i1i+1xk .

Observe como as janelas funcionam:

# 199  A
# 200  A B
# 208  A B C
# 210    B C D
# 200  E   C D
# 207  E F   D
# 240  E F G
# 269    F G H
# 260      G H
# 263        H

Nesse exemplo precisaríamos somar os números da janela A (199, 200, 208) e testar se isso é maior que a soma dos números da janela B (200, 208, 210). Então compararíamos B com C, C com D e assim por diante.

# A: 607 (NA)
# B: 618 (aumentou)
# C: 618 (não mudou)
# D: 617 (diminuiu)
# E: 647 (aumentou)
# F: 716 (aumentou)
# G: 769 (aumentou)
# H: 792 (aumentou)

Alterando o código da primeira parte, eu criei as janelas usando dplyr::lead() e depois comparei as somas utilizando o mesmo dplyr::lag(). Mais uma vez o dplyr::summarise() contou o número de TRUEs ignorando NAs.

"data-raw/01b_sonar_sweep.txt" |>
  readr::read_table(col_names = "depth") |>
  dplyr::mutate(
    depth1 = dplyr::lead(depth, n = 1),
    depth2 = dplyr::lead(depth, n = 2),
    sum_depth = depth + depth1 + depth2,
    prev_sum_depth = dplyr::lag(sum_depth),
    is_deeper = sum_depth > prev_sum_depth
  ) |>
  dplyr::summarise(n_deeper = sum(is_deeper, na.rm = TRUE)) |>
  dplyr::pull(n_deeper)
#> [1] 1257

Mergulhe (A)

A parte 1 do segundo dia do AoC pede para lermos uma lista de comandos para um submarino e calcular a sua posição final. Os comandos possíveis são forward X (soma X à posição horizontal), up X (subtrai X da profundidade) e down X (soma X à profundidade), então precisamos fazer um dplyr::group_by(command == "forward") para que um grupo represente a posição horizontal e um represente a profundidade.

Para concluir o código, como a resposta final é a posição horizontal multiplicada pela profundidade, temos que fazer um prod() ao final:

"data-raw/02a_dive.txt" |>
  readr::read_delim(" ", col_names = c("command", "x")) |>
  dplyr::mutate(x = ifelse(command == "up", -x, x)) |>
  dplyr::group_by(command == "forward") |>
  dplyr::summarise(x = sum(x)) |>
  dplyr::summarise(x = prod(x)) |>
  dplyr::pull(x)
#> [1] 1727835

Mergulhe (B)

A parte 2 complica um pouco a nossa vida. Os mesmos comandos agora possuem outro significado:

O meu código da primeira parte não permitiria resolver isso de forma eficiente. Minha solução foi fazer uma cumsum() da posição horizontal e uma da mira, que são as partes mais simples. Depois eu calculei a profundidade com cumsum(aim * x) (dado que a mira tinha sido calculada no passo anterior).

A saída, mais uma vez é o produto entre a posição horizontal e a profundidade. Dessa vez a resposta vai estar na última linha da tabela, então o código precisa de um tail(1).

"data-raw/02a_dive.txt" |>
  readr::read_delim(" ", col_names = c("command", "x")) |>
  dplyr::mutate(
    horizontal = ifelse(command == "forward", x, 0),
    horizontal = cumsum(horizontal),
    aim = ifelse(command == "down", x, 0),
    aim = ifelse(command == "up", -x, aim),
    aim = cumsum(aim),
    depth = ifelse(command == "forward", aim * x, 0),
    depth = cumsum(depth),
    output = horizontal * depth
  ) |>
  utils::tail(1) |>
  dplyr::pull(output)
#> [1] 1544000595

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₂ e de CO₂ do submarino, sendo que as regras são as seguintes:

  1. Jogue fora os número que não atendem ao critério daquele gás.

  2. Se restar apenas 1 número, essa é a taxa daquele gás.

  3. Caso contrário, repita o processo com o próximo bit.

E quais são os critérios?

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₂ e CO₂; 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)

Lula Gigante (A)

O quarto dia do AoC foi talvez o mais interessante até agora. Na primeira parte, precisávamos calcular a pontuação da cartela vencedora de um bingo americano: cada cartela é composta por 5 linhas e 5 colunas de números que devem ser riscados conforme eles são anunciados pelo sistema do submarino. A primeira cartela a riscar todos os números de uma linha ou coluna é a vencedora e sua pontuação é a soma de todos os números não riscados multiplicada pelo último número anunciado.

A entrada era composta por uma linha com os números anunciados em sequência e, posteriormente, todas as cartelas da platéia:

# 7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1
#
# 22 13 17 11  0
#  8  2 23  4 24
# 21  9 14 16  7
#  6 10  3 18  5
#  1 12 20 15 19
#
#  3 15  0  2 22
#  9 18 13 17  5
# 19  8  7 25 23
# 20 11 10 24  4
# 14 21 16 12  6
#
# 14 21 17 24  4
# 10 16 15  9 19
# 18  8 23 26 20
# 22 11 13  6  5
#  2  0 12  3  7

Eu escolhi um caminho simples para resolver o problema, apesar de o código não ter ficado tão bom assim. Primeiro eu li a sequência de números e criei uma função que transpunha uma matrix numérica e a empilhava com a original.

# Processar os números sorteados
draws <- "data-raw/04a_giant_squid.txt" |>
  readr::read_lines(n_max = 1) |>
  stringr::str_split(",") |>
  purrr::pluck(1) |>
  as.numeric()

# Converter as colunas de uma matrix para linhas e empilhar
cols_to_rows <- function(df) {
  df |>
    dplyr::select(-board, -id) |>
    as.matrix() |>
    t() |>
    tibble::as_tibble(rownames = "id") |>
    purrr::set_names("id", paste0("C", 1:5)) |>
    dplyr::mutate(board = df$board) |>
    dplyr::bind_rows(df) |>
    dplyr::relocate(board, id) |>
    purrr::set_names("id", "board", paste0("N", 1:5))
}

O objetivo de cols_to_rows() era criar uma tabela final com todas as linhas das cartelas e também todas as suas colunas; isso permitiu que eu riscasse os números sorteados aplicando dplyr::na_if() indiscriminadamente. Quando alguma linha da tabela fosse formada somente por NAs (indicando que uma linha ou coluna de alguma cartela estava completa), bastava extrair a cartela original, somar os seus valores não-NA e multiplicar o resultado pelo número sorteado mais recente. A função utilizada para isso se chamava winning_score() e operava recursivamente para poupar tempo.

# Calcular a pontuação da cartela vencedora
winning_score <- function(df, draws) {

  # Marcar o número sorteado com NA (nas linhas e colunas)
  df <- dplyr::mutate(df, dplyr::across(c(N1:N5), dplyr::na_if, draws[1]))

  # Filtrar possíveis linhas/colunas completas
  win <- dplyr::filter(df, dplyr::if_all(c(N1:N5), is.na))

  # Se houver pelo menos uma linha/coluna completa...
  if (nrow(win) > 0) {

    # Extrair a cartela vencedora, somar os não-NA e multiplicar por draws[1]
    output <- df |>
      dplyr::filter(id == win$id, stringr::str_starts(board, "R")) |>
      dplyr::select(-id, -board) |>
      purrr::flatten_dbl() |>
      sum(na.rm = TRUE) |>
      magrittr::multiply_by(draws[1])

    # Retornar a pontuação
    return(output)
  }

  # Recursão para o próximo sorteio
  winning_score(df, draws[-1])
}

# Ler cartelas, empilhas linhas com colunas e riscar usando NAs
"data-raw/04a_giant_squid.txt" |>
  readr::read_table(skip = 1, col_names = paste0("C", 1:5)) |>
  dplyr::mutate(board = (dplyr::row_number() - 1) %/% 5) |>
  dplyr::group_by(board) |>
  dplyr::mutate(id = paste0("R", 1:5)) |>
  dplyr::group_split() |>
  purrr::map_dfr(cols_to_rows) |>
  winning_score(draws)
#> [1] 33348

Lula Gigante (B)

O segundo item do problema pedia o contrário: calcular a pontuação da última cartela a ter uma linha ou coluna completa, ou seja, da cartela perdedora. Na minha solução todo o código permaneceu igual, salvo pela função winning_score(), que virou loosing_score(). A grande novidade é que, quando o programa encontrava uma cartela vencedora, ele verificava se aquela era a última. Se não fosse, ele removia aquela cartela da tabela e, se fosse, ele retornava a pontuação.

# Calcular a pontuação da cartela perdedora
loosing_score <- function(df, draws) {

  # Marcar o número sorteado com NA (nas linhas e colunas)
  df <- dplyr::mutate(df, dplyr::across(c(N1:N5), dplyr::na_if, draws[1]))

  # Filter possible complete rows or cols
  win <- dplyr::filter(df, dplyr::if_all(c(N1:N5), is.na))

  # Se houver pelo menos uma linha/coluna completa...
  if (nrow(win) > 0) {

    # Se restasse apenas uma cartela, calcular a sua pontuação
    if (length(unique(df$id)) == 1) {

      # Extrair a cartela perdedora, somar os não-NA e multiplicar por draws[1]
      output <- df |>
        dplyr::filter(stringr::str_starts(board, "R")) |>
        dplyr::select(-id, -board) |>
        purrr::flatten_dbl() |>
        sum(na.rm = TRUE) |>
        magrittr::multiply_by(draws[1])

      # Retornar a pontuação
      return(output)
    }

    # Jogar fora cartelas que já venceram
    df <- dplyr::filter(df, !id %in% win$id)
  }

  # Recursão para o próximo sorteio
  loosing_score(df, draws[-1])
}

Aventura Hidrotermal (A)

O quinto dia do AoC foi um pouco mais tranquilo do que o anterior porque eu tive ajuda da incrível Renata Hirota. Hoje tínhamos as coordenadas cartesianas do início e do fim de tubulações submarinas e o objetivo era descobrir quantos pontos do plano tinham mais de uma tubulação passando por eles. No primeiro item deveríamos considerar apenas as tubulações verticais e horizontais.

A minha ideia começava filtrando os pontos em que x1 == x2 ou y1 == y2 e expandindo as coordenadas para criar uma lista que contivesse todos os pontos pelos quais as tubulações passavam. Eu resolvi isso com o paste(x1:x2, y1:y2), pois a paste() repetiria a coordenada que não muda ao longo da coordenada que muda: paste(9:7, 7:7) := "9 7" "8 7" "7 7".

Depois disso bastava contar o números de ocorrências de cada ponto do plano, filtrar aqueles que ocorriam mais de 1 vez e contar quantos pontos restavam. Esta era a saída do problema.

"data-raw/05a_hydrothermal_venture.txt" |>
  readr::read_csv(col_names = c("x1", "y1x2", "y2")) |>
  tidyr::separate(sep = " -> ", col = "y1x2", into = c("y1", "x2")) |>
  dplyr::filter(x1 == x2 | y1 == y2) |>
  dplyr::mutate(
    dif_x = purrr::map2(x1, x2, seq),
    dif_y = purrr::map2(y1, y2, seq),
    coord = purrr::map2(dif_x, dif_y, paste)
  ) |>
  tidyr::unnest(coord) |>
  dplyr::count(coord) |>
  dplyr::filter(n > 1) |>
  nrow()
#> [1] 7142

Aventura Hidrotermal (B)

O segundo item parecia bastante mais complexo, pois agora deveríamos considerar todas as tubulações da entrada, removendo o dplyr::filter() do item anterior. Mas uma especificação do enunciado facilitou tudo: todas as linhas diagonais tinham inclinação de 45 graus.

Isso significa que a estratégia do paste() continuava funcionando! Note que paste(1:3, 1:3) := "1 1" "2 2" "3 3", então bastou tirar o dplyr::filter() que a solução estava pronta.

"data-raw/05b_hydrothermal_venture.txt" |>
  readr::read_csv(col_names = c("x1", "y1x2", "y2")) |>
  tidyr::separate(sep = " -> ", col = "y1x2", into = c("y1", "x2")) |>
  dplyr::mutate(
    dif_x = purrr::map2(x1, x2, seq),
    dif_y = purrr::map2(y1, y2, seq),
    coord = purrr::map2(dif_x, dif_y, paste)
  ) |>
  tidyr::unnest(coord) |>
  dplyr::count(coord) |>
  dplyr::filter(n > 1) |>
  nrow()
#> [1] 20012

Peixes-lanterna (A)

O dia 6 do AoC me pegou um pouco de surpresa. O primeiro item foi tranquilo de fazer: a entrada era uma lista de números que representavam os “contadores biológicos” de um cardume de peixes-lanterna e precisávamos retornar o número de peixes depois de 80 dias.

Os peixes adultos demoram 7 dias (contador vai de 6 até 0) para gerar um novo peixe bebê e um peixe bebê demora 9 dias (contador vai de 8 até 0) para gerar seu primeiro filhote.

Estado inicial   : 3,4,3,1,2
Depois de  1 dia : 2,3,2,0,1
Depois de  2 dias: 1,2,1,6,0,8
Depois de  3 dias: 0,1,0,5,6,7,8
Depois de  4 dias: 6,0,6,4,5,6,7,8,8
Depois de  5 dias: 5,6,5,3,4,5,6,7,7,8
Depois de  6 dias: 4,5,4,2,3,4,5,6,6,7
Depois de  7 dias: 3,4,3,1,2,3,4,5,5,6
Depois de  8 dias: 2,3,2,0,1,2,3,4,4,5
Depois de  9 dias: 1,2,1,6,0,1,2,3,3,4,8
Depois de 10 dias: 0,1,0,5,6,0,1,2,2,3,7,8
Depois de 11 dias: 6,0,6,4,5,6,0,1,1,2,6,7,8,8,8
Depois de 12 dias: 5,6,5,3,4,5,6,0,0,1,5,6,7,7,7,8,8
Depois de 13 dias: 4,5,4,2,3,4,5,6,6,0,4,5,6,6,6,7,7,8,8
Depois de 14 dias: 3,4,3,1,2,3,4,5,5,6,3,4,5,5,5,6,6,7,7,8
Depois de 15 dias: 2,3,2,0,1,2,3,4,4,5,2,3,4,4,4,5,5,6,6,7
Depois de 16 dias: 1,2,1,6,0,1,2,3,3,4,1,2,3,3,3,4,4,5,5,6,8
Depois de 17 dias: 0,1,0,5,6,0,1,2,2,3,0,1,2,2,2,3,3,4,4,5,7,8
Depois de 18 dias: 6,0,6,4,5,6,0,1,1,2,6,0,1,1,1,2,2,3,3,4,6,7,8,8,8,8

O meu código até que ficou bem simples. Precisei apenas de uma função que, todo dia, subtraia 1 de todos os contadores, criava 1 peixe com contador 8 para cada peixe com contador -1 e, por fim, subia todos os peixes com contador -1 para 6.

# Rodar n cíclos de reprodução do peixe-lanterna
reproduce <- function(fish, n = 80) {

  # Condição de parada
  if (n == 0) return(length(fish))

  # Reduzir contadores biológicos
  fish <- fish - 1L

  # Criar novos peixes e reiniciar contadores
  fish <- append(fish, rep_len(8L, length(fish[fish == -1L])))
  fish[fish == -1L] <- 6L

  # Recursão
  reproduce(fish, n = n - 1)
}

# Ler uma lista de peixes e reproduzir por 80 dias
"data-raw/06a_lanternfish.txt" |>
  readr::read_lines() |>
  stringr::str_split(",") |>
  purrr::pluck(1) |>
  as.integer() |>
  reproduce()
#> [1] 362666

Peixes-lanterna (B)

O segundo item do exercício não mudava essencialmente nada em relação ao primeiro. Assumindo espaço e recursos infinitos, quantos peixes teríamos depois de 256 dias?

Para resolver esse item, em teoria, seria necessário trocar apenas o valor do n por 256. Mas não foi o que aconteceu… Por causa da ineficiência do algoritmo, obter uma resposta demoraria horas e acabaria com a memória do meu computador. Foi necessário pensar em um novo método de resolver o problema.

A solução abaixo foi inspirada pela função table(). Para reduzir a exigência de espaço e não precisar iterar ao longo de um vetor com todos os peixes, eu agrupei os peixes com o mesmo contador biológico em apenas uma linha de uma tabela! Assim o programa nunca precisava lidar com mais de 9 linhas por dia, resolvendo as complicações com espaço e tempo.

# Rodar n cíclos de reprodução do peixe-lanterna
reproduce <- function(fish, n = 80) {

  # Condição de parada
  if (n == 0) return(sum(fish$n))

  # Reduzir contadores biológicos
  fish <- dplyr::mutate(fish, timer = timer - 1L)

  # Criar novos peixes
  babies <- fish |>
    dplyr::filter(timer == -1L) |>
    dplyr::mutate(timer = 8L)

  # Reiniciar contadores e recursão
  fish |>
    dplyr::bind_rows(babies) |>
    dplyr::mutate(timer = ifelse(timer == -1L, 6L, timer)) |>
    dplyr::group_by(timer) |>
    dplyr::summarise(n = sum(n)) |>
    reproduce(n = n - 1)
}

# Ler uma lista de peixes e reproduzir por 256 dias
"data-raw/06b_lanternfish.txt" |>
  readr::read_lines() |>
  stringr::str_split(",") |>
  purrr::pluck(1) |>
  as.integer() |>
  tibble::as_tibble() |>
  purrr::set_names("timer") |>
  dplyr::count(timer) |>
  reproduce(n = 256) |>
  format(scientific = FALSE)
#> [1] 1640526601595

A Traição das Baleias (A)

O dia 7 do AoC foi o mais rápido até agora. A nossa tarefa era determinar a posição horizontal na qual um exército de caranguejos deveria se alinhar, com a restrição de que deveríamos encontrar a posição que exigisse menos combustível.

Cada caranguejo estava equipado de um mini-submarino que gastava 1 unidade de combustível por unidade de deslocamento, logo o total de combustível gasto pela tropa para ir até a posição x seria simplesmente sum(abs(positions - x)). A saída era o combustível gasto para levar todos os caranguejos até a posição mais econômica.

# Ler vetor de posições iniciais
positions <- "data-raw/07a_the_treachery_of_whales.txt" |>
  readr::read_lines() |>
  stringr::str_split(",") |>
  purrr::pluck(1) |>
  as.integer()

# Iterar nas posições para encontrar a mais barata
cheapest <- Inf
for (pos in max(positions):min(positions)) {

  # Calcular o combustível gasto para a posição
  fuel <- sum(abs(positions - pos))

  # Trocar a resposta se essa posição for mais econômica
  if (fuel < cheapest) cheapest <- fuel
}

# Imprimir
cheapest
#> [1] 328318

Note que não era necessário testar nenhuma posição fora do intervalo max(positions):min(positions)! Qualquer posição fora disso seria menos econômica do que a ponta mais próxima a ela dentro do intervalo.

A Traição das Baleias (B)

O segundo item mantinha o mesmo problema, mas mudava o cálculo do gasto de combustível dos mini-submarinos: o primeiro movimento consumiria 1 unidade de combustível, o segundo consumiria 2 unidades, o terceiro consumiria 3 e assim por diante.

A única linha que muda dessa solução para a anterior é a que calcula o gasto de combustível para cada posição. Se um caranguejo estiver na posição a e quiser ir até a x, o seu consumo total será k=0|ax|k . Abaixo a operação sum(purrr::map_int(positions, ~sum(0:abs(.x - pos)))) faz isso para todos os caranguejos.

# Iterar nas posições para encontrar a mais barata
cheapest <- Inf
for (pos in max(positions):min(positions)) {

  # Calcular o combustível gasto para a posição
  fuel <- sum(purrr::map_int(positions, ~sum(0:abs(.x - pos))))

  # Trocar a resposta se essa posição for mais econômica
  if (fuel < cheapest) cheapest <- fuel
}

# Imprimir
cheapest
#> [1] 328318

Busca em Sete Segmentos (A)

O oitavo dia do AoC foi bastante difícil para mim. O problema começou pelo enunciado, que é longo e complexo, então realmente recomendo ler a versão original além do resumo que trago abaixo.

Dito isso, vamos lá. O problema dizia respeito a displays de sete segmentos, onde cada número é representado por um conjunto de segmentos acessos; de acordo com o diagrama abaixo, vemos que 0 é representado por abcefg, 1 é cf e assim por diante.

#   0:      1:      2:      3:      4:
#  aaaa    ....    aaaa    aaaa    ....
# b    c  .    c  .    c  .    c  b    c
# b    c  .    c  .    c  .    c  b    c
#  ....    ....    dddd    dddd    dddd
# e    f  .    f  e    .  .    f  .    f
# e    f  .    f  e    .  .    f  .    f
#  gggg    ....    gggg    gggg    ....

#   5:      6:      7:      8:      9:
#  aaaa    aaaa    aaaa    aaaa    aaaa
# b    .  b    .  .    c  b    c  b    c
# b    .  b    .  .    c  b    c  b    c
#  dddd    dddd    ....    dddd    dddd
# .    f  e    f  .    f  e    f  .    f
# .    f  e    f  .    f  e    f  .    f
#  gggg    gggg    ....    gggg    gggg

O desafio é que, no nosso submarino, todo os displays estão com os fios trocados e, para piorar, cada display tem um arranjo diferente. A entrada do problema é uma série de linhas como a abaixo: como os 10 dígitos são representados em um display específico (em qualquer ordem) e, depois da barra, 4 dígitos que precisamos decodificar.

# acedgfb cdfbe gcdfa fbcad dab cefabd cdfgeb eafb cagedb ab |
# cdfeb fcadb cdfeb cdbaf

Alguns dígitos são fáceis de identificar. Os números 1, 4, 7 e 8 usam números únicos de segmentos, então é possível perceber que, quando ab acenderam, o display estava tentando mostrar um 1. Seguindo a mesma lógica, dab é 7, eafb é 4 e acedgfb é 8.

O objetivo do primeiro item do dia 08 era contar quantas vezes os dígitos 1, 4, 7 e 8 aparecem nas saídas que devemos decodificar (lado direito da barra). A solução foi bem simples, pois bastou pivotar a tabela e filtrar as linhas que tinham stringr::str_length() igual a 2, 3, 4, ou 7.

"data-raw/08a_seven_segment_search.txt" |>
  readr::read_delim(" ", col_names = NULL) |>
  purrr::set_names(
    paste0("P", stringr::str_pad(1:10, 2, "left", "0")), "remove",
    paste0("V", stringr::str_pad(1:4, 2, "left", "0"))
  ) |>
  dplyr::select(-remove) |>
  dplyr::select(V01:V04) |>
  tidyr::pivot_longer(V01:V04, names_to = "col", values_to = "value") |>
  dplyr::filter(stringr::str_length(value) %in% c(2, 4, 3, 7)) |>
  nrow()
#> [1] 365

Busca em Sete Segmentos (B)

O verdadeiro problema veio no item 2. Aqui o exercício abandona qualquer pretexto de bondade e pede de uma vez para decodificarmos os dígitos depois da barra baseados nos 10 padrões antes da barra. A saída deveria ser a soma de todos os números de 4 dígitos decodificados.

Minha primeira tentativa de resolver o problema testava cada segmento em cada posição (essencialmente verificando todos os possíveis jeitos de embaralhar os fios) para ver em qual das configurações os padrões faziam sentido; depois seria só bater os padrões com os 4 dígitos da direita para ver quem é quem. Não preciso nem dizer que isso seria demorado demais para funcionar.

Depois de um tempo olhando para o arquivo de entrada, entretanto, me veio uma luz: talvez eu pudesse analisar a frequência com a qual cada segmento aparece nos padrões. Perceba, por exemplo, que no diagrama acima o segmento e está ligado em 4 dígitos (0, 2, 6 e 8). O fato importante é que ele é o único segmento com essa propriedade!

Partindo deste princípio, criei as seguinte regras para o código:

  1. O único segmento que aparecer 4 vezes nos padrões corresponderá ao e;

  2. O único segmento que aparecer 6 vezes nos padrões corresponderá ao b;

  3. O único segmento que aparecer 9 vezes nos padrões corresponderá ao f;

  4. No padrão com 2 segmentos acessos, aquele que não representar o e corresponderá ao c (número 1).

  5. No padrão com 3 segmentos acessos, aquele que não representar c ou f corresponderá ao a (número 7).

  6. No padrão com 4 segmentos acessos, aquele que não representar b, c ou f corresponderá ao d (número 4).

  7. O segmento que ainda não tiver correspondente corresponderá ao g.

O resto do código cuidava de organizar as letras de cada dígito de modo que fosse fácil transpor as correspondências dos 10 padrões para os 4 valores das saídas.

# Decodificar uma linha da entrada
decode <- function(entry) {

  # Encontra e quebra o padrão que tenha certa str_length()
  find_by_len <- function(patterns, len) {
    patterns |>
      magrittr::extract(stringr::str_length(patterns) == len) |>
      stringr::str_split("") |>
      purrr::pluck(1)
  }

  # Frequências de referência
  ref_freq <- list(
    "a" = 8,
    "b" = 6,
    "c" = 8,
    "d" = 7,
    "e" = 4,
    "f" = 9,
    "g" = 7
  )

  # Valores de referência
  ref_val <- list(
    "abdefg" = 6,
    "abcefg" = 0,
    "cf" = 1,
    "acdfg" = 3,
    "abcdfg" = 9,
    "abcdefg" = 8,
    "bcdf" = 4,
    "acf" = 7,
    "abdfg" = 5,
    "acdeg" = 2
  )

  # Calcular frequências desta entrada
  cur_freq <- entry |>
    dplyr::select(P01:P10) |>
    purrr::flatten_chr() |>
    stringr::str_split("") |>
    purrr::flatten_chr() |>
    table()

  # Criar um dicionário para traduzir os segmentos
  dict <- list()

  # Traduzir segmentos com frequências únicas
  dict[["e"]] <- names(cur_freq[cur_freq == 4])
  dict[["b"]] <- names(cur_freq[cur_freq == 6])
  dict[["f"]] <- names(cur_freq[cur_freq == 9])

  # Extrair padrões da entrada
  patterns <- entry |>
    dplyr::select(P01:P10) |>
    purrr::flatten_chr()

  # Determinar segmento que falta do 1
  one <- find_by_len(patterns, 2)
  dict[["c"]] <- one[!one %in% purrr::flatten_chr(dict)]

  # Determinar segmento que falta do 7
  seven <- find_by_len(patterns, 3)
  dict[["a"]] <- seven[!seven %in% purrr::flatten_chr(dict)]

  # Determinar segmento que falta do 4
  four <- find_by_len(patterns, 4)
  dict[["d"]] <- four[!four %in% purrr::flatten_chr(dict)]

  # Determinar último segmento que falta
  dict[["g"]] <- names(cur_freq)[!names(cur_freq) %in% purrr::flatten_chr(dict)]

  # Traduzir segmentos dos valores de saída
  entry |>
    dplyr::select(V01:V04) |>
    purrr::flatten_chr() |>
    stringr::str_split("") |>
    purrr::map(~names(dict)[match(.x, dict)]) |>
    purrr::map(sort) |>
    purrr::map(stringr::str_c, collapse = "") |>
    purrr::map(~purrr::flatten_chr(ref_val)[match(.x, names(ref_val))]) |>
    purrr::flatten_chr() |>
    as.integer() |>
    stringr::str_c(collapse = "") |>
    as.numeric()
}

# Ler entrada, mapear decode() e somar todas os valores de saída
"data-raw/08b_seven_segment_search.txt" |>
  readr::read_delim(" ", col_names = NULL) |>
  purrr::set_names(
    paste0("P", stringr::str_pad(1:10, 2, "left", "0")), "remove",
    paste0("V", stringr::str_pad(1:4, 2, "left", "0"))
  ) |>
  dplyr::select(-remove) |>
  tibble::rowid_to_column("id") |>
  tidyr::nest(entry = c(P01:V04)) |>
  dplyr::mutate(output = purrr::map_dbl(entry, decode)) |>
  dplyr::summarise(output = sum(output)) |>
  dplyr::pull(output)
#> [1] 975706

Bacia de Fumaça (A)

O dia 9 do AoC foi desafiador, apesar de não tanto quanto o anterior. Como sempre o problema envolvia uma historinha que não contribui muito para o entendimento do enunciado, então vamos direto ao ponto: recebemos uma matriz 100x100 que representa um mapa de alturas e precisávamos encontrar todos os pontos que eram cercados (em cima, embaixo, na esquerda e na direita) por pontos mais altos. Ademais sabíamos que as alturas iam de 0 a 9 e que as fronteiras fora do mapa podiam ser todas consideradas mais altas que o resto do mapa. A resposta do problema seria o risco total de todos os pontos baixos, onde o risco de um ponto é igual à sua altura + 1.

O problema não é tão complicado, pois bastaria iterar em todos os pontos da matriz e comparar cada um com seus vizinhos. O maior dezafio era lidar com as fronteiras do mapa. Para isso, resolvi cercar toda a matriz por noves e iterar no quadrado 2:101x2:101.

# Ler o mapa de alturas e estofar as fronteiras com 9
height <- "data-raw/09a_smoke_basin.txt" |>
  readr::read_lines() |>
  stringr::str_split("") |>
  purrr::flatten_chr() |>
  as.integer() |>
  matrix(nrow = 100, ncol = 100, byrow = TRUE) |>
  rbind(rep(9, 100)) |>
  {\(m) rbind(rep(9, 100), m)}() |>
  cbind(rep(9, 102)) |>
  {\(m) cbind(rep(9, 102), m)}()

# Iterar por todos os pontos
risk <- 0
for (i in 2:101) {
  for (j in 2:101) {

    # Verificar se é um ponto baixo e somar o risco ao total
    if (
      height[i, j] < height[i - 1, j] &&
      height[i, j] < height[i + 1, j] &&
      height[i, j] < height[i, j - 1] &&
      height[i, j] < height[i, j + 1]
    ) {
      risk <- risk + height[i, j] + 1
    }
  }
}

# Imprimir
risk
#> [1] 494

Bacia de Fumaça (B)

O segundo item já era mais complicado. Considerando que os pontos com altura 9 não pertencem a nenhuma bacia, precisávamos encontrar as 3 maiores bacias no nosso mapa. Uma bacia é definida por toda uma região cercada por noves e seu tamanho é igual ao número de pontos contíguos contidos nessa área.

O diagrama abaixo não estava no enunciado, mas ele me ajudou muito a entender o que era uma bacia. Para criá-lo, eu peguei um retângulo na ponta do meu mapa e substituí todos os números menores que 9 por um ., representando assim as bacias. Cada região cercada por noves é uma bacia diferente.

# ....999.........9.9....99......9....9..........9
# ...9.9.9.......9...9..9.......9.9...99.99.9.....
# ..9.....9.9.....9...99...........999.999.9.9....
# ..9......9.9...9....999.........9..9..9.....999.
# 99..........999......9999......9..9..9.....9...9
# ...........9..9........99...9.9.......9.........
# 9..............9...9..9..9.99.9......9..........
# ...........99.9...9.99....9..99.....9...........
# ............99....9..9.......9.......9..........
# 9........99999...9....9.9....9.......999........

Minha solução começou igual à do item anterior, mas desta vez criei também uma tabela com todos os pontos do mapa. Meu objetivo era fazer uma busca em largura e remover desta tabela os pontos já explorados.

# Criar uma tabela de pontos a explorar
points <- purrr::cross2(2:101, 2:101) |>
  purrr::map(purrr::flatten_int) |>
  purrr::transpose() |>
  purrr::set_names("i", "j") |>
  tibble::as_tibble() |>
  tidyr::unnest(c(i, j))

A seguir eu criei uma função que explorava uma bacia a partir de um ponto “semente”. O primeiro passo era verificar se o ponto já tinha sido explorado e retornar 0 se sim (indicando que aquele ponto não contribuiria mais para o tamanho da bacia). Se o ponto não tivesse sido explorado, então o código o removia da tabela de pontos e verificava se ele tinha altura 9, mais uma vez retornando 0 se sim. O final da função aplicava uma recursão nos 4 vizinhos do ponto, somando os tamanhos das 4 sub-bacias encontradas mais 1 (indicando que o ponto “semente” contribuia em 1 para o tamanho total da bacia).

# Explorar uma bacia
explore <- function(a, b) {

  # Pular se o ponto já tiver sido explorado
  if (nrow(dplyr::filter(points, i == a, j == b)) == 0) return(0)

  # Marcar o ponto como explorado
  points <<- dplyr::filter(points, i != a | j != b)

  # Se a altura for 9, então ele não faz parte da bacia
  if (height[a, b] == 9) return(0)

  # Adicionar os pontos vizinhos à bacia
  return(
    explore(a - 1, b) +
    explore(a + 1, b) +
    explore(a, b - 1) +
    explore(a, b + 1) + 1
  )
}

A resposta para o item era o produto dos tamanhos das 3 maiores bacias do mapa, então o programa terminava iterando na matriz, calculando o tamanho de todas as bacias e seguindo para obter o resultado final.

# Iterar por todos os pontos
basins <- matrix(rep(0, 10404), 102, 102)
for (i in 2:101) {
  for (j in 2:101) {
    basins[i, j] <- explore(i, j)
  }
}

# Multiplicar as 3 maiores bacias
basins |>
  sort(decreasing = TRUE) |>
  magrittr::extract(1:3) |>
  prod()
#> [1] 1048128

Pontuação de Sintaxe (A)

O dia 10 do AoC pedia para que resolvessemos um clássico problema de parentização com alguns facilitadores. Em resumo, recebíamos uma string composta por parênteses e seus amigos ("(", “[”, “{”, “<”, “>”, “}”, “]”, “)”) e precisávamos identificar se o fechamento de algum deles estava errado, por exemplo, “[}”, “{()()()>”, etc. Para cada string que tivesse um fechamento ilegal recebia uma quantidade de pontos de acordo com a tabela abaixo e, finalmente, a saída do exercício era a soma de todas as pontuações.

# ): 3 pontos.
# ]: 57 pontos.
# }: 1197 pontos.
# >: 25137 pontos.

Para quem nunca viu um problema desse tipo, a solução pode ser alcançada facilmente usando uma pilha. Cada caractere que abre um bloco é colocado na pilha e, para cada caractere que fecha um bloco, removemos o elemento do topo da pilha. Se os caracteres se complementam corretamente o algoritmo segue em frente, caso contrário ele busca a pontuação na tabela e retorna.

# Correspondência de valores
scores <- list(
  ")" = 3,
  "]" = 57,
  "}" = 1197,
  ">" = 25137
)

# Calcular a pontuação por caractere ilegal em uma linha
score_ilegal <- function(line) {
  stack <- flifo::lifo()

  # Iterar na linha até um elemento não corresponder
  symbols <- stringr::str_split(line, "")[[1]]
  for (symbol in symbols) {

    # Empilhar ou desempilhar (e calcular pontuação se necessário)
    if (symbol %in% c("(", "[", "{", "<")) {
      flifo::push(stack, symbol)
    } else {
      check <- flifo::pop(stack)
      if (
        (check == "{" && symbol != "}") ||
        (check == "(" && symbol != ")") ||
        (check == "[" && symbol != "]") ||
        (check == "<" && symbol != ">")
      ) {
        return(scores[names(scores) == symbol][[1]])
      }
    }
  }

  return(0)
}

# Iterar nas linhas e calcular pontuações
"data-raw/10a_syntax_scoring.txt" |>
  readr::read_lines() |>
  purrr::map_dbl(score_ilegal) |>
  sum()
#> [1] 216297

Pontuação de Sintaxe (B)

O segundo item do problema pedia para que começássemos removendo as linhas que tinham pontuação maior que 0 (então só foi necessário filtrar isso no código, que vou omitir). Depois disso o objetivo era completar as linhas que restavam.

O fato é que as linhas restantes estavam todas com um pedaço faltando, por exemplo, “[({(<(())[]>[[{[]{<()<»” precisa ainda de “}}]])})]” para ficar correta. Usando a lógica do item anterior, só precisávamos seguir o mesmo roteiro e, ao final da linha, contar os pontos dos caracteres que ainda haviam sobrado na pilha.

Desta vez a regra de pontuação era diferente: para cada caractere faltante, precisávamos multiplicar a pontuação corrente por 5 e então somar o valor do caractere de acordo com uma nova tabelha. A resposta final era a mediana da pontuação de todoas as linhas. Enfim, o código vai a seguir:

# Ler linhas e remover corrompidas
lines <- readr::read_lines("data-raw/10b_syntax_scoring.txt")
lines <- lines[purrr::map_dbl(lines, score_ilegal) == 0]

# Correspondência de valores
scores <- list(
  "(" = 1,
  "[" = 2,
  "{" = 3,
  "<" = 4
)

# Calcular a pontuação por caractere faltante em uma linha
score_complete <- function(line) {
  stack <- flifo::lifo()

  # Iterar na linha e remover parte completa
  symbols <- stringr::str_split(line, "")[[1]]
  for (symbol in symbols) {

    # Empilhar ou desempilhar
    if (symbol %in% c("(", "[", "{", "<")) {
      flifo::push(stack, symbol)
    } else {
      flifo::pop(stack)
    }
  }

  # Iterar no resto da pilha e calcular pontos
  score <- 0
  while (flifo::size(stack) > 0) {
    check <- flifo::pop(stack)
    score <- (score * 5) + scores[names(scores) == check][[1]]
  }

  return(score)
}

# Pegar mediana das pontuações
lines |>
  purrr::map_dbl(score_complete) |>
  median()
#> [1] 2165057169

Polvo-dumbo (A)

O dia 11 do AoC foi bastante complicado e o meu código talvez tenha ficado pior ainda. As instruções eram até simples: recebemos uma matriz 10x10 com os níveis de energia de 100 polvos-dumbo e precisávamos acompanhar seus níveis de energia ao longo de 100 iterações. As regras eram as seguintes:

Meu código seguia esse procedimento à risca e precisou de 3 loops aninhados para funcionar. O truque mais importante foi criar um clone dos polvos que marcava todos os polvos que já tinham piscado para garantir que nenhum deles ganharia mais energia durante aquele passo; este mecanismo envolvia marcar um polvo que piscava com 0 e um polvo que tinha piscado em qualquer ponto anterior do loop com -1 (para que ele não fosse contado duas vezes). O resultado final deveria ser o número de piscadas totais depois dos 100 passos.

# Ler matriz
dumbo <- "data-raw/11a_dumbo_octopus.txt" |>
  readr::read_table(col_names = FALSE) |>
  tidyr::separate(X1, paste0("C", 0:10), "") |>
  dplyr::select(-C0) |>
  dplyr::mutate_all(as.numeric) |>
  as.matrix()

# Iterar nos 100 passos
flashes <- 0
for (k in 1:100) {

  # Aumentar níveis de energia
  dumbo <- (dumbo + 1) %% 10

  # Adicionar energia aos polvos cujos vizinhos piscaram
  flag <- FALSE
  while(!flag) {

    # Contar piscadas
    flashes <- flashes + sum(dumbo == 0)

    # Adicionar energia aos polvos adjacentes a piscadas
    dumbo_ <- dumbo
    for (i in 1:10) {
      for (j in 1:10) {

        # Índices dos vizinhos
        i1 <- i - 1
        i2 <- min(i + 1, 10)
        j1 <- j - 1
        j2 <- min(j + 1, 10)

        # Adicionar energia nos índices (exceto no centro)
        if (dumbo[i, j] == 0) {
          dumbo_[i1:i2, j1:j2] <- dumbo_[i1:i2, j1:j2] + 1
          dumbo_[i, j] <- dumbo_[i, j] - 1
        }
      }
    }

    # Separar piscadas anteriores dos que piscaram na última iteração
    dumbo <- ifelse(dumbo == -1, 0, dumbo)

    # Sobrescrever as piscadas com 0 (eles não podem receber mais energia)
    dumbo <- ifelse(dumbo == 0, 0, dumbo_)

    # Verificar se o passo atual acabou
    if (!any(dumbo > 9)) {
      flag <- TRUE
    } else {

      # Prevenir piscadas antigas de serem contadas de novo
      dumbo <- ifelse(dumbo == 0, -1, dumbo)
      dumbo <- ifelse(dumbo > 9, 0, dumbo)
    }
  }
}

# Imprimir
flashes
#> [1] 1681

Polvo-dumbo (B)

Felizmente o segundo item o exercício de hoje foi bem mais simples. Eventualmente todos os polvos entram em sincronia, ou seja, passam a piscar todos juntos; o nosso objetivo era descobrir em que passo isso acontecia. A única coisa que precisei fazer com o código do item anterior foi ignorar o limite de passos e criar uma verificação para quando todos os polvos atingiam 0 de energia juntos.

# Ler matriz
dumbo <- "data-raw/11b_dumbo_octopus.txt" |>
  readr::read_table(col_names = FALSE) |>
  tidyr::separate(X1, paste0("C", 0:10), "") |>
  dplyr::select(-C0) |>
  dplyr::mutate_all(as.numeric) |>
  as.matrix()

# Iterar em 1000 passos
for (k in 1:1000) {
  print(k)

  # Aumentar níveis de energia
  dumbo <- (dumbo + 1) %% 10

  # Adicionar energia aos polvos cujos vizinhos piscaram
  flag <- FALSE
  while(!flag) {

    # Adicionar energia aos polvos adjacentes a piscadas
    dumbo_ <- dumbo
    for (i in 1:10) {
      for (j in 1:10) {

        # Índices dos vizinhos
        i1 <- i - 1
        i2 <- min(i + 1, 10)
        j1 <- j - 1
        j2 <- min(j + 1, 10)

        # Adicionar energia nos índices (exceto no centro)
        if (dumbo[i, j] == 0) {
          dumbo_[i1:i2, j1:j2] <- dumbo_[i1:i2, j1:j2] + 1
          dumbo_[i, j] <- dumbo_[i, j] - 1
        }
      }
    }

    # Separar piscadas anteriores dos que piscaram na última iteração
    dumbo <- ifelse(dumbo == -1, 0, dumbo)

    # Sobrescrever as piscadas com 0 (eles não podem receber mais energia)
    dumbo <- ifelse(dumbo == 0, 0, dumbo_)

    # Verificar se o passo atual acabou
    if (!any(dumbo > 9)) {
      flag <- TRUE
    } else {

      # Prevenir piscadas antigas de serem contadas de novo
      dumbo <- ifelse(dumbo == 0, -1, dumbo)
      dumbo <- ifelse(dumbo > 9, 0, dumbo)
    }
  }

  # Parar se todos os polvos tiverem piscado
  if (all(dumbo %in% c(0, -1))) {
    break()
  }
}

# Imprimir
k
#> [1] 276

Busca de Caminho (A)

O dia 12, juntamente com os anteriores, começou a me deixar preocupado com os próximos exercícios do Advent of Code. Aparentemente a dificuldade vai aumentando conforme o passar dos dias, mas já estou chegando no limite do meu conhecimento.

Mais uma vez temos um enunciado complicado, então leia a versão original se ficar difícil de entender aqui. Nosso objetivo esta vez era contar o número de caminhos que o nosso submarino podia tomar em um sistema de cavernas.

A entrada era uma lista de arestas nomeadas em um grafo. Os nossos caminhos deveriam sempre começar na caverna chamada “start” e terminar na chamada “end”, sendo que todas as outras eram divididas em dois grupos: grandes e pequenas. Uma caverna grande era demarcada por uma letra maiúscula e podia ser utilizada pelo nosso caminho qualquer número de vezes. Já uma caverna pequena (demarcada por uma letra minúscula), só podia ser utilizada uma vez no caminho.

Veja o exemplo abaixo. A primeira parte seria a entrada do problema, a segunda, o diagrama das cavernas e a terceira, os 10 possíveis caminhos para o nosso submarino.

# start-A
# start-b
# A-c
# A-b
# b-d
# A-end
# b-end

#     start
#     /   \
# c--A-----b--d
#     \   /
#      end

# start,A,b,A,c,A,end
# start,A,b,A,end
# start,A,b,end
# start,A,c,A,b,A,end
# start,A,c,A,b,end
# start,A,c,A,end
# start,A,end
# start,b,A,c,A,end
# start,b,A,end
# start,b,end

Minha solução envolvia uma tabela que representava todas as arestas do grafo do sistema de cavernas. A cada nova recursão, a última caverna poderia ser mantida na tabela ou removida (no caso das cavernas pequenas); toda vez que um caminho chegasse ao “end”, um contador global era incrementado.

# Contar caminhos distintos em um grafo
count <- 0
count_paths <- function(graph, path = "start") {

  # Verificar se o nó atual é "pequeno"
  cave <- tail(path, 1)
  is_small <- stringr::str_to_lower(cave) == cave

  # Condições de parada
  if (cave == "end") {count <<- count + 1; return(1)}
  if (!any(graph$orig == cave)) return(0)

  # Encontrar próximo nó do caminho
  searches <- graph |>
    dplyr::filter(orig == cave) |>
    dplyr::pull(dest) |>
    purrr::map(purrr::prepend, path)

  # Atualizar nós disponíveis
  graph <- if (is_small) dplyr::filter(graph, orig != cave) else graph

  # Iterar nos possíveis caminhos
  for (search in searches) {
    count_paths(graph, search)
  }

  # Retornar contador global
  return(count)
}

# Ler arestas do grafo e retornar conta dos caminhos
"data-raw/12a_passage_pathing.txt" |>
  readr::read_table(col_names = "path") |>
  tidyr::separate(path, c("orig", "dest"), "-") |>
  {\(d) dplyr::bind_rows(d, purrr::set_names(d, rev(names(d))))}() |>
  dplyr::filter(dest != "start", orig != "end") |>
  count_paths()
#> [1] 4792

Busca de Caminho (B)

O segundo item do problema mudava muito pouco o enunciado. Agora, ao invés de cada caverna pequena poder ser visitada apenas 1 vez, tínhamos um pequeno acréscimo de tempo. Isso queria dizer que, em cada caminho até o final do sistema de cavernas, podíamos visitar apenas 1 das cavernas pequenas até 2 vezes.

Minha solução foi criar um argumento chamado boost que indicava se já tínhamos usado o nosso excedente de tempo naquele caminho expecífico. Se não tivéssemos, poderíamos não retirar uma das cavernas pequenas da lista imediatamente. Esta estratégia funcionou, mas gerou caminhos repetidos (usando e não usando o boost), então, ao invés de contar os caminhos, passei a salvar os caminhos e contar o número de caminhos distintos no final.

# Pegar todos os caminhos distintos em um grafo
all_paths <- list()
get_paths <- function(graph, path = "start", boost = FALSE) {

  # Verificar se o nó atual é "pequeno"
  cave <- tail(path, 1)
  is_small <- stringr::str_to_lower(cave) == cave

  # Condições de parada
  if (cave == "end") {all_paths <<- append(all_paths, list(path)); return(1)}
  if (!any(graph$orig == cave)) return(0)

  # Encontrar próximo nó do caminho
  searches <- graph |>
    dplyr::filter(orig == cave) |>
    dplyr::pull(dest) |>
    purrr::map(purrr::prepend, path)

  # Atualizar nós disponíveis
  graph_ <- if (is_small) dplyr::filter(graph, orig != cave) else graph

  # Iterar nos possíveis caminhos
  for (search in searches) {
    get_paths(graph_, search, boost = boost)

    # Uma opção é não remover o nó do grafo e usar o boost
    if (!boost && is_small && cave != "start") {
      get_paths(graph, search, boost = TRUE)
    }
  }

  # Retornar lista global
  return(all_paths)
}

# Ler arestas do grafo e retornar conta dos caminhos distintos
"data-raw/12b_passage_pathing.txt" |>
  readr::read_table(col_names = "path") |>
  tidyr::separate(path, c("orig", "dest"), "-") |>
  {\(d) dplyr::bind_rows(d, purrr::set_names(d, rev(names(d))))}() |>
  dplyr::filter(dest != "start", orig != "end") |>
  get_paths() |>
  purrr::map_chr(stringr::str_c, collapse = "|") |>
  unique() |>
  length()
#> [1] 133360

Origami Transparente (A)

O dia 13 foi um belo alívio comparado com o dia anterior. Nossa missão hoje era descobrir o código de um sensor a partir de um código escrito em papel transparente. A entrada era uma série de coordenadas de pontos no papel e uma sequência de instruções de como dobrar o papel para obter o código final.

Partindo do princípio de que a matriz começava no ponto (0, 0) na esqueda superior, o primeiro item pedia para que lêssemos a nossa lista de coordenadas e contasse o número de pontos (#) visíveis depois de realizar a primeira instrução que nos era dada. Para ilustrar como as dobras ocorriam, veja os resultados de uma dobra em y = 7 e, depois, de uma dobra em x = 5:

# Papel inicial
# ...#..#..#.
# ....#......
# ...........
# #..........
# ...#....#.#
# ...........
# ...........
# ...........
# ...........
# ...........
# .#....#.##.
# ....#......
# ......#...#
# #..........
# #.#........

# Linha em y = 7
# ...#..#..#.
# ....#......
# ...........
# #..........
# ...#....#.#
# ...........
# ...........
# -----------
# ...........
# ...........
# .#....#.##.
# ....#......
# ......#...#
# #..........
# #.#........

# Resultado da primeira dobra
# #.##..#..#.
# #...#......
# ......#...#
# #...#......
# .#.#..#.###
# ...........
# ...........

# Linha em x = 5
# #.##.|#..#.
# #...#|.....
# .....|#...#
# #...#|.....
# .#.#.|#.###
# .....|.....
# .....|.....

# Resultado final
# #####
# #...#
# #...#
# #...#
# #####
# .....
# .....

O maior desafio no código em R foi arrumar todas as coordenadas e sub-matrizes para um sistema que começa em 1 e não em 0. Eu também resolvi fazer uma aposta: o primeiro item pedia para fazer apenas a primeira dobra, então eu imaginei que o segundo item pediria para fazer todas. Minha decisão, portanto, foi tentar já generalizar meu algortimo para que ele funcionasse com o mínimo de alterações possíveis para realizar várias dobras.

# Ler tabela de onde os pontos estão
dots <- "data-raw/13a_transparent_origami.txt" |>
  readr::read_lines() |>
  stringr::str_subset("^[0-9]") |>
  tibble::tibble() |>
  purrr::set_names("dot") |>
  tidyr::separate(dot, c("x", "y"), ",") |>
  dplyr::mutate_all(as.integer) |>
  dplyr::mutate_all(`+`, 1L)

# Ler instruções das dobras
instructions <- "data-raw/13a_transparent_origami.txt" |>
  readr::read_lines() |>
  stringr::str_subset("^[^0-9]") |>
  tibble::tibble() |>
  purrr::set_names("fold") |>
  tidyr::separate(fold, c("axis", "line"), "=") |>
  dplyr::mutate(
    axis = stringr::str_sub(axis, -1),
    line = as.integer(line) + 1L
  )

# Colocar os pontos no papel
paper <- matrix(FALSE, nrow = max(dots$y), ncol = max(dots$x))
for (i in seq_len(nrow(dots))) {
  paper[dots$y[i], dots$x[i]] <- TRUE
}

# Rodar apenas a primeira instrução
for (i in 1) {

  # Achar o eixo e o ponto da dobra
  axis <- instructions$axis[i]
  line <- instructions$line[i]

  # Dobras de acordo com o eixo
  if (axis == "x") {

    # Número de colunas à direita da dobra
    size <- length((line + 1):dim(paper)[2])

    # Pegar colunas à direita, invertê-las e fazer um OR com o lado esquerdo
    paper[, (line - size):(line - 1)] <-
      paper[, (line + 1):(line + size)][, size:1] |
      paper[, (line - size):(line - 1)]

    # Descartar colunas representando o papel dobrado
    paper <- paper[, 1:(line - 1)]

  } else {

    # Número de linhas abaixo da dobra
    size <- length((line + 1):dim(paper)[1])

    # Pegar linhas abaixo da dobra, invertê-las e fazer um AND com as acima
    paper[(line - size):(line - 1), ] <-
      paper[(line + 1):(line + size), ][size:1, ] |
      paper[(line - size):(line - 1), ]

    # Descartar linhas representando o papel dobrado
    paper <- paper[1:(line - 1), ]
  }
}

# Contar pontos no papel
sum(paper)
#> [1] 765

Origami Transparente (B)

E minha aposta valeu à pena! De fato o enunciado da parte 2 pedia para que realizássemos todas as dobras do nosso conjunto de instruções. No final, se tudo estivesse correto, os # e . do papel deveriam formar 8 letras maiúsculas.

A única alteração no código foi trocar a condição do for:

# Iterar por todas as instruções
for (i in seq_len(nrow(instructions)))

E, no final, também foi necessário fazer um print melhor da matriz:

# Imprimir os pontos de um jeito mais amigável
paper <- ifelse(paper, "#", ".")
for (i in seq_len(nrow(paper))) {
  cat(paper[i, ])
  cat("\n")
}
# # # # . . # # # # . # . . # . # # # # . # . . . . # # # . . . # # . . # . . # .
# # . . # . . . . # . # . # . . . . . # . # . . . . # . . # . # . . # . # . . # .
# # . . # . . . # . . # # . . . . . # . . # . . . . # . . # . # . . . . # # # # .
# # # # . . . # . . . # . # . . . # . . . # . . . . # # # . . # . # # . # . . # .
# # . # . . # . . . . # . # . . # . . . . # . . . . # . . . . # . . # . # . . # .
# # . . # . # # # # . # . . # . # # # # . # # # # . # . . . . . # # # . # . . # .

Polimerização Estendida (A)

O 14º dia do AoC foi muito demorado de resolver para mim. Apesar de ambas as soluções abaixo serem “simples”, levei horas para encontrar um jeito razoável de resolver o segundo item e, infelizmente, só consegui depois de olhar uma dica na internet que deixou tudo mais simples.

Desta vez nossa missão era estender um molde de polímero através de um conjunto de regras de reação. A primeira linha da entrada era o molde e, a partir daí, tínhamos as regras de inserção:

# NNCB
#
# CH -> B
# HH -> N
# CB -> H
# NH -> C
# HB -> C
# HC -> B
# HN -> C
# NN -> C
# BH -> H
# NC -> B
# NB -> B
# BN -> B
# BB -> N
# BC -> B
# CC -> N
# CN -> C

As regras eram fáceis de entender. Cada uma delas indicava que, quando os dois elementos da esquerda se encontravam, entre eles apareceria o elemento da direita. Uma rodada de reação envolvia estender todos os pares da cadeia polimérica; no caso do exemplo, isso transformaria NNCB em NCNBCHB.

Após 10 iterações, deveríamos contar o número de ocorrências do elemento mais comum da cadeia e subtrair dele o número de ocorrências do elemento menos comum da cadeia. Esta seria a resposta do problema.

Meu código para o primeiro item acabou seguindo o que chamamos de estratégia de força bruta. A cada iteração, eu quebrava o polímero nos seus pares de elementos e fazia um join com a tabela de regras; depois era só colar tudo em uma string só e seguir em frente. No final eu só precisava encontrar as letras mais e menos comuns da string e subtraí-las.

# Ler modelo como string
poly <- readr::read_lines("data-raw/14a_extended_polymerization.txt", n_max = 1)

# Ler regras como tabela
rules <- "data-raw/14a_extended_polymerization.txt" |>
  readr::read_table(skip = 1, col_names = FALSE) |>
  purrr::set_names("pair", "rm", "insertion") |>
  dplyr::select(-rm) |>
  dplyr::mutate(insertion = stringr::str_replace(
    pair, "(.)(.)", paste0("\\1", insertion, "\\2")
  ))

# Executar uma rodada de inserções
do_insertions <- function(poly) {
  poly |>
    stringr::str_split("") |>
    purrr::pluck(1) |>
    purrr::accumulate(~paste0(stringr::str_sub(.x, -1), .y)) |>
    utils::tail(-1) |>
    purrr::map_chr(~rules[rules$pair == .x, ]$insertion) |>
    purrr::reduce(~paste0(.x, stringr::str_sub(.y, -2))) |>
    stringr::str_c(collapse = "")
}

# Rodar do_insertions() 10 vezes e fazer el. mais comum - el. menos comum
10 |>
  seq_len() |>
  purrr::reduce(~do_insertions(.x), .init = poly) |>
  stringr::str_split("") |>
  table() |>
  {\(t) list(t[which.max(t)], t[which.min(t)])}() |>
  purrr::reduce(`-`) |>
  abs() |>
  unname()
#> [1] 2584

Polimerização Estendida (B)

O segundo item parecia suspeitamente simples, mas eu estava redondamente enganado. A única instrução era repetir o problema do primeiro item para 40 iterações ao invés de 10. Pode parecer que eu não precisaria nem mudar meu código, mas note que no primeiro item a minha cadeia polimérica só chegou a ter 19457 letras. No segundo item a cadeia chegaria a… mais de 20 trilhões.

Seria necessário mudar de estratégia e foi aí que eu empaquei. Tentei diversas formas de manter apenas o número de letras na cadeia, sem armazenar a cadeia em si, mas nada funcionava. Eu até notei que a primeira e a última letras da cadeia nunca mudavam, mas isso não me ajudou.

Depois de procurar por dicas no subreddit do AoC, finalmente achei uma boa alma que havia feito uma observação incrível:

Sempre podemos manter apenas a contagem de pares distintos na cadeia. Se tivermos, por exemplo, um par AC aparecendo n = 10 vezes na cadeia e uma regra AC -> B, então na próxima iteração podemos adicionar à nossa contagem AB e BC, cada uma aparecendo n = 10 vezes.

Até aí eu já sabia, era essencialmente o que eu fazia manualmente no item 1. O problema é que, mantendo apenas as contagens dos pares, isso repetiria a letra B duas vezes, totalizando A 10 vezes, C 10 vezes e B 20 vezes. A ideia que veio a seguir, entretanto, foi o que realmente resolveu o problema:

Se pensarmos na cadeia como um todo, todos as letras serão contadas 2 vezes, exceto pela primeira e pela última, pois elas nunca ficam no meio de uma reação. O número de ocorrências de cada letra é, portanto, n / 2, exceto pelas letras que aparecem no início e no fim, paras quais a fórmula é (n + 1) / 2.

Depois disso o item 2 podia ser solucionado facilmente.

# Registrar a primeira e a última letras da cadeia original
orig <- "data-raw/14b_extended_polymerization.txt" |>
  readr::read_lines(n_max = 1) |>
  stringr::str_replace("^(.).*?(.)$", "\\1\\2") |>
  stringr::str_split("") |>
  purrr::pluck(1)

# Ler modelo já no formato de contagem de pares
poly <- "data-raw/14b_extended_polymerization.txt" |>
  readr::read_lines(n_max = 1) |>
  stringr::str_split("") |>
  purrr::pluck(1) |>
  purrr::accumulate(~paste0(stringr::str_sub(.x, -1), .y)) |>
  utils::tail(-1) |>
  tibble::tibble() |>
  purrr::set_names("pair") |>
  dplyr::count(pair)

# Ler regras como tabela
rules <- "data-raw/14b_extended_polymerization.txt" |>
  readr::read_table(skip = 1, col_names = FALSE) |>
  purrr::set_names("pair", "rm", "insertion") |>
  dplyr::select(-rm) |>
  dplyr::mutate(insertion = stringr::str_replace(
    pair, "(.)(.)", paste0("\\1", insertion, "\\2")
  ))

# Executar uma rodada de inserções
do_insertions <- function(poly) {
  poly |>
    dplyr::left_join(rules, "pair") |>
    dplyr::mutate(
      insertion = purrr::map(insertion, stringr::str_extract, c("^..", "..$"))
    ) |>
    tidyr::unnest(insertion) |>
    dplyr::group_by(pair = insertion) |>
    dplyr::summarise(n = sum(n))
}

# Rodar do_insertions() 40 vezes e fazer el. mais comum - el. menos comum
40 |>
  seq_len() |>
  purrr::reduce(~do_insertions(.x), .init = poly) |>
  dplyr::mutate(elem = stringr::str_split(pair, "")) |>
  tidyr::unnest(elem) |>
  dplyr::group_by(elem) |>
  dplyr::summarise(n = sum(n)) |>
  dplyr::mutate(
    n = ifelse(elem %in% orig, n + 1, n),
    n = n / 2
  ) |>
  dplyr::filter(n == max(n) | n == min(n)) |>
  dplyr::pull(n) |>
  purrr::reduce(`-`) |>
  abs() |>
  format(scientific = FALSE)
#> [1] 3816397135460

Quítons (A)

No 15º dia do AoC eu demorei muito mais do que deveria. Apesar de ter entendido bem o enunciado e ter identificado rapidamente o caminho para a solução, eu empaquei na implementação do algoritmo. No final achei melhor pegar uma versão pronta do algoritmo para não perder mais horas com isso.

Novamente o enunciado envolvia um submarino, uma caverna, etc. Passar por cada ponto da caverna vinha com um certo risco que variava entre 1 e 9 e nosso objetivo era levar o submarino do ponto esquerdo superior até o ponto esquerdo inferior passando pelo caminho com menor risco total. A saída do programa deveria ser a soma do risco de todos os pontos do caminho (sem incluir o ponto de entrada, pois já começávamos nele).

A esse ponto, qualquer um que tenha aprendido sobre grafos já deve estar com o sentido aranha ativado. Esse é um problema clássico da Computação que pode ser facilmente solucionado pelo algoritmo de Dijkstra. Algumas alterações são necessárias, mas todas podem ser feitas antes de executar o algoritmo.

O passo-a-passo do código é mais ou menos o seguinte:

  1. Marcar todos os pontos como não visitados. Criar um conjunto com todos os pontos não visitador chamado conjunto não visitado.

  2. Atribuir a todos os pontos um risco temporário: ele deve ser 0 para o nó inicial e infinito para o resto. O risco temporário de um ponto v é o risco total do caminho de menor risco já descoberto entre v e o ponto inicial. Como no começo não conhecemos nenhum outro ponto além do inicial, todos os riscos temporários começam como infinito. Fixar o ponto inicial como o atual.

  3. Para o ponto atual, considerar todos os seus vizinhos não vizitados e calcular os seus riscos temporários através do ponto atual. Comparar o novo risco temporário ao risco atual e atribuir o menor dos dois. Por exemplo, se o risco do ponto atual A é 6 e o seu vizinho B tem risco 2, então o risco de chegar em B por A é 6 + 2 = 8. Se o risco temporário de B até agora era maior que 8, então ele deve virar 8. Caso contrário, nada muda.

  4. Quando já tivermos considerado todos os vizinhos não visitados do ponto atual, marcar o ponto atual como visitado e removê-lo do conjunto não visitado. Um ponto visitado nunca será checado de novo.

  5. Se o ponto final houver sido marcado como visitado, então parar. O algoritmo terminou e o risco total do melhor caminho até o distino é igual ao risco temporário que foi atribuido ao destino.

  6. Caso contrário, selecionar o ponto não visitado que tem o menor risco temporário e torná-lo o ponto atual. Voltar ao passo 3.

Normalmente o algoritmo de Dijkstra é aplicado em grafos nos quais os custos de cada passo do caminho são atribuídos à arestas do grafo e não aos nós, como é o nosso caso. Para resolver esse problema, temos que fazer uma certa ginástica para que os custos sejam transferidos para as arestas. Cada par de nós vizinhos ganham duas arestas direcionadas, cada uma com o risco do nó para o qual ela aponta:

# Ponto atual com seus 4 vizinhos
#   7
# 9 3 1
#   6
#
# Arestas indo para o ponto atual (todas têm risco 3)
#       o
#       3
#       ↓
# o 3 → x ← 3 o
#       ↑
#       3
#       o
#
# Arestas saindo do ponto atual (todas têm o risco do vizinho)
#       o
#       ↑
#       7
# o ← 9 x 1 → o
#       6
#       ↓
#       o

Eu queria ter de fato implementado o algoritmo de Dijkstra no R por conta própria, mas eu cometi vários erros pelo caminho (eram 7:30, não me julgue) e, para não passar a manhã toda nisso, resolvi usar o pacote cppRouting para aplicar o algoritmo.

# Ler os riscos da caverna como uma matriz
cave <- "data-raw/15a_chiton.txt" |>
  readr::read_lines() |>
  stringr::str_split("") |>
  purrr::flatten_chr() |>
  as.integer() |>
  matrix(100, 100, byrow = TRUE)

# Criar uma tabela com os custos entre vizinhos
graph <- tibble::tibble()
for (i in 1:prod(dim(cave))) {

  vals <- c()
  if (i %% 100 != 0)  vals <- append(vals, i + 1L)
  if (i %% 100 != 1)  vals <- append(vals, i - 1L)
  if (i > 100)        vals <- append(vals, i - 100L)
  if (i < 9901)       vals <- append(vals, i + 100L)

  node <- tibble::tibble(from_vertex = i, to_vertex = vals, cost = cave[vals])
  graph <- dplyr::bind_rows(graph, node)
}

# Criar grafo e executar o algoritmo de Dijkstra
path <- graph |>
  cppRouting::makegraph(directed = TRUE) |>
  cppRouting::get_path_pair(from = 1L, to = 10000L) |>
  purrr::pluck(1) |>
  as.integer()

# Calcular o risco total do caminho (subtraíndo o custo da entrada)
graph |>
  dplyr::filter(to_vertex %in% path) |>
  dplyr::group_by(to_vertex) |>
  dplyr::summarise(cost = cost[1]) |>
  dplyr::summarise(risk = sum(cost)) |>
  dplyr::pull(risk) |>
  magrittr::subtract(cave[1])
#> [1] 811

Quítons (B)

O segundo item seguia a mesma lógica de outros problemas desse ano: igual ao primeiro item, mas maior. Como eu estava usando um algoritmo bastante eficiente, não tive problema nenhum nessa parte.

Aqui descobríamos que, na verdade, a caverna era 5 vezes maior em cada dimensão (ou seja, 25 vezes mais pontos). A caverna completa era, entretanto, composta por cópias da sessão original com riscos mais elevados; para obter a versão final da caverna era necessário juntar 25 cópias da original somando um certo fator a cada cópia.

# +0 +1 +2 +3 +4
# +1 +2 +3 +4 +5
# +2 +3 +4 +5 +6
# +3 +4 +5 +6 +7
# +4 +5 +6 +7 +8

Seguindo o guia acima, vemos que o canto superior esquerdo da caverna maior era igual à sessão original e, sucessivamente, chegávamos ao canto direito inferior, que era igual à sessão original, mas o risco de cada ponto era acrescido de 8. O único detalhe é que, quando o risco de um ponto passava de 9, ele voltava para 1 (igual aos polvos-dumbo que vimos anteriormente). O resto da solução era igual.

# Criar clones da caverna, somar fator de risco e juntar
cave <- cbind(
  rbind(cave + 0L, cave + 1L, cave + 2L, cave + 3L, cave + 4L),
  rbind(cave + 1L, cave + 2L, cave + 3L, cave + 4L, cave + 5L),
  rbind(cave + 2L, cave + 3L, cave + 4L, cave + 5L, cave + 6L),
  rbind(cave + 3L, cave + 4L, cave + 5L, cave + 6L, cave + 7L),
  rbind(cave + 4L, cave + 5L, cave + 6L, cave + 7L, cave + 8L)
)

# Reduzir pontos que passaram de 9
cave[cave > 9] <- cave[cave > 9] - 9

Decodificador de Pacotes (A)

O 16º problema do AoC foi bastante diverido. O enunciado era extremamente longo e cheio de detalhes, mas consegui fazer uma implementação direta e eficiente que só não funcionou de primeira por causa de um detalhe obscuro da função strtoi().

Hoje nosso objetivos era decodificar pacotes binários. Eles chegavam ao nosso submarino em hexadecimal e, depois de convertidos para binário eles tinham as seguintes características:

Simples? Longe disso. Vejamos alguns exemplos:

# Pacote literal (valor)
# D2FE28
# 110100101111111000101000
# VVVTTTAaaaaBbbbbCcccc
#
# - VVV são a versão do pacote, 6.
# - TTT são o tipo, 4. Então este pacote carrega um valor.
# - A é 1 (continuar lendo), então aaaa são o primeiro pedaço do valor.
# - B é 1 (continuar lendo), então bbbb são o segundo pedaço do valor.
# - C é 0 (parar de ler), então cccc são o último pedaço do valor.
# - O resto são bits extras.
# - Portanto, o valor carregado por este pacote é 011111100101 = 2021.
#
# Pacote operador com indicador 0
# 38006F45291200
# 00111000000000000110111101000101001010010001001000000000
# VVVTTTILLLLLLLLLLLLLLLAAAAAAAAAAABBBBBBBBBBBBBBBB
#
# - VVV são a versão do pacote, 1.
# - TTT são o tipo, 6. Então este pacote carrega um operador.
# - I é o indicador, 0. Então este pacote tem 15 bits com os comprimentos
#   dos sub-pacotes.
# - LLLLLLLLLLLLLLL contêm a soma dos comprimentos dos sub-pacotes, 27.
# - AAAAAAAAAAA são um sub-pacote carregando um valor, 10.
# - BBBBBBBBBBBBBBBB são um sub-pacote carregando um valor, 20.
#
# Pacote operador com indicador 1
# EE00D40C823060
# 11101110000000001101010000001100100000100011000001100000
# VVVTTTILLLLLLLLLLLAAAAAAAAAAABBBBBBBBBBBCCCCCCCCCCC
# - VVV são a versão do pacote, 7.
# - TTT são o tipo, 3. Então este pacote carrega um operador.
# - I é o indicador, 1. Então este pacote tem 11 bits com os número de
#   sub-pacotes.
# - LLLLLLLLLLL contêm o número de sub-pacotes, 3.
# - AAAAAAAAAAA são um sub-pacote carregando um valor, 1.
# - BBBBBBBBBBB são um sub-pacote carregando um valor, 2.
# - CCCCCCCCCCC são um sub-pacote carregando um valor, 3.

O ponto positivo desse enunciado enorme é que conseguimos implementar os recursos necessários quase em sequência.

# Converter string hexadecimal para string binária
hex_to_bits <- function(hex) {
  hex |>
    stringr::str_split("") |>
    purrr::pluck(1) |>
    purrr::map(~paste(rev(as.integer(intToBits(strtoi(.x, 16)))))) |>
    purrr::map(magrittr::extract, 29:32) |>
    purrr::flatten_chr() |>
    stringr::str_c(collapse = "")
}

# Pegar a versão de um pacote
get_version <- function(pkt) {
  strtoi(stringr::str_sub(pkt, 1, 3), 2)
}

# Pegar o tipo de um pacote
get_type <- function(pkt) {
  strtoi(stringr::str_sub(pkt, 4, 6), 2)
}

O objetivo final deste item era parsear a hierarquia de pacotes da nossa entrada e somar as versões de todos. Minha solução envolveu, desta forma, cirar uma “classe” que podia conter a versão e o comprimento de um pacote. O comprimento era importante para descartar o número certo de bits do pacote quando tivéssemos terminado de processar um sub-pacote.

Se um pacote fosse do tipo operador, então sua “classe” também conteria todos os seus sub-pacotes como elementos sem nome. O código abaixo implementa o processamento dos dois tipos de pacotes; note como foram implementadas as “classes”:

# Pegar o valor de um pacote literal
get_literal <- function(pkt) {
  interval <- c(7, 11)

  # Iterar até o último pedaço ser encontrado
  literal <- ""
  flag <- FALSE
  while (!flag) {

    # Pegar o grupo especificado pelo intervalo
    group <- stringr::str_sub(pkt, interval[1], interval[2])
    literal <- stringr::str_c(literal, stringr::str_sub(group, 2))

    # Parar se este é o último pedaço, caso contrário somar 5 ao intervalo
    if (!as.integer(stringr::str_sub(group, 1, 1))) {
      flag <- TRUE
    } else {
      interval <- interval + 5
    }
  }

  # Retornar a "classe" que descreve o pacote
  return(list(
    version = get_version(pkt),
    len = interval[2],
    value = strtoi(literal, 2)
  ))
}

# Processar um pacote operador
get_operator <- function(pkt) {
  indicator <- stringr::str_sub(pkt, 7, 7)

  # Inicializar "classe"
  out <- list(
    version = get_version(pkt)
  )

  # Lidar com os 2 indicadores
  if (as.integer(indicator)) {

    # Pegar o número de sub-pacotes e separar a cauda do pacote
    num <- strtoi(stringr::str_sub(pkt, 8, 18), 2)
    rest <- stringr::str_sub(pkt, 19)
    out$len <- 18

    # Iterar no número de pacotes
    for (i in seq_len(num)) {

      # Processar sub-pacote
      sub <- if (get_type(rest) == 4) get_literal(rest) else get_operator(rest)
      out$len <- out$len + sub$len
      out <- c(out, list(sub))

      # Atualizar a cauda dado o compimento do último sub-pacote
      rest <- stringr::str_sub(rest, sub$len + 1)
    }
  } else {

    # Pegar o limite de comprimento dos sub-pacotes e separar a cauda
    lim <- strtoi(stringr::str_sub(pkt, 8, 22), 2)
    rest <- stringr::str_sub(pkt, 23)
    out$len <- 22

    # Iterar enquanto os sub-pacotes não tiverem passado do limite
    while (lim > 0) {

      # Processar sub-pacote
      sub <- if (get_type(rest) == 4) get_literal(rest) else get_operator(rest)
      out$len <- out$len + sub$len
      out <- c(out, list(sub))

      # Atualizar a cauda dado o compimento do último sub-pacote
      rest <- stringr::str_sub(rest, sub$len + 1)
      lim <- lim - sub$len
    }
  }

  return(out)
}

O último passo do meu código era achatar toda a estrutura de árvore que seria devolvida pelas funções acima e somar todos os comprimentos.

# Somar todas as versões do pacote representado por um hex
sum_versions <- function(hex) {

  # Pegar a árvore de pacotes representada pelo hex
  pkt <- hex_to_bits(hex)
  pkts <- if (get_type(pkt) == 4) get_literal(pkt) else get_operator(pkt)

  # Achatar árvore
  while (purrr::vec_depth(pkts) > 2) {
    pkts <- purrr::flatten(pkts)
  }

  # Somar versões
  pkts |>
    magrittr::extract(names(pkts) == "version") |>
    purrr::reduce(sum)
}

# Ler pacotes de um hex e somar versões
"data-raw/16a_packet_decoder.txt" |>
  readr::read_lines() |>
  sum_versions()
#> [1] 991

Decodificador de Pacotes (B)

O segundo item era mais ou menos o que eu já esperava. Os tipos dos pacotes tinham um significado maior, ou seja, cada sub-tipo de pacote operador indicava uma operação matemática que deveria ser aplicada no valor dos seus sub-pacotes.

Ou seja, se um pacote tiver a estrutura (operador + (operador * (valor 1) (valor 2)) (valor 3)), então a expressão aritmética resultante seria (1 * 2) + 3). Nosso objetivo final era calcular o valor da expressão que o nosso pacote representava. Felizmente, o meu script anterior funcionava muito bem com essa alteração!

Eu troquei o elemento version da “classe” por type (o tipo do operador) e adicionei o seguinte no final do código:

# Avaliar a árvore de pacotes
get_value <- function(tree) {

  # Funções correspondentes aos tipos
  fun <- switch(as.character(tree$type),
    "0" = sum,
    "1" = prod,
    "2" = min,
    "3" = max,
    "5" = `>`,
    "6" = `<`,
    "7" = `==`,
  )

  # Aplicar função aos sub-pacotes
  apply_fun <- function(tree) {
    tree |>
      purrr::keep(names(tree) == "") |>
      purrr::map(get_value) |>
      purrr::reduce(fun)
  }

  # Aplicar recursivamente
  if (tree$type == 4) tree$value else as.numeric(apply_fun(tree))
}

# Decodificar a expressão de um pacote hex
decode <- function(hex) {
  pkt <- hex_to_bits(hex)
  tree <- if (get_type(pkt) == 4) get_literal(pkt) else get_operator(pkt)

  get_value(tree)
}

# Ler pacotes de um hex e calcular o valor da expressão
"data-raw/16b_packet_decoder.txt" |>
  readr::read_lines() |>
  decode() |>
  format(scientific = FALSE)
#> [1] 1264485568252

P.S.: Mas isso não funcionou de primeira! Eu recebi um belo NA ao final da execução e demorei para entender a causa… No final eu descobri que a função strtoi() retorna NA quando o resultado é grande demais. A solução foi trocá-la por uma função própria:

strton <- function(x) {
  y <- as.numeric(strsplit(x, "")[[1]])
  sum(y * 2^rev((seq_along(y) - 1)))
}

Cesta de Três (A)

O 17º dia do AoC foi uma ótima quebra em relação aos últimos. O enunciado era simples de entender e a solução foi fácil de criar, tudo que eu precisava depois de uma semana cansativa.

Hoje precisávamos tentar encontrar a chave do nosso submarino em uma fossa marinha. A sonda que tínhamos a bordo podia ser arremessada a partir do ponto (0, 0) com qualquer velocidade inteira tanto no eixo x quanto no y. A entrada do problema era a posição do alvo e a saída do primeiro item deveria ser a altura máxima que podíamos arremessar a sonda de modo que ela ainda atingisse o alvo.

As regras para a aceleração da sonda a cada passo eram as seguintes:

O grande truque do exercício era identificar todas as velocidades possíveis da sonda e depois verificar qual o levava à maior altura. Como o alvo estava sempre abaixo e à direita do (0, 0), podíamos estabelecer os limites inferiores e superiores para as velocidades x e y:

# Velocidade inicial: (6,3)
# ..................................
# .........(3,0).#..#.(2,-1)........
# .....(4,1).#........#.(1,-2)......
# ..................................
# (5,2).#..............#.(0,-3).....
# ..................................
# ..................................
# S.(6,3)..............#.(0,-4).....
# ..................................
# ..................................
# ..................................
# .....................#.(0,-5).....
# ....................TTTTTTTTTTT...
# ....................TTTTTTTTTTT...
# ....................TTTTTTTTTTT...
# ....................TTTTTTTTTTT...
# ....................T#T(0,-6)TT...
# ..................................
# ..................................

Note no diagrama acima a simetria da trajetória no eixo y. Assim fica mais fácil entender porque, por exemplo, se o limite inferior do alvo for y = -10, então nunca podemos jogar a sonda para cima com velocidade maior que 9; ela voltará para y = 0 com velocidade -10 e acertará exatamente a fronteira de baixo do alvo.

# Ler alvo como uma tabela de coordenadas
target <- "data-raw/17a_trick_shot.txt" |>
  readr::read_lines() |>
  stringr::str_split("[=,]") |>
  purrr::pluck(1) |>
  stringr::str_subset("^[0-9-]") |>
  stringr::str_replace("\\.\\.", ":") |>
  purrr::map(~eval(parse(text = .x))) |>
  purrr::cross() |>
  purrr::transpose() |>
  purrr::set_names("x", "y") |>
  tibble::as_tibble() |>
  tidyr::unnest(c(x, y))

# Todas as possíveis combinações de velocidades x e y válidas
vels <- purrr::cross(list(
  1:max(target$x),
  min(target$y):abs(min(target$y))
))

Para calcular a altura máxima que poderíamos arremessar a sonda, eu simulei a trajetória a partir de cada um dos pares de velocidades válidas e guardei a altura máxima à qual a sonda chegava. No final da iteração, se a sonda de fato atingisse o alvo, então eu comparava a altura máxima dessa combinação com a altura máxima global e mantinha a maior.

# Verificar quais pares de velocidades funcionam e pegar a altura máxima
max_height <- 0
for (vel in vels) {

  # Posição inicial
  x_pos <- 0
  y_pos <- 0

  # Velocidades iniciais
  x_vel <- vel[[1]]
  y_vel <- vel[[2]]

  # Encontrar a altura máxima deste par de velocidades
  max_height_ <- 0
  while (y_pos >= min(target$y) && x_pos <= max(target$x)) {

    # Atualizar posições
    x_pos <- x_pos + x_vel
    y_pos <- y_pos + y_vel

    # Atualizar altura máxima local
    if (y_pos > max_height_) max_height_ <- y_pos

    # Se o par de fato leva ao alvo, atualizar altura máxima global
    if (x_pos %in% target$x && y_pos %in% target$y) {
      if (max_height_ > max_height) max_height <- max_height_
    }

    # Atualizar velocidades
    x_vel <- if (x_vel > 0) x_vel - 1 else 0
    y_vel <- y_vel - 1
  }
}

# Retornar a altura máxima global
max_height
#> [1] 4753

Cesta de Três (B)

Chegando no item 2, eu percebi que tinha dado muita sorte. O enunciado aqui pedia para encontrarmos o número de velocidades iniciais da sonda que a faziam chegar ao alvo. Meu código anterior já encontrava todos os pares válidos, mas utilizava isso para atualizar a altura máxima; só era necessário trocar a variável sendo atualizada dentro do while.

# Verificar pares de velocidades que funcionam e contá-los
n_works <- 0
for (vel in vels) {

  # Posição inicial
  x_pos <- 0
  y_pos <- 0

  # Velocidades iniciais
  x_vel <- vel[[1]]
  y_vel <- vel[[2]]

  # Encontrar a altura máxima deste par de velocidades
  max_height_ <- 0
  while (y_pos >= min(target$y) && x_pos <= max(target$x)) {

    # Atualizar posições
    x_pos <- x_pos + x_vel
    y_pos <- y_pos + y_vel

    # Se o par de fato leva ao alvo, atualizar contador
    if (x_pos %in% target$x && y_pos %in% target$y) {
      n_works <- n_works + 1
      break
    }

    # Atualizar velocidades
    x_vel <- if (x_vel > 0) x_vel - 1 else 0
    y_vel <- y_vel - 1
  }
}

# Retornar número de velocidades que funcionam
n_works

Peixe-Caracol (A)

Chegou o dia 18 do AoC e mais uma vez o problema não foi muito difícil apesar do enunciado monstruoso. Uma coisa que notei hoje é que havia vários caminhos para resolver o exercício que pareciam igualmente razoáveis. No final eu decidi usar regex, uma das melhores e mais temidas funcionalidades de qualquer linguagem de programação.

O enunciado pedia para aprendermos a fazer somas usando os números dos peixes-caracol… A primeira característica desse sistema aritmético é que um número é representado por pares de elementos na forma [x,y], que podem ser números normais ou outros pares; por exemplo [[1,2],3]. Além disso, há duas limitações para os números: nunca pode haver um par dentro de 4 ou mais pares e nenhum número normal pode ser maior que 9.

A soma dos peixes-caracol coloca cada um dos dois números como elementos de um novo par. Se o primeiro número for [a,b] e o segundo [x,y], então a soma deles é [[a,b],[x,y]]. Obviamente isso pode criar um número que viola a as limitações acima, então precisamos aplicar as regras da explosão e da quebra. Abaixo eu descrevo as regras e as funções que criei para implementar cada uma:

A regra da explosão sempre vem primeiro e ela deve ser aplicada o maior número possível de vezes antes de partirmos para a regra da quebra.

# Exemplo:
# [[6,[5,[4,[3,2]]]],1]
#
# Passos da explosão:
# 1. Encontrar o primeiro par simples que está dentro de 4 ou mais pares
# [3,2]
#
# 2. Denominar as partes do par com x e y:
# [x,y] = [3,2]
#
# 3. Somar x ao número normal mais próximo à esquerda (se houver)
# [[6,[5,[4 + 3,[3,2]]]],1]
# [[6,[5,[7,[3,2]]]],1]
#
# 4. Somar y ao número normal mais próximo à direita (se houver)
# [[6,[5,[7,[3,2]]]],1 + 2]
# [[6,[5,[7,[3,2]]]],3]
#
# 5. Substituir o par por 0
# [[6,[5,[7,0]]],3]
# Encontrar posição de um par que precisa ser explodido
find_explode <- function(num) {
  chrs <- stringr::str_split(num, "")[[1]]

  # Iterar nos caracteres para encontrar um par profundo demais
  counter <- 0
  for (i in seq_along(chrs)) {
    if (chrs[i] == "[") {
      counter <- counter + 1
    } else if (chrs[i] == "]") {
      counter <- counter - 1

      # Se o par for profundo demais, retornar
      if (counter >= 4) {

        # Encontrar o começo do par
        len <- num |>
          stringr::str_sub(end = i) |>
          stringr::str_extract("\\[[^\\[]*?$") |>
          stringr::str_length() |>
          magrittr::subtract(1)

        # Retornar "coordenadas" do par
        return(c(i - len, i))
      }
    }
  }

  # Se não ouver par para explodir, returnar NULL
  return(NULL)
}

# Aplicar o algoritmo da explosão
explode <- function(num) {

  # Encontrar um par para explodir
  pos <- find_explode(num)

  # Se não houver par, retornar o número
  if (is.null(pos)) return(num)

  # Extrair números normais do par
  pair <- num |>
    stringr::str_sub(pos[1], pos[2]) |>
    stringr::str_extract_all("[0-9]+") |>
    purrr::pluck(1) |>
    as.numeric()

  # Pegar a parte esquerda do número (até o par que vai explodir)
  lhs <- stringr::str_sub(num, end = pos[1] - 1)

  # Encontrar o número normal mais próximo de pair[1] e somar
  left_num <- lhs |>
    stringr::str_extract("[0-9]+(?=[^0-9]+$)") |>
    as.numeric() |>
    magrittr::add(pair[1])

  # Pegar a parte direita do número (a partir do par que vai explodir)
  rhs <- stringr::str_sub(num, pos[2] + 1)

  # Encontrar o número normal mais próximo de pair[2] e somar
  right_num <- rhs |>
    stringr::str_extract("^[^0-9]+[0-9]+") |>
    stringr::str_remove("^[^0-9]+") |>
    as.numeric() |>
    magrittr::add(pair[2])

  # Substituir os números normais que mudamos
  lhs <- stringr::str_replace(lhs, "[0-9]+([^0-9]+)$", paste0(left_num, "\\1"))
  rhs <- stringr::str_replace(rhs, "^([^0-9]+)[0-9]+", paste0("\\1", right_num))

  # Colar as partes esquerda e direita de volta
  return(paste0(lhs, "0", rhs))
}

Se não houver mais como aplicar a explosão, então podemos fazer uma quebra e voltar para o começo do algoritmo: aplicar quantas explosões forem possíveis e depois tentar uma quebra. Quando nenhuma regra puder ser aplicada, então encontramos o resultado da soma.

# Exemplo:
# [11,1]
#
# Passos da quebra:
# 1. Encontrar o primeiro número normal maior que 9
# 11
#
# 2. Criar um novo par onde o elemento da esquerda é o número dividido por 2
#    arredondado para baixo e o elemento da direita é o número dividido por 2
#    arredondado para cima.
# [5,6]
#
# 3. Substituir o número normal pelo par criado
# [[5,6],1]
# Aplicar o algoritmo da quebra
split <- function(num) {

  # Verificar se algo precisa ser quebrado e retornar o número se não
  if (!stringr::str_detect(num, "[0-9]{2,}")) return(num)

  # Criar um par a partir das metades do primeiro número normal > 9
  pair <- num |>
    stringr::str_extract("[0-9]{2,}") |>
    as.numeric() |>
    {\(n) paste0("[", floor(n / 2), ",", ceiling(n / 2), "]")}()

  # Substituir o número normal pelo par criado
  stringr::str_replace(num, "[0-9]{2,}", pair)
}

Agora que sabemos como explodir e qubrar, podemos implementar o algoritmo completo da soma dos peixes-caracol. Notem o next no loop; ele é essencial por causa da exigência de aplicarmos a explosão quantas vezes forem necessárias.

# Soma dos peixes-caracol
snailfish_sum <- function(num1, num2) {

  # Juntar números como elementos de um novo par
  num <- paste0("[", num1, ",", num2, "]")

  # Aplicar explosão e quebra até o número não mudar mais
  num_ <- ""
  while (num_ != num) {
    num_ <- num

    # Explodir e, se o número tiver mudado, voltar
    num <- explode(num)
    if (num_ != num) next

    # Qubrar
    num <- split(num)
  }

  return(num)
}

Mas o enunciado não pedia para simplesmente implementarmos a soma dos peixes-caracol… A resposta final deveria ser a magnitude do número obtido a partir de somas sucessivas. Essencialmente, a nossa entrada era uma sequência de números A, B, C, D, etc. e devíamos calcular (((A + B) + C) + D) + .... Já a magnitude de um número envolve outro algoritmo; a magnitude de um [x,y] qualquer é 3*x + 2*y, mas devemos aplicar isso recursivamente, entrando nas camadas mais profundas do número e voltando para a superfície.

# Fazer uma rodada do algoritmo da magnitude
get_one_magnitude <- function(num) {

  # Pegar a magnitude do par mais à esquerda
  val <- num |>
    stringr::str_extract("\\[[^\\[\\]]+\\]") |>
    stringr::str_extract_all("[0-9]+") |>
    purrr::pluck(1) |>
    as.numeric() |>
    {\(n) 3 * n[1] + 2 * n[2]}() |>
    as.character()

  # Trocar o par pela sua magnitude
  stringr::str_replace(num, "\\[[^\\[\\]]+\\]", val)
}

# Aplicar o algoritmo completo da magnitude
get_magnitude <- function(num) {

  # Enquanto ainda houver pares, fazer uma rodada do cálculo
  while (stringr::str_detect(num, "\\[")) {
    num <- get_one_magnitude(num)
  }

  # Retornar magnitude convertida para um valor numérico
  return(as.numeric(num))
}

Enfim, depois de uma parede de texto e uma parede de código, podemos finalmente juntar tudo na solução do primeiro item.

# Reduce list of numbers with snalfish addition and get magnitude
"data-raw/18a_snailfish.txt" |>
  readr::read_lines() |>
  purrr::reduce(snailfish_sum) |>
  get_magnitude()
#> [1] 4124

Peixes-Caracol (B)

Em um ato de bondade, o autor do Advent of Code fez um item 2 bem simples. Dados todos os números A, B, C, D, etc. que recebemos como entrada, precisávamos combinar todos para encontrar a maior magnitude possível. Minha solução foi gerar todas as somas possíveis (A + B, B + A, A + C, C + A, etc., notando que A + B != B + A) e simplesmente calcular a magnitude de todas. A resposta do item devia ser justamente essa maior magnitude possível.

# Cruzar os números consigo mesmos e somar toda combinação
"data-raw/18b_snailfish.txt" |>
  readr::read_lines() |>
  {\(ns) list(ns, ns)}() |>
  purrr::cross(`==`) |>
  purrr::map_dbl(~get_magnitude(snailfish_sum(.x[[1]], .x[[2]]))) |>
  max()
#> [1] 4673

Detectores de Sinalizadores (A)

Neville Chamberlain tem uma frase que eu gosto muito: “na guerra não há vencedores, todos são perdedores.” É assim que eu me senti com o dia 19 do AoC. No total eu demorei mais de 6 horas de programação intensa para resolver o problema de hoje. Joguei meu código fora múltiplas vezes, quase desisti, mas no final perseverei. Não acho que eu tenha resolvido o problema; assim como Chamberlain, acredito que o problema só perdeu antes.

Por esse motivo, o post de hoje vai ser um pouco diferente. Em primeiro lugar, é impossível resumir as mais de 400 linhas do enunciado de forma efetiva e, em segundo, explicar o raciocínio por trás da minha solução seria tão exaustivo quanto. Sendo assim, vou fazer um super resumo do enunciado e deixar a explicação do código a cargo dos comentários. Quem sabe um dia eu não revisito esse exercício para dar um passo-a-passo melhor.

O grosso da pergunta é o seguinte: temos 36 detectores e uma série de sinalizadores espalhados pelo oceano em posições fixas. A entrada são as coordenadas dos sinalizadores que são vistos por cada detector relativas à posição desse detector. Cada detector também pode estar em uma de 24 orientações (olhando para +x com o topo apontado para +y, olhando para -y com o topo apontado para +z, etc.). Se dois detectores tiverem uma intersecção entre os seus cubos de detecção, então deve haver pelo menos 12 sinalizadores nesse volume. A pergunta pede para calcularmos o número de sinalizadores que estão nessa região do mar.

# Converter c(x,y,z) para "x,y,z"
vec_to_str <- function(vec) {
  stringr::str_c(vec, collapse = ",")
}

# Converter "x,y,z" para c(x,y,z)
str_to_vec <- function(str) {
  as.integer(stringr::str_split(str, ",")[[1]])
}

# Atalho para escolhe(n,2) de uma lista
choose_pairs <- function(l) {
  seq_along(l) |>
    list(seq_along(l)) |>
    purrr::cross(`==`) |>
    purrr::transpose() |>
    purrr::map(purrr::flatten_int) |>
    purrr::set_names("a", "b") |>
    dplyr::as_tibble() |>
    dplyr::rowwise() |>
    dplyr::mutate(ordered = paste0(sort(c(a, b)), collapse = ",")) |>
    dplyr::group_by(ordered) |>
    dplyr::slice_head(n = 1) |>
    dplyr::ungroup() |>
    dplyr::select(-ordered) |>
    dplyr::mutate(
      a = purrr::map(a, ~l[[.x]]),
      b = purrr::map(b, ~l[[.x]])
    )
}

# Aplicar todas as rotações de um ponto
apply_rotations <- function(point) {
  rotations <- list(
    list(c(-1, 0, 0), c(0, -1, 0), c(0, 0, 1)),
    list(c(-1, 0, 0), c(0, 0, -1), c(0, -1, 0)),
    list(c(-1, 0, 0), c(0, 0, 1), c(0, 1, 0)),
    list(c(-1, 0, 0), c(0, 1, 0), c(0, 0, -1)),
    list(c(0, -1, 0), c(-1, 0, 0), c(0, 0, -1)),
    list(c(0, -1, 0), c(0, 0, -1), c(1, 0, 0)),
    list(c(0, -1, 0), c(0, 0, 1), c(-1, 0, 0)),
    list(c(0, -1, 0), c(1, 0, 0), c(0, 0, 1)),
    list(c(0, 0, -1), c(-1, 0, 0), c(0, 1, 0)),
    list(c(0, 0, -1), c(0, -1, 0), c(-1, 0, 0)),
    list(c(0, 0, -1), c(0, 1, 0), c(1, 0, 0)),
    list(c(0, 0, -1), c(1, 0, 0), c(0, -1, 0)),
    list(c(0, 0, 1), c(-1, 0, 0), c(0, -1, 0)),
    list(c(0, 0, 1), c(0, -1, 0), c(1, 0, 0)),
    list(c(0, 0, 1), c(0, 1, 0), c(-1, 0, 0)),
    list(c(0, 0, 1), c(1, 0, 0), c(0, 1, 0)),
    list(c(0, 1, 0), c(-1, 0, 0), c(0, 0, 1)),
    list(c(0, 1, 0), c(0, 0, -1), c(-1, 0, 0)),
    list(c(0, 1, 0), c(0, 0, 1), c(1, 0, 0)),
    list(c(0, 1, 0), c(1, 0, 0), c(0, 0, -1)),
    list(c(1, 0, 0), c(0, -1, 0), c(0, 0, -1)),
    list(c(1, 0, 0), c(0, 0, -1), c(0, 1, 0)),
    list(c(1, 0, 0), c(0, 0, 1), c(0, -1, 0)),
    list(c(1, 0, 0), c(0, 1, 0), c(0, 0, 1))
  )

  # Criar uma tabela com (x, y, z) rotacionados e um ID de rotação
  rotations |>
    purrr::map(purrr::map, `*`, point) |>
    purrr::map(purrr::map, sum) |>
    purrr::map(purrr::flatten_dbl) |>
    dplyr::tibble() |>
    purrr::set_names("point") |>
    dplyr::mutate(rotation = rotations) |>
    tibble::rowid_to_column() |>
    tidyr::unnest(point) |>
    dplyr::mutate(coord = rep(c("x", "y", "z"), dplyr::n() / 3)) |>
    tidyr::pivot_wider(names_from = coord, values_from = point) |>
    dplyr::mutate(rotation = purrr::map_chr(rotation, paste, collapse = ",")) |>
    dplyr::select(x, y, z, rotation)
}

# Fábrica de função para transformar um ponto com rotação + translação
factory_transform <- function(df) {

  # Extrair a operação de rotação da df
  rot <- df$rotation |>
    stringr::str_split("c\\(") |>
    purrr::pluck(1) |>
    stringr::str_remove("\\),?") |>
    stringr::str_subset(",") |>
    stringr::str_split(", ") |>
    purrr::map(as.numeric)

  # Extrair a operação de translação da df
  trans <- c(df$dif_x, df$dif_y, df$dif_z)

  # Retornar função que aplica a transformação
  function(vec) {
    rot |>
      purrr::map(`*`, vec) |>
      purrr::map(sum) |>
      purrr::flatten_dbl() |>
      magrittr::add(trans)
  }
}

# Pegar todas as intersecções entre detectores
get_intersections <- function(points) {

  # Parear os detectores e retornar as suas intersecções
  points |>
    purrr::map(choose_pairs) |>
    purrr::map(
      dplyr::mutate, # Intersecções são baseadas nas distâncias entre pontos
      dist = purrr::map2_dbl(a, b, ~sum((.x - .y)**2))
    ) |>
    choose_pairs() |>
    dplyr::rowwise() |>
    dplyr::group_split() |>
    purrr::map(~dplyr::inner_join(.x[["a"]][[1]], .x[["b"]][[1]], "dist")) |>
    purrr::keep(~nrow(.x) >= 66) # 66 = C(12, 2) = 12 pontos na intersec.
}

# Pegar todas as transformações que podem converter pairs1 em pairs2
get_transforms <- function(pairs1, pairs2) {

  # Criar uma função que leva pairs1[2] a pairs2[2a] ou pairs2[2b]
  dplyr::bind_rows(
    dplyr::mutate(
      apply_rotations(pairs1$a.x[[2]]),
      ref_x = pairs2$a.y[[2]][1],
      ref_y = pairs2$a.y[[2]][2],
      ref_z = pairs2$a.y[[2]][3]
    ),
    dplyr::mutate(
      apply_rotations(pairs1$a.x[[2]]),
      ref_x = pairs2$b.y[[2]][1],
      ref_y = pairs2$b.y[[2]][2],
      ref_z = pairs2$b.y[[2]][3]
    )
  ) |>
    dplyr::mutate(
      dif_x = ref_x - x,
      dif_y = ref_y - y,
      dif_z = ref_z - z
    ) |>
    dplyr::rowwise() |>
    dplyr::group_split() |>
    purrr::map(factory_transform)
}

# Encontrar a função correta de transformação
find_transform <- function(df, funs) {

  # Dadas as funções de transformação, encontrar uma que converte os pontos de
  # df (conjunto de intersecções) corretamente
  df |>
    tibble::rowid_to_column("pair_id") |>
    dplyr::rowwise() |>
    dplyr::group_split() |>
    purrr::map(~{
      .x |>
        dplyr::mutate(,
          fun_a.x = list(purrr::map(funs, ~.x(a.x[[1]]))),
          fun_id = list(seq_along(funs))
        ) |>
        tidyr::unnest(dplyr::starts_with("fun")) |>
        dplyr::select(-dist) |>
        tidyr::unnest(dplyr::everything())
    }) |>
    dplyr::bind_rows() |>
    dplyr::mutate(
      a_works = a.y == fun_a.x,
      b_works = b.y == fun_a.x
    ) |>
    dplyr::group_by(pair_id, fun_id) |>
    dplyr::summarise(
      some_works = all(a_works) || all(b_works), .groups = "drop"
    ) |>
    dplyr::ungroup() |>
    dplyr::group_by(fun_id) |>
    dplyr::summarise(works = sum(some_works)) |>
    dplyr::slice_max(works) |>
    dplyr::pull(fun_id)
}

# Ler pontos como uma lista de vetores
points <- "data-raw/19a_beacon_scanner.txt" |>
  readr::read_lines() |>
  tibble::tibble() |>
  purrr::set_names("point") |>
  dplyr::mutate(
    scanner = as.integer(stringr::str_detect(point, "scanner")),
    scanner = cumsum(scanner) - 1
  ) |>
  dplyr::filter(!stringr::str_detect(point, "scanner")) |>
  dplyr::filter(point != "") |>
  dplyr::group_split(scanner) |>
  purrr::map(dplyr::pull, point) |>
  purrr::map(purrr::map, str_to_vec)


# Reduzir detectores a uma única região
while (length(points) > 1) {

  # Pegar um par de detectores que tem uma intersecção
  pairs <- get_intersections(points)[[1]]

  # Pegar todas as funções de transformação
  funs <- get_transforms(
    dplyr::select(pairs, a.x, b.x),
    dplyr::select(pairs, a.y, b.y)
  )

  # Encontrar a função correta
  transformation <- funs[[find_transform(pairs, funs)]]

  # Converter pontos para strings
  pairs <- pairs |>
    dplyr::select(-dist) |>
    dplyr::mutate_all(purrr::map_chr, vec_to_str)

  # Criar uma cópia dos pontos que também é strings
  points_ <- purrr::map(points, purrr::map_chr, vec_to_str)

  # Encontrar detector usado como referência por transformation()
  for (i in seq_along(points_)) {

    ref <- all(c(pairs$a.y, pairs$b.y) %in% points_[[i]])
    if (ref) reference <- i
  }

  # Encontrar detector que foi transformado por transformation()
  for (i in seq_along(points_)) {

    trns <- all(c(pairs$a.x, pairs$b.x) %in% points_[[i]])
    if (trns) transformed <- i
  }

  # Aplicar transformation() em todos os pontos do detector e adicionar pontos
  # transformados ao detector de referência
  points_[[reference]] <- points[[transformed]] |>
    purrr::map(transformation) |>
    purrr::map_chr(vec_to_str) |>
    c(points_[[reference]]) |>
    unique()

  # Atualizar lista de pontos
  points_[[transformed]] <- NULL
  points <- purrr::map(points_, purrr::map, str_to_vec)
}

# Calcular o número de pontos em uma única região contígua
sum(lengths(points))
#> [1] 408

Detectores de Sinalizadores (B)

O segundo item pedia para que encontrássemos a maior distância de Manhattan entre detectores distintos.

# Reduzir detectores a uma única região, guardando as funções de tranform.
save_funs <- list()
while (length(points) > 1) {

  # Pegar um par de detectores que tem uma intersecção
  pairs <- get_intersections(points)[[1]]

  # Pegar todas as funções de transformação
  funs <- get_transforms(
    dplyr::select(pairs, a.x, b.x),
    dplyr::select(pairs, a.y, b.y)
  )

  # Encontrar a função correta
  transformation <- funs[[find_transform(pairs, funs)]]
  save_funs <- c(save_funs, transformation)

  # Converter pontos para strings
  pairs <- pairs |>
    dplyr::select(-dist) |>
    dplyr::mutate_all(purrr::map_chr, vec_to_str)

  # Criar uma cópia dos pontos que também é strings
  points_ <- purrr::map(points, purrr::map_chr, vec_to_str)

  # Encontrar detector usado como referência por transformation()
  for (i in seq_along(points_)) {

    ref <- all(c(pairs$a.y, pairs$b.y) %in% points_[[i]])
    if (ref) reference <- i
  }

  # Encontrar detector que foi transformado por transformation()
  for (i in seq_along(points_)) {

    trns <- all(c(pairs$a.x, pairs$b.x) %in% points_[[i]])
    if (trns) transformed <- i
  }

  # Aplicar transformation() em todos os pontos do detector e adicionar pontos
  # transformados ao detector de referência
  points_[[reference]] <- points[[transformed]] |>
    purrr::map(transformation) |>
    purrr::map_chr(vec_to_str) |>
    c(points_[[reference]]) |>
    unique()

  # Atualizar lista de pontos
  points_[[transformed]] <- NULL
  points <- purrr::map(points_, purrr::map, str_to_vec)
}

# Aplicar transformações aos detectores e tirar distância de Manhattan
save_funs |>
  purrr::map(~.x(c(0, 0, 0))) |>
  choose_pairs() |>
  dplyr::mutate(dist = purrr::map2_dbl(a, b, ~sum(abs(.x - .y)))) |>
  dplyr::slice_max(dist) |>
  dplyr::pull(dist)

Mapa da Fossa (A)

Depois de um domingo assustadoramente difícil, o problema do dia 20 do AoC foi bastante tranquilo de resolver. Tanto o enunciado quanto a solução me pareceram simples (apesar de algumas reclamações na internet sobre uma pegadinha que vou explicar em breve).

Hoje nós recebemos uma imagem na forma de uma matriz composta por pontos luminosos # e pontos escuros .. O outro componente da entrada era uma lista de “conversões”: nós deveríamos converter cada quadrado 3x3 da imagem em um número binário onde # = 1 e . = 0 e encontrar o elemento de índice correspondente da lista de conversões; o ponto do centro do quadrado deveria ser substituido por esse elemento da lista.

# Um quadrado 3x3
# # . . # .
# #[. . .].
# #[# . .]#
# .[. # .].
# . . # # #
#
# Número correspondente
# ...#...#. = 000100010 = 34
#
# 34o elemento da lista de conversões
# 0         10        20        30 [34]   40        50        60        70
# |         |         |         |   |     |         |         |         |
# ..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..##

Entretanto, essa operação, denominada realce, tinha um detalhe a mais. A nossa imagem de entrada era, na verdade, infinita! Em todas as direções, a imagem deveria ser completa por infinitos pontos escuros. Nosso objetivo era contar o número de pontos luminosos que restavam na nossa imagem após 2 aplicações do realce.

Como é possível imaginar, os pontos escuros infinitos não podem fazer diferença nessa contagem (senão a resposta seria incalculável). Note que um quadrado composto só por pontos escuros equivale ao índice 0 da lista e, no exemplo acima, isso é convertido para um novo ponto escuro; ou seja, as bordas infinitas continuam sendo escuras após o realce.

A pegadinha, porém, era que a lista de conversões na entrada do problema começava com # e não ., ou seja, os infinitos pontos escuros iam virar infinitos pontos luminosos depois de um realce. Felizmente, na segunda aplicação, todos os quadrados luminosos apontariam para o 511º elemento da lista e esse sim era um .. Em conclusão, desde que aplicássemos um número par de realces, as fronteiras infinitas da imagem seriam escuras e o número de pontos luminosos poderia ser contado.

Sendo assim, o código que resolvia o problema era bem simples, bastava adicionar uma borda escura à imagem para levar em conta a fronteira infinita e seguir em frente.

# Converter uma região 3x3 em um número
img_to_int <- function(image) {

  # Achatar a matriz para uma só coluna
  bits <- ifelse(image == ".", 0, 1)
  binary <- paste0(as.vector(t(bits)), collapse = "")

  # String para inteiro
  strtoi(binary, base = 2)
}

# Aplicar realce
enhance <- function(image, algo) {

  # Iterar nas linhas e colunas, sem passar pela borda
  new_image <- image
  for (i in 2:(nrow(image) - 1)) {
    for (j in 2:(ncol(image) - 1)) {

      # Trocar [i,j] pelo índice correspondente em `algo`
      ind <- img_to_int(image[(-1:1 + i), (-1:1 + j)])
      new_image[i, j] <- algo[ind + 1]
    }
  }

  # Remover borda e retornar
  new_image[2:(nrow(image) - 1), 2:(ncol(image) - 1)]
}

# Adicionar borda
add_padding <- function(image) {

  # Adicionar mais 2 linhas em cima e embaixo
  image <- rbind(
    image[1, ], image[1, ],
    image,
    image[nrow(image), ], image[nrow(image), ]
  )

  # Adicionar 2 colunas na esquerda e na direita
  image <- cbind(
    image[, 1], image[, 1],
    image,
    image[, ncol(image)], image[, ncol(image)]
  )

  return(image)
}

# Ler lista de realce como um vetor de strings
algo <- "data-raw/20a_trench_map.txt" |>
  readr::read_lines(n_max = 1) |>
  stringr::str_split("") |>
  purrr::pluck(1)

# Ler imagem como uma matriz (e adicionar bordas)
image <- "data-raw/20a_trench_map.txt" |>
  readr::read_lines(skip = 2) |>
  purrr::prepend(rep(paste0(rep(".", 100), collapse = ""), 3)) |>
  append(rep(paste0(rep(".", 100), collapse = ""), 3)) |>
  {\(s) stringr::str_c("...", s, "...")}() |>
  stringr::str_split("") |>
  purrr::flatten_chr() |>
  matrix(106, 106, byrow = TRUE)

# Aplicar o realce duas vezes e contar pontos luminosos
image |>
  enhance(algo) |>
  add_padding() |>
  enhance(algo) |>
  magrittr::equals("#") |>
  sum()
#> [1] 5498

Mapa da Fossa (B)

O segundo item pedia apenas para aplicarmos o algoritmo 50 ao invés de 2 vezes e contar o número de pontos luminosos. Como o nosso algoritmo generaliza as bordas, podemos simplesmente aplicá-lo mais vezes.

# Aplicar o realce 50 vezes
image <- enhance(image, algo)
for (i in seq_len(49)) {
  image <- enhance(add_padding(image), algo)
}

# Contar pontos luminosos
image |>
  magrittr::equals("#") |>
  sum()
#> [1] 16014

Dados de Dirac (A)

O dia 21 do AoC começou bem. O primeiro item foi bastante direto e tranquilo… O que complicou tudo foi o segundo.

Começamos aprendendo as regras de um jogo chamado Dados de Dirac. Ele é composto um tabuleiro circular que vai de 1 a 10, um dado e dois peões para representar os dois jogadores. Cada jogador rola o dado 3 vezes, soma os resultados e anda aquele número de casas no tabuleiro; o número da casa em que ele caiu é então adicionado à pontuação do jogador. Cada jogador começa em uma casa escolhida aleatoriamente e ganha o primeiro a atingir 1000 ou mais pontos.

O primeiro item pedia para simularmos um jogo de Dados de Dirac com um dado determinístico antes de partirmos para a versão oficial. Nós recebemos como entrada a posição de início de cada jogador e a mecânica de funcionamento do dado: ele ia de 1 a 100 e seu resultado sempre vinha nessa ordem (ou seja, o primeiro jogador rolaria 1, 2, 3, o segundo rolaria 4, 5, 6, etc.). Nosso objetivo era simular o jogo até que alguém ganhasse e retornar a pontuação do jogador perdedor multiplicada pelo número de vezes que o dado foi rolado naquele jogo.

# Ler posições iniciais
pos <- "data-raw/21a_dirac_dice.txt" |>
  readr::read_lines() |>
  stringr::str_extract("[0-9]+$") |>
  as.numeric()

# Posições iniciais
p1_pos <- pos[1]
p2_pos <- pos[2]

# Pontuações iniciais
p1_pts <- 0
p2_pts <- 0

# Fazer os dados irem do valor máximo para 1
die_mod <- function(e1, e2) ((e1 - 1) %% e2) + 1

# Iterar até o jogo acabar
die <- 1
counter <- 0
while (TRUE) {

  # J1 rola 3 vezes
  p1_rolls <- die:(die + 2)
  p1_rolls <- die_mod(p1_rolls, 100)

  # Atualizar estado do dado e contador de rolagem
  die <- die_mod(p1_rolls[3] + 1, 100)
  counter <- counter + 3

  # Atualizar pontuação do J1
  p1_pos <- p1_pos + sum(p1_rolls)
  p1_pos <- die_mod(p1_pos, 10)
  p1_pts <- p1_pts + p1_pos

  # Parar se J1 ganhou
  if (p1_pts >= 1000) break

  # J2 rola 3 vezes
  p2_rolls <- die:(die + 2)
  p2_rolls <- die_mod(p2_rolls, 100)

  # Atualizar estado do dado e contador de rolagem
  die <- die_mod(p2_rolls[3] + 1, 100)
  counter <- counter + 3

  # Atualizar pontuação do J2
  p2_pos <- p2_pos + sum(p2_rolls)
  p2_pos <- die_mod(p2_pos, 10)
  p2_pts <- p2_pts + p2_pos

  # Parar se J2 ganhou
  if (p2_pts >= 1000) break
}

# Contador * pontuação do perdedor
min(p1_pts, p2_pts) * counter
#> [1] 597600

Dados de Dirac (B)

Bem direto, certo? Uma pena que o segundo item não tinha nada a ver… Agora deveríamos simular o jogo com o epônimo Dado de Dirac. Ele tem 3 lados (de 1 a 3) e, cada vez que ele é rolado, um universo paralelo é criado para cada possível resultado. Em suma, no final do jogo haveria um universo para cada caminho que o jogo poderia hipoteticamente tomar. Felizmente, com o Dado de Dirac, o jogo ia só até 21 pontos.

Nossa missão era, dadas as posições iniciais, calcular em quantos universos ganhava o jogador que ganhava mais vezes. Não parece tão difícil até você perceber que teremos algo em torno de 700 trilhões de universos para considerar. Espero que esteja claro que tentar gerar todas as rodadas não vai funcionar.

A solução ideal para esse problema é programação dinâmica (PD) que, apesar do nome esotérico, não é tão misteriosa assim. De forma bem superficial, um algoritmo que usa PD começa dividindo o problema principal em sub-problemas mais simples e armazenando seus resultados; a parte vital é, então, utilizar esses resultados já calculados para evitar contas desnecessárias mais para frente.

Concretamente, queremos dividir o jogo em estados distintos definidos pelos quartetos (p1_pos, p2_pos, p1_pts, p2_pts). Vejamos como funcionaria um trecho desse algoritmo:

  1. Começamos por um estado no final do jogo: (3, 8, 19, 21). Neste universo, sabemos que o J2 ganhou, então salvamos a informação (3, 8, 19, 21) = (0, 1).

  2. Mais para frente, encontramos o estado (3, 5, 19, 13). O J2 pode rolar uma série de valores aqui que precisamos verificar, mas, se ele rolar 1 + 1 + 1, sabemos que cairemos no estado (3, 8, 19, 21)! Sendo assim, podemos pular este cálculo e verificar apenas as outras rolagens possíveis.

  3. Com PD, calcularemos primeiro estados mais fáceis e, conforme formos evoluindo para o começo do jogo, já teremos calculado o número de vitórias de cada jogador em cada futuro. Assim, basta somar esses futuros e passar para um estado anterior.

# Ler posições iniciais
pos <- "data-raw/21b_dirac_dice.txt" |>
  readr::read_lines() |>
  stringr::str_extract("[0-9]+$") |>
  as.numeric()

# Posições iniciais
p1_pos <- pos[1]
p2_pos <- pos[2]

# Fazer os dados irem do valor máximo para 1
die_mod <- function(e1, e2) ((e1 - 1) %% e2) + 1

# Criar um identificar para `states`
id <- function(a, b, c, d) paste0(a, ",", b, ",", c, ",", d)

# Contar vitórias de cada jogador a partir de cada estado do jogo
states <- list()
count_states <- function(p1_pos, p2_pos, p1_pts = 0, p2_pts = 0) {
  this_id <- id(p1_pos, p2_pos, p1_pts, p2_pts)

  # Condições de parada
  if (p1_pts >= 21) return(c(1, 0))
  if (p2_pts >= 21) return(c(0, 1))
  if (this_id %in% names(states)) return(states[[this_id]])

  # Todas as combinações possíveis de rolagens
  rolls <- list(1:3, 1:3, 1:3) |>
    purrr::cross() |>
    purrr::map(purrr::flatten_int) |>
    purrr::map_int(sum)

  # Iterar nas rolagens e fazer a recursão para os próximos estados
  wins_total <- c(0, 0)
  for (roll in rolls) {
    p1_pos_ <- die_mod(p1_pos + roll, 10)

    # Ir para o próximo estado e somar vitórias
    wins <- count_states(p2_pos, p1_pos_, p2_pts, p1_pts + p1_pos_)
    wins_total <- wins_total + rev(wins)
  }

  # Atualizar `states` e retornar
  states[[this_id]] <<- wins_total
  return(wins_total)
}

# Rodar programação dinâmica
count_states(p1_pos, p2_pos) |>
  max() |>
  format(scientific = FALSE)
#> [1] 634769613696613

Reinicialização do Reator (A)

O dia 22 do AoC foi mais um cujo enunciado não apresentou dificuldades. Não que a resolução tenha sido fácil, mas pelo menos o problema foi fácil de entender.

Essencialmente tínhamos que reiniciar o reator do submarino seguindo uma série de instruções (a entrada do problema). O reator era composto por uma grade gigantesca feita de cubos 1x1x1 que começavam todos desligados; cada instrução nos dava uma região do reator que precisava ser desligada ou ligada:

# on x=10..12,y=10..12,z=10..12
# on x=11..13,y=11..13,z=11..13
# off x=9..11,y=9..11,z=9..11
# on x=10..10,y=10..10,z=10..10

O primeiro comando da lista acima, por exemplo, ligava todos os cubos dentro da matrix reator[10:12, 10:12, 10:12]. Nosso objetivo no primeiro item era contar todos os cubos que estariam acessos no final do processo de reinicialização, mas levando em conta apenas os cubos dentro da região denotada por x=-50..50,y=-50..50,z=-50..50.

O código era bastante simples de escrever usando a função array() do R, prestando atenção apenas ao fato de que as coordenadas da array deveríam ir de 1 a 101 e não de -50 a 50.

# Ler todos os passos como uma tabela
steps <- "data-raw/22a_reactor_reboot.txt" |>
  readr::read_lines() |>
  stringr::str_split("[ ,]|(\\.\\.)") |>
  purrr::transpose() |>
  purrr::set_names("state", "x1", "x2", "y1", "y2", "z1", "z2") |>
  purrr::map(purrr::flatten_chr) |>
  tibble::as_tibble() |>
  dplyr::mutate(
    dplyr::across(dplyr::ends_with("1"), stringr::str_remove, "[a-z]="),
    dplyr::across(c(-state), as.integer),
    x = purrr::map2(x1, x2, `:`),
    y = purrr::map2(y1, y2, `:`),
    z = purrr::map2(z1, z2, `:`)
  ) |>
  dplyr::select(state, x, y, z)

# Criar reator como uma array 3D
reactor <- array(rep("off", 303), dim = c(101, 101, 101))

# Iterar nos passos
for (i in seq_len(nrow(steps))) {

  # Coordenadas do cubóide
  x <- steps$x[[i]] + 51
  y <- steps$y[[i]] + 51
  z <- steps$z[[i]] + 51

  # Eliminar o que estiver fora do cubo -50:50
  x <- x[x >= 1 & x <= 101]
  y <- y[y >= 1 & y <= 101]
  z <- z[z >= 1 & z <= 101]

  # Atribuir estado
  reactor[x, y, z] <- steps$state[i]
}

# Contar cubos ligados
sum(reactor == "on")
#> [1] 647076

Reinicialização do Reator (B)

Sem muita surpresa, o item 2 pedia para contarmos o número de cubos ligados ao final do processo de reinicialização em todo o reator. Olhando o código acima, parece que só seria necessário mudar as dimensões da array e tirar os filtros dentro do loop, certo? Infelizmente não, pois com esse algoritmo ineficiente precisaríamos contar aproximadamente 2 quadrilhões de cubos…

A solução foi, então, calcular apenas os limites das regiões e lidar com as suas intersecções. Ou seja, se dois cubóides tiverem que ser ligados, então podemos tomar nota das suas coordenadas e adicionar um novo cubóide de “subtração” na nossa lista que servirá para remover uma cópia da intersecção que foi ligada “duas vezes”. Resumidamente, estaremos contando apenas os volumes de cada cubóide ligado e subtraíndo o volume de cada intersecção para não contar nada duas vezes.

# Ler todos os passos como uma tabela
steps <- "data-raw/22b_reactor_reboot.txt" |>
  readr::read_lines() |>
  stringr::str_split("[ ,]|(\\.\\.)") |>
  purrr::transpose() |>
  purrr::set_names("state", "x1", "x2", "y1", "y2", "z1", "z2") |>
  purrr::map(purrr::flatten_chr) |>
  tibble::as_tibble() |>
  dplyr::mutate(
    dplyr::across(dplyr::ends_with("1"), stringr::str_remove, "[a-z]="),
    dplyr::across(c(-state), as.integer),
    state = ifelse(state == "on", 1L, -1L),
  )

# Iterar nos passos Iterate over steps
cuboids <- dplyr::slice_head(steps, n = 1)
for (i in 2:nrow(steps)) {

  # Iterar nos cubóides que já vimos
  for (j in seq_len(nrow(cuboids))) {

    # Calcular intersecção
    x1_inter <- max(steps$x1[i], cuboids$x1[j])
    x2_inter <- min(steps$x2[i], cuboids$x2[j])
    y1_inter <- max(steps$y1[i], cuboids$y1[j])
    y2_inter <- min(steps$y2[i], cuboids$y2[j])
    z1_inter <- max(steps$z1[i], cuboids$z1[j])
    z2_inter <- min(steps$z2[i], cuboids$z2[j])

    # Adicionar intersecção à lista (com sinal virado)
    if (x1_inter <= x2_inter && y1_inter <= y2_inter && z1_inter <= z2_inter) {
      cuboids <- tibble::add_row(cuboids,
        state = cuboids$state[j] * -1L,
        x1 = x1_inter, x2 = x2_inter,
        y1 = y1_inter, y2 = y2_inter,
        z1 = z1_inter, z2 = z2_inter,
      )
    }
  }

  # Adicionar cubóide à lista se ele estiver ligado
  if (steps$state[i] == 1) {
    cuboids <- tibble::add_row(cuboids,
      state = steps$state[i],
      x1 = steps$x1[i], x2 = steps$x2[i],
      y1 = steps$y1[i], y2 = steps$y2[i],
      z1 = steps$z1[i], z2 = steps$z2[i],
    )
  }
}

# Contar cubos ligados
on <- 0
for (i in seq_len(nrow(cuboids))) {

  # Calcular volume
  x <- cuboids$x2[i] - cuboids$x1[i] + 1
  y <- cuboids$y2[i] - cuboids$y1[i] + 1
  z <- cuboids$z2[i] - cuboids$z1[i] + 1

  # Adicionar/remover à/da conta
  on <- on + (x * y * z * cuboids$state[i])
}

# Imprimir
format(on, scientific = FALSE)
#> [1] 1233304599156793

Anfípodes (A e B)

O dia 23 do AoC foi… Estranho. O enunciado era fácil de entender, mas o código foi impossível de fazer. E não estou exagerando: eu literamente não consegui fazer o código para resolver o exercício. É verdade que eu fiquei doente hoje, então não sei se meus neurônios estavam de cama.

No meu desespero, fui olhar o subreddit do Advent em busca de sugestões de outros programadores e, quando cheguei lá, descobri que várias pessoas estavam resolvendo o problema na mão! Uma boa alma tinha até criado um helper online!

No final, a minha lição do dia de hoje é que nem sempre o jeito mais rápido de resolver um problema é programando; às vezes é mais fácil usar a cabeça mesmo. No caso, a cabeça da Renata Hirota, que resolveu o problema na mão em 10 minutos depois de eu ter passado o dia inteiro na frente do computador tentando achar uma solução.

Sendo assim, deixo vocês com uma tirinha do XKCD:

Unidade lógica e aritmética (A e B)

O penúltimo dia do AoC de 2021 chegou e, com ele, mais um problema que era mais fácil de resolver na mão! Hoje e ontem vão ficar na história como exercícios de lógica e não de programação.

Sem mais delongas, deixo vocês com outra tirinha do XKCD:

Pepino-do-mar (A)

Finalmente chegamos ao último dia do AoC deste ano! O problema de hoje foi um verdadeiro presente de Natal: bem mais simples que todos os dias anteriores. Nossa missão era acompanhar os movimentos de dois grupos de pepinos-do-mar e encontrar o momento em que eles não poderiam mais se mover.

Os pepinos estavam dispostos em uma matriz retangular e se moviam na direção para a qual estavam apontando. Se o espaço em frente ao pepino estivesse vago (.), então ele se movia.

# Estado inicial:
# ...>...
# .......
# ......>
# v.....>
# ......>
# .......
# ..vvv..
#
# Depis de 1 passo:
# ..vv>..
# .......
# >......
# v.....>
# >......
# .......
# ....v..
#
# Depois de 58 passos (todos travados):
# ..>>v>vv..
# ..v.>>vv..
# ..>>v>>vv.
# ..>>>>>vv.
# v......>vv
# v>v....>>v
# vvv.....>>
# >vv......>
# .>v.vv.v..

Meu código ficou simples. Eu li o mapa do fundo do mar como uma matriz e calculei todos os pepinos que podiam se mover; quando nenhum mais pudesse, eu retornava o número de passos transcorridos.

# Ler fundo do mar como matriz
seafloor <- "data-raw/25a_sea_cucumber.txt" |>
  readr::read_lines() |>
  stringr::str_split("") |>
  purrr::flatten_chr() |>
  matrix(nrow = 137, ncol = 139, byrow = TRUE)

# Iterar enquanto ainda há movimentos
i <- 0
while (TRUE) {
  i <- i + 1

  # Todos os pepinos
  e <- which(seafloor == ">")
  s <- which(seafloor == "v")

  # As suas próximas posições
  next_e <- ((e + 137) %% 19043) + ((e + 137) %% 19043 == 0) * 19043
  next_s <- s + 1 - (s %% 137 == 0) * 137

  # Mover todos os pepinos virados para a esquerda
  allowed_e <- seafloor[next_e] == "."
  seafloor[next_e[allowed_e]] <- seafloor[e[allowed_e]]
  seafloor[e[allowed_e]] <- "."

  # Mover todos os pepinos virados para baixo
  allowed_s <- seafloor[next_s] == "."
  seafloor[next_s[allowed_s]] <- seafloor[s[allowed_s]]
  seafloor[s[allowed_s]] <- "."

  # Verificar condição de parada
  if (all(!allowed_e) && all(!allowed_s)) break
}

# Imprimir
print(i)
#> [1] 518

Pepino-do-mar (B)

O segundo item me pegou de surpresa porque… Não havia segundo item! A historinha que estava sendo contada ao longo do AoC foi finalmente concluída e ganhamos a última estrela de graça.

E esse foi o fim da aventura. Muito obrigado por me acompanhar nesses últimos 25 dias de programação intensa! Espero que tenham gostado e, até que enfim, boas festas!

#r #aoc #cs

Responda a este post por email ↪