13 min read

Advent of Code Day 14

Introduction

Advent of code 2018, day 14, was a tough one to do in R. Here is a link. I did this problem a lot of different ways, trying to find a nice way to do it. In the end, I did it using vectors in base R, data.table and finally using the standard template library in Rcpp. I had never used Rcpp really before, or even programmed in C++, so this was a bit of a challenge! I do know C pretty well, but not great, so that was some help. I’ll try to document some of the learning steps that I needed to go through.

First Star

Here’s a very brief recap of the puzzle. We are building a vector that can get arbitrarily long by adding the values at two positions of the vector (and doing some manipulation), then we change the two positions by moving them to the right by between 1 and 10 spaces. If the new position falls off the end of the vector, then we wrap around to the beginning.

My puzzle input was 380621, so the first job was to find the value of the ten recipes that follow the 380,621st recipe. OK, that doesn’t seem to hard to do in R.

This was essentially my first attempt.

make_recipe <- function(sc, e1, e2) {
  s <- sc[e1] + sc[e2]
  if(floor(s/10) == 0)
    return(s)
  return(c(floor(s/10), s%%10))
}

my_mod <- function(x, m) {
  ifelse(x%%m == 0, m, x%%m)
}


sc <- c(3, 7) #first two recipes have values of 3 and 7.
e1 <- 1       #this is the location of elf 1's recipe
e2 <- 2       #this is the location of elf 2's recipe

sc <- c(sc, make_recipe(sc, e1, e2))   #add the value of the new recipe to the end of the vector.
a <- length(sc)                        #find number of recipes so we can move the elves
e1 <- my_mod(e1 + sc[e1] + 1, a)       #move elf 1
e2 <- my_mod(e2 + sc[e2] + 1, a)       #move elf 2

system.time(
while(a < 400000) {
  sc <- c(sc, make_recipe(sc, e1, e2))
  a <- length(sc)
  e1 <- my_mod(e1 + sc[e1] + 1, a)
  e2 <- my_mod(e2 + sc[e2] + 1, a)
#  if(a %% 10000 == 0)
#    print(a)  #Just so we can monitor progress! 
} #This takes a pretty long time on my computer, but it does eventually finish.
)
sc[380622:380631] #First star

That wasn’t too, too bad, but let’s see if we can’t speed it up. The elapsed time was 365.496, which seems excessive! I think the big slow-down was that I was building the vector one or two elements at a time. In the version below, I decided to build up a bunch of recipes until I needed to wrap the elf’s position back around to the start of the recipe vector. Then, I would mostly be adding vectors to smallish vectors, and occasionally combining two longer vectors. This should be faster.

make_recipe <- function(sc, e1, e2) {
  s <- sc[e1] + sc[e2]
  if(floor(s/10) == 0)
    return(s)
  return(c(floor(s/10), s%%10))
}

my_mod <- function(x, m) {
  ifelse(x%%m == 0, m, x%%m)
}


sc <- c(3, 7) #first two recipes have values of 3 and 7.
e1 <- 1       #this is the location of elf 1's recipe
e2 <- 2       #this is the location of elf 2's recipe

sc <- c(sc, make_recipe(sc, e1, e2))   #add the value of the new recipe to the end of the vector.
a <- length(sc)                        #find number of recipes so we can move the elves
e1 <- my_mod(e1 + sc[e1] + 1, a)       #move elf 1
e2 <- my_mod(e2 + sc[e2] + 1, a)       #move elf 2

builder <- integer(0)
system.time(
while(a < 400000) {
  if(e1 < length(sc) - 20 && e2 < length(sc) - 20) {  # I am being careful here. I don't want the elves to wrap without
    builder <- c(builder, make_recipe(sc, e1, e2))    # knowing about it! 
  } else {
    if(length(builder) > 0) {                         # Here, we need to either just add the new recipes to the end,
      sc <- c(sc, builder)                            # or add all of the built recipes.
      builder <- integer(0)
    }
    sc <- c(sc, make_recipe(sc, e1, e2))
  }
  a <- length(sc)
  e1 <- my_mod(e1 + sc[e1] + 1, a)
  e2 <- my_mod(e2 + sc[e2] + 1, a)
 # if(length(builder) == 0)                             # again, just measuring progress.
 #    print(a)
}
)
##    user  system elapsed 
##  20.647   9.976  32.488
sc[380622:380631]
##  [1] 6 9 8 5 1 0 3 1 2 2

Woo-hoo! That was much better. The elapsed time was 29 seconds, a better than 10x speed-up. There are several things I might try to speed this up further, and those are going to be looked at once I get to the second part of the challenge.

Second Star

