Skip to main content

Day 7: The Treachery of Whales

by @tgodzik

Puzzle description

https://adventofcode.com/2021/day/7

Part 1: Fast and craby

The whale is trying to swallow our submarine! Fortunately, there is a small armada of crabs, let's call it a crabmada, in their tiny crabmarines that want to help us out! However, the crabs are not particularly efficient and will need our help, we need to align all their little crabmarines using the least fuel possible, so that they can blow a hole in the ocean floor.

Firstly, let's start with modeling a single crabmarine:

case class Crabmarine(horizontal: Int):
def moveForward(): Crabmarine = this.copy(horizontal = horizontal + 1)
def moveBackward(): Crabmarine = this.copy(horizontal = horizontal - 1)

The crabmarine can move in two directions, forward and backwards.

Let's translate our input into a list of Crabmarine:

val horizontalPositions = input.split(",").map(_.toInt).toList
val crabmarines = horizontalPositions.map(horizontal => Crabmarine(horizontal))

A list of crabmarines can form a crabmada:

case class Crabmada(crabmarines: List[Crabmarine]):
// we don't want an empty list here!
require(crabmarines.nonEmpty)

def align() = ???

and it can have a method for aliging all its crabs.

Now the tricky part, how can we figure out the horizontal position that minimizes the amount of fuel the crabs will use? We have only two possible moves, forwards and backwards, and we know for sure that all the edge crabs will need to move. Let's model our solution as one of two moves:

  • moving all the edge crabmarines with the current maximum horizontal position backwards
  • moving all the edge crabmarines with the current minimal horizontal position forwards

In each interation we can move the crabmarines that use less fuel, which guarantees us that the result will be minimal at the end when all the crabs reach the same horizontal position.

This can take a form of a recursive function:

  @tailrec
final def align(
situation: List[Crabmarine] = crabmarines,
fuelCost: Int = 0
): Int =
val allTheSame = situation.forall(_.horizontal == situation.head.horizontal)
if allTheSame then fuelCost
else
val maxHorizontal = situation.maxBy(_.horizontal)
val minHorizontal = situation.minBy(_.horizontal)

val fuelCostForMax = situation.count {
crabmarine => crabmarine.horizontal == maxHorizontal.horizontal
}
val fuelCostForMin = situation.count {
crabmarine => crabmarine.horizontal == minHorizontal.horizontal
}
if fuelCostForMax < fuelCostForMin then
val updated = situation.map { crabmarine =>
if crabmarine.horizontal == maxHorizontal.horizontal then
crabmarine.moveBackward()
else crabmarine
}
align(updated, fuelCost + fuelCostForMax)
else
val updated = situation.map { crabmarine =>
if crabmarine.horizontal == minHorizontal.horizontal then
crabmarine.moveForward()
else crabmarine
}
align(updated, fuelCost + fuelCostForMin)

First we check if all the crabmarines already align. If not we need to check what is the cost of moving all the crabs on each of the edges. We move all the ones on the edge that will use less fuel (less crabs to move). Then we invoke the function again with the updated positions of the crabmarines.

Additionally, we use here a @tailrec annotation, which makes sure that our function can be translated by the compiler into an iterative solution without ever exceeding the maximum stack.

Final code for Part 1


case class Crabmarine(horizontal: Int):
def moveForward(): Crabmarine = this.copy(horizontal = horizontal + 1)
def moveBackward(): Crabmarine = this.copy(horizontal = horizontal - 1)

case class Crabmada(crabmarines: List[Crabmarine]):
// we don't want an empty list here!
require(crabmarines.nonEmpty)

@tailrec
final def align(
situation: List[Crabmarine] = crabmarines,
fuelCost: Int = 0
): Int =
val allTheSame = situation.forall(_.horizontal == situation.head.horizontal)
if allTheSame then fuelCost
else
val maxHorizontal = situation.maxBy(_.horizontal)
val minHorizontal = situation.minBy(_.horizontal)

val fuelCostForMax = situation.count {
crabmarine => crabmarine.horizontal == maxHorizontal.horizontal
}
val fuelCostForMin = situation.count {
crabmarine => crabmarine.horizontal == minHorizontal.horizontal
}
if fuelCostForMax < fuelCostForMin then
val updated = situation.map { crabmarine =>
if crabmarine.horizontal == maxHorizontal.horizontal then
crabmarine.moveBackward()
else crabmarine
}
align(updated, fuelCost + fuelCostForMax)
else
val updated = situation.map { crabmarine =>
if crabmarine.horizontal == minHorizontal.horizontal then
crabmarine.moveForward()
else crabmarine
}
align(updated, fuelCost + fuelCostForMin)

