“Si podés optimizar el trabajo que hacen los montacargas, quizá tengan tiempo de sobra para atravesar la pared.”
Traducción de la publicación original en inglés.
Github: GCaggianese/AoC-2025/D4
Puzzle Dia 4: Printing Department
Parte uno:
- Input de matriz donde
.= rollo de papel,@= espacio vacío - Válida = espacio vacío con 5+ rollos de papel adyacentes
- Devolver la cantidad de posiciones válidas
Parte dos:
- Propagación: las posiciones válidas se convierten en rollos de papel
- Repetir hasta que no aparezcan nuevas posiciones válidas
- Devolver el conteo total de todas las posiciones activadas

Incluí una implementación en Python de la solución que es bastante directa de entender.
Implementación en FASM
Licencia
;;; SPDX-FileCopyrightText: 2026 Germán Caggianese <german.caggianese@pm.me>
;;;
;;; SPDX-License-Identifier: Apache-2.0
Encabezado
format ELF64 executable
entry _start
Macro de matriz
Esta macro parsea el archivo de input en tiempo de compilación, convirtiendo:
.(rollo de papel) → 1@(espacio vacío) → 0- Los saltos de línea y otros caracteres se omiten
El bloque virtual carga el archivo en un espacio de direccionamiento, y luego iteramos cada byte y emitimos solo los significativos.
macro matrix_from_file name, filename {
virtual at 0
name#.data:: file filename
name#.size = $
end virtual
label name byte
name#.len = 0
repeat name#.size
load name#.ch byte from name#.data:(%-1)
if name#.ch = '.'
db 1
name#.len = name#.len + 1
else if name#.ch = '@'
db 0
name#.len = name#.len + 1
end if
end repeat
}
Segmento de datos
Cargar el input y definir constantes
segment readable writeable
matrix_from_file my_matrix, './input.txt'
MATRIX_SIZE = my_matrix.len
ROWS = 138
COLS = 138
Buffers de trabajo
result: guarda las sumas de vecinos después de cada cálculobackup: preserva el estado original de la matriz para la parte 2
result rb ROWS * COLS
backup rb ROWS * COLS
Offsets de dirección para conectividad de 8 vecinos
Cada par es (row~offset~, col~offset~) como bytes con signo.
directions db -1,-1, -1,0, -1,1, 0,-1, 0,1, 1,-1, 1,0, 1,1
NUM_DIRS = 8
Contadores y strings de salida
counter_p1 dq 0
counter_p2 dq 0
msg_p1 db 'Counter P1: '
msg_p1_len = $ - msg_p1
msg_p2 db 'Counter P2: '
msg_p2_len = $ - msg_p2
buffer rb 16
newline db 0x0A
segment readable executable
Lógica principal
_start:
Backup de la matriz original
Necesitamos preservar el estado original porque la parte 2 lo va a modificar.
- Copiar my~matrix~ → backup
xor rcx, rcx
.backup_loop:
cmp rcx, MATRIX_SIZE
jge .part1_start
mov al, [my_matrix + rcx]
mov [backup + rcx], al
inc rcx
jmp .backup_loop
Parte 1: cálculo de una sola pasada
Sumar vecinos
Para cada celda, sumar su valor más todos los vecinos válidos. Nota:
r12= índice de filar13= índice de columna
.part1_start:
xor r12, r12
.p1_row_loop:
cmp r12, ROWS
jge .p1_correction
xor r13, r13
.p1_col_loop:
cmp r13, COLS
jge .p1_next_row
Calcular el índice lineal: rax = row * COLS + col. Nota:
r14= suma (empieza con el valor de la celda)r15= índice de dirección
mov rax, r12
imul rax, COLS
add rax, r13
movzx r14d, byte [my_matrix + rax]
xor r15, r15
.p1_dir_loop:
cmp r15, NUM_DIRS
jge .p1_store_result
Cargar offsets de dirección
movsx r8, byte [directions + r15*2] ; r8 = di
movsx r9, byte [directions + r15*2 + 1] ; r9 = dj
Calcular la posición del vecino. Nota:
r10= ni = row + dir11= nj = col + dj
mov r10, r12
add r10, r8
mov r11, r13
add r11, r9
Chequeo de límites: 0 <= ni < ROWS && 0 <= nj < COLS
cmp r10, 0
jl .p1_next_dir
cmp r10, ROWS
jge .p1_next_dir
cmp r11, 0
jl .p1_next_dir
cmp r11, COLS
jge .p1_next_dir
Sumar el valor del vecino a la suma
mov rax, r10
imul rax, COLS
add rax, r11
movzx eax, byte [my_matrix + rax]
add r14, rax
.p1_next_dir:
inc r15
jmp .p1_dir_loop
.p1_store_result:
mov rax, r12
imul rax, COLS
add rax, r13
mov byte [result + rax], r14b
inc r13
jmp .p1_col_loop
.p1_next_row:
inc r12
jmp .p1_row_loop
Corrección
Las celdas de borde tienen menos vecinos, así que compensamos:
- Celdas de borde (no esquina): +3
- Celdas de esquina: +3 +2 = +5
Esto normaliza todas las celdas como si estuvieran rodeadas por espacio vacío.
Fila superior: +3
.p1_correction:
xor rcx, rcx
.p1_top_row:
cmp rcx, COLS
jge .p1_bottom_row
add byte [result + rcx], 3
inc rcx
jmp .p1_top_row
Fila inferior: +3
.p1_bottom_row:
xor rcx, rcx
.p1_bottom_loop:
cmp rcx, COLS
jge .p1_left_col
mov rax, (ROWS-1) * COLS
add rax, rcx
add byte [result + rax], 3
inc rcx
jmp .p1_bottom_loop
Columna izquierda (excluyendo esquinas): +3
.p1_left_col:
mov rcx, 1
.p1_left_loop:
cmp rcx, ROWS-1
jge .p1_right_col
mov rax, rcx
imul rax, COLS
add byte [result + rax], 3
inc rcx
jmp .p1_left_loop
Columna derecha (excluyendo esquinas): +3
.p1_right_col:
mov rcx, 1
.p1_right_loop:
cmp rcx, ROWS-1
jge .p1_corners
mov rax, rcx
imul rax, COLS
add rax, COLS-1
add byte [result + rax], 3
inc rcx
jmp .p1_right_loop
Esquinas: +2 adicional
.p1_corners:
add byte [result], 2 ; [0][0]
add byte [result + COLS - 1], 2 ; [0][COLS-1]
add byte [result + (ROWS-1) * COLS], 2 ; [ROWS-1][0]
add byte [result + (ROWS-1) * COLS + COLS - 1], 2 ; [ROWS-1][COLS-1]
Filtro (multiplicación elemento a elemento con el original invertido)
Poner en cero las posiciones donde el original tenía un 1 (rollo de papel). Solo nos importan los espacios vacíos (@) que tienen muchos vecinos.
xor rcx, rcx
.p1_filter_loop:
cmp rcx, MATRIX_SIZE
jge .p1_count
cmp byte [my_matrix + rcx], 1
jne .p1_filter_next
mov byte [result + rcx], 0
.p1_filter_next:
inc rcx
jmp .p1_filter_loop
Contar celdas con valor ≥ 5
Note:
r12= contador
.p1_count:
xor r12, r12
xor rcx, rcx
.p1_count_loop:
cmp rcx, MATRIX_SIZE
jge .p1_done
cmp byte [result + rcx], 5
jl .p1_count_next
inc r12
.p1_count_next:
inc rcx
jmp .p1_count_loop
.p1_done:
mov [counter_p1], r12
Parte 2: loop de propagación
Restaurar la matriz original e iterar hasta saturación. En cada iteración, las celdas con ≥5 vecinos se convierten en rollos de papel.
Restaurar my~matrix~ desde backup
xor rcx, rcx
.restore_loop:
cmp rcx, MATRIX_SIZE
jge .p2_main_loop
mov al, [backup + rcx]
mov [my_matrix + rcx], al
inc rcx
jmp .restore_loop
mov qword [counter_p2], 0
.p2_main_loop:
Sumar vecinos (mismo algoritmo que en la parte 1)
xor r12, r12
.p2_row_loop:
cmp r12, ROWS
jge .p2_correction
xor r13, r13
.p2_col_loop:
cmp r13, COLS
jge .p2_next_row
mov rax, r12
imul rax, COLS
add rax, r13
movzx r14d, byte [my_matrix + rax]
xor r15, r15
.p2_dir_loop:
cmp r15, NUM_DIRS
jge .p2_store_result
movsx r8, byte [directions + r15*2]
movsx r9, byte [directions + r15*2 + 1]
mov r10, r12
add r10, r8
mov r11, r13
add r11, r9
cmp r10, 0
jl .p2_next_dir
cmp r10, ROWS
jge .p2_next_dir
cmp r11, 0
jl .p2_next_dir
cmp r11, COLS
jge .p2_next_dir
mov rax, r10
imul rax, COLS
add rax, r11
movzx eax, byte [my_matrix + rax]
add r14, rax
.p2_next_dir:
inc r15
jmp .p2_dir_loop
.p2_store_result:
mov rax, r12
imul rax, COLS
add rax, r13
mov byte [result + rax], r14b
inc r13
jmp .p2_col_loop
.p2_next_row:
inc r12
jmp .p2_row_loop
Corrección (igual que en la parte 1)
.p2_correction:
xor rcx, rcx
.p2_top_row:
cmp rcx, COLS
jge .p2_bottom_row
add byte [result + rcx], 3
inc rcx
jmp .p2_top_row
.p2_bottom_row:
xor rcx, rcx
.p2_bottom_loop:
cmp rcx, COLS
jge .p2_left_col
mov rax, (ROWS-1) * COLS
add rax, rcx
add byte [result + rax], 3
inc rcx
jmp .p2_bottom_loop
.p2_left_col:
mov rcx, 1
.p2_left_loop:
cmp rcx, ROWS-1
jge .p2_right_col
mov rax, rcx
imul rax, COLS
add byte [result + rax], 3
inc rcx
jmp .p2_left_loop
.p2_right_col:
mov rcx, 1
.p2_right_loop:
cmp rcx, ROWS-1
jge .p2_corners
mov rax, rcx
imul rax, COLS
add rax, COLS-1
add byte [result + rax], 3
inc rcx
jmp .p2_right_loop
.p2_corners:
add byte [result], 2
add byte [result + COLS - 1], 2
add byte [result + (ROWS-1) * COLS], 2
add byte [result + (ROWS-1) * COLS + COLS - 1], 2
Filtro
xor rcx, rcx
.p2_filter_loop:
cmp rcx, MATRIX_SIZE
jge .p2_count_and_activate
cmp byte [my_matrix + rcx], 1
jne .p2_filter_next
mov byte [result + rcx], 0
.p2_filter_next:
inc rcx
jmp .p2_filter_loop
Contar y activar
Contar celdas ≥5 y activarlas en my_matrix para la siguiente iteración.
.p2_count_and_activate:
xor rbx, rbx ; rbx = new_activations this iteration
xor rcx, rcx
.p2_count_loop:
cmp rcx, MATRIX_SIZE
jge .p2_check_continue
cmp byte [result + rcx], 5
jl .p2_count_next
inc rbx ; new_activations++
mov byte [my_matrix + rcx], 1 ; activate: becomes paper roll
.p2_count_next:
inc rcx
jmp .p2_count_loop
.p2_check_continue:
; counter_p2 += new_activations
add [counter_p2], rbx
; if new_activations > 0, continue propagating
test rbx, rbx
jnz .p2_main_loop
Salida
Imprimir el resultado de la parte 1
.print_results:
; Print "Counter P1: "
mov rax, 1
mov rdi, 1
mov rsi, msg_p1
mov rdx, msg_p1_len
syscall
; Convert counter_p1 to string
mov rax, [counter_p1]
call .print_number_in_rax
Imprimir el resultado de la parte 2
; Print "Counter P2: "
mov rax, 1
mov rdi, 1
mov rsi, msg_p2
mov rdx, msg_p2_len
syscall
; Convert counter_p2 to string
mov rax, [counter_p2]
call .print_number_in_rax
jmp .exit
Subrutina para imprimir números
Convierte el valor en rax a string decimal y lo imprime con newline.
.print_number_in_rax:
lea rdi, [buffer + 15]
mov rcx, 10
test rax, rax
jnz .convert_loop
; Handle zero case
dec rdi
mov byte [rdi], '0'
jmp .do_print
.convert_loop:
test rax, rax
jz .do_print
xor rdx, rdx
div rcx ; rax = quotient, rdx = remainder
add dl, '0'
dec rdi
mov [rdi], dl
jmp .convert_loop
.do_print:
; Calculate length and print
lea rdx, [buffer + 15]
sub rdx, rdi ; rdx = length
mov rsi, rdi
mov rax, 1
mov rdi, 1
syscall
; Print newline
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
ret
Salida
.exit:
;; 下次見~
mov rax, 60
xor rdi, rdi
syscall
Compilar y ejecutar
Compilar main.asm con fasm main.asm y ejecutarlo con ./main. Debería funcionar en todo sistema x86 y x86-64. El input está hardcodeado para estar en ./input.txt
Salida esperada:
Counter P1: <number>
Counter P2: <number>
Algunos benchmarks
Python3
❯ time python3 draft.py Counter P1: 1549 Counter P2: 8887
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ Executed in 1.16 secs fish external usr time 1.15 secs 0.00 micros 1.15 secs sys time 0.01 secs 871.00 micros 0.00 secs
PyPy3
❯ time pypy3 draft.py Counter P1: 1549 Counter P2: 8887
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ Executed in 215.26 millis fish external usr time 193.14 millis 0.00 millis 193.14 millis sys time 21.98 millis 1.14 millis 20.84 millis
FASM
❯ time ./main #FASM Counter P1: 1549 Counter P2: 8887
\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ Executed in 29.71 millis fish external usr time 28.89 millis 0.00 micros 28.89 millis sys time 0.77 millis 773.00 micros 0.00 millis
Como era esperable, FASM está en el orden de 10.7x más rápido que el JIT de PyPy, y 58x más rápido que Python. Sé que mi código FASM no es el más eficiente, no está optimizado, y el Python tampoco; este benchmark es solo un dato divertido.