For the second star, I need to keep going until I see the pattern 380621. That could take a while. There are some really cool theorems about waiting for patterns like this; I think they follow from Blackwell’s Theorem. For example, the expected time before observing HTHHTHTT is less than the expected time before observing TTTTTTTT. Anyway, I noticed that this builder idea wasn’t working well with longer vectors, because the builder itself was getting way too big. So, I just made a maximum builder size. Let’s compare the speed of that versus the speed of the previous algorithm in the first star case.

sc <- c(3, 7) #first two recipes have values of 3 and 7.
e1 <- 1       #this is the location of elf 1's recipe
e2 <- 2       #this is the location of elf 2's recipe

sc <- c(sc, make_recipe(sc, e1, e2))   #add the value of the new recipe to the end of the vector.
a <- length(sc)                        #find number of recipes so we can move the elves
e1 <- my_mod(e1 + sc[e1] + 1, a)       #move elf 1
e2 <- my_mod(e2 + sc[e2] + 1, a)       #move elf 2

builder <- integer(0)
system.time(
while(a < 400000) {
  if(e1 < length(sc) - 20 && e2 < length(sc) - 20 && length(builder) < 1000) {  #ONLY CHANGE: Max builder size is 1000
    builder <- c(builder, make_recipe(sc, e1, e2))    
  } else {
    if(length(builder) > 0) {                         # Here, we need to either just add the new recipes to the end,
      sc <- c(sc, builder)                            # or add all of the built recipes.
      builder <- integer(0)
    }
    sc <- c(sc, make_recipe(sc, e1, e2))
  }
  a <- length(sc)
  e1 <- my_mod(e1 + sc[e1] + 1, a)
  e2 <- my_mod(e2 + sc[e2] + 1, a)
  #if(length(builder) == 0)                             # again, just measuring progress.
  #  print(a)
}
)
##    user  system elapsed 
##   4.950   0.904   5.905
sc[380622:380631]
##  [1] 6 9 8 5 1 0 3 1 2 2

That’s about 6 seconds. Things are looking up! Note that is a 60x speed up over the original algorithm! I happen to know that I am going to need to build a vector of length about 40 million for the second star, which would mean that we would need 600 seconds, even if there is no slowdown as the vectors get longer (which I seriously doubt).

library(data.table)
sc <- rep(0L, 500000) #assume max size is 500,000
sc <- data.table(sc)    

make_recipe_data_table <- function(sce1, sce2) {
  s <- sce1 + sce2  #sc$sc[e1] + sc$sc[e2]
  if(floor(s/10) == 0)
    return(s)
  return(c(floor(s/10), s%%10))
} #thought this might be faster. not sure why, now?

my_mod_data_table <- function(x, m) {
  ifelse(x%%m == 0, m, x%%m)
}


cur_pos <- 1L
set(sc, i = c(1L, 2L), j = 1L, value = c(3L, 7L))  #note that to keep things faster, we have to specify that everything is an integer. Don't want to force data.table to cast, as that slowed things way down.
e1 <- 1L
e2 <- 2L
cur_pos <- cur_pos + 2L

sce1 <- sc$sc[e1]
sce2 <- sc$sc[e2]

add_vec <- as.integer(make_recipe_data_table(sce1, sce2))
if(length(add_vec) == 1L) {
  rows <- cur_pos
} else{
  rows <- c(cur_pos, cur_pos + 1L)
}

set(sc, i = rows, j = 1L, value = add_vec)
cur_pos <- cur_pos + length(add_vec)
a <- cur_pos - 1L
e1 <- as.integer(my_mod_data_table(e1 + sc$sc[e1] + 1, a))
e2 <- as.integer(my_mod_data_table(e2 + sc$sc[e2] + 1, a))


continue <- TRUE
system.time(
while(a < 400000) {
  sces <- sc$sc[c(e1, e2)]
  add_vec <- as.integer(make_recipe_data_table(sces[1], sces[2]))
  if(length(add_vec) == 1L) {
    rows <- cur_pos
  } else{
    rows <- c(cur_pos, cur_pos + 1L)
  }
  
  set(sc, i = rows, j = 1L, value = add_vec)
  cur_pos <- cur_pos + length(add_vec)
  a <- cur_pos - 1L
  e1 <- my_mod_data_table(e1 + sces[1] + 1L, a)
  e2 <- my_mod_data_table(e2 + sces[2] + 1L, a)
  #if(i%%1000000L == 0) {
  #  print(a)
    #ss <- str_flatten(as.character(sc$sc))
    #continue <- !str_detect(ss, "380621")
    #print(continue)
#  }
}
)
##    user  system elapsed 
##   9.490   0.057   9.615

