library(tidyverse)
library(tictoc)
myCubeColour <- adjustcolor("#ff1199", alpha.f = 0.8)
dat <- read.table("aoc18.txt", sep = ",") |>
  rename(x = V1, y = V2, z = V3) |>
  mutate(nb = paste(x,y,z,sep = ","))
head(dat)
neighbours <- function(x, y, z) {
  tibble(
    x = c(x - 1, x + 1,     x,     x,     x,     x),
    y = c(    y,     y, y - 1, y + 1,     y,     y),
    z = c(    z,     z,     z,     z, z - 1, z + 1)
  )
}

neighbours_str <- function(x, y, z) {
  neighbours(x,y,z) |> mutate(str = paste(x,y,z,sep = ",")) |> pull(str)
}
neighbours_str(0,0,0)
[1] "-1,0,0" "1,0,0"  "0,-1,0" "0,1,0"  "0,0,-1" "0,0,1" 
gen_neighbours <- function(dat) {
  res <- tibble()
  
  for (r in 1:nrow(dat)) {
    res <- rbind(res,
                 tibble(
                   nb = neighbours_str(dat[r, ]$x,
                                       dat[r, ]$y,
                                       dat[r, ]$z),
                   x = dat[r, ]$x,
                   y = dat[r, ]$y,
                   z = dat[r, ]$z
                 ))
  }
  res
}

Part 1:

tic()
the_neighbours <- gen_neighbours(dat)
toc()
22.22 sec elapsed
head(the_neighbours)

The answer is…

the_answer <- the_neighbours |> filter(!nb %in% dat$nb)
the_answer |> nrow()
[1] 3494

3494 is correct.

Part 2:

Let’s have a look…

#remotes::install_github('coolbutuseless/isocubes')
library(isocubes)
library(grid)
library(purrr)
grid.newpage()
isocubesGrob(dat, ysize = 1/40, fill = myCubeColour) |>
  grid.draw() 

So, the plan for this one – another graph, this time undirected. Plop a cuboid over the top of the shape above, remove the shape, leaving space, and then setup a graph that represents all adjacent space cubes. Finally, for each of those cubes, see if it’s possible to reach somewhere outside, say the mins of x, y, and z.

The following makes the space:

empty_cube <- function(dat) {
  x_ran <- c(min(dat$x) - 1, max(dat$x) + 1)
  y_ran <- c(min(dat$y) - 1, max(dat$y) + 1)
  z_ran <- c(min(dat$z) - 1, max(dat$z) + 1)
  
  expand.grid(x = x_ran[1]:x_ran[2],
              y = y_ran[1]:y_ran[2],
              z = z_ran[1]:z_ran[2]) |>
    mutate(nb = paste(x, y, z, sep = ",")) |>
    filter(!nb %in% dat$nb)
}
frame_around <- empty_cube(dat)

Now the graph action.

library(igraph)
buildgraph <- function(dat) {
  res <- data.frame()
  
  for (r in 1:nrow(dat)) {
    neighbours <- neighbours_str(dat[r,]$x, dat[r,]$y, dat[r,]$z)
    
    there_str <- dat |>
      filter(nb %in% neighbours) |>
      pull(nb)
    
    here_str  <-
      rep(paste(dat[r,]$x, dat[r,]$y, dat[r,]$z, sep = ","),
          length(there_str))
    
    res <-
      rbind(res, data.frame(here = here_str, there = there_str))
  }
  
  res |> unique() |> graph.data.frame(directed = FALSE)
}
tic()
space_graph <- buildgraph(frame_around)
toc()
327.23 sec elapsed
all_outside <- function(the_graph, the_space_dat) {
  res <- the_space_dat
  res$outside <- NA
  vs <- attr(V(space_graph), "names")
  
  epitome_of_out <- c(min(the_space_dat$x),
                      min(the_space_dat$y),
                      min(the_space_dat$z)) |> paste(collapse = ",")
  
  for (r in 1:nrow(res)) {
    there_str <- res[r,]$nb
    if (!there_str %in% vs)
      res$outside[r] <- FALSE
    else
      res$outside[r] <-
        !is.infinite(distances(the_graph, epitome_of_out, there_str, mode = "out"))
  }
  
  res
}
tic()
inside <- all_outside(space_graph, frame_around) |>
  filter(!outside)
