Day 11 Seating System
This is my attempt to solve Day 11.
11.1 Part 1
We are given that in our input:
.
is the floorL
is an empty seat#
is an occupied seat
It will make our life simpler to add a border of empty seats round our inputs, and convert the input to a matrix of individual characters
process_input <- function(input) {
dl <- paste(rep(".", str_length(input[[1]])), collapse = "")
m <- paste0(".", c(dl, input, dl), ".") %>%
str_extract_all(".", simplify = TRUE)
# give this matrix an s3 class of "day11"
structure(m, class = c("day11", class(m)))
}
# implement a print method for our day11 class
print.day11 <- function(x, ...) {
cat(paste(apply(x, 1, paste, collapse = ""), collapse = "\n"), "\n")
}
psample <- process_input(sample)
pactual <- process_input(actual)
We can now have a quick look at our sample input:
## ............
## .L.LL.LL.LL.
## .LLLLLLL.LL.
## .L.L.L..L...
## .LLLL.LL.LL.
## .L.LL.LL.LL.
## .L.LLLLL.LL.
## ...L.L......
## .LLLLLLLLLL.
## .L.LLLLLL.L.
## .L.LLLLL.LL.
## ............
Now we can build a function to run n iteration’s:
p1_run_iterations <- function(input, n = 1, count = 0) {
# this is a recursive function, when n is less than 1 stop iterating and
# return whatever input is given
if (n < 1) return (list(input = input, count = count))
# take a copy of the current state - we will modify this copied state and
# return it for the next iteration
next_state <- input
for (r in 2:(nrow(input) - 1)) {
for (c in 2:(ncol(input) - 1)) {
# skip this cell if it's a .
if (input[r, c] == ".") next()
# get adjacent seats
adjacent <- c(input[r - 1, c - 1],
input[r - 1, c ],
input[r - 1, c + 1],
input[r , c - 1],
input[r , c + 1],
input[r + 1, c - 1],
input[r + 1, c ],
input[r + 1, c + 1])
# count how many are occupied
occupied <- sum(ifelse(adjacent == "#", 1, 0))
if (input[r, c] == "L") {
if (occupied == 0) {
next_state[r, c] <- "#"
}
} else {
# it can only be "#" now
if (occupied >= 4) {
next_state[r, c] <- "L"
}
}
}
}
if (all(next_state == input)) {
return (list(input = input, count = count))
}
# run the next itertion, decreasing n by 1
p1_run_iterations(next_state, n - 1, count + 1)
}
We can now test our function runs as expected on the sample.
## $input
## ............
## .#.##.##.##.
## .#######.##.
## .#.#.#..#...
## .####.##.##.
## .#.##.##.##.
## .#.#####.##.
## ...#.#......
## .##########.
## .#.######.#.
## .#.#####.##.
## ............
##
## $count
## [1] 1
## $input
## ............
## .#.LL.L#.##.
## .#LLLLLL.L#.
## .L.L.L..L...
## .#LLL.LL.L#.
## .#.LL.LL.LL.
## .#.LLLL#.##.
## ...L.L......
## .#LLLLLLLL#.
## .#.LLLLLL.L.
## .#.#LLLL.##.
## ............
##
## $count
## [1] 2
These match the examples, we just need to check it terminates correctly:
## [1] TRUE
We just need a way to count the seats occupied now:
Which we can run on the variable p1s
from above:
## [1] TRUE
While this does run, it’s not particular fast on the actual data. We come back to part 1 later.
11.2 Part 2
We now need to modify our run iterations function. I am going to embed a function that will search for the first seat, and update the tolerance from 4 to 5.
In order to speed up computation we first calculate the “first seat” found from any position. In order to remember these seats we switch from a recursive function to use a loop. Otherwise, the function remains the same as in part 1.
p2_run_iterations <- function(input, n) {
# create a function to find the first seat that can be seen from r, c in the
# direction rd, cd. rd / cd should be 1, -1 or 0, but both should not be 0
find_first_seat <- function(r, c, rd, cd) {
rr <- r
cc <- c
if (input[rr, cc] == ".") {
# return a cell that will be a "."
return (c(1, 1))
}
repeat {
rr <- rr + rd
cc <- cc + cd
if (rr < 1 | rr > nrow(input) |
cc < 1 | cc > ncol(input) ) {
# return a cell that will be a "."
return (c(1, 1))
} else if (input[rr, cc] != ".") {
# return the indexes
return (c(rr, cc))
}
}
}
first_seats <- map(2:(nrow(input) - 1), function(r) {
map(2:(ncol(input) - 1), function(c) {
list(
rd = c(-1, -1, -1, 0, 0, 1, 1, 1),
cd = c(-1, 0, 1, -1, 1, -1, 0, 1)
) %>%
pmap(find_first_seat, r = r, c = c) %>%
discard(compose(any, is.na))
})
})
count <- 0
state <- input
while (count < n) {
# take a copy of the current state - we will modify this copied state and
# return it for the next iteration
next_state <- state
for (r in 2:(nrow(state) - 1)) {
for (c in 2:(ncol(state) - 1)) {
# skip this cell if it's a .
if (state[r, c] == ".") next()
# our first seats only iterated over the "inside" range, so we need to
# subtract 1 from the r and c index
adjacent <- map_chr(first_seats[[r - 1]][[c - 1]],
~state[.x[[1]], .x[[2]]])
# count how many are occupied
occupied <- sum(adjacent == "#")
if (state[r, c] == "L") {
if (occupied == 0) {
next_state[r, c] <- "#"
}
} else {
# it can only be "#" now
if (occupied >= 5) {
next_state[r, c] <- "L"
}
}
}
}
if (all(next_state == state)) {
break()
}
state <- next_state
count <- count + 1
}
list(input = state, count = count)
}
We can test our function works as expected after 1 iteration:
## $input
## ............
## .#.##.##.##.
## .#######.##.
## .#.#.#..#...
## .####.##.##.
## .#.##.##.##.
## .#.#####.##.
## ...#.#......
## .##########.
## .#.######.#.
## .#.#####.##.
## ............
##
## $count
## [1] 1
And after 2 iterations:
## $input
## ............
## .#.LL.LL.L#.
## .#LLLLLL.LL.
## .L.L.L..L...
## .LLLL.LL.LL.
## .L.LL.LL.LL.
## .L.LLLLL.LL.
## ...L.L......
## .LLLLLLLLL#.
## .#.LLLLLL.L.
## .#.LLLLL.L#.
## ............
##
## $count
## [1] 2
We can use the count_seats()
function again to test our new function works:
## [1] TRUE
Again, this function runs very slowly on the actual data.
11.3 Solving faster
R is much better at vectorised operations, so if we could reduce the steps to summing matrices our code should run much faster.
First, we are to treat our input as a matrix that contains either 0 for an unoccupied seat, 1 for an occupied seat, and
NA
for the floor.
We then create functions for part 1 and for part 2 which returns 8 set’s of indices for the directions that we are to look in for seats. These index sets will be used to return a matrix the same size as the inner part of our matrix (we ignore the border).
Part 1’s function is pretty simple, we simply shift the matrix one up, one to the left, then just one up, then one up and one to the right, etc. This function is pretty slow as we have to allocate quite a lot of memory.
part_one <- function(input) {
nr <- nrow(input)
nc <- ncol(input)
cross2(1:3, 1:3) %>%
discard(~ .x[[1]] == 2 && .x[[2]] == 2) %>%
map(function(.x) {
cross_df(
list(
row = .x[[1]]:(nr - 3 + .x[[1]]),
col = .x[[2]]:(nc - 3 + .x[[2]])
)
) %>%
as.matrix()
})
}
Part 2 is slightly more complex as we need to find the seat according to the more complex rules.
part_two <- function(input) {
nr <- nrow(input)
nc <- ncol(input)
find_in_direction <- function(r, c, rd, cd) {
ri <- r
ci <- c
repeat {
ri <- ri + rd
ci <- ci + cd
# we have reached the boundary, exit
if (ri < 1 | ri > nr | ci < 1 | ci > nc) {
return (list(row = ri - rd, col = ci - cd))
}
# we have found a seat, return
if (!is.na(input[ri, ci])) {
return (list(row = ri, col = ci))
}
}
}
# get the indices of each cell in the inner part of the input matrix
ixs <- cross_df(list(r = 2:(nr - 1), c = 2:(nc - 1)))
# now find the values in each direction
list(
pmap_dfr(ixs, find_in_direction, -1, -1),
pmap_dfr(ixs, find_in_direction, -1, 0),
pmap_dfr(ixs, find_in_direction, -1, 1),
pmap_dfr(ixs, find_in_direction, 0, -1),
pmap_dfr(ixs, find_in_direction, 0, 1),
pmap_dfr(ixs, find_in_direction, 1, -1),
pmap_dfr(ixs, find_in_direction, 1, 0),
pmap_dfr(ixs, find_in_direction, 1, 1)
) %>%
map(as.matrix)
}
We now create a solving function, which takes the data as the first argument, either part_one()
or part_two()
as the
second argument, and the tolerance (4 for part 1, 5 for part 2).
solve <- function(x, add_mat_fn, tolerance) {
input <- ifelse(unclass(x) == "L", 0, NA)
nr <- nrow(input)
nc <- ncol(input)
add_mat_ix <- add_mat_fn(input)
repeat {
# use this to check later if our matrix has changed
t <- input
# use our matrix indices, find the values for the 8 shifted matrices, then
# add the 8 matrices together to give us one matrix that tells us how many
# adjacent seats are occupied
add_mat <- add_mat_ix %>%
map(~input[.x] %>%
replace_na(0) %>%
matrix(nrow = nr - 2, ncol = nc - 2)) %>%
reduce(`+`)
# find the unoccupied seats in the input
y <- which(input == 0, arr.ind = TRUE)
# and the occupied seats
z <- which(input == 1, arr.ind = TRUE)
# update the unoccupied seats
input[y] <- 1 * (add_mat[y - 1] == 0)
# update the occupied seats
input[z] <- 1 * (add_mat[z - 1] < tolerance)
# check to see if our matrix has changed
if (all(t == input, na.rm = TRUE)) break()
}
# now just return the number of occupied seats
sum(input, na.rm = TRUE)
}
We can now run our new function’s and see if they give us the same results as above:
## [1] 37
## [1] 2483
## [1] 26
## [1] 2285
Elapsed Time: 5.57s