I tried builder lengths of 500 and 2000 as well, and the 1000 length seemed to be the fastest.

sc <- c(3, 7) #first two recipes have values of 3 and 7.
e1 <- 1       #this is the location of elf 1's recipe
e2 <- 2       #this is the location of elf 2's recipe

sc <- c(sc, make_recipe(sc, e1, e2))   #add the value of the new recipe to the end of the vector.
a <- length(sc)                        #find number of recipes so we can move the elves
e1 <- my_mod(e1 + sc[e1] + 1, a)       #move elf 1
e2 <- my_mod(e2 + sc[e2] + 1, a)       #move elf 2

builder <- integer(0)
system.time(
while(a < 400000) {
  if(e1 < length(sc) - 20 && e2 < length(sc) - 20 && length(builder) < 1000) {  #ONLY CHANGE: Max builder size is 1000
    builder <- c(builder, make_recipe(sc, e1, e2))    
  } else {
    if(length(builder) > 0) {                         # Here, we need to either just add the new recipes to the end,
      sc <- c(sc, builder)                            # or add all of the built recipes.
      builder <- integer(0)
    }
    sc <- c(sc, make_recipe(sc, e1, e2))
  }
  a <- length(sc)
  e1 <- my_mod(e1 + sc[e1] + 1, a)
  e2 <- my_mod(e2 + sc[e2] + 1, a)
  #if(length(builder) == 0)                             # again, just measuring progress.
  #  print(a)
}
)
##    user  system elapsed 
##   5.190   0.875   6.145

For the second star, we had to look until we got the pattern 380621. I had a hard time with this one, basically because I wasn’t patient enough, and I thought my code must have a bug.

My idea was that I would pretty much do what I did above, but I would have a maximum builder size so that part doesn’t start slowing down too much. I feel like I could have had a builder for my builder, but I didn’t want to go crazy. But wait, first, I wanted to know what parts of my previous algorithm were slowing me down the most.

make_recipe_2 <- function(sce1, sce2) {
  s <- sce1 + sce2  #sc$sc[e1] + sc$sc[e2]
  if(floor(s/10) == 0)
    return(s)
  return(c(floor(s/10), s%%10))
}

sc <- rep(1, 4000000)
microbenchmark::microbenchmark(
  make_recipe(sc, 1000, 200000),
  c(sc, 1),
  {sce1 <- sc[1000]; sce2 <- sc[200000]; make_recipe_2(sce1, sce2)},
  {d <- sample(4000000, 2); sc[d]},
  {d <- sample(4000000, 2); sc[d[1]]; sc[d[2]]}
)
## Unit: microseconds
##                                                                          expr
##                                                  make_recipe(sc, 1000, 2e+05)
##                                                                      c(sc, 1)
##  {     sce1 <- sc[1000]     sce2 <- sc[2e+05]     make_recipe_2(sce1, sce2) }
##                                       {     d <- sample(4e+06, 2)     sc[d] }
##                       {     d <- sample(4e+06, 2)     sc[d[1]]     sc[d[2]] }
##        min         lq        mean     median         uq       max neval
##      1.034     3.9760    11.31215    12.1425    17.0285    39.012   100
##  10014.234 13465.5705 17120.52131 17175.6540 18713.6340 66800.367   100
##      1.644     2.8445   110.69945    13.3150    15.6845  9928.314   100
##   2314.042  2600.5335  4998.77946  2997.7615  8734.1630 13446.779   100
##   2318.337  2608.5985  5566.62225  2905.7695  8793.2450 61288.794   100
##  cld
##  a  
##    c
##  a  
##   b 
##   b

I also wondered whether it would be faster to not pass sc to make_recipe, but oddly it doesn’t seem like it is. At any rate, adding those values to the end of the long vectors is for sure what is slowing things down. Let’s see how I do:

sc <- c(3, 7)
e1 <- 1
e2 <- 2

a <- length(sc)
sc <- c(sc, make_recipe(sc, e1, e2))
e1 <- my_mod(e1 + sc[e1] + 1, a)
e2 <- my_mod(e2 + sc[e2] + 1, a)

old_sc <- sc
builder <- integer(0)
continue <- TRUE          #we need to run this loop until we see the pattern 380621. Ha.
i <- 1                    
build_max <- 1000         #at the beginning, we will build up vectors of length 1000 to add to sc. As sc gets longer, the build also gets longer.
                          #I should have optimized this more, but I played around with it and this seems OK? Note that it is *WAY* faster to get
                          #to 400,000. Which is good, because otherwise we would have had a long, long wait. 