toc()
146 sec elapsed
the_answer <- the_neighbours |> filter(!nb %in% dat$nb &
                                       !nb %in% inside$nb)
the_answer |> nrow()
[1] 2062

YES.

LS0tDQp0aXRsZTogIkRheSAxODogQm9pbGluZyBCb3VsZGVycyINCmF1dGhvcjogIkBBbmRpQHRlY2gubGdidCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIGNvZGVfZm9sZGluZzogbm9uZQ0KLS0tDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkodGljdG9jKQ0KYGBgDQoNCg0KYGBge3J9DQpteUN1YmVDb2xvdXIgPC0gYWRqdXN0Y29sb3IoIiNmZjExOTkiLCBhbHBoYS5mID0gMC44KQ0KYGBgDQoNCg0KYGBge3J9DQpkYXQgPC0gcmVhZC50YWJsZSgiYW9jMTgudHh0Iiwgc2VwID0gIiwiKSB8Pg0KICByZW5hbWUoeCA9IFYxLCB5ID0gVjIsIHogPSBWMykgfD4NCiAgbXV0YXRlKG5iID0gcGFzdGUoeCx5LHosc2VwID0gIiwiKSkNCmhlYWQoZGF0KQ0KYGBgDQoNCg0KYGBge3J9DQpuZWlnaGJvdXJzIDwtIGZ1bmN0aW9uKHgsIHksIHopIHsNCiAgdGliYmxlKA0KICAgIHggPSBjKHggLSAxLCB4ICsgMSwgICAgIHgsICAgICB4LCAgICAgeCwgICAgIHgpLA0KICAgIHkgPSBjKCAgICB5LCAgICAgeSwgeSAtIDEsIHkgKyAxLCAgICAgeSwgICAgIHkpLA0KICAgIHogPSBjKCAgICB6LCAgICAgeiwgICAgIHosICAgICB6LCB6IC0gMSwgeiArIDEpDQogICkNCn0NCg0KbmVpZ2hib3Vyc19zdHIgPC0gZnVuY3Rpb24oeCwgeSwgeikgew0KICBuZWlnaGJvdXJzKHgseSx6KSB8PiBtdXRhdGUoc3RyID0gcGFzdGUoeCx5LHosc2VwID0gIiwiKSkgfD4gcHVsbChzdHIpDQp9DQpgYGANCg0KDQpgYGB7cn0NCm5laWdoYm91cnNfc3RyKDAsMCwwKQ0KYGBgDQoNCmBgYHtyfQ0KZ2VuX25laWdoYm91cnMgPC0gZnVuY3Rpb24oZGF0KSB7DQogIHJlcyA8LSB0aWJibGUoKQ0KICANCiAgZm9yIChyIGluIDE6bnJvdyhkYXQpKSB7DQogICAgcmVzIDwtIHJiaW5kKHJlcywNCiAgICAgICAgICAgICAgICAgdGliYmxlKA0KICAgICAgICAgICAgICAgICAgIG5iID0gbmVpZ2hib3Vyc19zdHIoZGF0W3IsIF0keCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdFtyLCBdJHksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRbciwgXSR6KSwNCiAgICAgICAgICAgICAgICAgICB4ID0gZGF0W3IsIF0keCwNCiAgICAgICAgICAgICAgICAgICB5ID0gZGF0W3IsIF0keSwNCiAgICAgICAgICAgICAgICAgICB6ID0gZGF0W3IsIF0keg0KICAgICAgICAgICAgICAgICApKQ0KICB9DQogIHJlcw0KfQ0KYGBgDQoNCg0KDQoNCiMjIFBhcnQgMToNCg0KYGBge3J9DQp0aWMoKQ0KdGhlX25laWdoYm91cnMgPC0gZ2VuX25laWdoYm91cnMoZGF0KQ0KdG9jKCkNCmBgYA0KDQpgYGB7cn0NCmhlYWQodGhlX25laWdoYm91cnMpDQpgYGANCg0KDQpUaGUgYW5zd2VyIGlzLi4uDQoNCmBgYHtyfQ0KdGhlX2Fuc3dlciA8LSB0aGVfbmVpZ2hib3VycyB8PiBmaWx0ZXIoIW5iICVpbiUgZGF0JG5iKQ0KdGhlX2Fuc3dlciB8PiBucm93KCkNCmBgYA0KDQozNDk0IGlzIGNvcnJlY3QuDQoNCg0KIyMgUGFydCAyOg0KDQpMZXQncyBoYXZlIGEgbG9vay4uLg0KDQpgYGB7cn0NCiNyZW1vdGVzOjppbnN0YWxsX2dpdGh1YignY29vbGJ1dHVzZWxlc3MvaXNvY3ViZXMnKQ0KbGlicmFyeShpc29jdWJlcykNCmxpYnJhcnkoZ3JpZCkNCmxpYnJhcnkocHVycnIpDQpgYGANCg0KYGBge3J9DQpncmlkLm5ld3BhZ2UoKQ0KaXNvY3ViZXNHcm9iKGRhdCwgeXNpemUgPSAxLzQwLCBmaWxsID0gbXlDdWJlQ29sb3VyKSB8Pg0KICBncmlkLmRyYXcoKSANCmBgYA0KDQoNClNvLCB0aGUgcGxhbiBmb3IgdGhpcyBvbmUgLS0gYW5vdGhlciBncmFwaCwgdGhpcyB0aW1lIHVuZGlyZWN0ZWQuIFBsb3AgYSBjdWJvaWQgb3ZlciB0aGUgdG9wIG9mIHRoZSBzaGFwZSBhYm92ZSwgcmVtb3ZlIHRoZSBzaGFwZSwgbGVhdmluZyBzcGFjZSwgYW5kIHRoZW4gc2V0dXAgYSBncmFwaCB0aGF0IHJlcHJlc2VudHMgYWxsIGFkamFjZW50IHNwYWNlIGN1YmVzLiBGaW5hbGx5LCBmb3IgZWFjaCBvZiB0aG9zZSBjdWJlcywgc2VlIGlmIGl0J3MgcG9zc2libGUgdG8gcmVhY2ggc29tZXdoZXJlIG91dHNpZGUsIHNheSB0aGUgbWlucyBvZiB4LCB5LCBhbmQgei4NCg0KDQpUaGUgZm9sbG93aW5nIG1ha2VzIHRoZSBzcGFjZToNCg0KYGBge3J9DQplbXB0eV9jdWJlIDwtIGZ1bmN0aW9uKGRhdCkgew0KICB4X3JhbiA8LSBjKG1pbihkYXQkeCkgLSAxLCBtYXgoZGF0JHgpICsgMSkNCiAgeV9yYW4gPC0gYyhtaW4oZGF0JHkpIC0gMSwgbWF4KGRhdCR5KSArIDEpDQogIHpfcmFuIDwtIGMobWluKGRhdCR6KSAtIDEsIG1heChkYXQkeikgKyAxKQ0KICANCiAgZXhwYW5kLmdyaWQoeCA9IHhfcmFuWzFdOnhfcmFuWzJdLA0KICAgICAgICAgICAgICB5ID0geV9yYW5bMV06eV9yYW5bMl0sDQogICAgICAgICAgICAgIHogPSB6X3JhblsxXTp6X3JhblsyXSkgfD4NCiAgICBtdXRhdGUobmIgPSBwYXN0ZSh4LCB5LCB6LCBzZXAgPSAiLCIpKSB8Pg0KICAgIGZpbHRlcighbmIgJWluJSBkYXQkbmIpDQp9DQpgYGANCg0KYGBge3J9DQpmcmFtZV9hcm91bmQgPC0gZW1wdHlfY3ViZShkYXQpDQpgYGANCg0KDQpOb3cgdGhlIGdyYXBoIGFjdGlvbi4NCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoaWdyYXBoKQ0KYGBgDQoNCg0KDQpgYGB7cn0NCmJ1aWxkZ3JhcGggPC0gZnVuY3Rpb24oZGF0KSB7DQogIHJlcyA8LSBkYXRhLmZyYW1lKCkNCiAgDQogIGZvciAociBpbiAxOm5yb3coZGF0KSkgew0KICAgIG5laWdoYm91cnMgPC0gbmVpZ2hib3Vyc19zdHIoZGF0W3IsXSR4LCBkYXRbcixdJHksIGRhdFtyLF0keikNCiAgICANCiAgICB0aGVyZV9zdHIgPC0gZGF0IHw+DQogICAgICBmaWx0ZXIobmIgJWluJSBuZWlnaGJvdXJzKSB8Pg0KICAgICAgcHVsbChuYikNCiAgICANCiAgICBoZXJlX3N0ciAgPC0NCiAgICAgIHJlcChwYXN0ZShkYXRbcixdJHgsIGRhdFtyLF0keSwgZGF0W3IsXSR6LCBzZXAgPSAiLCIpLA0KICAgICAgICAgIGxlbmd0aCh0aGVyZV9zdHIpKQ0KICAgIA0KICAgIHJlcyA8LQ0KICAgICAgcmJpbmQocmVzLCBkYXRhLmZyYW1lKGhlcmUgPSBoZXJlX3N0ciwgdGhlcmUgPSB0aGVyZV9zdHIpKQ0KICB9DQogIA0KICByZXMgfD4gdW5pcXVlKCkgfD4gZ3JhcGguZGF0YS5mcmFtZShkaXJlY3RlZCA9IEZBTFNFKQ0KfQ0KYGBgDQoNCg0KYGBge3J9DQp0aWMoKQ0Kc3BhY2VfZ3JhcGggPC0gYnVpbGRncmFwaChmcmFtZV9hcm91bmQpDQp0b2MoKQ0KYGBgDQoNCmBgYHtyfQ0KYWxsX291dHNpZGUgPC0gZnVuY3Rpb24odGhlX2dyYXBoLCB0aGVfc3BhY2VfZGF0KSB7DQogIHJlcyA8LSB0aGVfc3BhY2VfZGF0DQogIHJlcyRvdXRzaWRlIDwtIE5BDQogIHZzIDwtIGF0dHIoVihzcGFjZV9ncmFwaCksICJuYW1lcyIpDQogIA0KICBlcGl0b21lX29mX291dCA8LSBjKG1pbih0aGVfc3BhY2VfZGF0JHgpLA0KICAgICAgICAgICAgICAgICAgICAgIG1pbih0aGVfc3BhY2VfZGF0JHkpLA0KICAgICAgICAgICAgICAgICAgICAgIG1pbih0aGVfc3BhY2VfZGF0JHopKSB8PiBwYXN0ZShjb2xsYXBzZSA9ICIsIikNCiAgDQogIGZvciAociBpbiAxOm5yb3cocmVzKSkgew0KICAgIHRoZXJlX3N0ciA8LSByZXNbcixdJG5iDQogICAgaWYgKCF0aGVyZV9zdHIgJWluJSB2cykNCiAgICAgIHJlcyRvdXRzaWRlW3JdIDwtIEZBTFNFDQogICAgZWxzZQ0KICAgICAgcmVzJG91dHNpZGVbcl0gPC0NCiAgICAgICAgIWlzLmluZmluaXRlKGRpc3RhbmNlcyh0aGVfZ3JhcGgsIGVwaXRvbWVfb2Zfb3V0LCB0aGVyZV9zdHIsIG1vZGUgPSAib3V0IikpDQogIH0NCiAgDQogIHJlcw0KfQ0KYGBgDQoNCg0KYGBge3J9DQp0aWMoKQ0KaW5zaWRlIDwtIGFsbF9vdXRzaWRlKHNwYWNlX2dyYXBoLCBmcmFtZV9hcm91bmQpIHw+DQogIGZpbHRlcighb3V0c2lkZSkNCnRvYygpDQpgYGANCg0KDQpgYGB7cn0NCnRoZV9hbnN3ZXIgPC0gdGhlX25laWdoYm91cnMgfD4gZmlsdGVyKCFuYiAlaW4lIGRhdCRuYiAmDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAhbmIgJWluJSBpbnNpZGUkbmIpDQp0aGVfYW5zd2VyIHw+IG5yb3coKQ0KYGBgDQoNCllFUy4NCg==