Advent of R: Dia 18

O Advent of Code é um Calendário do Advento desenvolvido por Eric Wastl composto por 25 pequenos exercícios de programação que vão sendo disponibilizados, um a um, entre 1º de dezembro e o Natal de cada ano.

Meu objetivo com o Advent of R é resolver todos os problemas do Advent of Code 2021 em R e documentar o processo através desta série de posts. Todo dia entre 01/12/2021 e 25/12/2021 eu vou tentar resolver o novo problema, documentar a minha solução aqui no blog e subir os meus scripts completos para um repositório público no GitHub.

A minha esperança é que, com essa série, mais pessoas pratiquem seus conhecimentos de R resolvendo exercícios divertidos e desafiadores! Ao final da jornada vamos todos ter afiado nossas habilidades de R e, quem sabe, divulgado essa linguagem incrível para mais pessoas. Boas festas e bom código!

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
comments powered by Disqus