while(continue) {
  i <- i + 1
  if(e1 < a - 11 && e2 < a - 11 && length(builder) < build_max) {
    builder <- c(builder, make_recipe(sc, e1, e2))
  } else {
    if(length(builder) > 0) {
      sc <- c(sc, builder)
      builder <- integer(0)
    }
    sc <- c(sc, make_recipe(sc, e1, e2))
  }
  a <- length(sc)
  e1 <- my_mod(e1 + sc[e1] + 1, a)
  e2 <- my_mod(e2 + sc[e2] + 1, a)
  if(i%%20000 == 0) {                                  #The str_flatten + str_detect takes a really long time when the vector gets long. So, I kept
    print(a)                                           #track of what hadn't yet been checked and just checked them. We do this every 20K iterates, but 
    sc_cont <- sc                                      #that number was picked out of my ass.
    e1_cont <- e1
    e2_cont <- e2
    a_cont <- a
    builder_cont <- builder
    l1 <- max(a - 40000, 1)
    ss <- stringr::str_flatten(as.character(sc[l1:a]))
    continue <- !stringr::str_detect(ss, "380621")
    if(a > 500000)
      build_max <- 1500
    if(a > 1000000)
      build_max <- 2000
    if(a > 2000000)
      build_max <- 2500
    if(a > 4000000)
      build_max <- 3000
    if(a > 8000000)
      build_max <- 3500
    if(a > 16000000)
      build_max <- 4000
  }
}

If you are patient enough, the above code will, I think, eventually work. I have never been patient enough. I have heard people say that I should pre-allocate memory for the vector rather than building it on the fly. I tried pre-allocating sc to have 30 million zeroes, but that was way slower. Maybe if I would have only pre-allocated builder, that would have helped. I’m just not sure.

At any rate, I next decided to use data.table, because it seems that building the vector is still what is taking time. I didn’t use much from data.table. I used set, which is the data.table way of doing vec[] <-, and to access previously set values in the vector, I used $ which as I understand it, casts to a regular vector and used []. I looked into using native data.table things, but they were slower when I benchmarked.

library(data.table)
sc <- rep(0L, 50000000) #assume max size is 50,000,000
sc <- data.table(sc)    

make_recipe_data_table <- function(sce1, sce2) {
  s <- sce1 + sce2  #sc$sc[e1] + sc$sc[e2]
  if(floor(s/10) == 0)
    return(s)
  return(c(floor(s/10), s%%10))
} #thought this might be faster. not sure why, now?

my_mod_data_table <- function(x, m) {
  ifelse(x%%m == 0, m, x%%m)
}


cur_pos <- 1L
set(sc, i = c(1L, 2L), j = 1L, value = c(3L, 7L))  #note that to keep things faster, we have to specify that everything is an integer. Don't want to force data.table to cast, as that slowed things way down.
e1 <- 1L
e2 <- 2L
cur_pos <- cur_pos + 2L

sce1 <- sc$sc[e1]
sce2 <- sc$sc[e2]

add_vec <- as.integer(make_recipe_data_table(sce1, sce2))
if(length(add_vec) == 1L) {
  rows <- cur_pos
} else{
  rows <- c(cur_pos, cur_pos + 1L)
}

set(sc, i = rows, j = 1L, value = add_vec)
cur_pos <- cur_pos + length(add_vec)
a <- cur_pos - 1L
e1 <- as.integer(my_mod_data_table(e1 + sc$sc[e1] + 1, a))
e2 <- as.integer(my_mod_data_table(e2 + sc$sc[e2] + 1, a))


continue <- TRUE

for(i in 1:18000000) {
  sces <- sc$sc[c(e1, e2)]
  add_vec <- as.integer(make_recipe_data_table(sces[1], sces[2]))
  if(length(add_vec) == 1L) {
    rows <- cur_pos
  } else{
    rows <- c(cur_pos, cur_pos + 1L)
  }
  
  set(sc, i = rows, j = 1L, value = add_vec)
  cur_pos <- cur_pos + length(add_vec)
  a <- cur_pos - 1L
  e1 <- my_mod_data_table(e1 + sces[1] + 1L, a)
  e2 <- my_mod_data_table(e2 + sces[2] + 1L, a)
  if(i%%1000000L == 0) {
    print(a)
    #ss <- str_flatten(as.character(sc$sc))
    #continue <- !str_detect(ss, "380621")
    #print(continue)
  }
}
## [1] 1300566
## [1] 2604631
## [1] 3908479
## [1] 5212451
## [1] 6515907
## [1] 7819555
## [1] 9122859
## [1] 10426309
## [1] 11729730
## [1] 13032975
## [1] 14336480
## [1] 15640992
## [1] 16944212
## [1] 18247938
## [1] 19551908
## [1] 20855419
## [1] 22158850
## [1] 23462488