Day 20: Trench Map
Puzzle description
https://adventofcode.com/2021/day/20
Modeling and parsing the input
The input is an image enhancement algorithm string, and an initial input image.
The image is a black and white rectangle. We model the pixels with an enumeration:
enum Pixel:
case Lit, Dark
A pixel can either be lit, or dark.
In the input text, lit pixels are represented by the character "#", whereas dark pixels are represented by the character "." (dot). We use pattern matching to parse them:
object Pixel:
def parse(char: Char): Pixel =
char match
case '#' => Pixel.Lit
case '.' => Pixel.Dark
end Pixel
In case the input is malformed (ie, it contains a character other than "#"
or "."), the method parse
raises an exception.
The enhancement algorithm string is provided as a line of 512 pixels. We parse it as follows:
class Enhancer(enhancementString: IndexedSeq[Pixel])
object Enhancer:
def parse(input: String): Enhancer =
Enhancer(input.map(Pixel.parse))
end Enhancer
There is a subtlety regarding the input image to which we want to apply the enhancement algorithm: its size is infinite. Initially, its pixels are all dark except in the rectangle we are given as initial input. So, we model an image as a two-dimensional indexed sequence, and the color of all the pixels that are out of the bounds of that two-dimensional indexed sequence:
class Image(pixels: IndexedSeq[IndexedSeq[Pixel]], outOfBoundsPixel: Pixel):
require(pixels.map(_.length).distinct.size == 1, "All the rows must have the same length")
val height = pixels.length
val width = pixels(0).length
Since there is no direct way to model two-dimensional collections in the
standard library, we model the table of pixels as a collection of rows,
where each row is a collection of pixels. We add the call to require
to
make sure that all the lines have the same length.
We parse the input image by parsing every line of the provided rectangular
area, and by setting the color of the out-of-bounds pixels to Dark
:
object Image:
def parse(input: String): Image =
val pixels =
input
.linesIterator.map(line => line.map(Pixel.parse))
.toIndexedSeq
val outOfBoundsPixel = Pixel.Dark
Image(pixels, outOfBoundsPixel)
end Image
Last, we parse both sections of the input with the following method:
def parseEnhancerAndImage(input: String): (Enhancer, Image) =
val enhancerAndImage = input.split("\n\n")
val enhancer = Enhancer.parse(enhancerAndImage(0))
val image = Image.parse(enhancerAndImage(1))
(enhancer, image)
Image enhancement algorithm
The image enhancement algorithm needs to compute an integer value for every
location of the input image. The integer value is made of 9 bits, whose
value is taken from the state of the 9 pixels around the location (a lit
pixel means 1
, and a dark pixel means 0
).
That 9-bit integer value is then used as an index in the enhancement algorithm string to find the state of the output pixel at that location.
We implement the algorithm as a method of the class Enhancer
:
class Enhancer(enhancementString: IndexedSeq[Pixel]):
def enhance(image: Image): Image =
val pixels =
for y <- -1 until (image.height + 1)
yield
for x <- -1 until (image.width + 1)
yield enhancementString(locationValue(image, x, y))
val outOfBoundsPixel =
val value = if image.outOfBoundsPixel == Pixel.Dark then 0 else 511
enhancementString(value)
Image(pixels, outOfBoundsPixel)
end enhance
end Enhancer
Since we look at the 9 pixels around every location, we look at locations
outside the image rectangle where some of these 9 pixels overlap with the
rectangle (hence the bounds -1
and height + 1
for the y
coordinates).
The 9-bit integer value of each location is computed by an auxiliary method,
locationValue
, which is shown below.
Last, we also compute the “enhancement” of the pixels that are out of the
bounds of the image rectangle. Since these pixels are all the same, the 9
pixels around any location out of the bounds of the rectangle will always be
either all lit or all dark. The binary value corresponding to all pixels
dark is 000000000
, which equals 0
, and the binary value corresponding to
all pixels lit is 111111111
, which equals 511
.
Here is the implementation of the auxiliary method locationValue
:
def locationValue(image: Image, x: Int, y: Int): Int =
var result = 0
for
yy <- (y - 1) to (y + 1)
xx <- (x - 1) to (x + 1)
do
result = result << 1
if image.pixel(xx, yy) == Pixel.Lit then
result = result | 1
end if
end for
result
end locationValue
We read the 9 pixels around the provided location, starting from the
top-left corner, and going to the right. If a pixel is lit, we interpret it
as a 1
. At every iteration, we shift the previously computed result one
bit to the left before reading the new bit.
To read the pixel value in the image, we use a handy method pixel
, defined
in the class Image
:
def pixel(x: Int, y: Int): Pixel =
if y < 0 || y >= height then outOfBoundsPixel
else if x < 0 || x >= width then outOfBoundsPixel
else pixels(y)(x)
This method implements the fact that the image has an infinite size. It
checks the bounds of the location to access, and when the location is out of
the bounds of the rectangle image, it returns the outOfBoundsPixel
color
of the image.
Solution of part 1
We were asked to apply two times in a row the enhancement algorithm on the input image, and to compute the number of lit pixels in the output image:
def part1(input: String): Int =
val (enhancer, image0) = parseEnhancerAndImage(input)
val image1 = enhancer.enhance(image0)
val image2 = enhancer.enhance(image1)
image2.countLitPixels()
The method countLitPixels
is defined as follows in the class Image
:
class Image(pixels: IndexedSeq[IndexedSeq[Pixel]], outOfBoundsPixel: Pixel):
def countLitPixels(): Int =
pixels.view.flatten.count(_ == Pixel.Lit)
We flatten the rows of pixels into a single collection of pixels, and we
count the lit pixels on it. The call to view
before flatten
allows us to
traverse the rows of pixels without constructing the flattened collection.
Full code for part 1
def part1(input: String): Int =
val (enhancer, image0) = parseEnhancerAndImage(input)
val image1 = enhancer.enhance(image0)
val image2 = enhancer.enhance(image1)
image2.countLitPixels()
def parseEnhancerAndImage(input: String): (Enhancer, Image) =
val enhancerAndImage = input.split("\n\n")
val enhancer = Enhancer.parse(enhancerAndImage(0))
val image = Image.parse(enhancerAndImage(1))
(enhancer, image)
enum Pixel:
case Lit, Dark
object Pixel:
def parse(char: Char): Pixel =
char match
case '#' => Pixel.Lit
case '.' => Pixel.Dark
end Pixel
object Enhancer:
def parse(input: String): Enhancer =
Enhancer(input.map(Pixel.parse))
end Enhancer
class Enhancer(enhancementString: IndexedSeq[Pixel]):
def enhance(image: Image): Image =
val pixels =
for y <- -1 until (image.height + 1)
yield
for x <- -1 until (image.width + 1)
yield enhancementString(locationValue(image, x, y))
val outOfBoundsPixel =
val value = if image.outOfBoundsPixel == Pixel.Dark then 0 else 511
enhancementString(value)
Image(pixels, outOfBoundsPixel)
end enhance
private def locationValue(image: Image, x: Int, y: Int): Int =
var result = 0
for
yy <- (y - 1) to (y + 1)
xx <- (x - 1) to (x + 1)
do
result = result << 1
if image.pixel(xx, yy) == Pixel.Lit then
result = result | 1
end if
end for
result
end locationValue
end Enhancer
class Image(pixels: IndexedSeq[IndexedSeq[Pixel]], val outOfBoundsPixel: Pixel):
require(pixels.map(_.length).distinct.size == 1, "All the rows must have the same length")
val height = pixels.length
val width = pixels(0).length
def pixel(x: Int, y: Int): Pixel =
if y < 0 || y >= height then outOfBoundsPixel
else if x < 0 || x >= width then outOfBoundsPixel
else pixels(y)(x)
def countLitPixels(): Int =
pixels.view.flatten.count(_ == Pixel.Lit)
end Image
object Image:
def parse(input: String): Image =
val pixels =
input
.linesIterator.map(line => line.map(Pixel.parse))
.toIndexedSeq
val outOfBoundsPixel = Pixel.Dark
Image(pixels, outOfBoundsPixel)
Solution for part 2
In part 2, we had to apply the “enhancement algorithm” 50 times instead of just twice:
def part2(input: String): Int =
val (enhancer, image) = parseEnhancerAndImage(input)
LazyList
.iterate(image)(enhancer.enhance)
.apply(50)
.countLitPixels()
We parse the input with the same method as in part1, parseEnhancerAndImage
.
To apply the enhancer 50 times, we use a LazyList
. First, we create an
infinite lazy list whose first element is the parsed input image
, and
whose n + 1
element is computed by calling enhancer.enhance
on the
element n
. Then, we compute its 50th element by calling .apply(50)
. As a
consequence, only the first 50 elements will be computed at all.
Finally, we call countLitPixels()
on the output image to count its number
of lit pixels.
Run it in the browser
Part 1
Part 2
Run it locally
You can get this solution locally by cloning the scalacenter/scala-advent-of-code repository.
$ git clone https://github.com/scalacenter/scala-advent-of-code
$ cd scala-advent-of-code
You can run it with scala-cli.
$ scala-cli 2021 -M day20.part1
The solution is: 5301
$ scala-cli 2021 -M day20.part2
The solution is: 19492
Solutions from the community
Share your solution to the Scala community by editing this page.