library(tidyverse)
library(stringr)
library(tictoc)

Read in the problem:

dat <- read_lines("aoc04.txt")
lets <- str_split_fixed(dat, "", n = str_length(dat[1]))

Here’s the left corner:

lets[1:10, 1:10]
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
 [1,] "S"  "A"  "M"  "M"  "S"  "M"  "S"  "X"  "M"  "M"  
 [2,] "M"  "A"  "S"  "A"  "A"  "A"  "M"  "A"  "M"  "M"  
 [3,] "S"  "A"  "M"  "X"  "S"  "M"  "S"  "M"  "M"  "M"  
 [4,] "M"  "A"  "S"  "A"  "X"  "A"  "A"  "A"  "S"  "X"  
 [5,] "S"  "X"  "M"  "A"  "S"  "M"  "S"  "M"  "S"  "A"  
 [6,] "S"  "X"  "M"  "A"  "M"  "A"  "X"  "A"  "M"  "M"  
 [7,] "M"  "A"  "X"  "S"  "X"  "X"  "M"  "A"  "M"  "X"  
 [8,] "S"  "S"  "M"  "M"  "A"  "M"  "S"  "A"  "M"  "X"  
 [9,] "X"  "M"  "A"  "M"  "A"  "M"  "S"  "A"  "S"  "X"  
[10,] "X"  "S"  "M"  "M"  "A"  "X"  "S"  "A"  "M"  "X"  
nrow(lets)
[1] 140
ncol(lets)
[1] 140

I don’t want to worry about walking off the edge of the matrix; this means I don’t have to:

val <- function(mat, r, c) {
  if (between(r, 1, nrow(mat)) &&
      between(c, 1, ncol(mat)))
    mat[r, c]
  else
    NA
}

Part 1

Turn a vector, potentially with missingness (assuming it’s at the ends), into a string with NAs removed:

stringify <- function(vec) {
  vec |> na.omit() |> paste(collapse = "")
}

Walk along the matrix, mat, starting at the start and stepping the steps, e.g., using a negative step to move backwards.

walk <- function(mat, start_r, start_c, r_step, c_step, n) {
  res <- rep(NA, n)

  r <- start_r
  c <- start_c
  i <- 1
  
  while (i <= n) {
    res[i] <- mat |> val(r, c)
    r <- r + r_step
    c <- c + c_step
    i <- i + 1
  }
  
  stringify(res)
}

For example, east:

lets |> walk(1, 1, 1, 1, 4)
[1] "SAMA"

South west:

lets |> walk(1, 4, 1, -1, 4)
[1] "MSAM"

All the directions we want to wander:

directions <- expand.grid(r_step = c(-1, 0, 1),
                          c_step = c(-1, 0, 1)) |>
  filter(r_step != 0 | c_step != 0)
directions

Scan in all directions from point (r,c):

scan_point <- function(mat, r, c, n) {
  map2_chr(directions$r_step,
           directions$c_step,
           \(r_step, c_step) walk(mat, r, c, r_step, c_step, n))
}
everywhere <- expand.grid(r = 1:nrow(lets),
                          c = 1:ncol(lets))
tic()
all_of_em <- map2(everywhere$r,
                  everywhere$c,
                  \(r, c) scan_point(lets, r, c, 4))
toc()
45.55 sec elapsed

That was very slow, maybe because I saved 156800 strings, whereas I could just have counted matches.

The answer:

map_int(all_of_em, \(xs) sum(xs == "XMAS")) |> sum()
[1] 2547

Part 2

Sweep across, walking three steps southeast and southwest. Count how often we get “MAS” or “SAM” on those.

target <- c("MAS", "SAM")

is_cross_mas <- function(mat, r, c) {
  text_SE <- walk(mat, r - 1, c - 1, 1, 1, 3)
  text_SW <- walk(mat, r - 1, c + 1, 1, -1, 3)
  
  (text_SE %in% target) && (text_SW %in% target)
}

I could strip off the outer border where there can’t be a match; however, val defined at the top means I don’t have to.

tic()
cross_count <- map2_lgl(
                everywhere$r,
                everywhere$c,
                \(r, c) is_cross_mas(lets, r, c)
               )
toc()
8.64 sec elapsed

The answer:

sum(cross_count)
[1] 1939
LS0tDQp0aXRsZTogIkRheSA0OiBDZXJlcyBTZWFyY2giDQphdXRob3I6IEFuZGlGDQpkYXRlOiA0IERlYyAyMDI0DQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOiANCiAgICBjb2RlX2ZvbGRpbmc6IG5vbmUNCi0tLQ0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHN0cmluZ3IpDQpsaWJyYXJ5KHRpY3RvYykNCmBgYA0KDQpSZWFkIGluIHRoZSBwcm9ibGVtOg0KDQpgYGB7cn0NCmRhdCA8LSByZWFkX2xpbmVzKCJhb2MwNC50eHQiKQ0KbGV0cyA8LSBzdHJfc3BsaXRfZml4ZWQoZGF0LCAiIiwgbiA9IHN0cl9sZW5ndGgoZGF0WzFdKSkNCmBgYA0KDQpIZXJlJ3MgdGhlIGxlZnQgY29ybmVyOg0KDQpgYGB7cn0NCmxldHNbMToxMCwgMToxMF0NCmBgYA0KYGBge3J9DQpucm93KGxldHMpDQpuY29sKGxldHMpDQpgYGANCg0KSSBkb24ndCB3YW50IHRvIHdvcnJ5IGFib3V0IHdhbGtpbmcgb2ZmIHRoZSBlZGdlIG9mIHRoZSBtYXRyaXg7IHRoaXMgbWVhbnMgSSBkb24ndCBoYXZlIHRvOg0KDQpgYGB7cn0NCnZhbCA8LSBmdW5jdGlvbihtYXQsIHIsIGMpIHsNCiAgaWYgKGJldHdlZW4ociwgMSwgbnJvdyhtYXQpKSAmJg0KICAgICAgYmV0d2VlbihjLCAxLCBuY29sKG1hdCkpKQ0KICAgIG1hdFtyLCBjXQ0KICBlbHNlDQogICAgTkENCn0NCmBgYA0KDQoNCiMjIyBQYXJ0IDENCg0KVHVybiBhIHZlY3RvciwgcG90ZW50aWFsbHkgd2l0aCBtaXNzaW5nbmVzcyAoYXNzdW1pbmcgaXQncyBhdCB0aGUgZW5kcyksIGludG8gYSBzdHJpbmcgd2l0aCBOQXMgcmVtb3ZlZDoNCg0KYGBge3J9DQpzdHJpbmdpZnkgPC0gZnVuY3Rpb24odmVjKSB7DQogIHZlYyB8PiBuYS5vbWl0KCkgfD4gcGFzdGUoY29sbGFwc2UgPSAiIikNCn0NCmBgYA0KDQoNCldhbGsgYWxvbmcgdGhlIG1hdHJpeCwgX21hdF8sIHN0YXJ0aW5nIGF0IHRoZSBzdGFydCBhbmQgc3RlcHBpbmcgdGhlIHN0ZXBzLCBlLmcuLCB1c2luZyBhIG5lZ2F0aXZlIHN0ZXAgdG8gbW92ZSBiYWNrd2FyZHMuDQoNCmBgYHtyfQ0Kd2FsayA8LSBmdW5jdGlvbihtYXQsIHN0YXJ0X3IsIHN0YXJ0X2MsIHJfc3RlcCwgY19zdGVwLCBuKSB7DQogIHJlcyA8LSByZXAoTkEsIG4pDQoNCiAgciA8LSBzdGFydF9yDQogIGMgPC0gc3RhcnRfYw0KICBpIDwtIDENCiAgDQogIHdoaWxlIChpIDw9IG4pIHsNCiAgICByZXNbaV0gPC0gbWF0IHw+IHZhbChyLCBjKQ0KICAgIHIgPC0gciArIHJfc3RlcA0KICAgIGMgPC0gYyArIGNfc3RlcA0KICAgIGkgPC0gaSArIDENCiAgfQ0KICANCiAgc3RyaW5naWZ5KHJlcykNCn0NCmBgYA0KDQoNCkZvciBleGFtcGxlLCBlYXN0Og0KDQpgYGB7cn0NCmxldHMgfD4gd2FsaygxLCAxLCAxLCAxLCA0KQ0KYGBgDQoNClNvdXRoIHdlc3Q6DQoNCmBgYHtyfQ0KbGV0cyB8PiB3YWxrKDEsIDQsIDEsIC0xLCA0KQ0KYGBgDQoNCkFsbCB0aGUgZGlyZWN0aW9ucyB3ZSB3YW50IHRvIHdhbmRlcjoNCg0KYGBge3J9DQpkaXJlY3Rpb25zIDwtIGV4cGFuZC5ncmlkKHJfc3RlcCA9IGMoLTEsIDAsIDEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBjX3N0ZXAgPSBjKC0xLCAwLCAxKSkgfD4NCiAgZmlsdGVyKHJfc3RlcCAhPSAwIHwgY19zdGVwICE9IDApDQpkaXJlY3Rpb25zDQpgYGANCg0KDQpTY2FuIGluIGFsbCBkaXJlY3Rpb25zIGZyb20gcG9pbnQgKHIsYyk6DQoNCmBgYHtyfQ0Kc2Nhbl9wb2ludCA8LSBmdW5jdGlvbihtYXQsIHIsIGMsIG4pIHsNCiAgbWFwMl9jaHIoZGlyZWN0aW9ucyRyX3N0ZXAsDQogICAgICAgICAgIGRpcmVjdGlvbnMkY19zdGVwLA0KICAgICAgICAgICBcKHJfc3RlcCwgY19zdGVwKSB3YWxrKG1hdCwgciwgYywgcl9zdGVwLCBjX3N0ZXAsIG4pKQ0KfQ0KYGBgDQoNCmBgYHtyfQ0KZXZlcnl3aGVyZSA8LSBleHBhbmQuZ3JpZChyID0gMTpucm93KGxldHMpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBjID0gMTpuY29sKGxldHMpKQ0KYGBgDQoNCmBgYHtyfQ0KdGljKCkNCmFsbF9vZl9lbSA8LSBtYXAyKGV2ZXJ5d2hlcmUkciwNCiAgICAgICAgICAgICAgICAgIGV2ZXJ5d2hlcmUkYywNCiAgICAgICAgICAgICAgICAgIFwociwgYykgc2Nhbl9wb2ludChsZXRzLCByLCBjLCA0KSkNCnRvYygpDQpgYGANCg0KVGhhdCB3YXMgdmVyeSBzbG93LCBtYXliZSBiZWNhdXNlIEkgc2F2ZWQgYHIgbWFwX2ludChhbGxfb2ZfZW0sIFwoeHMpIGxlbmd0aCh4cykpIHw+IHN1bSgpYCBzdHJpbmdzLCB3aGVyZWFzIEkgY291bGQganVzdCBoYXZlIGNvdW50ZWQgbWF0Y2hlcy4NCg0KVGhlIGFuc3dlcjoNCg0KYGBge3J9DQptYXBfaW50KGFsbF9vZl9lbSwgXCh4cykgc3VtKHhzID09ICJYTUFTIikpIHw+IHN1bSgpDQpgYGANCg0KDQojIyMgUGFydCAyDQoNClN3ZWVwIGFjcm9zcywgd2Fsa2luZyB0aHJlZSBzdGVwcyBzb3V0aGVhc3QgYW5kIHNvdXRod2VzdC4gQ291bnQgaG93IG9mdGVuIHdlIGdldCAiTUFTIiBvciAiU0FNIiBvbiB0aG9zZS4NCg0KYGBge3J9DQp0YXJnZXQgPC0gYygiTUFTIiwgIlNBTSIpDQoNCmlzX2Nyb3NzX21hcyA8LSBmdW5jdGlvbihtYXQsIHIsIGMpIHsNCiAgdGV4dF9TRSA8LSB3YWxrKG1hdCwgciAtIDEsIGMgLSAxLCAxLCAxLCAzKQ0KICB0ZXh0X1NXIDwtIHdhbGsobWF0LCByIC0gMSwgYyArIDEsIDEsIC0xLCAzKQ0KICANCiAgKHRleHRfU0UgJWluJSB0YXJnZXQpICYmICh0ZXh0X1NXICVpbiUgdGFyZ2V0KQ0KfQ0KYGBgDQoNCg0KSSBjb3VsZCBzdHJpcCBvZmYgdGhlIG91dGVyIGJvcmRlciB3aGVyZSB0aGVyZSBjYW4ndCBiZSBhIG1hdGNoOyBob3dldmVyLCBfdmFsXyBkZWZpbmVkIGF0IHRoZSB0b3AgbWVhbnMgSSBkb24ndCBoYXZlIHRvLiANCg0KYGBge3J9DQp0aWMoKQ0KY3Jvc3NfY291bnQgPC0gbWFwMl9sZ2woDQogICAgICAgICAgICAgICAgZXZlcnl3aGVyZSRyLA0KICAgICAgICAgICAgICAgIGV2ZXJ5d2hlcmUkYywNCiAgICAgICAgICAgICAgICBcKHIsIGMpIGlzX2Nyb3NzX21hcyhsZXRzLCByLCBjKQ0KICAgICAgICAgICAgICAgKQ0KdG9jKCkNCmBgYA0KDQpUaGUgYW5zd2VyOg0KDQpgYGB7cn0NCnN1bShjcm9zc19jb3VudCkNCmBgYA0KDQoNCg0K