def part1(input: String): Int =
val horizontalPositions = input.split(",").map(_.toInt).toList
val crabmarines =
horizontalPositions.map(horizontal => ConstantCostCrabmarine(horizontal))
Crabmada(crabmarines).align()

Part 2: Craby engineering

Turns out we were severily mistaken and the crabmarines use more fuel the further they move. That means our solution will not help out our little sea friends. We need to modify the solution to take that into account.

Let's try to model the new and old submarines using a new class hierarchy, we will add the current fuel cost for the new model of the submarine we are simulating:

sealed trait Crabmarine:
def moveForward(): Crabmarine
def moveBackward(): Crabmarine
def horizontal: Int
def fuelCost: Int

case class ConstantCostCrabmarine(horizontal: Int) extends Crabmarine:
def fuelCost: Int = 1
def moveForward(): Crabmarine = this.copy(horizontal = horizontal + 1)
def moveBackward(): Crabmarine = this.copy(horizontal = horizontal - 1)

case class IncreasingCostCrabmarine(horizontal: Int, fuelCost: Int = 1)
extends Crabmarine:
def moveForward() =
this.copy(horizontal = horizontal + 1, fuelCost = fuelCost + 1)
def moveBackward() =
this.copy(horizontal = horizontal - 1, fuelCost = fuelCost + 1)

We define a new trait Crabmarine with all the old properties plus the new fuel cost property. We can define the previous model of submarine with a constant fuelCost of 1 and name it ConstantCostCrabmarine.

The new model IncreasingCostCrabmarine used for part 2 has a field fuelCost, which increases with each move.

Now we only need to modify the way we calculate the fuelCost to actually use our new property.

val fuelCostForMax = situation.collect {
case crabmarine if crabmarine.horizontal == maxHorizontal.horizontal =>
crabmarine.fuelCost
}.sum
val fuelCostForMin = situation.collect {
case crabmarine if crabmarine.horizontal == minHorizontal.horizontal =>
crabmarine.fuelCost
}.sum

That's it, everything else can stays the same!

Final code for part 2

sealed trait Crabmarine:
def moveForward(): Crabmarine
def moveBackward(): Crabmarine
def horizontal: Int
def fuelCost: Int

case class ConstantCostCrabmarine(horizontal: Int) extends Crabmarine:
def fuelCost: Int = 1
def moveForward(): Crabmarine = this.copy(horizontal = horizontal + 1)
def moveBackward(): Crabmarine = this.copy(horizontal = horizontal - 1)

case class IncreasingCostCrabmarine(horizontal: Int, fuelCost: Int = 1)
extends Crabmarine:
def moveForward() =
this.copy(horizontal = horizontal + 1, fuelCost = fuelCost + 1)
def moveBackward() =
this.copy(horizontal = horizontal - 1, fuelCost = fuelCost + 1)

case class Crabmada(crabmarines: List[Crabmarine]):

require(crabmarines.nonEmpty)

@tailrec
final def align(
situation: List[Crabmarine] = crabmarines,
fuelCost: Int = 0
): Int =
val allTheSame = situation.forall(_.horizontal == situation.head.horizontal)
if allTheSame then fuelCost
else
val maxHorizontal = situation.maxBy(_.horizontal)
val minHorizontal = situation.minBy(_.horizontal)

val fuelCostForMax = situation.collect {
case crabmarine if crabmarine.horizontal == maxHorizontal.horizontal =>
crabmarine.fuelCost
}.sum
val fuelCostForMin = situation.collect {
case crabmarine if crabmarine.horizontal == minHorizontal.horizontal =>
crabmarine.fuelCost
}.sum
if fuelCostForMax < fuelCostForMin then
val updated = situation.map { crabmarine =>
if crabmarine.horizontal == maxHorizontal.horizontal then
crabmarine.moveBackward()
else crabmarine
}
align(updated, fuelCost + fuelCostForMax)
else

val updated = situation.map { crabmarine =>
if crabmarine.horizontal == minHorizontal.horizontal then
crabmarine.moveForward()
else crabmarine
}
align(updated, fuelCost + fuelCostForMin)
end if
end if
end align
end Crabmada


def part2(input: String): Int =
val horizontalPositions = input.split(",").map(_.toInt).toList
val crabmarines =
horizontalPositions.map(horizontal => IncreasingCostCrabmarine(horizontal))
Crabmada(crabmarines).align()

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 day7.part1
The solution is 355150

$ scala-cli 2021 -M day7.part2
The solution is 98368490

You can replace the content of the input/day7 file with your own input from adventofcode.com to get your own solution.

Solutions from the community

There are most likely some other solutions that we could have used. In particular some advent coders had luck with using median and average for determining the final horizontal positions of the crabmarines.

Share your solution to the Scala community by editing this page.