Advent of R: Dia 16

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!

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:

  • Os 3 primeiros bits representavam a versão do pacote;

  • Os 3 bits seguintes representavam o tipo do pacote, que podia cair em dois casos:

    • Se o tipo (na forma decimal) fosse igual a 4, então o pacote representaria um valor. Isso queria dizer que o resto do pacote poderia ser quebrado em pedaços de 5 bits com a seguinte configuração:

      • Se o pedaço começasse com 1, então os 4 bits a seguir eram parte do valor e deveríamos continuar lendo o pacote;

      • Se o pedaço começassem em 0, então os 4 bits a seguir eram o final do valor e poderíamos parar de ler o pacote.

    • Se o tipo do pacote fosse diferente de 4, então o pacote representaria um operador. Isso queria dizer que o bit de número 7 indicava o modo do pacote:

      • Se o indicador fosse 1, então os próximos 15 bits seriam iguais à soma dos comprimentos de todos os sub-pacotes contidos naquele pacote operador;

      • Se o indicador fosse 0, então os próximos 11 bits seriam iguais ao número de sub-pacotes contidos naquele pacote operador.

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.

  • A operação 0 é soma (sum()).

  • A operação 1 é produto (prod()).

  • A operação 2 é mínimo (min()).

  • A operação 3 é máximo (max()).

  • A operação 5 é maior que (>).

  • A operação 6 é menor que (<).

  • A operação 7 é igual (==).

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