diff --git a/2021/index.html b/2021/index.html index 98e9d1431..b20f802ac 100644 --- a/2021/index.html +++ b/2021/index.html @@ -5,13 +5,13 @@ Scala Center Advent of Code | Scala Center Advent of Code - +
Skip to main content
Credit to https://github.com/OlegIlyenko/scala-icon

Learn Scala 3

A simpler, safer and more concise version of Scala, the famous object-oriented and functional programming language.

Solve Advent of Code puzzles

Challenge your programming skills by solving Advent of Code puzzles.

Share with the community

Get or give support to the community. Share your solutions with the community.

- + \ No newline at end of file diff --git a/2022/index.html b/2022/index.html index 93c2a9fef..2c355aca9 100644 --- a/2022/index.html +++ b/2022/index.html @@ -5,13 +5,13 @@ Scala Center Advent of Code | Scala Center Advent of Code - +
Skip to main content
Credit to https://github.com/OlegIlyenko/scala-icon

Learn Scala 3

A simpler, safer and more concise version of Scala, the famous object-oriented and functional programming language.

Solve Advent of Code puzzles

Challenge your programming skills by solving Advent of Code puzzles.

Share with the community

Get or give support to the community. Share your solutions with the community.

- + \ No newline at end of file diff --git a/2022/puzzles/day01/index.html b/2022/puzzles/day01/index.html index 58824203b..24e360c98 100644 --- a/2022/puzzles/day01/index.html +++ b/2022/puzzles/day01/index.html @@ -5,14 +5,14 @@ Day 1: Calorie Counting | Scala Center Advent of Code - +
Skip to main content

Day 1: Calorie Counting

by @bishabosha

Puzzle description

https://adventofcode.com/2022/day/1

Solution Summary

First transform the input into a List of Inventory, each Inventory is a list of Int, representing the calorie count of an item in the inventory, this is handled in scanInventories.

Part 1

Given the List of Inventory, we must first find the total calorie count of each inventory.

For a single Inventory, we do this using the sum method on its items property (found in the List class). e.g. inventory.items.sum.

Then use the map method on the List class, to transform each Inventory to its total calorie count with an anonymous function.

Then sort the resulting list of total calorie counts in descending order, this is provided by scala.math.Ordering.Int.reverse.

The maxInventories method handles the above, returning the top n total calorie counts.

For part 1, use maxInventories with n == 1 to create a singleton list of the largest calorie count.

Part 2

As in part 1, construct the list of sorted total calorie counts with maxInventories. But instead, we need the first 3 elements. We then need to sum the resulting list.

Final Code

import scala.math.Ordering

def part1(input: String): Int =
maxInventories(scanInventories(input), 1).head

def part2(input: String): Int =
maxInventories(scanInventories(input), 3).sum

case class Inventory(items: List[Int])

def scanInventories(input: String): List[Inventory] =
val inventories = List.newBuilder[Inventory]
var items = List.newBuilder[Int]
for line <- input.linesIterator do
if line.isEmpty then
inventories += Inventory(items.result())
items = List.newBuilder
else items += line.toInt
inventories.result()

def maxInventories(inventories: List[Inventory], n: Int): List[Int] =
inventories
.map(inventory => inventory.items.sum)
.sorted(using Ordering.Int.reverse)
.take(n)

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day02/index.html b/2022/puzzles/day02/index.html index d4a3db659..3caa35cba 100644 --- a/2022/puzzles/day02/index.html +++ b/2022/puzzles/day02/index.html @@ -5,13 +5,13 @@ Day 2: Rock Paper Scissors | Scala Center Advent of Code - +
Skip to main content

Day 2: Rock Paper Scissors

by @bishabosha

Puzzle description

https://adventofcode.com/2022/day/2

Final Code

import Position.*

def part1(input: String): Int =
scores(input, pickPosition).sum

def part2(input: String): Int =
scores(input, winLoseOrDraw).sum

enum Position:
case Rock, Paper, Scissors

// two positions after this one, wrapping around
def winsAgainst: Position = fromOrdinal((ordinal + 2) % 3)

// one position after this one, wrapping around
def losesAgainst: Position = fromOrdinal((ordinal + 1) % 3)

end Position

def readCode(opponent: String) = opponent match
case "A" => Rock
case "B" => Paper
case "C" => Scissors

def scores(input: String, strategy: (Position, String) => Position) =
for case s"$x $y" <- input.linesIterator yield
val opponent = readCode(x)
score(opponent, strategy(opponent, y))

def winLoseOrDraw(opponent: Position, code: String): Position = code match
case "X" => opponent.winsAgainst // we need to lose
case "Y" => opponent // we need to tie
case "Z" => opponent.losesAgainst // we need to win

def pickPosition(opponent: Position, code: String): Position = code match
case "X" => Rock
case "Y" => Paper
case "Z" => Scissors

def score(opponent: Position, player: Position): Int =
val pointsOutcome =
if opponent == player then 3 // tie
else if player.winsAgainst == opponent then 6 // win
else 0 // lose

// Rock = 1, Paper = 2, Scissors = 3
val pointsPlay = player.ordinal + 1

pointsPlay + pointsOutcome
end score

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day03/index.html b/2022/puzzles/day03/index.html index 5831978e5..73a5609ff 100644 --- a/2022/puzzles/day03/index.html +++ b/2022/puzzles/day03/index.html @@ -5,13 +5,13 @@ Day 3: Rucksack Reorganization | Scala Center Advent of Code - +
Skip to main content

Day 3: Rucksack Reorganization

by @bishabosha

Puzzle description

https://adventofcode.com/2022/day/3

Final Code

def part1(input: String): Int =
val intersections =
for line <- input.linesIterator yield
val (left, right) = line.splitAt(line.length / 2)
(priorities(left) & priorities(right)).head
intersections.sum

def part2(input: String): Int =
val badges =
for case Seq(a, b, c) <- input.linesIterator.grouped(3) yield
(priorities(a) & priorities(b) & priorities(c)).head
badges.sum

def priorities(str: String) = str.foldLeft(Priorities.emptySet)(_ add _)

object Priorities:
opaque type Set = Long // can fit all 52 priorities in a bitset

// encode priorities as a random access lookup
private val lookup =
val arr = new Array[Int](128) // max key is `'z'.toInt == 122`
for (c, i) <- (('a' to 'z') ++ ('A' to 'Z')).zipWithIndex do
arr(c.toInt) = i + 1
IArray.unsafeFromArray(arr)

val emptySet: Set = 0L

extension (priorities: Set)
infix def add(c: Char): Set = priorities | (1L << lookup(c.toInt))
infix def &(that: Set): Set = priorities & that
def head: Int = java.lang.Long.numberOfTrailingZeros(priorities)

end Priorities

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day04/index.html b/2022/puzzles/day04/index.html index c7d9d000f..eb7f493ef 100644 --- a/2022/puzzles/day04/index.html +++ b/2022/puzzles/day04/index.html @@ -5,13 +5,13 @@ Day 4: Camp Cleanup | Scala Center Advent of Code - +
Skip to main content

Day 4: Camp Cleanup

by @bishabosha

Puzzle description

https://adventofcode.com/2022/day/4

Final Code

def part1(input: String): Int =
foldPairs(input, subsumes)

def part2(input: String): Int =
foldPairs(input, overlaps)

def subsumes(x: Int, y: Int)(a: Int, b: Int): Boolean = x <= a && y >= b
def overlaps(x: Int, y: Int)(a: Int, b: Int): Boolean = x <= a && y >= a || x <= b && y >= b

def foldPairs(input: String, hasOverlap: (Int, Int) => (Int, Int) => Boolean): Int =
val matches =
for line <- input.linesIterator yield
val Array(x,y,a,b) = line.split("[,-]").map(_.toInt): @unchecked
hasOverlap(x,y)(a,b) || hasOverlap(a,b)(x,y)
matches.count(identity)

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day05/index.html b/2022/puzzles/day05/index.html index 050b8afdb..2f023252a 100644 --- a/2022/puzzles/day05/index.html +++ b/2022/puzzles/day05/index.html @@ -5,13 +5,13 @@ Day 5: Supply Stacks | Scala Center Advent of Code - +
Skip to main content

Day 5: Supply Stacks

by @bishabosha

Puzzle description

https://adventofcode.com/2022/day/5

Final Code

def part1(input: String): String =
moveAllCrates(input, _ reverse_::: _) // concat in reverse order

def part2(input: String): String =
moveAllCrates(input, _ ::: _) // concat in normal order

/** each column is 4 chars wide (or 3 if terminal) */
def parseRow(row: String) =
for i <- 0 to row.length by 4 yield
if row(i) == '[' then
row(i + 1) // the crate id
else
'#' // empty slot

def parseColumns(header: IndexedSeq[String]): IndexedSeq[List[Char]] =
val crates :+ colsStr = header: @unchecked
val columns = colsStr.split(" ").filter(_.nonEmpty).length

val rows = crates.map(parseRow(_).padTo(columns, '#')) // pad empty slots at the end

// transpose the rows to get the columns, then remove the terminal empty slots from each column
rows.transpose.map(_.toList.filterNot(_ == '#'))
end parseColumns

def moveAllCrates(input: String, moveCrates: (List[Char], List[Char]) => List[Char]): String =
val (headerLines, rest0) = input.linesIterator.span(_.nonEmpty)
val instructions = rest0.drop(1) // drop the empty line after the header

def move(cols: IndexedSeq[List[Char]], n: Int, idxA: Int, idxB: Int) =
val (toMove, aRest) = cols(idxA).splitAt(n)
val b2 = moveCrates(toMove, cols(idxB))
cols.updated(idxA, aRest).updated(idxB, b2)

val columns = parseColumns(headerLines.to(IndexedSeq))

val columns1 = instructions.foldLeft(columns) { case (columns, s"move $n from $a to $b") =>
move(columns, n.toInt, a.toInt - 1, b.toInt - 1)
}
columns1.map(_.head).mkString
end moveAllCrates

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day06/index.html b/2022/puzzles/day06/index.html index 2f95ccff4..959a9f035 100644 --- a/2022/puzzles/day06/index.html +++ b/2022/puzzles/day06/index.html @@ -5,7 +5,7 @@ Day 6: Tuning Trouble | Scala Center Advent of Code - + @@ -21,7 +21,7 @@ the multiset described above, you only care about the first and last element of each window, which can be represented by two indexes into the string.

The final optimisation is to only update the set when the last element of the window is different to the first element of the previous window.

The final optimised code is presented below, including an implementation of the multiset:

def part1(input: String): Int =
findIndexOptimal(input, n = 4)

def part2(input: String): Int =
findIndexOptimal(input, n = 14)

class MultiSet:
private val counts = new Array[Int](26)
private var uniqueElems = 0

def size = uniqueElems

def add(c: Char) =
val count = counts(c - 'a')
if count == 0 then
uniqueElems += 1
counts(c - 'a') += 1

def remove(c: Char) =
val count = counts(c - 'a')
if count > 0 then
if count == 1 then
uniqueElems -= 1
counts(c - 'a') -= 1
end MultiSet

def findIndexOptimal(input: String, n: Int): Int =
val counts = MultiSet()
def loop(i: Int, j: Int): Int =
if counts.size == n then
i + n // found the index
else if j >= input.length then
-1 // window went beyond the end
else
val previous = input(i)
val last = input(j)
if previous != last then
counts.remove(previous)
counts.add(last)
loop(i = i + 1, j = j + 1)
end loop
input.iterator.take(n).foreach(counts.add) // add up-to the first `n` elements
loop(i = 0, j = n)

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day07/index.html b/2022/puzzles/day07/index.html index 0d4b619fb..49d017905 100644 --- a/2022/puzzles/day07/index.html +++ b/2022/puzzles/day07/index.html @@ -5,13 +5,13 @@ Day 7: No Space Left On Device | Scala Center Advent of Code - +
Skip to main content

Day 7: No Space Left On Device

code by Jan Boerman

Puzzle description

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

Solution

First of all, we need to create types for commands, to differentiate the input:

enum Command:
case ChangeDirectory(directory: String)
case ListFiles

enum TerminalOutput:
case Cmd(cmd: Command)
case Directory(name: String)
case File(size: Int, name: String)

Let's make a directory structure, in which we will define files as mutable.Map, that can contain name (String) and size (Integer), will have reference to parent directory, and will be able to contain subdirectories:

class DirectoryStructure(val name: String,
val subDirectories: mutable.Map[String, DirectoryStructure],
val files: mutable.Map[String, Int],
val parent: DirectoryStructure | Null)

And now we need to come up with a way to parse out input code:

def input (str: String) = str.linesIterator.map {
case s"$$ cd $directory" => Cmd(ChangeDirectory(directory))
case s"$$ ls" => Cmd(ListFiles)
case s"dir $directory" => Directory(directory)
case s"$size $file" => File(size.toInt, file)
}.toList

We have to come up with a way to calculate directory size -- we can use sum for the size of all files in directory and define size of all of the following subdirectories recursively, which will take care of problem:

def directorySize(dir: DirectoryStructure): Int =
dir.files.values.sum + dir.subDirectories.values.map(directorySize).sum

Now we need to create a function to build the directory structure from the input. For that we can use match and separate input, -- for that we can use cases and recursion will do the rest for us:

def buildState(input: List[TerminalOutput], currentDir: DirectoryStructure | Null, rootDir: DirectoryStructure): Unit = input match
case Cmd(ChangeDirectory("/")) :: t => buildState(t, rootDir, rootDir)
case Cmd(ChangeDirectory("..")) :: t => buildState(t, currentDir.parent, rootDir)
case Cmd(ChangeDirectory(name)) :: t => buildState(t, currentDir.subDirectories(name), rootDir)
case Cmd(ListFiles) :: t => buildState(t, currentDir, rootDir)
case File(size, name) :: t =>
currentDir.files.put(name, size)
buildState(t, currentDir, rootDir)
case Directory(name) :: t =>
currentDir.subDirectories.put(name, DirectoryStructure(name, mutable.Map.empty, mutable.Map.empty, currentDir))
buildState(t, currentDir, rootDir)
case Nil => ()

And now, we need to assemble the program, in part one, we will search for all directories with size smaller 100000, and calculate the sum of their sizes.

def part1(output: String): Int =
val rootDir = buildData(output)
collectSizes(rootDir, _ < 100000).sum

In part two, we are looking for the smallest directory, which size is big enough to free up enough space on the filesystem to install update (30,000,00). We have to find out how much space is required for update, considering our available unused space:

def part2(output: String): Int =
val rootDir = buildData(output)
val totalUsed = directorySize(rootDir)
val totalUnused = 70_000_000 - totalUsed
val required = 30_000_000 - totalUnused
collectSizes(rootDir, _ >= required).min

Final Code

import scala.annotation.tailrec
import scala.collection.mutable

import TerminalOutput.*
import Command.*

def input (str: String) = str.linesIterator.map {
case s"$$ cd $directory" => Cmd(ChangeDirectory(directory))
case s"$$ ls" => Cmd(ListFiles)
case s"dir $directory" => Directory(directory)
case s"$size $file" => File(size.toInt, file)
}.toList

enum Command:
case ChangeDirectory(directory: String)
case ListFiles

enum TerminalOutput:
case Cmd(cmd: Command)
case Directory(name: String)
case File(size: Int, name: String)

class DirectoryStructure(val name: String,
val subDirectories: mutable.Map[String, DirectoryStructure],
val files: mutable.Map[String, Int],
val parent: DirectoryStructure | Null)

def buildState(input: List[TerminalOutput], currentDir: DirectoryStructure | Null, rootDir: DirectoryStructure): Unit = input match
case Cmd(ChangeDirectory("/")) :: t => buildState(t, rootDir, rootDir)
case Cmd(ChangeDirectory("..")) :: t => buildState(t, currentDir.parent, rootDir)
case Cmd(ChangeDirectory(name)) :: t => buildState(t, currentDir.subDirectories(name), rootDir)
case Cmd(ListFiles) :: t => buildState(t, currentDir, rootDir)
case File(size, name) :: t =>
currentDir.files.put(name, size)
buildState(t, currentDir, rootDir)
case Directory(name) :: t =>
currentDir.subDirectories.put(name, DirectoryStructure(name, mutable.Map.empty, mutable.Map.empty, currentDir))
buildState(t, currentDir, rootDir)
case Nil => ()

def directorySize(dir: DirectoryStructure): Int =
dir.files.values.sum + dir.subDirectories.values.map(directorySize).sum

def collectSizes(dir: DirectoryStructure, criterion: Int => Boolean): Iterable[Int] =
val mySize = directorySize(dir)
val children = dir.subDirectories.values.flatMap(collectSizes(_, criterion))
if criterion(mySize) then mySize :: children.toList else children

def buildData(output: String) =
val rootDir = new DirectoryStructure("/", mutable.Map.empty, mutable.Map.empty, null)
buildState(input(output), null, rootDir)
rootDir


def part1(output: String): Int =
val rootDir = buildData(output)
collectSizes(rootDir, _ < 100000).sum

def part2(output: String): Int =
val rootDir = buildData(output)
val totalUsed = directorySize(rootDir)
val totalUnused = 70_000_000 - totalUsed
val required = 30_000_000 - totalUnused
collectSizes(rootDir, _ >= required).min

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day08/index.html b/2022/puzzles/day08/index.html index 59189165d..78a65d6be 100644 --- a/2022/puzzles/day08/index.html +++ b/2022/puzzles/day08/index.html @@ -5,7 +5,7 @@ Day 8: Treetop Tree House | Scala Center Advent of Code - + @@ -28,7 +28,7 @@ For example trees of height 3 can see lengths(3) trees. And we update this list with each new tree we see, if it's x big, all trees at least x small will only see that tree, and all other trees will see one more: at index i of value v: if i <= x then 1 else v+1.

We can then use this in a similar way to what we did with max and rollingMax before:

val rollingLengths = line.scanRight( List.fill(10)(0) ){
case (curr, lengths) =>
lengths.zipWithIndex.map{ case (v, i) => if i <= curr then 1 else v+1 }
}.init

We then get the score by reading lengths at the appropriate point, again as was done with rollingMax:

rollingLengths.zip(line).map{ case (lengths, curr) => lengths(curr) }

By combining everything, noticing once again our calculation is the same for each line, we get:

def computeScore(ls: HeightField): ScoreField = ls.map{ line =>
val rollingLengths = line.scanRight( List.fill(10)(0) ){
case (curr, lengths) =>
lengths.zipWithIndex.map{ case (v, i) => if i <= curr then 1 else v+1 }
}.init
rollingLengths.zip(line).map{ case (lengths, curr) => lengths(curr) }
}

Where ScoreField is identical to HeightField, but serves to make the code more readable:

type ScoreField = Field[Int]

We can use the same trick as before to get all the other directions for free:

val scoreFields: List[ScoreField] = computeInAllDirections(parsed, computeScore)

This time instead of or-ing, we need to multiply "A tree's scenic score is found by multiplying together its viewing distance in each of the four directions.":

val scoreField: ScoreField = scoreFields.reduce(combine(_ * _))

And this time the last step is to get the heighest value instead of the sum:

scoreField.megaReduce(_ max _)

Final Code

def part1(input: String): Int =
val parsed = parse(input)
val visibilityFields: List[VisibilityField] = computeInAllDirections(parsed, computeVisibility)
val visibilityField: VisibilityField = visibilityFields.reduce(combine(_ | _))
visibilityField.megaMap(if _ then 1 else 0).megaReduce(_ + _)

def part2(input: String): Int =
val parsed = parse(input)
val scoreFields: List[ScoreField] = computeInAllDirections(parsed, computeScore)
val scoreField: ScoreField = scoreFields.reduce(combine(_ * _))
scoreField.megaReduce(_ max _)

type Field[A] = List[List[A]]

extension [A](xss: Field[A])
def megaZip[B](yss: Field[B]): Field[(A, B)] = (xss zip yss).map( (xs, ys) => xs zip ys )
def megaMap[B](f: A => B): Field[B] = xss.map(_.map(f))
def megaReduce(f: (A,A) => A): A = xss.map(_.reduce(f)).reduce(f)

def combine[A](op: ((A,A)) => A)(f1: Field[A], f2: Field[A]): Field[A] = f1.megaZip(f2).megaMap(op)

def computeInAllDirections[A, B](xss: Field[A], f: Field[A] => Field[B]): List[Field[B]] =
for
transpose <- List(false, true)
reverse <- List(false, true)
yield
val t = if transpose then xss.transpose else xss
val in = if reverse then t.map(_.reverse) else t
val res = f(in)
val r = if reverse then res.map(_.reverse) else res
val out = if transpose then r.transpose else r
out

type HeightField = Field[Int]
type ScoreField = Field[Int]

type VisibilityField = Field[Boolean]

def parse(input: String): HeightField = input.split('\n').toList.map(line => line.map(char => char.asDigit).toList)

def computeVisibility(ls: HeightField): VisibilityField = ls.map{ line =>
val rollingMax = line.scanLeft(-1){ case (max, curr) => Math.max(max, curr) }.init
rollingMax.zip(line).map{ case (max, curr) => max < curr) }
}

def computeScore(ls: HeightField): ScoreField = ls.map{ line =>
val rollingLengths = line.scanRight( List.fill(10)(0) ){
case (curr, lengths) =>
lengths.zipWithIndex.map{ case (v, i) => if i <= curr then 1 else v+1 }
}.init
rollingLengths.zip(line).map{ case (lengths, curr) => lengths(curr) }
}

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day09/index.html b/2022/puzzles/day09/index.html index 44f19d122..0c0c6dff1 100644 --- a/2022/puzzles/day09/index.html +++ b/2022/puzzles/day09/index.html @@ -5,7 +5,7 @@ Day 9: Rope Bridge | Scala Center Advent of Code - + @@ -27,7 +27,7 @@ Each line you can extract the direction and steps count with a pattern binding val (s"$dir $n") = line, then use Direction.valueOf to lookup the direction, and .toInt to convert n to the number of steps.

Then to run n steps, create the steps iterator, then drop n elements to advance the state n steps, then take the next() element:

def uniquePositions(input: String, knots: Int): Int =
val end = input.linesIterator.foldLeft(initialState(knots)) { case (state, line) =>
val (s"$dir $n") = line: @unchecked
steps(state, Direction.valueOf(dir)).drop(n.toInt).next()
}
end.uniques.size

Part 1 needs 2 knots, and part 2 needs 10 knots, they can be implemented as such:

def part1(input: String): Int =
uniquePositions(input, knots = 2)

def part2(input: String): Int =
uniquePositions(input, knots = 10)

Final Code

import Direction.*

def part1(input: String): Int =
uniquePositions(input, knots = 2)

def part2(input: String): Int =
uniquePositions(input, knots = 10)

case class Position(x: Int, y: Int):
def moveOne(dir: Direction): Position = dir match
case U => Position(x, y + 1)
case D => Position(x, y - 1)
case L => Position(x - 1, y)
case R => Position(x + 1, y)

def follow(head: Position): Position =
val dx = head.x - x
val dy = head.y - y
if dx.abs > 1 || dy.abs > 1 then Position(x + dx.sign, y + dy.sign) // follow the head
else this // stay put

case class State(uniques: Set[Position], head: Position, knots: List[Position])

enum Direction:
case U, D, L, R

def followAll(head: Position, knots: List[Position]) =
var prev = head // head was already moved with `moveOne`
val buf = List.newBuilder[Position]
for knot <- knots do
val next = knot.follow(prev)
buf += next
prev = next
(prev, buf.result())
end followAll

def step(dir: Direction, state: State) =
val head1 = state.head.moveOne(dir)
val (last, knots1) = followAll(head1, state.knots)
State(state.uniques + last, head1, knots1)

def steps(state: State, dir: Direction): Iterator[State] =
Iterator.iterate(state)(state => step(dir, state))

def initialState(knots: Int) =
val zero = Position(0, 0)
State(Set(zero), zero, List.fill(knots - 1)(zero))

def uniquePositions(input: String, knots: Int): Int =
val end = input.linesIterator.foldLeft(initialState(knots)) { case (state, line) =>
val (s"$dir $n") = line: @unchecked
steps(state, Direction.valueOf(dir)).drop(n.toInt).next()
}
end.uniques.size

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day10/index.html b/2022/puzzles/day10/index.html index 10902fbc4..e4e10a040 100644 --- a/2022/puzzles/day10/index.html +++ b/2022/puzzles/day10/index.html @@ -5,14 +5,14 @@ Day 10: Cathode-Ray Tube | Scala Center Advent of Code - +
Skip to main content

Day 10: Cathode-Ray Tube

code and article by Mewen Crespo (reviewed by Jamie Thompson)

Puzzle description

https://adventofcode.com/2022/day/10

Solution

Today's goal is to simulate the register's values over time. Once this is done, the rest falls in place rather quickly. From the puzzle description, we know there are two commands availaible: noop and addx. This can be implemented with a enum:

enum Command:
case Noop
case Addx(x: Int)

Now, we need to parse this commands from the string. This can be done using a for loop to match each line of the input:

import Command.*

def commandsIterator(input: String): Iterator[Command] =
for line <- input.linesIterator yield line match
case "noop" => Noop
case s"addx $x" if x.toIntOption.isDefined => Addx(x.toInt)
case _ => throw IllegalArgumentException(s"Invalid command '$line''")

Here you can use linesIterator to retrieve the lines (it returns an Iterator[String]) and mapped every line using a for .. yield comprehension with a match body. Note the use of the string interpolator s for a simple way to parse strings.

tip

Error checking: Althought not necessary in this puzzle, it is a good practice to check the validity of the input. Here, we checked that the string matched with $x is a valid integer string before entering the second case and throw an exception if none of the first cases were matched.

Now we are ready to compute the registers values. We choose to implement it as an Iterator[Int] which will return the register's value each cycle at a time. For this, we need to loop throught the commands. If the command is a noop, then the next cycle will have the same value. If the command is a addx x then the next cycle will be the same value and the cycle afterward will be x more. There is an issue here: the addx command generates two cycles whereas the noop command generates only one.

To circumvent this issue, generate an Iterator[List[Int]] first which will be flattened afterwards. The first iterator is constructed using the scanLeft method to yield the following code:

val RegisterStartValue = 1

def registerValuesIterator(input: String): Iterator[Int] =
val steps = commandsIterator(input).scanLeft(RegisterStartValue :: Nil) { (values, cmd) =>
val value = values.last
cmd match
case Noop => value :: Nil
case Addx(x) => value :: value + x :: Nil
}
steps.flatten

Notice that at each step we call .last on the accumulated List[Int] value which, in this case, is the register's value at the start of the last cycle.

Part 1

In the first part, the challenge asks you to compute the strength at the 20th cycle and then every 40th cycle. This can be done using a combination of drop (to skip the first 19 cycles), grouped (to group the cycles by 40) and map(_.head) (to only take the first cycle of each group of 40). The computation of the strengths is, on the other hand, done using the zipWithIndex method and a for ... yield comprehension. This leads to the following code:

def registerStrengthsIterator(input: String): Iterator[Int] =
val it = for (reg, i) <- registerValuesIterator(input).zipWithIndex yield (i + 1) * reg
it.drop(19).grouped(40).map(_.head)

The result of Part 1 is the sum of this iterator:

def part1(input: String): Int = registerStrengthsIterator(input).sum

Part 2

In the second part, we are asked to draw a CRT output. As stated in the puzzle description, the register is interpreted as the position of a the sprite ###. The CRT iterates throught each line and, if the sprites touches the touches the current position, draws a #. Otherwise the CRT draws a .. The register's cycles are stepped in synced with the CRT.

First, the CRT's position is just the cycle's index modulo the CRT's width (40 in our puzzle). Then, the CRT draw the sprite if and only if the register's value is the CRT's position, one more or one less. In other words, if (reg_value - (cycle_id % 40)).abs <= 1. Using the zipWithIndex method to obtain the cycles' indexes we end up with the following code:

val CRTWidth: Int = 40

def CRTCharIterator(input: String): Iterator[Char] =
for (reg, crtPos) <- registerValuesIterator(input).zipWithIndex yield
if (reg - (crtPos % CRTWidth)).abs <= 1 then
'#'
else
'.'

Now, concatenate the chars and add new lines at the required places. This is done using the mkString methods:

def part2(input: String): String =
CRTCharIterator(input).grouped(CRTWidth).map(_.mkString).mkString("\n")

Final Code

import Command.*

def part1(input: String): Int =
registerStrengthsIterator(input).sum

def part2(input: String): String =
CRTCharIterator(input).grouped(CRTWidth).map(_.mkString).mkString("\n")

enum Command:
case Noop
case Addx(x: Int)

def commandsIterator(input: String): Iterator[Command] =
for line <- input.linesIterator yield line match
case "noop" => Noop
case s"addx $x" if x.toIntOption.isDefined => Addx(x.toInt)
case _ => throw IllegalArgumentException(s"Invalid command '$line''")

val RegisterStartValue = 1

def registerValuesIterator(input: String): Iterator[Int] =
val steps = commandsIterator(input).scanLeft(RegisterStartValue :: Nil) { (values, cmd) =>
val value = values.last
cmd match
case Noop => value :: Nil
case Addx(x) => value :: value + x :: Nil
}
steps.flatten

def registerStrengthsIterator(input: String): Iterator[Int] =
val it = for (reg, i) <- registerValuesIterator(input).zipWithIndex yield (i + 1) * reg
it.drop(19).grouped(40).map(_.head)

val CRTWidth: Int = 40

def CRTCharIterator(input: String): Iterator[Char] =
for (reg, crtPos) <- registerValuesIterator(input).zipWithIndex yield
if (reg - (crtPos % CRTWidth)).abs <= 1 then
'#'
else
'.'

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day11/index.html b/2022/puzzles/day11/index.html index 13f46bae9..615c208e8 100644 --- a/2022/puzzles/day11/index.html +++ b/2022/puzzles/day11/index.html @@ -5,13 +5,13 @@ Day 11: Monkey in the Middle | Scala Center Advent of Code - +
Skip to main content

Day 11: Monkey in the Middle

Puzzle description

https://adventofcode.com/2022/day/11

Final Code

import scala.collection.immutable.Queue

def part1(input: String): Long =
run(initial = parseInput(input), times = 20, adjust = _ / 3)

def part2(input: String): Long =
run(initial = parseInput(input), times = 10_000, adjust = identity)

type Worry = Long
type Op = Worry => Worry
type Monkeys = IndexedSeq[Monkey]

case class Monkey(
items: Queue[Worry],
divisibleBy: Int,
ifTrue: Int,
ifFalse: Int,
op: Op,
inspected: Int
)

def iterate[Z](times: Int)(op: Z => Z)(z: Z): Z =
(0 until times).foldLeft(z) { (z, _) => op(z) }

def run(initial: Monkeys, times: Int, adjust: Op): Long =
val lcm = initial.map(_.divisibleBy.toLong).product
val monkeys = iterate(times)(round(adjust, lcm))(initial)
monkeys.map(_.inspected.toLong).sorted.reverseIterator.take(2).product

def round(adjust: Op, lcm: Worry)(monkeys: Monkeys): Monkeys =
monkeys.indices.foldLeft(monkeys) { (monkeys, index) =>
turn(index, monkeys, adjust, lcm)
}

def turn(index: Int, monkeys: Monkeys, adjust: Op, lcm: Worry): Monkeys =
val monkey = monkeys(index)
val Monkey(items, divisibleBy, ifTrue, ifFalse, op, inspected) = monkey

val monkeys1 = items.foldLeft(monkeys) { (monkeys, item) =>
val inspected = op(item)
val nextWorry = adjust(inspected) % lcm
val thrownTo =
if nextWorry % divisibleBy == 0 then ifTrue
else ifFalse
val thrownToMonkey =
val m = monkeys(thrownTo)
m.copy(items = m.items :+ nextWorry)
monkeys.updated(thrownTo, thrownToMonkey)
}
val monkey1 = monkey.copy(
items = Queue.empty,
inspected = inspected + items.size
)
monkeys1.updated(index, monkey1)
end turn

def parseInput(input: String): Monkeys =

def eval(by: String): Op =
if by == "old" then identity
else Function.const(by.toInt)

def parseOperator(op: String, left: Op, right: Op): Op =
op match
case "+" => old => left(old) + right(old)
case "*" => old => left(old) * right(old)

IArray.from(
for
case Seq(
s"Monkey $n:",
s" Starting items: $items",
s" Operation: new = $left $operator $right",
s" Test: divisible by $div",
s" If true: throw to monkey $ifTrue",
s" If false: throw to monkey $ifFalse",
_*
) <- input.linesIterator.grouped(7)
yield
val op = parseOperator(operator, eval(left), eval(right))
val itemsQueue = items.split(", ").map(_.toLong).to(Queue)
Monkey(itemsQueue, div.toInt, ifTrue.toInt, ifFalse.toInt, op, inspected = 0)
)
end parseInput

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day12/index.html b/2022/puzzles/day12/index.html index 018784a39..981a0125e 100644 --- a/2022/puzzles/day12/index.html +++ b/2022/puzzles/day12/index.html @@ -5,13 +5,13 @@ Day 12: Hill Climbing Algorithm | Scala Center Advent of Code - +
Skip to main content

Day 12: Hill Climbing Algorithm

Puzzle description

https://adventofcode.com/2022/day/12

Solution

Today's challenge is to simulate the breadth-first search over a graph. First, let's create a standard Point class and define addition on it:

case class Point(x: Int, y: Int):
def move(dx: Int, dy: Int):
Point = Point(x + dx, y + dy)
override def toString: String =
s"($x, $y)"
end Point

Now we need a representation that will serve as a substitute for moves:

val up    = (0, 1)
val down = (0, -1)
val left = (-1, 0)
val right = (1, 0)
val possibleMoves = List(up, down, left, right)

Let's make a path function that will help us to calculate the length of our path to the point, based on our moves, that we defined before:

def path(point: Point, net: Map[Point, Char]): Seq[Point] =
possibleMoves.map(point.move).filter(net.contains)

A function that fulfills our need to match an entry with the point we are searching for:

def matching(point: Point, net: Map[Point, Char]): Char =
net(point) match
case 'S' => 'a'
case 'E' => 'z'
case other => other

Now we just need to put the program together. First of all, let's map out our indices to the source, so we can create a queue for path representation. After that we need to create a map, to keep track the length of our path. For that we will need to map E entry to zero. The last part is the implementation of bfs on a Queue.

def solution(source: IndexedSeq[String], srchChar: Char): Int =
// create a sequence of Point objects and their corresponding character in source
val points =
for
y <- source.indices
x <- source.head.indices
yield
Point(x, y) -> source(y)(x)
val p = points.toMap
val initial = p.map(_.swap)('E')
val queue = collection.mutable.Queue(initial)
val length = collection.mutable.Map(initial -> 0)
//bfs
while queue.nonEmpty do
val visited = queue.dequeue()
if p(visited) == srchChar then
return length(visited)
for visited1 <- path(visited, p) do
val shouldAdd =
!length.contains(visited1)
&& matching(visited, p) - matching(visited1, p) <= 1
if shouldAdd then
queue.enqueue(visited1)
length(visited1) = length(visited) + 1
end for
end while
throw IllegalStateException("unexpected end of search area")
end solution

In part one srchChar is 'S', but since our method in non-exhaustive, we may apply the same function for 'a'

def part1(data: String): Int =
solution(IndexedSeq.from(data.linesIterator), 'S')
def part2(data: String): Int =
solution(IndexedSeq.from(data.linesIterator), 'a')

And that's it!

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day13/index.html b/2022/puzzles/day13/index.html index 0781ff39c..f0e4c83cb 100644 --- a/2022/puzzles/day13/index.html +++ b/2022/puzzles/day13/index.html @@ -5,13 +5,13 @@ Day 13: Distress Signal | Scala Center Advent of Code - +
Skip to main content

Day 13: Distress Signal

by Jamie Thompson

Puzzle description

https://adventofcode.com/2022/day/13

Final Code

import scala.collection.immutable.Queue
import scala.math.Ordered.given
import Packet.*

def part1(input: String): Int =
findOrderedIndices(input)

def part2(input: String): Int =
findDividerIndices(input)

def findOrderedIndices(input: String): Int =
val indices = (
for
case (Seq(a, b, _*), i) <- input.linesIterator.grouped(3).zipWithIndex
if readPacket(a) <= readPacket(b)
yield
i + 1
)
indices.sum

def findDividerIndices(input: String): Int =
val dividers = List("[[2]]", "[[6]]").map(readPacket)
val lookup = dividers.toSet
val packets = input
.linesIterator
.filter(_.nonEmpty)
.map(readPacket)
val indices = (dividers ++ packets)
.sorted
.iterator
.zipWithIndex
.collect { case (p, i) if lookup.contains(p) => i + 1 }
indices.take(2).product

enum Packet:
case Nested(packets: List[Packet])
case Num(value: Int)

case class State(number: Int, values: Queue[Packet]):
def nextWithDigit(digit: Int): State = // add digit to number
copy(number = if number == -1 then digit else number * 10 + digit)

def nextWithNumber: State =
if number == -1 then this // no number to commit
else
// reset number, add accumulated number to values
State.empty.copy(values = values :+ Num(number))

object State:
val empty = State(-1, Queue.empty)

def readPacket(input: String): Packet =
def loop(i: Int, state: State, stack: List[Queue[Packet]]): Packet =
input(i) match // assume that list is well-formed.
case '[' =>
loop(i + 1, State.empty, state.values :: stack) // push old state to stack
case ']' => // add trailing number, close packet
val packet = Nested(state.nextWithNumber.values.toList)
stack match
case values1 :: rest => // restore old state
loop(i + 1, State.empty.copy(values = values1 :+ packet), rest)
case Nil => // terminating case
packet
case ',' => loop(i + 1, state.nextWithNumber, stack)
case n => loop(i + 1, state.nextWithDigit(n.asDigit), stack)
end loop
if input.nonEmpty && input(0) == '[' then
loop(i = 1, State.empty, stack = Nil)
else
throw IllegalArgumentException(s"Invalid input: `$input`")
end readPacket

given PacketOrdering: Ordering[Packet] with

def nestedCompare(ls: List[Packet], rs: List[Packet]): Int = (ls, rs) match
case (l :: ls1, r :: rs1) =>
val res = compare(l, r)
if res == 0 then nestedCompare(ls1, rs1) // equal, look at next element
else res // less or greater

case (_ :: _, Nil) => 1 // right ran out of elements first
case (Nil, _ :: _) => -1 // left ran out of elements first
case (Nil, Nil) => 0 // equal size
end nestedCompare

def compare(left: Packet, right: Packet): Int = (left, right) match
case (Num(l), Num(r)) => l compare r
case (Nested(l), Nested(r)) => nestedCompare(l, r)
case (num @ Num(_), Nested(r)) => nestedCompare(num :: Nil, r)
case (Nested(l), num @ Num(_)) => nestedCompare(l, num :: Nil)
end compare

end PacketOrdering

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day14/index.html b/2022/puzzles/day14/index.html index 90692c434..e3fcdf485 100644 --- a/2022/puzzles/day14/index.html +++ b/2022/puzzles/day14/index.html @@ -5,13 +5,13 @@ Day 14: Regolith Reservoir | Scala Center Advent of Code - +
Skip to main content

Day 14: Regolith Reservoir

Puzzle description

https://adventofcode.com/2022/day/14

Final Solution

def part1(input: String): Int =
val search = parseInput(input)
search.states
.takeWhile(_.fallingPath.head.y < search.lowestRock)
.last
.sand
.size

def part2(input: String): Int =
parseInput(input).states.last.sand.size

def parseInput(input: String): Scan =
val paths = input.linesIterator
.map { line =>
line.split(" -> ").map { case s"$x,$y" => Point(x.toInt, y.toInt) }.toList
}
val rocks = paths.flatMap { path =>
path.sliding(2).flatMap {
case List(p1, p2) =>
val dx = p2.x - p1.x
val dy = p2.y - p1.y

if dx == 0 then (p1.y to p2.y by dy.sign).map(Point(p1.x, _))
else (p1.x to p2.x by dx.sign).map(Point(_, p1.y))
case _ => None
}
}.toSet
Scan(rocks)

case class Point(x: Int, y: Int)

case class Scan(rocks: Set[Point]):
val lowestRock = rocks.map(_.y).max
val floor = lowestRock + 2

case class State(fallingPath: List[Point], sand: Set[Point]):
def isFree(p: Point) = !sand(p) && !rocks(p)

def next: Option[State] = fallingPath.headOption.map {
case sandUnit @ Point(x, y) =>
val down = Some(Point(x, y + 1)).filter(isFree)
val downLeft = Some(Point(x - 1, y + 1)).filter(isFree)
val downRight = Some(Point(x + 1, y + 1)).filter(isFree)

down.orElse(downLeft).orElse(downRight).filter(_.y < floor) match
case Some(fallingPos) =>
State(fallingPos :: fallingPath, sand)
case None =>
State(fallingPath.tail, sand + sandUnit)
}

def states: LazyList[State] =
val source = Point(500, 0)
LazyList.unfold(State(List(source), Set.empty)) { _.next.map(s => s -> s) }
end Scan

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day15/index.html b/2022/puzzles/day15/index.html index 10dd3043b..2f7ca5d46 100644 --- a/2022/puzzles/day15/index.html +++ b/2022/puzzles/day15/index.html @@ -5,14 +5,14 @@ Day 15: Beacon Exclusion Zone | Scala Center Advent of Code - +
Skip to main content

Day 15: Beacon Exclusion Zone

Puzzle description

https://adventofcode.com/2022/day/15

Explanation

Part 1

We first model and parse the input:

case class Position(x: Int, y: Int)

def parse(input: String): List[(Position, Position)] =
input.split("\n").toList.map{
case s"Sensor at x=$sx, y=$sy: closest beacon is at x=$bx, y=$by" =>
(Position(sx.toInt, sy.toInt), Position(bx.toInt, by.toInt))
}

We then model the problem-specific knowledge:

def distance(p1: Position, p2: Position): Int =
Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)

def distanceToLine(p: Position, y: Int): Int =
Math.abs(p.y - y)

We use it to compute how much of a line is covered by one (by lineCoverage) and all (by coverOfLine) sensors:

def lineCoverage(sensor: Position, radius: Int, lineY: Int): Range =
val radiusInLine = radius - distanceToLine(sensor, lineY)

// if radiusInLine is smaller than 0, the range will be empty
(sensor.x - radiusInLine) to (sensor.x + radiusInLine)

def coverOfLine(sensorsWithDistances: List[(Position, Int)], line: Int) =
sensorsWithDistances.map( (sensor, radius) => lineCoverage(sensor, radius, line) ).filter(_.nonEmpty)

This is enought to solve part one:

def part1(input: String): Int =
val parsed: List[(Position, Position)] = parse(input)
val beacons: Set[Position] = parsed.map(_._2).toSet
val sensorsWithDistances: List[(Position, Int)] =
parsed.map( (sensor, beacon) => (sensor, distance(sensor, beacon)) )

val line = 2000000
val cover: List[Range] = coverOfLine(sensorsWithDistances, line)
val beaconsOnLine: Set[Position] = beacons.filter(_.y == line)
val count: Int = cover.map(_.size).sum - beaconsOnLine.size
count

Part 2

We wish to remove ranges from other ranges, sadly there is no built-in method to do so, instead rellying on a cast to a collection, which makes computation much much slower. Therefore we define our own difference method which returns zero, one or two ranges:

def smartDiff(r1: Range, r2: Range): List[Range] =
val innit = r1.start to Math.min(r2.start - 1, r1.last)
val tail = Math.max(r1.start, r2.last + 1) to r1.last
val res = if innit == tail then
List(innit)
else
List(innit, tail)
res.filter(_.nonEmpty).toList

This allows us to subtract the cover from our target interval like so:

def remainingSpots(target: Range, cover: List[Range]): Set[Int] = 

def rec(partialTarget: List[Range], remainingCover: List[Range]): List[Range] =
if remainingCover.isEmpty then
partialTarget
else
val (curr: Range) :: rest = remainingCover: @unchecked
rec(
partialTarget = partialTarget.flatMap( r => smartDiff(r, curr) ),
remainingCover = rest
)

rec(List(target), cover).flatten.toSet

We can then iterate through all lines, and computing for each which positions are free. As per the problem statement, we know there will only be one inside the square of side 0 to 4_000_000. We then compute the solution's tuning frequency.

def part2(input: String): Any =

val parsed: List[(Position, Position)] = parse(input)
val beacons: Set[Position] = parsed.map(_._2).toSet
val sensorsWithDistances: List[(Position, Int)] =
parsed.map( (sensor, beacon) => (sensor, distance(sensor, beacon)) )

val target: Range = 0 to 4_000_000
val spots: Seq[Position] = target.flatMap{
line =>
val cover: List[Range] = coverOfLine(sensorsWithDistances, line)
val beaconsOnLine: Set[Position] = beacons.filter(_.y == line)

val remainingRanges: List[Range] = cover.foldLeft(List(target)){
case (acc: List[Range], range: Range) =>
acc.flatMap( r => smartDiff(r, range) )
}
val potential = remainingRanges.flatten.toSet

val spotsOnLine = potential diff beaconsOnLine.map( b => b.x )
spotsOnLine.map( x => Position(x, line) )
}
def tuningFrequency(p: Position): BigInt = BigInt(p.x) * 4_000_000 + p.y

println(spots.mkString(", "))
assert(spots.size == 1)
tuningFrequency(spots.head)

Final Code

case class Position(x: Int, y: Int)

def parse(input: String): List[(Position, Position)] =
input.split("\n").toList.map{
case s"Sensor at x=$sx, y=$sy: closest beacon is at x=$bx, y=$by" =>
(Position(sx.toInt, sy.toInt), Position(bx.toInt, by.toInt))
}

def distance(p1: Position, p2: Position): Int =
Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)

def distanceToLine(p: Position, y: Int): Int =
Math.abs(p.y - y)

def lineCoverage(sensor: Position, radius: Int, lineY: Int): Range =
val radiusInLine = radius - distanceToLine(sensor, lineY)

// if radiusInLine is smaller than 0, the range will be empty
(sensor.x - radiusInLine) to (sensor.x + radiusInLine)

def coverOfLine(sensorsWithDistances: List[(Position, Int)], line: Int) =
sensorsWithDistances.map( (sensor, radius) => lineCoverage(sensor, radius, line) ).filter(_.nonEmpty)

def smartDiff(r1: Range, r2: Range): List[Range] =
val innit = r1.start to Math.min(r2.start - 1, r1.last)
val tail = Math.max(r1.start, r2.last + 1) to r1.last
val res = if innit == tail then
List(innit)
else
List(innit, tail)
res.filter(_.nonEmpty).toList

def remainingSpots(target: Range, cover: List[Range]): Set[Int] =

def rec(partialTarget: List[Range], remainingCover: List[Range]): List[Range] =
if remainingCover.isEmpty then
partialTarget
else
val (curr: Range) :: rest = remainingCover: @unchecked
rec(
partialTarget = partialTarget.flatMap( r => smartDiff(r, curr) ),
remainingCover = rest
)

rec(List(target), cover).flatten.toSet

def part1(input: String): Int =
val parsed: List[(Position, Position)] = parse(input)
val beacons: Set[Position] = parsed.map(_._2).toSet
val sensorsWithDistances: List[(Position, Int)] =
parsed.map( (sensor, beacon) => (sensor, distance(sensor, beacon)) )

val line = 2000000
val cover: List[Range] = coverOfLine(sensorsWithDistances, line)
val beaconsOnLine: Set[Position] = beacons.filter(_.y == line)
val count: Int = cover.map(_.size).sum - beaconsOnLine.size
count

def part2(input: String): Any =

val parsed: List[(Position, Position)] = parse(input)
val beacons: Set[Position] = parsed.map(_._2).toSet
val sensorsWithDistances: List[(Position, Int)] =
parsed.map( (sensor, beacon) => (sensor, distance(sensor, beacon)) )

val target: Range = 0 to 4_000_000
val spots: Seq[Position] = target.flatMap{
line =>
val cover: List[Range] = coverOfLine(sensorsWithDistances, line)
val beaconsOnLine: Set[Position] = beacons.filter(_.y == line)

val remainingRanges: List[Range] = cover.foldLeft(List(target)){
case (acc: List[Range], range: Range) =>
acc.flatMap( r => smartDiff(r, range) )
}
val potential = remainingRanges.flatten.toSet

val spotsOnLine = potential diff beaconsOnLine.map( b => b.x )
spotsOnLine.map( x => Position(x, line) )
}
def tuningFrequency(p: Position): BigInt = BigInt(p.x) * 4_000_000 + p.y

println(spots.mkString(", "))
assert(spots.size == 1)
tuningFrequency(spots.head)

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day16/index.html b/2022/puzzles/day16/index.html index e646c9c2b..2b413228e 100644 --- a/2022/puzzles/day16/index.html +++ b/2022/puzzles/day16/index.html @@ -5,13 +5,13 @@ Day 16: Proboscidea Volcanium | Scala Center Advent of Code - +
Skip to main content

Day 16: Proboscidea Volcanium

code by Tyler Coles (javadocmd.com), Quentin Bernet, @sjrd, and @bishabosha

Puzzle description

https://adventofcode.com/2022/day/16

Final Code

type Id = String
case class Room(id: Id, flow: Int, tunnels: List[Id])

type Input = List[Room]
// $_ to avoid tunnel/tunnels distinction and so on
def parse(xs: String): Input = xs.split("\n").map{ case s"Valve $id has flow rate=$flow; tunnel$_ lead$_ to valve$_ $tunnelsStr" =>
val tunnels = tunnelsStr.split(", ").toList
Room(id, flow.toInt, tunnels)
}.toList

case class RoomsInfo(
/** map of rooms by id */
rooms: Map[Id, Room],
/** map from starting room to a map containing the best distance to all other rooms */
routes: Map[Id, Map[Id, Int]],
/** rooms containing non-zero-flow valves */
valves: Set[Id]
)

// precalculate useful things like pathfinding
def constructInfo(input: Input): RoomsInfo =
val rooms: Map[Id, Room] = Map.from(for r <- input yield r.id -> r)
val valves: Set[Id] = Set.from(for r <- input if r.flow > 0 yield r.id)
val tunnels: Map[Id, List[Id]] = rooms.mapValues(_.tunnels).toMap
val routes: Map[Id, Map[Id, Int]] = (valves + "AA").iterator.map{ id => id -> computeRoutes(id, tunnels) }.toMap
RoomsInfo(rooms, routes, valves)

// a modified A-star to calculate the best distance to all rooms rather then the best path to a single room
def computeRoutes(start: Id, neighbors: Id => List[Id]): Map[Id, Int] =

case class State(frontier: List[(Id, Int)], scores: Map[Id, Int]):

private def getScore(id: Id): Int = scores.getOrElse(id, Int.MaxValue)
private def setScore(id: Id, s: Int) = State((id, s + 1) :: frontier, scores + (id -> s))

def dequeued: (Id, State) =
val sorted = frontier.sortBy(_._2)
(sorted.head._1, copy(frontier = sorted.tail))

def considerEdge(from: Id, to: Id): State =
val toScore = getScore(from) + 1
if toScore >= getScore(to) then this
else setScore(to, toScore)
end State

object State:
def initial(start: Id) = State(List((start, 0)), Map(start -> 0))

def recurse(state: State): State =
if state.frontier.isEmpty then
state
else
val (curr, currState) = state.dequeued
val newState = neighbors(curr)
.foldLeft(currState) { (s, n) =>
s.considerEdge(curr, n)
}
recurse(newState)

recurse(State.initial(start)).scores

end computeRoutes


// find the best path (the order of valves to open) and the total pressure released by taking it
def bestPath(map: RoomsInfo, start: Id, valves: Set[Id], timeAllowed: Int): Int =
// each step involves moving to a room with a useful valve and opening it
// we don't need to track each (empty) room in between
// we limit our options by only considering the still-closed valves
// and `valves` has already culled any room with a flow value of 0 -- no point in considering these rooms!

val valvesLookup = IArray.from(valves)
val valveCount = valvesLookup.size
val _activeValveIndices = Array.fill[Boolean](valveCount + 1)(true) // add an extra valve for the initial state
def valveIndexLeft(i: Int) = _activeValveIndices(i)
def withoutValve(i: Int)(f: => Int) =
_activeValveIndices(i) = false
val result = f
_activeValveIndices(i) = true
result
val roomsByIndices = IArray.tabulate(valveCount)(i => map.rooms(valvesLookup(i)))

def recurse(hiddenValve: Int, current: Id, timeLeft: Int, totalValue: Int): Int = withoutValve(hiddenValve):
// recursively consider all plausible options
// we are finished when we no longer have time to reach another valve or all valves are open
val routesOfCurrent = map.routes(current)
var bestValue = totalValue
for index <- 0 to valveCount do
if valveIndexLeft(index) then
val id = valvesLookup(index)
val distance = routesOfCurrent(id)
// how much time is left after we traverse there and open the valve?
val t = timeLeft - distance - 1
// if `t` is zero or less this option can be skipped
if t > 0 then
// the value of choosing a particular valve (over the life of our simulation)
// is its flow rate multiplied by the time remaining after opening it
val value = roomsByIndices(index).flow * t
val recValue = recurse(hiddenValve = index, id, t, totalValue + value)
if recValue > bestValue then
bestValue = recValue
end if
end if
end for
bestValue
end recurse
recurse(valveCount, start, timeAllowed, 0)

def part1(input: String) =
val time = 30
val map = constructInfo(parse(input))
bestPath(map, "AA", map.valves, time)
end part1

def part2(input: String) =
val time = 26
val map = constructInfo(parse(input))

// in the optimal solution, the elephant and I will have divided responsibility for switching the valves
// 15 (useful valves) choose 7 (half) yields only 6435 possible divisions which is a reasonable search space!
val valvesA = map.valves.toList
.combinations(map.valves.size / 2)
.map(_.toSet)

// NOTE: I assumed an even ditribution of valves would be optimal, and that turned out to be true.
// However I suppose it's possible an uneven distribution could have been optimal for some graphs.
// To be safe, you could re-run this using all reasonable values of `n` for `combinations` (1 to 7) and
// taking the best of those.

// we can now calculate the efforts separately and sum their values to find the best
val allPaths =
for va <- valvesA yield
val vb = map.valves -- va
val scoreA = bestPath(map, "AA", va, time)
val scoreB = bestPath(map, "AA", vb, time)
scoreA + scoreB

allPaths.max
end part2

Run it in the browser

Part 1

Warning: This is pretty slow and may cause the UI to freeze (close tab if problematic)

Part 2

Warning: This is pretty slow and may cause the UI to freeze (close tab if problematic)

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day17/index.html b/2022/puzzles/day17/index.html index 867b75a9e..5776146cf 100644 --- a/2022/puzzles/day17/index.html +++ b/2022/puzzles/day17/index.html @@ -5,13 +5,13 @@ Day 17: Pyroclastic Flow | Scala Center Advent of Code - +
Skip to main content
- + \ No newline at end of file diff --git a/2022/puzzles/day18/index.html b/2022/puzzles/day18/index.html index ba6e5f83a..f9e355a16 100644 --- a/2022/puzzles/day18/index.html +++ b/2022/puzzles/day18/index.html @@ -5,13 +5,13 @@ Day 18: Boiling Boulders | Scala Center Advent of Code - +
Skip to main content

Day 18: Boiling Boulders

by LaurenceWarne

Puzzle description

https://adventofcode.com/2022/day/18

Solution

Part 1

To solve the first part, we can first count the total number of cubes and multiply this by six (as a cube has six sides), and then subtract the number of sides which are connected.

As this requires checking if two cubes are adjacent, let's first define a function which we can use to determine cubes adjacent to a given cube:

def adjacent(x: Int, y: Int, z: Int): Set[(Int, Int, Int)] = {
Set(
(x + 1, y, z),
(x - 1, y, z),
(x, y + 1, z),
(x, y - 1, z),
(x, y, z + 1),
(x, y, z - 1)
)
}
info

Note that since cubes are given to be 1⨉1⨉1, they can be represented as a single integral (x, y, z) coordinate which makes up the input for the adjacent function. Then two cubes are adjacent (one of each of their sides touch) if and only if exactly one of their (x, y, z) components differ by one, and the rest by zero.

Now given our cubes, we can implement our strategy with a fold:

def sides(cubes: Set[(Int, Int, Int)]): Int = {
cubes.foldLeft(0) { case (total, (x, y, z)) =>
val adj = adjacent(x, y, z)
val numAdjacent = adj.filter(cubes).size
total + 6 - numAdjacent
}
}

We use a Set for fast fast membership lookups which we need to determine which adjacent spaces for a given cube contain other cubes.

Part 2

The second part is a bit more tricky. Lets introduce some nomenclature: we'll say a 1⨉1⨉1 empty space is on the interior if it lies in an air pocket, else we'll say the space is on the exterior.

A useful observation is that if we consider empty spaces which have a taxicab distance of at most two from any cube, and join these spaces into connected components, then the connected components we are left with form distinct air pockets in addition to one component containing empty spaces on the exterior.

This component can always be identified since the space with the largest x component will always lie in it. So we can determine empty spaces in the interior adjacent to cubes like so:

def interior(cubes: Set[(Int, Int, Int)]): Set[(Int, Int, Int)] = {
val allAdj = cubes.flatMap((x, y, z) => adjacent(x, y, z).filterNot(cubes))
val sts = allAdj.map { case adj @ (x, y, z) =>
adjacent(x, y, z).filterNot(cubes) + adj
}
def cc(sts: List[Set[(Int, Int, Int)]]): List[Set[(Int, Int, Int)]] = {
sts match {
case Nil => Nil
case set :: rst =>
val (matching, other) = rst.partition(s => s.intersect(set).nonEmpty)
val joined = matching.foldLeft(set)(_ ++ _)
if (matching.nonEmpty) cc(joined :: other) else joined :: cc(other)
}
}
val conn = cc(sts.toList)
val exterior = conn.maxBy(_.maxBy(_(0)))
conn.filterNot(_ == exterior).foldLeft(Set())(_ ++ _)
}

Where the nested function cc is used to generate a list of connected components. We can now slightly modify our sides function to complete part two:

def sidesNoPockets(cubes: Set[(Int, Int, Int)]): Int = {
val int = interior(cubes)
val allAdj = cubes.flatMap(adjacent)
allAdj.foldLeft(sides(cubes)) { case (total, (x, y, z)) =>
val adj = adjacent(x, y, z)
if (int((x, y, z))) total - adj.filter(cubes).size else total
}
}

Let's put this all together:

def part1(input: String): Int = sides(cubes(input))
def part2(input: String): Int = sidesNoPockets(cubes(input))

def cubes(input: String): Set[(Int, Int, Int)] =
val cubesIt = input.linesIterator.collect {
case s"$x,$y,$z" => (x.toInt, y.toInt, z.toInt)
}
cubesIt.toSet

Which gives use our desired results.

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day19/index.html b/2022/puzzles/day19/index.html index 19717b210..33ad7bcd9 100644 --- a/2022/puzzles/day19/index.html +++ b/2022/puzzles/day19/index.html @@ -5,13 +5,13 @@ Day 19: Not Enough Minerals | Scala Center Advent of Code - +
Skip to main content
- + \ No newline at end of file diff --git a/2022/puzzles/day20/index.html b/2022/puzzles/day20/index.html index 4770771bf..3db0fc0ff 100644 --- a/2022/puzzles/day20/index.html +++ b/2022/puzzles/day20/index.html @@ -5,13 +5,13 @@ Day 20: Grove Positioning System | Scala Center Advent of Code - +
Skip to main content
- + \ No newline at end of file diff --git a/2022/puzzles/day21/index.html b/2022/puzzles/day21/index.html index cd2e4494f..9fa22433e 100644 --- a/2022/puzzles/day21/index.html +++ b/2022/puzzles/day21/index.html @@ -5,13 +5,13 @@ Day 21: Monkey Math | Scala Center Advent of Code - +
Skip to main content

Day 21: Monkey Math

Puzzle description

https://adventofcode.com/2022/day/21

Final Code

import annotation.tailrec
import Operation.*

def part1(input: String): Long =
resolveRoot(input)

def part2(input: String): Long =
whichValue(input)

enum Operator(val eval: BinOp, val invRight: BinOp, val invLeft: BinOp):
case `+` extends Operator(_ + _, _ - _, _ - _)
case `-` extends Operator(_ - _, _ + _, (x, y) => y - x)
case `*` extends Operator(_ * _, _ / _, _ / _)
case `/` extends Operator(_ / _, _ * _, (x, y) => y / x)

enum Operation:
case Binary(op: Operator, depA: String, depB: String)
case Constant(value: Long)

type BinOp = (Long, Long) => Long
type Resolved = Map[String, Long]
type Source = Map[String, Operation]
type Substitutions = List[(String, PartialFunction[Operation, Operation])]

def readAll(input: String): Map[String, Operation] =
Map.from(
for case s"$name: $action" <- input.linesIterator yield
name -> action.match
case s"$x $binop $y" =>
Binary(Operator.valueOf(binop), x, y)
case n =>
Constant(n.toLong)
)

@tailrec
def reachable(names: List[String], source: Source, resolved: Resolved): Resolved = names match
case name :: rest =>
source.get(name) match
case None => resolved // return as name is not reachable
case Some(operation) => operation match
case Binary(op, x, y) =>
(resolved.get(x), resolved.get(y)) match
case (Some(a), Some(b)) =>
reachable(rest, source, resolved + (name -> op.eval(a, b)))
case _ =>
reachable(x :: y :: name :: rest, source, resolved)
case Constant(value) =>
reachable(rest, source, resolved + (name -> value))
case Nil =>
resolved
end reachable

def resolveRoot(input: String): Long =
val values = reachable("root" :: Nil, readAll(input), Map.empty)
values("root")

def whichValue(input: String): Long =
val source = readAll(input) - "humn"

@tailrec
def binarySearch(name: String, goal: Option[Long], resolved: Resolved): Long =

def resolve(name: String) =
val values = reachable(name :: Nil, source, resolved)
values.get(name).map(_ -> values)

def nextGoal(inv: BinOp, value: Long): Long = goal match
case Some(prev) => inv(prev, value)
case None => value

(source.get(name): @unchecked) match
case Some(Operation.Binary(op, x, y)) =>
((resolve(x), resolve(y)): @unchecked) match
case (Some(xValue -> resolvedX), _) => // x is known, y has a hole
binarySearch(y, Some(nextGoal(op.invLeft, xValue)), resolvedX)
case (_, Some(yValue -> resolvedY)) => // y is known, x has a hole
binarySearch(x, Some(nextGoal(op.invRight, yValue)), resolvedY)
case None =>
goal.get // hole found
end binarySearch

binarySearch(goal = None, name = "root", resolved = Map.empty)
end whichValue

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day22/index.html b/2022/puzzles/day22/index.html index 9cdbd415f..f42e4aed7 100644 --- a/2022/puzzles/day22/index.html +++ b/2022/puzzles/day22/index.html @@ -5,13 +5,13 @@ Day 22: Monkey Map | Scala Center Advent of Code - +
Skip to main content
- + \ No newline at end of file diff --git a/2022/puzzles/day23/index.html b/2022/puzzles/day23/index.html index 2578bb0a0..d4b14d09e 100644 --- a/2022/puzzles/day23/index.html +++ b/2022/puzzles/day23/index.html @@ -5,13 +5,13 @@ Day 23: Unstable Diffusion | Scala Center Advent of Code - +
Skip to main content
- + \ No newline at end of file diff --git a/2022/puzzles/day24/index.html b/2022/puzzles/day24/index.html index 9dab00e50..7ec213ada 100644 --- a/2022/puzzles/day24/index.html +++ b/2022/puzzles/day24/index.html @@ -5,13 +5,13 @@ Day 24: Blizzard Basin | Scala Center Advent of Code - +
Skip to main content

Day 24: Blizzard Basin

Puzzle description

https://adventofcode.com/2022/day/24

Solution

Today's problem is similar to Day 12, where we need to find our way through a maze. It's made more challenging by impassable blizzards moving through the maze. We can use a similar approach to that of Day 12 still, but we'll improve a little bit further by using A* search instead of a standard breadth first search.

We'll need some kind of point and a few functions that are useful on the 2d grid. A simple tuple (Int, Int) will suffice, and we'll add the functions as extension methods. We'll use Manhattan distance as the A* heuristic function, and we'll need the neighbours in cardinal directions.

type Coord = (Int, Int)
extension (coord: Coord)
def x = coord._1
def y = coord._2
def up = (coord.x, coord.y - 1)
def down = (coord.x, coord.y + 1)
def left = (coord.x - 1, coord.y)
def right = (coord.x + 1, coord.y)
def cardinals = Seq(coord.up, coord.down, coord.left, coord.right)
def manhattan(rhs: Coord) = (coord.x - rhs.x).abs + (coord.y - rhs.y).abs
def +(rhs: Coord) = (coord.x + rhs.x, coord.y + rhs.y)

Before we get to the search, let's deal with the input.

case class Blizzard(at: Coord, direction: Coord)

def parseMaze(in: Seq[String]) =
val start = (in.head.indexOf('.'), 0) // start in the empty spot in the top row
val end = (in.last.indexOf('.'), in.size - 1) // end in the empty spot in the bottom row
val xDomain = 1 to in.head.size - 2 // where blizzards are allowed to go
val yDomain = 1 to in.size - 2
val initialBlizzards =
for
y <- in.indices
x <- in(y).indices
if in(y)(x) != '.' // these aren't blizzards!
if in(y)(x) != '#'
yield in(y)(x) match
case '>' => Blizzard(at = (x, y), direction = (1, 0))
case '<' => Blizzard(at = (x, y), direction = (-1, 0))
case '^' => Blizzard(at = (x, y), direction = (0, -1))
case 'v' => Blizzard(at = (x, y), direction = (0, 1))

??? // ...to be implemented

Ok, let's deal with the blizzards. The blizzards move toroidally, which is to say they loop around back to the start once they fall off an edge. This means that, eventually, the positions and directions of all blizzards must loop at some point. Naively, after xDomain.size * yDomain.size minutes, every blizzard must have returned to it's original starting location. Let's model that movement and calculate the locations of all the blizzards up until that time. With it, we'll have a way to tell us where the blizzards are at a given time t, for any t.

def move(blizzard: Blizzard, xDomain: Range, yDomain: Range) =
blizzard.copy(at = cycle(blizzard.at + blizzard.direction, xDomain, yDomain))

def cycle(coord: Coord, xDomain: Range, yDomain: Range): Coord = (cycle(coord.x, xDomain), cycle(coord.y, yDomain))

def cycle(n: Int, bounds: Range): Int =
if n > bounds.max then bounds.min // we've fallen off the end, go to start
else if n < bounds.min then bounds.max // we've fallen off the start, go to the end
else n // we're chillin' in bounds still

We can replace the ??? in parseMaze now. And we'll need a return type for the function. We can cram everything into a Maze case class. For the blizzards, we actually only need to care about where they are after this point, as they'll prevent us from moving to those locations. We'll throw away the directions and just keep the set of Coords the blizzards are at.

case class Maze(xDomain: Range, yDomain: Range, blizzards: Seq[Set[Coord]], start: Coord, end: Coord)

def parseMaze(in: Seq[String]): Maze =
/* ...omitted for brevity... */
def tick(blizzards: Seq[Blizzard]) = blizzards.map(move(_, xDomain, yDomain))
val allBlizzardLocations = Iterator.iterate(initialBlizzards)(tick)
.take(xDomain.size * yDomain.size)
.map(_.map(_.at).toSet)
.toIndexedSeq

Maze(xDomain, yDomain, allBlizzardLocations, start, end)

But! We can do a little better for the blizzards. The blizzards actually cycle for any common multiple of xDomain.size and yDomain.size. Using the least common multiple would be sensible to do the least amount of computation.

def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b)
def lcm(a: Int, b: Int): Int = a * b / gcd(a, b)
def tick(blizzards: Seq[Blizzard]) = blizzards.map(move(_, xDomain, yDomain))
val allBlizzardLocations = Iterator.iterate(initialBlizzards)(tick)
.take(lcm(xDomain.size, yDomain.size))
.map(_.map(_.at).toSet)
.toIndexedSeq

Great! Let's solve the maze.

import scala.collection.mutable
case class Step(at: Coord, time: Int)

def solve(maze: Maze): Step =
// order A* options by how far we've taken + an estimated distance to the end
given Ordering[Step] = Ordering[Int].on((step: Step) => step.at.manhattan(maze.end) + step.time).reverse
val queue = mutable.PriorityQueue[Step]()
val visited = mutable.Set.empty[Step]

def inBounds(coord: Coord) = coord match
case c if c == maze.start || c == maze.end => true
case c => maze.xDomain.contains(c.x) && maze.yDomain.contains(c.y)

queue += Step(at = maze.start, time = 0)
while queue.head.at != maze.end do
val step = queue.dequeue
val time = step.time + 1
// where are the blizzards for our next step? we can't go there
val blizzards = maze.blizzards(time % maze.blizzards.size)
// we can move in any cardinal direction, or chose to stay put; but it needs to be in the maze
val options = (step.at.cardinals :+ step.at).filter(inBounds).map(Step(_, time))
// queue up the options if they are possible; and if we have not already queued them
queue ++= options
.filterNot(o => blizzards(o.at)) // the option must not be in a blizzard
.filterNot(visited) // avoid duplicate work
.tapEach(visited.add) // keep track of what we've enqueued

queue.dequeue

That's pretty much it! Part 1 is then:

def part1(in: Seq[String]) = solve(parseMaze(in)).time

Part 2 requires solving the maze 3 times. Make it to the end (so, solve part 1 again), go back to the start, then go back to the end. We can use the same solve function, but we need to generalize a bit so we can start the solver at an arbitrary time. This will allow us to keep the state of the blizzards for subsequent runs. We actually only need to change one line!

def solve(maze: Maze, startingTime: Int = 0): Step =
/* the only line we need to change is... */
queue += Step(at = maze.start, time = startingTime)

Then part 2 requires calling solve 3 times. We need to be a little careful with the start/end locations and starting times.

def part2(in: Seq[String]) =
val maze = parseMaze(in)
val first = solve(maze)
val second = solve(maze.copy(start = maze.end, end = maze.start), first.time)
solve(maze, second.time).time

That's Day 24. Huzzah!

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2022/puzzles/day25/index.html b/2022/puzzles/day25/index.html index 7430f581e..12725bdc3 100644 --- a/2022/puzzles/day25/index.html +++ b/2022/puzzles/day25/index.html @@ -5,13 +5,13 @@ Day 25: Full of Hot Air | Scala Center Advent of Code - +
Skip to main content

Day 25: Full of Hot Air

Puzzle description

https://adventofcode.com/2022/day/25

Final Code

def part1(input: String): String =
totalSnafu(input)

val digitToInt = Map(
'0' -> 0,
'1' -> 1,
'2' -> 2,
'-' -> -1,
'=' -> -2,
)
val intToDigit = digitToInt.map(_.swap)

def showSnafu(value: Long): String =
val reverseDigits = Iterator.unfold(value)(v =>
Option.when(v != 0) {
val mod = math.floorMod(v, 5).toInt
val digit = if mod > 2 then mod - 5 else mod
intToDigit(digit) -> (v - digit) / 5
}
)
if reverseDigits.isEmpty then "0"
else reverseDigits.mkString.reverse

def readSnafu(line: String): Long =
line.foldLeft(0L)((acc, digit) =>
acc * 5 + digitToInt(digit)
)

def totalSnafu(input: String): String =
showSnafu(value = input.linesIterator.map(readSnafu).sum)

Run it in the browser

Part 1 (Only 1 part today)

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2023/index.html b/2023/index.html index 543a4ff5a..ef48d2d0a 100644 --- a/2023/index.html +++ b/2023/index.html @@ -5,13 +5,13 @@ Scala Center Advent of Code | Scala Center Advent of Code - +
Skip to main content

Scala Advent of Code 2023 byScala Center

Credit to https://github.com/OlegIlyenko/scala-icon

Learn Scala 3

A simpler, safer and more concise version of Scala, the famous object-oriented and functional programming language.

Solve Advent of Code puzzles

Challenge your programming skills by solving Advent of Code puzzles.

Share with the community

Get or give support to the community. Share your solutions with the community.

- + \ No newline at end of file diff --git a/2023/puzzles/day01/index.html b/2023/puzzles/day01/index.html index 3593bdb99..9c26b744d 100644 --- a/2023/puzzles/day01/index.html +++ b/2023/puzzles/day01/index.html @@ -5,7 +5,7 @@ Day 1: Trebuchet?! | Scala Center Advent of Code - + @@ -22,7 +22,7 @@ Instead, we manually iterate over all the indices to see if a match starts there. This is equivalent to looking for prefix matches in all the suffixes of line. Conveniently, line.tails iterates over all such suffixes, and Regex.findPrefixOf will look only for prefixes.

Our fixed computation for matches is now:

val matchesIter =
for
lineTail <- line.tails
oneMatch <- digitReprRegex.findPrefixOf(lineTail)
yield
oneMatch
val matches = matchesIter.toList

Final Code

def part1(input: String): String =
// Convert one line into the appropriate coordinates
def lineToCoordinates(line: String): Int =
val firstDigit = line.find(_.isDigit).get
val lastDigit = line.findLast(_.isDigit).get
s"$firstDigit$lastDigit".toInt

// Convert each line to its coordinates and sum all the coordinates
val result = input
.linesIterator
.map(lineToCoordinates(_))
.sum
result.toString()
end part1

/** The textual representation of digits. */
val stringDigitReprs = Map(
"one" -> 1,
"two" -> 2,
"three" -> 3,
"four" -> 4,
"five" -> 5,
"six" -> 6,
"seven" -> 7,
"eight" -> 8,
"nine" -> 9,
)

/** All the string representation of digits, including the digits themselves. */
val digitReprs = stringDigitReprs ++ (1 to 9).map(i => i.toString() -> i)

def part2(input: String): String =
// A regex that matches any of the keys of `digitReprs`
val digitReprRegex = digitReprs.keysIterator.mkString("|").r

def lineToCoordinates(line: String): Int =
// Find all the digit representations in the line
val matchesIter =
for
lineTail <- line.tails
oneMatch <- digitReprRegex.findPrefixOf(lineTail)
yield
oneMatch
val matches = matchesIter.toList

// Convert the string representations into actual digits and form the result
val firstDigit = digitReprs(matches.head)
val lastDigit = digitReprs(matches.last)
s"$firstDigit$lastDigit".toInt
end lineToCoordinates

// Process lines as in part1
val result = input
.linesIterator
.map(lineToCoordinates(_))
.sum
result.toString()
end part2

Run it in the browser

Part 1

Part 2

Solutions from the community

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

- + \ No newline at end of file diff --git a/2023/puzzles/day02/index.html b/2023/puzzles/day02/index.html index f4170ef01..3e45ed1c2 100644 --- a/2023/puzzles/day02/index.html +++ b/2023/puzzles/day02/index.html @@ -5,7 +5,7 @@ Day 2: Cube Conundrum | Scala Center Advent of Code - + @@ -14,7 +14,7 @@ In a single pass over the puzzle input it will:

case class Colors(color: String, count: Int)
case class Game(id: Int, hands: List[List[Colors]])
type Summary = Game => Int

def solution(input: String, summarise: Summary): Int =
input.linesIterator.map(parse andThen summarise).sum

def parse(line: String): Game = ???

part1 and part2 will use this framework, plugging in the appropriate summarise function.

Parsing

Let's fill in the parse function as follows:

def parseColors(pair: String): Colors =
val (s"$value $name") = pair: @unchecked
Colors(color = name, count = value.toInt)

def parse(line: String): Game =
val (s"Game $id: $hands0") = line: @unchecked
val hands1 = hands0.split("; ").toList
val hands2 = hands1.map(_.split(", ").toList.map(parseColors))
Game(id = id.toInt, hands = hands2)

Summary

As described above, to summarise each game, we evaluate it as a possibleGame, where if it is a validGame summarise as the game's id, otherwise 0.

A game is valid if for all hands in the game, all the colors in each hand has a count that is less-than or equal-to the count of same color from the possibleCubes configuration.

val possibleCubes = Map(
"red" -> 12,
"green" -> 13,
"blue" -> 14,
)

def validGame(game: Game): Boolean =
game.hands.forall: hand =>
hand.forall:
case Colors(color, count) =>
count <= possibleCubes.getOrElse(color, 0)

val possibleGame: Summary =
case game if validGame(game) => game.id
case _ => 0

def part1(input: String): Int = solution(input, possibleGame)

Part 2

Summary

In part 2, the summary of a game requires us to find the minimumCubes necessary to make a possible game. What this means is for any given game, across all hands calculating the maximum cubes drawn for each color.

In Scala we can accumulate the maximum counts for each cube in a Map from color to count. Take the initial maximums as all zero:

val initial = Seq("red", "green", "blue").map(_ -> 0).toMap

Then for each game we can compute the maximum cubes drawn in each game as follows

def minimumCubes(game: Game): Int =
var maximums = initial
for
hand <- game.hands
Colors(color, count) <- hand
do
maximums += (color -> (maximums(color) `max` count))
maximums.values.product

Finally we can complete the solution by using minimumCubes to summarise each game:

def part2(input: String): Int = solution(input, minimumCubes)

Final Code

case class Colors(color: String, count: Int)
case class Game(id: Int, hands: List[List[Colors]])
type Summary = Game => Int

def parseColors(pair: String): Colors =
val (s"$value $name") = pair: @unchecked
Colors(color = name, count = value.toInt)

def parse(line: String): Game =
val (s"Game $id: $hands0") = line: @unchecked
val hands1 = hands0.split("; ").toList
val hands2 = hands1.map(_.split(", ").toList.map(parseColors))
Game(id = id.toInt, hands = hands2)

def solution(input: String, summarise: Summary): Int =
input.linesIterator.map(parse andThen summarise).sum

val possibleCubes = Map(
"red" -> 12,
"green" -> 13,
"blue" -> 14,
)

def validGame(game: Game): Boolean =
game.hands.forall: hand =>
hand.forall:
case Colors(color, count) =>
count <= possibleCubes.getOrElse(color, 0)

val possibleGame: Summary =
case game if validGame(game) => game.id
case _ => 0

def part1(input: String): Int = solution(input, possibleGame)

val initial = Seq("red", "green", "blue").map(_ -> 0).toMap

def minimumCubes(game: Game): Int =
var maximums = initial
for
hand <- game.hands
Colors(color, count) <- hand
do
maximums += (color -> (maximums(color) `max` count))
maximums.values.product

def part2(input: String): Int = solution(input, minimumCubes)

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2023/puzzles/day03/index.html b/2023/puzzles/day03/index.html index 2d57e14b1..a179cfb81 100644 --- a/2023/puzzles/day03/index.html +++ b/2023/puzzles/day03/index.html @@ -5,13 +5,13 @@ Day 3: Gear Ratios | Scala Center Advent of Code - +
Skip to main content

Day 3: Gear Ratios

by @bishabosha and @iusildra

Puzzle description

https://adventofcode.com/2023/day/3

Solution summary

The solution models the input as a grid of numbers and symbols.

  1. Define some models to represent the input:
    • case class Coord(x: Int, y: Int) to represent one coordinate on the grid
    • case class Symbol(sym: String, pos: Coord) to represent one symbol and its location
    • case class PartNumber(value: Int, start: Coord, end: Coord) to represent one number and its starting/ending location
  2. Parse the input to create a dense collection of symbols and numbers
  3. Separate the symbols from the numbers
  4. Then summarise the whole grid as follows:
    • in part1, find all numbers adjacent to a symbol, and sum the total of the resulting number values,
    • in part2,
      1. Find all numbers adjacent to a symbol whose char value is *
      2. Filter out the * symbol with less/more than 2 adjacent numbers
      3. For each * symbol remaining, take the product of its two number values
      4. Sum the resulting products
    • a symbol is adjacent to a number (and vice-versa) if that symbol is inside the number's bounding box on the grid at 1 unit away (see manhattan distance)

Global

We want a convenient way to represent a coordinate to be able to compute whether one element is within the bounding box of another.

case class Coord(x: Int, y: Int):
def within(start: Coord, end: Coord) =
if y < start.y || y > end.y then false
else if x < start.x || x > end.x then false
else true

We also want to easily distinguish a Symbol from a Number, and to know wether a Symbol is adjacent to a Number:

case class PartNumber(value: Int, start: Coord, end: Coord)
case class Symbol(sym: String, pos: Coord):
def neighborOf(number: PartNumber) = pos.within(
Coord(number.start.x - 1, number.start.y - 1),
Coord(number.end.x + 1, number.end.y + 1)
)

Then we need to parse the input to get every Symbol and Number:

import scala.util.matching.Regex.Match

object IsInt:
def unapply(in: Match): Option[Int] = in.matched.toIntOption

def findPartsAndSymbols(source: String) =
val extractor = """(\d+)|[^.\d]""".r
source.split("\n").zipWithIndex.flatMap: (line, i) =>
extractor
.findAllMatchIn(line)
.map:
case m @ IsInt(nb) =>
PartNumber(nb, Coord(m.start, i), Coord(m.end - 1, i))
case s => Symbol(s.matched, Coord(s.start, i))

The object IsInt with the .unapply method is called an extractor. It allows to define patterns to match on. Here it will give me a number if it can parse it from a string

The findPartsAndSymbols does the parsing and returns a collection of PartNumber and Symbol. What we want to match on is either a number or a symbol (which is anything except the . and a digit). The regex match gives us some information (such as starting / ending position of the matched string) which we use to create the PartNumber and Symbol instances.

The m @ IsInt(nb) is a pattern match that will match on the IsInt extractor and binds the parsed integer to nb and the value being matched to m. A similar way to achieve this is:

.map: m =>
m match
case IsInt(nb) => PartNumber(nb, Coord(m.start, i), Coord(m.end - 1, i))
case s => Symbol(s.matched, Coord(s.start, i))

Part 1

Compute part1 as described above:

  1. Find all numbers and symbols in the grid
  2. Filter out the symbols in a separate collection
  3. For each number element of the grid and if it has a least one symbol neighbor, return its value
  4. Sum the resulting values
def part1(input: String) =
val all = findPartsAndSymbols(input)
val symbols = all.collect { case s: Symbol => s }
all
.collect:
case n: PartNumber if symbols.exists(_.neighborOf(n)) =>
n.value
.sum

Part 2

We might want to represent a Gear to facilitate the computation of the gear ratios:

case class Gear(part: PartNumber, symbol: Symbol)

(Note: a case class is not necessary here, a tuple would do the job)

Compute part2 as described above:

  1. Find all numbers and symbols in the grid
  2. Filter out the symbols in a separate collection
  3. For each number element of the grid and if it has one * neighbor, return a Gear with the number and the * symbol. For any other cases, return None
    • The .flatMap method will filter out the None values when flattening, so we get a collection of Gear only
  4. Group them by symbol and map the values to the number values
    • So we obtain a Map[Symbol, List[Int]] instead of a Map[Symbol, List[Gear]]
  5. Filter out the symbols with less/more than 2 adjacent numbers
  6. For each entry remaining, take the product of its two number values and sum the resulting products
def part2(input: String) =
val all = findPartsAndSymbols(input)
val symbols = all.collect { case s: Symbol => s }
all
.flatMap:
case n: PartNumber =>
symbols
.find(_.neighborOf(n))
.filter(_.sym == "*")
.map(Gear(n, _))
case _ => None
.groupMap(_.symbol)(_.part.value)
.filter(_._2.length == 2)
.foldLeft(0) { _ + _._2.product }

Final code

case class Coord(x: Int, y: Int):
def within(start: Coord, end: Coord) =
if y < start.y || y > end.y then false
else if x < start.x || x > end.x then false
else true
case class PartNumber(value: Int, start: Coord, end: Coord)
case class Symbol(sym: String, pos: Coord):
def neighborOf(number: PartNumber) = pos.within(
Coord(number.start.x - 1, number.start.y - 1),
Coord(number.end.x + 1, number.end.y + 1)
)

object IsInt:
def unapply(in: Match): Option[Int] = in.matched.toIntOption

def findPartsAndSymbols(source: String) =
val extractor = """(\d+)|[^.\d]""".r
source.split("\n").zipWithIndex.flatMap: (line, i) =>
extractor
.findAllMatchIn(line)
.map:
case m @ IsInt(nb) =>
PartNumber(nb, Coord(m.start, i), Coord(m.end - 1, i))
case s => Symbol(s.matched, Coord(s.start, i))

def part1(input: String) =
val all = findPartsAndSymbols(input)
val symbols = all.collect { case s: Symbol => s }
all
.collect:
case n: PartNumber if symbols.exists(_.neighborOf(n)) =>
n.value
.sum

case class Gear(part: PartNumber, symbol: Symbol)

def part2(input: String) =
val all = findPartsAndSymbols(input)
val symbols = all.collect { case s: Symbol => s }
all
.flatMap:
case n: PartNumber =>
symbols
.find(_.neighborOf(n))
.filter(_.sym == "*")
.map(Gear(n, _))
case _ => None
.groupMap(_.symbol)(_.part.value)
.filter(_._2.length == 2)
.foldLeft(0) { _ + _._2.product }

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2023/puzzles/day04/index.html b/2023/puzzles/day04/index.html index 46752bc99..bddcf2f2c 100644 --- a/2023/puzzles/day04/index.html +++ b/2023/puzzles/day04/index.html @@ -5,7 +5,7 @@ Day 4: Scratchcards | Scala Center Advent of Code - + @@ -33,7 +33,7 @@ card. (It is summed up in numCards in the accumulator.)

Why track by relative index instead of absolute?

Final code

def countWinning(card: String): Int =
val numbers = card
.substring(card.indexOf(":") + 1) // discard "Card X:"
.split(" ")
.filterNot(_.isEmpty())
val (winningNumberStrs, givenNumberStrs) = numbers.span(_ != "|")
val winningNumbers = winningNumberStrs.map(_.toInt).toSet
// drop the initial "|"
val givenNumbers = givenNumberStrs.drop(1).map(_.toInt).toSet
winningNumbers.intersect(givenNumbers).size
end countWinning

def winningCounts(input: String): Iterator[Int] =
input.linesIterator.map(countWinning)
end winningCounts

def part1(input: String): String =
winningCounts(input)
.map(winning => if winning > 0 then Math.pow(2, winning - 1).toInt else 0)
.sum.toString()
end part1

def part2(input: String): String =
winningCounts(input)
// we only track the multiplicities of the next few cards as needed, not all of them;
// and the first element always exists, and corresponds to the current card;
// and the elements are always positive (because there is at least 1 original copy of each card)
.foldLeft((0, Vector(1))){ case ((numCards, multiplicities), winning) =>
val thisMult = multiplicities(0)
val restMult = multiplicities
.drop(1)
// these are the original copies of the next few cards
.padTo(Math.max(1, winning), 1)
.zipWithIndex
// these are the extra copies we just won
.map((mult, idx) => if idx < winning then mult + thisMult else mult)
(numCards + thisMult, restMult)
}
._1.toString()
end part2

Run it in the browser

Part 1

Part 2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2023/puzzles/day05/index.html b/2023/puzzles/day05/index.html index 4dd8f021e..b1456ca20 100644 --- a/2023/puzzles/day05/index.html +++ b/2023/puzzles/day05/index.html @@ -5,7 +5,7 @@ Day 5: If You Give A Seed A Fertilizer | Scala Center Advent of Code - + @@ -27,7 +27,7 @@ destinationStart.

Often, there is part of the interval that is above the end of the current property range. In this case we must make a new sub-interval that begins after the end of the property range.

If we do find an above sub-interval, then we need to check that against the next Property. Otherwise then we can shortcut the computation and not check any of the following properties.

def findNext(resource: Resource, map: ResourceMap): Seq[Resource] =
val ResourceMap(from, to, properties) = map
val (newResources, explore) =
val initial = (Seq.empty[Resource], Option(resource))
properties.foldLeft(initial) {
case ((acc, Some(explore)), prop) =>
val Resource(start, end, _) = explore
val propStart = prop.sourceStart
val propEnd = prop.sourceEnd
val underRange = Option.when(start < propStart)(
Resource(start, Math.min(propStart - 1, end), to)
)
val overlaps =
start >= propStart && start <= propEnd
|| end >= propStart && end <= propEnd
|| start <= propStart && end >= propEnd
val inRange = Option.when(overlaps) {
val delay = prop.destinationStart - propStart
Resource(
Math.max(start, propStart) + delay,
Math.min(end, propEnd) + delay,
to
)
}
val aboveRange = Option.when(end > propEnd)(
Resource(Math.max(start, propEnd + 1), end, to)
)
(Seq(underRange, inRange, acc).flatten, aboveRange)
case ((acc, None), _) => (acc, None)
}
Seq(newResources, explore).flatten
end findNext

Parsing

In this section we list the code to parse the input into ResourceMap and Resource.

object ResourceMap:
// parse resource maps from lines
def buildFromLines(lines: Seq[String]): Seq[ResourceMap] =
def isRangeLine(line: String) =
line.forall(ch => ch.isDigit || ch.isSpaceChar)
lines.filter(line =>
!line.isBlank &&
(line.endsWith("map:") || isRangeLine(line))
).foldLeft(Seq.empty[(String, Seq[String])]) {
case (acc, line) if line.endsWith("map:") =>
(line, Seq.empty) +: acc
case (Seq((definition, properties), last*), line) =>
(definition, line +: properties) +: last
}
.flatMap(build)

def build(map: String, ranges: Seq[String]): Option[ResourceMap] =
val mapRow = map.replace("map:", "").trim.split("-to-")
val properties = ranges
.map(line => line.split(" ").flatMap(_.toLongOption))
.collect:
case Array(startFrom, startTo, range) =>
Property(startFrom, startTo, range)
def resourceKindOf(optStr: Option[String]) =
optStr.map(_.capitalize).map(ResourceKind.valueOf)
for
from <- resourceKindOf(mapRow.headOption)
to <- resourceKindOf(mapRow.lastOption)
yield
ResourceMap(from, to, properties.sortBy(_.sourceStart))
end ResourceMap

object Seeds:
private def parseSeedsRaw(line: String): Seq[Long] =
if !line.startsWith("seeds:") then Seq.empty[Long]
else
line.replace("seeds:", "")
.trim
.split(" ")
.flatMap(_.toLongOption)

// parse seeds without range
def parseWithoutRange(line: String): Seq[Resource] =
parseSeedsRaw(line).map: start =>
Resource(start, start, ResourceKind.Seed)

// parse seeds with range
def parse(line: String): Seq[Resource] =
parseSeedsRaw(line)
.grouped(2)
.map { case Seq(start, length) =>
Resource(start, start + length - 1, ResourceKind.Seed)
}
.toSeq
end Seeds

Final Code

final case class Resource(
start: Long, end: Long, kind: ResourceKind)

enum ResourceKind:
case Seed, Soil, Fertilizer, Water,
Light, Temperature, Humidity, Location

final case class ResourceMap(
from: ResourceKind,
to: ResourceKind,
properties: Seq[Property]
)

final case class Property(
destinationStart: Long,
sourceStart: Long,
rangeLength: Long
):

lazy val sourceEnd: Long = sourceStart + rangeLength - 1
end Property

def findNext(resource: Resource, map: ResourceMap): Seq[Resource] =
val ResourceMap(from, to, properties) = map
val (newResources, explore) =
val initial = (Seq.empty[Resource], Option(resource))
properties.foldLeft(initial) {
case ((acc, Some(explore)), prop) =>
val Resource(start, end, _) = explore
val propStart = prop.sourceStart
val propEnd = prop.sourceEnd
val underRange = Option.when(start < propStart)(
Resource(start, Math.min(propStart - 1, end), to)
)
val overlaps =
start >= propStart && start <= propEnd
|| end >= propStart && end <= propEnd
|| start <= propStart && end >= propEnd
val inRange = Option.when(overlaps) {
val delay = prop.destinationStart - propStart
Resource(
Math.max(start, propStart) + delay,
Math.min(end, propEnd) + delay,
to
)
}
val aboveRange = Option.when(end > propEnd)(
Resource(Math.max(start, propEnd + 1), end, to)
)
(Seq(underRange, inRange, acc).flatten, aboveRange)
case ((acc, None), _) => (acc, None)
}
Seq(newResources, explore).flatten
end findNext

object ResourceMap:
// parse resource maps from lines
def buildFromLines(lines: Seq[String]): Seq[ResourceMap] =
def isRangeLine(line: String) =
line.forall(ch => ch.isDigit || ch.isSpaceChar)
lines.filter(line =>
!line.isBlank &&
(line.endsWith("map:") || isRangeLine(line))
).foldLeft(Seq.empty[(String, Seq[String])]) {
case (acc, line) if line.endsWith("map:") =>
(line, Seq.empty) +: acc
case (Seq((definition, properties), last*), line) =>
(definition, line +: properties) +: last
}
.flatMap(build)

def build(map: String, ranges: Seq[String]): Option[ResourceMap] =
val mapRow = map.replace("map:", "").trim.split("-to-")
val properties = ranges
.map(line => line.split(" ").flatMap(_.toLongOption))
.collect:
case Array(startFrom, startTo, range) =>
Property(startFrom, startTo, range)
def resourceKindOf(optStr: Option[String]) =
optStr.map(_.capitalize).map(ResourceKind.valueOf)
for
from <- resourceKindOf(mapRow.headOption)
to <- resourceKindOf(mapRow.lastOption)
yield
ResourceMap(from, to, properties.sortBy(_.sourceStart))
end ResourceMap

object Seeds:
private def parseSeedsRaw(line: String): Seq[Long] =
if !line.startsWith("seeds:") then Seq.empty[Long]
else
line.replace("seeds:", "")
.trim
.split(" ")
.flatMap(_.toLongOption)

// parse seeds without range
def parseWithoutRange(line: String): Seq[Resource] =
parseSeedsRaw(line).map: start =>
Resource(start, start, ResourceKind.Seed)

// parse seeds with range
def parse(line: String): Seq[Resource] =
parseSeedsRaw(line)
.grouped(2)
.map { case Seq(start, length) =>
Resource(start, start + length - 1, ResourceKind.Seed)
}
.toSeq
end Seeds

def calculate(seeds: Seq[Resource], maps: Seq[ResourceMap]): Long =
def inner(resource: Resource): Seq[Resource] =
if resource.kind == ResourceKind.Location then
Seq(resource)
else
val map = maps.find(_.from == resource.kind).get
findNext(resource, map).flatMap(inner)
seeds.flatMap(inner).minBy(_.start).start
end calculate

type ParseSeeds = String => Seq[Resource]

def solution(input: String, parse: ParseSeeds): Long =
val lines = input.linesIterator.toSeq
val seeds = lines.headOption.map(parse).getOrElse(Seq.empty)
val maps = ResourceMap.buildFromLines(lines)
calculate(seeds, maps)

def part1(input: String): Long =
solution(input, Seeds.parseWithoutRange)

def part2(input: String): Long =
solution(input, Seeds.parse)

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2023/puzzles/day06/index.html b/2023/puzzles/day06/index.html index 19e7c266f..a1f6c9b7e 100644 --- a/2023/puzzles/day06/index.html +++ b/2023/puzzles/day06/index.html @@ -5,7 +5,7 @@ Day 6: Wait For It | Scala Center Advent of Code - + @@ -33,7 +33,7 @@ We can find the roots as follows:

val disc = math.sqrt(t * t - 4 * d)
val root1 = t / 2 - disc / 2
val root2 = t / 2 + disc / 2

Counting the integers between the roots

The idea is to take the ceiling of the smaller root, the floor of the larger root, then count the integers in this closed interval:

root2.floor - root1.ceil + 1

Edge cases

In one of the given test cases with t = 30 and d = 200 both roots happen to be integers themselves: x1 = 10 and x2 = 20.

quad

In this case, the valid solutions are the integers 11, 12, 13, 14, 15, 16, 17, 18 and 19, excluding the roots themselves. So we have to check if either root is an integer itself, and if so, exclude it. Because the roots give us equality x * (t-x) = d. For the lower endpoint of the interval, we'd have to increase it by 1, and for the upper endpoint we'd have to decrease it by 1.

// are the roots integers themselves?
val int1 = root1.ceil.toLong
val endPt1 = if int1 == root1 then int1 + 1L else int1
val int2 = root2.floor.toLong
val endPt2 = if int2 == root2 then int2 - 1L else int2

Parsing the input

Part 2 deals with large numbers, so we'll have to use Long.

For part 1, we can parse both lines (times and distances) to sequences of Long, then zip them.

// input looks like: Time:        61     67     75     71
// we want: 61, 67, 75, 71
def parse1(line: String) = line match
case s"Time: $x" => x.split(" ").filter(_.nonEmpty).map(_.toLong)
case s"Distance: $x" => x.split(" ").filter(_.nonEmpty).map(_.toLong)

For part 2, we can filter out the space characters to obtain one Long value from each line.

// input looks like: Time:        61     67     75     71
// we want: 61677571
def parse2(line: String) = line match
case s"Time: $x" => x.filterNot(_.isSpaceChar).toLong
case s"Distance: $x" => x.filterNot(_.isSpaceChar).toLong

The input is given in two lines, one for times and one for distances. We can split them with .split("\n").

Final code

Remember that for part 1, we need to multiply the individual results!

def parse1(line: String) = line match
case s"Time: $x" => x.split(" ").filter(_.nonEmpty).map(_.toLong)
case s"Distance: $x" => x.split(" ").filter(_.nonEmpty).map(_.toLong)

def parse2(line: String) = line match
case s"Time: $x" => x.filterNot(_.isSpaceChar).toLong
case s"Distance: $x" => x.filterNot(_.isSpaceChar).toLong

def solve(time: Long, distance: Long): Long =
val (t, d) = (time.toDouble, distance.toDouble)
val disc = math.sqrt(t * t - 4 * d)
val (root1, root2) = (t / 2 - disc / 2, t / 2 + disc / 2)

val int1 = root1.ceil.toLong
val endPt1 = if int1 == root1 then int1 + 1L else int1

val int2 = root2.floor.toLong
val endPt2 = if int2 == root2 then int2 - 1L else int2

endPt2 - endPt1 + 1L

def part1(input: String): Long =
val lines = input.split("\n")
val (times, distances) = (parse1(lines(0)), parse1(lines(1)))
val solutions = times.zip(distances).map((t, d) => solve(t, d))
solutions.product
end part1

def part2(input: String): Long =
val lines = input.split("\n")
val (time, distance) = (parse2(lines(0)), parse2(lines(1)))
solve(time, distance)
end part2

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/2023/puzzles/day07/index.html b/2023/puzzles/day07/index.html index c62fc8810..27dba6de8 100644 --- a/2023/puzzles/day07/index.html +++ b/2023/puzzles/day07/index.html @@ -5,13 +5,13 @@ Day 7: Camel Cards | Scala Center Advent of Code - +
-
Skip to main content

Day 7: Camel Cards

by @anatoliykmetyuk

Puzzle description

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

Part 1 Solution

The problem, in its essence, is a simplified version of the classic poker problem where you are required to compare poker hands according to certain rules.

Domain

We'll start by defining the domain of the problem:

type Card = Char
type Hand = String
case class Bet(hand: Hand, bid: Int)
enum HandType:
case HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind

We can then define the constructors to create a Bet and a HandType:

object Bet:
def apply(s: String): Bet = Bet(s.take(5), s.drop(6).toInt)

object HandType:
def apply(hand: Hand): HandType =
val cardCounts: List[Int] =
hand.groupBy(identity).values.toList.map(_.length).sorted.reverse

cardGroups match
case 5 :: _ => HandType.FiveOfAKind
case 4 :: _ => HandType.FourOfAKind
case 3 :: 2 :: Nil => HandType.FullHouse
case 3 :: _ => HandType.ThreeOfAKind
case 2 :: 2 :: _ => HandType.TwoPair
case 2 :: _ => HandType.OnePair
case _ => HandType.HighCard
end apply

A Bet is created from a String e.g. "5678A 364" - that is, the hand and the bid amount.

A HandType is a bit more complicated: it is calculated from Hand - e.g. "5678A" - according to the rules specified in the challenge. Since the essence of hand scoring lies in how many occurrences of a given card there are in the hand, we utilize Scala's declarative collection capabilities to group the cards and calculate their occurrences. We can then use a match expression to look for the occurrences patterns as specified in the challenge, in descending order of value.

Comparison

The objective of the challenge is to sort bets and calculate the final winnings. Let's address the sorting part. Scala collections are good enough at sorting, so we don't need to implement the sorting proper. But for Scala to do its job, it needs to know the ordering function of the elements. We need to define how to compare two bets:

val ranks = "23456789TJQKA"
given cardOrdering: Ordering[Card] = Ordering.by(ranks.indexOf(_))
given handOrdering: Ordering[Hand] = (h1: Hand, h2: Hand) =>
val h1Type = HandType(h1)
val h2Type = HandType(h2)
if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal
else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)
given betOrdering: Ordering[Bet] = Ordering.by(_.hand)

We define three orderings: one for cards, one for hands, and one for bets.

The card ordering is simple: we compare the cards according to their rank. The hand ordering is implemented according to the spec of the challenge: we first compare the hand types, and if they are equal, we compare the individual cards of the hands.

The bet ordering is then defined in terms of hand ordering.

Calculating the winnings

Given the work we've done so far, calculating the winnings is a matter of sorting the bets and calculating the winnings for each:

def calculateWinnings(bets: List[Bet]): Int =
bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum

def parse(input: String): List[Bet] =
input.linesIterator.toList.map(Bet(_))

def part1(input: String): Int =
calculateWinnings(parse(input))

We read the bets from the input string, sort them, and calculate the winnings for each bet.

Part 2 Solution

The second part of the challenge changes the meaning of the J card. Now it's a Joker, which can be used as any card to produce the best hand possible. In practice, it means determining the prevailing card of the hand and becoming that card: such is the winning strategy of using the Joker. Another change in the rules is that now J is the weakest card when used in tiebreaking comparisons.

We can re-use most of the logic of the Part 1 solution. However because of the different set of rules, we need to create an abstraction to describe the rules for each part, then change the hand scoring logic to take the rules abstraction into account.

Rules

We define a Rules trait that encapsulates the rules of the game and implement it for both cases:

trait Rules:
val rankValues: String
val wildcard: Option[Card]

val standardRules = new Rules:
val rankValues = "23456789TJQKA"
val wildcard = None

val jokerRules = new Rules:
val rankValues = "J23456789TQKA"
val wildcard = Some('J')

Comparison

We then need to change the hand type estimation logic to take the rules into account:

object HandType:
def apply(hand: Hand)(using rules: Rules): HandType =
val cardCounts: Map[Card, Int] =
hand.groupBy(identity).mapValues(_.length).toMap

val cardGroups: List[Int] = rules.wildcard match
case Some(card) if cardCounts.keySet.contains(card) =>
val wildcardCount = cardCounts(card)
val cardGroupsNoWildcard = cardCounts.removed(card).values.toList.sorted.reverse
cardGroupsNoWildcard match
case Nil => List(wildcardCount)
case _ => cardGroupsNoWildcard.head + wildcardCount :: cardGroupsNoWildcard.tail
case _ => cardCounts.values.toList.sorted.reverse

cardGroups match
case 5 :: _ => HandType.FiveOfAKind
case 4 :: _ => HandType.FourOfAKind
case 3 :: 2 :: Nil => HandType.FullHouse
case 3 :: _ => HandType.ThreeOfAKind
case 2 :: 2 :: _ => HandType.TwoPair
case 2 :: _ => HandType.OnePair
case _ => HandType.HighCard
end apply
end HandType

The logic is the same as in the Part 1 solution, except that now we need to take the wildcard into account. If the wildcard is present in the hand, we need to calculate the hand type as if the wildcard was not present, and then add the wildcard count to the largest group of cards. If the wildcard is not present, we calculate the hand type as before. We also handle the case when the hand is composed entirely of wildcards.

We then need to change the card comparison logic to also depend on the rules:

given cardOrdering(using rules: Rules): Ordering[Card] = Ordering.by(rules.rankValues.indexOf(_))

The rest of the orderings stay the same, except we need to make them also depend on the Rules as they all use cardOrdering in some way:

given handOrdering(using Rules): Ordering[Hand] = (h1: Hand, h2: Hand) =>
val h1Type = HandType(h1)
val h2Type = HandType(h2)
if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal
else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)
given betOrdering(using Rules): Ordering[Bet] = Ordering.by(_.hand)

Calculating the winnings

The winnings calculation also stays the same, except for the addition of the Rules parameter, which is required for sorting the bets.

def calculateWinnings(bets: List[Bet])(using Rules): Int =
bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum

Finally, we can calculate the winnings as before while specifying the rules under which to do the calculation:

def part2(input: String): Int =
calculateWinnings(parse(input))(using jokerRules)

Complete Code

type Card = Char
type Hand = String

case class Bet(hand: Hand, bid: Int)
object Bet:
def apply(s: String): Bet = Bet(s.take(5), s.drop(6).toInt)

enum HandType:
case HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind
object HandType:
def apply(hand: Hand)(using rules: Rules): HandType =
val cardCounts: Map[Card, Int] =
hand.groupBy(identity).mapValues(_.length).toMap

val cardGroups: List[Int] = rules.wildcard match
case Some(card) if cardCounts.keySet.contains(card) =>
val wildcardCount = cardCounts(card)
val cardGroupsNoWildcard = cardCounts.removed(card).values.toList.sorted.reverse
cardGroupsNoWildcard match
case Nil => List(wildcardCount)
case _ => cardGroupsNoWildcard.head + wildcardCount :: cardGroupsNoWildcard.tail
case _ => cardCounts.values.toList.sorted.reverse

cardGroups match
case 5 :: _ => HandType.FiveOfAKind
case 4 :: _ => HandType.FourOfAKind
case 3 :: 2 :: Nil => HandType.FullHouse
case 3 :: _ => HandType.ThreeOfAKind
case 2 :: 2 :: _ => HandType.TwoPair
case 2 :: _ => HandType.OnePair
case _ => HandType.HighCard
end apply
end HandType

trait Rules:
val rankValues: String
val wildcard: Option[Card]

val standardRules = new Rules:
val rankValues = "23456789TJQKA"
val wildcard = None

val jokerRules = new Rules:
val rankValues = "J23456789TQKA"
val wildcard = Some('J')


given cardOrdering(using rules: Rules): Ordering[Card] = Ordering.by(rules.rankValues.indexOf(_))
given handOrdering(using Rules): Ordering[Hand] = (h1: Hand, h2: Hand) =>
val h1Type = HandType(h1)
val h2Type = HandType(h2)
if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal
else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)
given betOrdering(using Rules): Ordering[Bet] = Ordering.by(_.hand)

def calculateWinnings(bets: List[Bet])(using Rules): Int =
bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum

def parse(input: String): List[Bet] =
input.linesIterator.toList.map(Bet(_))

def part1(input: String): Int =
println(calculateWinnings(parse(input))(using standardRules))

def part2(input: String): Int =
println(calculateWinnings(parse(input))(using jokerRules))

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- +
Skip to main content

Day 7: Camel Cards

by @anatoliykmetyuk

Puzzle description

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

Part 1 Solution

The problem, in its essence, is a simplified version of the classic poker problem where you are required to compare poker hands according to certain rules.

Domain

We'll start by defining the domain of the problem:

type Card = Char
type Hand = String
case class Bet(hand: Hand, bid: Int)
enum HandType:
case HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind

We can then define the constructors to create a Bet and a HandType:

object Bet:
def apply(s: String): Bet = Bet(s.take(5), s.drop(6).toInt)

object HandType:
def apply(hand: Hand): HandType =
val cardCounts: List[Int] =
hand.groupBy(identity).values.toList.map(_.length).sorted.reverse

cardGroups match
case 5 :: _ => HandType.FiveOfAKind
case 4 :: _ => HandType.FourOfAKind
case 3 :: 2 :: Nil => HandType.FullHouse
case 3 :: _ => HandType.ThreeOfAKind
case 2 :: 2 :: _ => HandType.TwoPair
case 2 :: _ => HandType.OnePair
case _ => HandType.HighCard
end apply

A Bet is created from a String e.g. "5678A 364" - that is, the hand and the bid amount.

A HandType is a bit more complicated: it is calculated from Hand - e.g. "5678A" - according to the rules specified in the challenge. Since the essence of hand scoring lies in how many occurrences of a given card there are in the hand, we utilize Scala's declarative collection capabilities to group the cards and calculate their occurrences. We can then use a match expression to look for the occurrences patterns as specified in the challenge, in descending order of value.

Comparison

The objective of the challenge is to sort bets and calculate the final winnings. Let's address the sorting part. Scala collections are good enough at sorting, so we don't need to implement the sorting proper. But for Scala to do its job, it needs to know the ordering function of the elements. We need to define how to compare two bets:

val ranks = "23456789TJQKA"
given cardOrdering: Ordering[Card] = Ordering.by(ranks.indexOf(_))
given handOrdering: Ordering[Hand] = (h1: Hand, h2: Hand) =>
val h1Type = HandType(h1)
val h2Type = HandType(h2)
if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal
else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)
given betOrdering: Ordering[Bet] = Ordering.by(_.hand)

We define three orderings: one for cards, one for hands, and one for bets.

The card ordering is simple: we compare the cards according to their rank. The hand ordering is implemented according to the spec of the challenge: we first compare the hand types, and if they are equal, we compare the individual cards of the hands.

The bet ordering is then defined in terms of hand ordering.

Calculating the winnings

Given the work we've done so far, calculating the winnings is a matter of sorting the bets and calculating the winnings for each:

def calculateWinnings(bets: List[Bet]): Int =
bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum

def parse(input: String): List[Bet] =
input.linesIterator.toList.map(Bet(_))

def part1(input: String): Int =
calculateWinnings(parse(input))

We read the bets from the input string, sort them, and calculate the winnings for each bet.

Part 2 Solution

The second part of the challenge changes the meaning of the J card. Now it's a Joker, which can be used as any card to produce the best hand possible. In practice, it means determining the prevailing card of the hand and becoming that card: such is the winning strategy of using the Joker. Another change in the rules is that now J is the weakest card when used in tiebreaking comparisons.

We can re-use most of the logic of the Part 1 solution. However because of the different set of rules, we need to create an abstraction to describe the rules for each part, then change the hand scoring logic to take the rules abstraction into account.

Rules

We define a Rules trait that encapsulates the rules of the game and implement it for both cases:

trait Rules:
val rankValues: String
val wildcard: Option[Card]

val standardRules = new Rules:
val rankValues = "23456789TJQKA"
val wildcard = None

val jokerRules = new Rules:
val rankValues = "J23456789TQKA"
val wildcard = Some('J')

Comparison

We then need to change the hand type estimation logic to take the rules into account:

object HandType:
def apply(hand: Hand)(using rules: Rules): HandType =
val cardCounts: Map[Card, Int] =
hand.groupBy(identity).mapValues(_.length).toMap

val cardGroups: List[Int] = rules.wildcard match
case Some(card) if cardCounts.keySet.contains(card) =>
val wildcardCount = cardCounts(card)
val cardGroupsNoWildcard = cardCounts.removed(card).values.toList.sorted.reverse
cardGroupsNoWildcard match
case Nil => List(wildcardCount)
case _ => cardGroupsNoWildcard.head + wildcardCount :: cardGroupsNoWildcard.tail
case _ => cardCounts.values.toList.sorted.reverse

cardGroups match
case 5 :: _ => HandType.FiveOfAKind
case 4 :: _ => HandType.FourOfAKind
case 3 :: 2 :: Nil => HandType.FullHouse
case 3 :: _ => HandType.ThreeOfAKind
case 2 :: 2 :: _ => HandType.TwoPair
case 2 :: _ => HandType.OnePair
case _ => HandType.HighCard
end apply
end HandType

The logic is the same as in the Part 1 solution, except that now we need to take the wildcard into account. If the wildcard is present in the hand, we need to calculate the hand type as if the wildcard was not present, and then add the wildcard count to the largest group of cards. If the wildcard is not present, we calculate the hand type as before. We also handle the case when the hand is composed entirely of wildcards.

We then need to change the card comparison logic to also depend on the rules:

given cardOrdering(using rules: Rules): Ordering[Card] = Ordering.by(rules.rankValues.indexOf(_))

The rest of the orderings stay the same, except we need to make them also depend on the Rules as they all use cardOrdering in some way:

given handOrdering(using Rules): Ordering[Hand] = (h1: Hand, h2: Hand) =>
val h1Type = HandType(h1)
val h2Type = HandType(h2)
if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal
else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)
given betOrdering(using Rules): Ordering[Bet] = Ordering.by(_.hand)

Calculating the winnings

The winnings calculation also stays the same, except for the addition of the Rules parameter, which is required for sorting the bets.

def calculateWinnings(bets: List[Bet])(using Rules): Int =
bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum

Finally, we can calculate the winnings as before while specifying the rules under which to do the calculation:

def part2(input: String): Int =
calculateWinnings(parse(input))(using jokerRules)

Complete Code

type Card = Char
type Hand = String

case class Bet(hand: Hand, bid: Int)
object Bet:
def apply(s: String): Bet = Bet(s.take(5), s.drop(6).toInt)

enum HandType:
case HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind
object HandType:
def apply(hand: Hand)(using rules: Rules): HandType =
val cardCounts: Map[Card, Int] =
hand.groupBy(identity).mapValues(_.length).toMap

val cardGroups: List[Int] = rules.wildcard match
case Some(card) if cardCounts.keySet.contains(card) =>
val wildcardCount = cardCounts(card)
val cardGroupsNoWildcard = cardCounts.removed(card).values.toList.sorted.reverse
cardGroupsNoWildcard match
case Nil => List(wildcardCount)
case _ => cardGroupsNoWildcard.head + wildcardCount :: cardGroupsNoWildcard.tail
case _ => cardCounts.values.toList.sorted.reverse

cardGroups match
case 5 :: _ => HandType.FiveOfAKind
case 4 :: _ => HandType.FourOfAKind
case 3 :: 2 :: Nil => HandType.FullHouse
case 3 :: _ => HandType.ThreeOfAKind
case 2 :: 2 :: _ => HandType.TwoPair
case 2 :: _ => HandType.OnePair
case _ => HandType.HighCard
end apply
end HandType

trait Rules:
val rankValues: String
val wildcard: Option[Card]

val standardRules = new Rules:
val rankValues = "23456789TJQKA"
val wildcard = None

val jokerRules = new Rules:
val rankValues = "J23456789TQKA"
val wildcard = Some('J')


given cardOrdering(using rules: Rules): Ordering[Card] = Ordering.by(rules.rankValues.indexOf(_))
given handOrdering(using Rules): Ordering[Hand] = (h1: Hand, h2: Hand) =>
val h1Type = HandType(h1)
val h2Type = HandType(h2)
if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal
else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)
given betOrdering(using Rules): Ordering[Bet] = Ordering.by(_.hand)

def calculateWinnings(bets: List[Bet])(using Rules): Int =
bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum

def parse(input: String): List[Bet] =
input.linesIterator.toList.map(Bet(_))

def part1(input: String): Int =
println(calculateWinnings(parse(input))(using standardRules))

def part2(input: String): Int =
println(calculateWinnings(parse(input))(using jokerRules))

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

+ \ No newline at end of file diff --git a/2023/puzzles/day08/index.html b/2023/puzzles/day08/index.html index de5e35115..2f9b34a12 100644 --- a/2023/puzzles/day08/index.html +++ b/2023/puzzles/day08/index.html @@ -5,13 +5,13 @@ Day 8: Haunted Wasteland | Scala Center Advent of Code - +
Skip to main content

Day 8: Haunted Wasteland

Puzzle description

https://adventofcode.com/2023/day/8

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/404.html b/404.html index e100b0e60..54092bab2 100644 --- a/404.html +++ b/404.html @@ -5,13 +5,13 @@ Page Not Found | Scala Center Advent of Code - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/assets/js/de026f3e.d21d9c34.js b/assets/js/de026f3e.19ddd6d8.js similarity index 95% rename from assets/js/de026f3e.d21d9c34.js rename to assets/js/de026f3e.19ddd6d8.js index 4301e3a7f..fe3c88b8a 100644 --- a/assets/js/de026f3e.d21d9c34.js +++ b/assets/js/de026f3e.19ddd6d8.js @@ -1 +1 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[1220],{5961:(e,a,n)=>{n.r(a),n.d(a,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>r,metadata:()=>s,toc:()=>o});var t=n(7462),i=(n(7294),n(3905));n(6340);const r={},l="Day 7: Camel Cards",s={unversionedId:"2023/puzzles/day07",id:"2023/puzzles/day07",title:"Day 7: Camel Cards",description:"by @anatoliykmetyuk",source:"@site/target/mdoc/2023/puzzles/day07.md",sourceDirName:"2023/puzzles",slug:"/2023/puzzles/day07",permalink:"/scala-advent-of-code/2023/puzzles/day07",draft:!1,editUrl:"https://github.com/scalacenter/scala-advent-of-code/edit/website/docs/2023/puzzles/day07.md",tags:[],version:"current",frontMatter:{},sidebar:"adventOfCodeSidebar",previous:{title:"Day 6: Wait For It",permalink:"/scala-advent-of-code/2023/puzzles/day06"},next:{title:"Day 8: Haunted Wasteland",permalink:"/scala-advent-of-code/2023/puzzles/day08"}},d={},o=[{value:"Puzzle description",id:"puzzle-description",level:2},{value:"Part 1 Solution",id:"part-1-solution",level:2},{value:"Domain",id:"domain",level:3},{value:"Comparison",id:"comparison",level:3},{value:"Calculating the winnings",id:"calculating-the-winnings",level:3},{value:"Part 2 Solution",id:"part-2-solution",level:2},{value:"Rules",id:"rules",level:3},{value:"Comparison",id:"comparison-1",level:3},{value:"Calculating the winnings",id:"calculating-the-winnings-1",level:3},{value:"Complete Code",id:"complete-code",level:2},{value:"Solutions from the community",id:"solutions-from-the-community",level:2}],c={toc:o};function p(e){let{components:a,...n}=e;return(0,i.kt)("wrapper",(0,t.Z)({},c,n,{components:a,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"day-7-camel-cards"},"Day 7: Camel Cards"),(0,i.kt)("p",null,"by ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/anatoliykmetyuk"},"@anatoliykmetyuk")),(0,i.kt)("h2",{id:"puzzle-description"},"Puzzle description"),(0,i.kt)("p",null,(0,i.kt)("a",{parentName:"p",href:"https://adventofcode.com/2023/day/7"},"https://adventofcode.com/2023/day/7")),(0,i.kt)("h2",{id:"part-1-solution"},"Part 1 Solution"),(0,i.kt)("p",null,"The problem, in its essence, is a simplified version of the classic poker problem where you are required to compare poker hands according to certain rules."),(0,i.kt)("h3",{id:"domain"},"Domain"),(0,i.kt)("p",null,"We'll start by defining the domain of the problem:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"type Card = Char\ntype Hand = String\ncase class Bet(hand: Hand, bid: Int)\nenum HandType:\n case HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind\n")),(0,i.kt)("p",null,"We can then define the constructors to create a ",(0,i.kt)("inlineCode",{parentName:"p"},"Bet")," and a ",(0,i.kt)("inlineCode",{parentName:"p"},"HandType"),":"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"object Bet:\n def apply(s: String): Bet = Bet(s.take(5), s.drop(6).toInt)\n\nobject HandType:\n def apply(hand: Hand): HandType =\n val cardCounts: List[Int] =\n hand.groupBy(identity).values.toList.map(_.length).sorted.reverse\n\n cardGroups match\n case 5 :: _ => HandType.FiveOfAKind\n case 4 :: _ => HandType.FourOfAKind\n case 3 :: 2 :: Nil => HandType.FullHouse\n case 3 :: _ => HandType.ThreeOfAKind\n case 2 :: 2 :: _ => HandType.TwoPair\n case 2 :: _ => HandType.OnePair\n case _ => HandType.HighCard\n end apply\n")),(0,i.kt)("p",null,"A ",(0,i.kt)("inlineCode",{parentName:"p"},"Bet")," is created from a ",(0,i.kt)("inlineCode",{parentName:"p"},"String")," e.g. ",(0,i.kt)("inlineCode",{parentName:"p"},'"5678A 364"')," - that is, the hand and the bid amount."),(0,i.kt)("p",null,"A ",(0,i.kt)("inlineCode",{parentName:"p"},"HandType")," is a bit more complicated: it is calculated from ",(0,i.kt)("inlineCode",{parentName:"p"},"Hand")," - e.g. ",(0,i.kt)("inlineCode",{parentName:"p"},'"5678A"')," - according to the rules specified in the challenge. Since the essence of hand scoring lies in how many occurrences of a given card there are in the hand, we utilize Scala's declarative collection capabilities to group the cards and calculate their occurrences. We can then use a ",(0,i.kt)("inlineCode",{parentName:"p"},"match")," expression to look for the occurrences patterns as specified in the challenge, in descending order of value."),(0,i.kt)("h3",{id:"comparison"},"Comparison"),(0,i.kt)("p",null,"The objective of the challenge is to sort bets and calculate the final winnings. Let's address the sorting part. Scala collections are good enough at sorting, so we don't need to implement the sorting proper. But for Scala to do its job, it needs to know the ordering function of the elements. We need to define how to compare two bets:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'val ranks = "23456789TJQKA"\ngiven cardOrdering: Ordering[Card] = Ordering.by(ranks.indexOf(_))\ngiven handOrdering: Ordering[Hand] = (h1: Hand, h2: Hand) =>\n val h1Type = HandType(h1)\n val h2Type = HandType(h2)\n if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal\n else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)\ngiven betOrdering: Ordering[Bet] = Ordering.by(_.hand)\n')),(0,i.kt)("p",null,"We define three orderings: one for cards, one for hands, and one for bets."),(0,i.kt)("p",null,"The card ordering is simple: we compare the cards according to their rank. The hand ordering is implemented according to the spec of the challenge: we first compare the hand types, and if they are equal, we compare the individual cards of the hands."),(0,i.kt)("p",null,"The bet ordering is then defined in terms of hand ordering."),(0,i.kt)("h3",{id:"calculating-the-winnings"},"Calculating the winnings"),(0,i.kt)("p",null,"Given the work we've done so far, calculating the winnings is a matter of sorting the bets and calculating the winnings for each:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"def calculateWinnings(bets: List[Bet]): Int =\n bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum\n\ndef parse(input: String): List[Bet] =\n input.linesIterator.toList.map(Bet(_))\n\ndef part1(input: String): Int =\n calculateWinnings(parse(input))\n")),(0,i.kt)("p",null,"We read the bets from the input string, sort them, and calculate the winnings for each bet."),(0,i.kt)("h2",{id:"part-2-solution"},"Part 2 Solution"),(0,i.kt)("p",null,"The second part of the challenge changes the meaning of the ",(0,i.kt)("inlineCode",{parentName:"p"},"J")," card. Now it's a Joker, which can be used as any card to produce the best hand possible. In practice, it means determining the prevailing card of the hand and becoming that card: such is the winning strategy of using the Joker. Another change in the rules is that now ",(0,i.kt)("inlineCode",{parentName:"p"},"J")," is the weakest card when used in tiebreaking comparisons."),(0,i.kt)("p",null,"We can re-use most of the logic of the Part 1 solution. However because of the different set of rules, we need to create an abstraction to describe the rules for each part, then change the hand scoring logic to take the rules abstraction into account."),(0,i.kt)("h3",{id:"rules"},"Rules"),(0,i.kt)("p",null,"We define a ",(0,i.kt)("inlineCode",{parentName:"p"},"Rules")," trait that encapsulates the rules of the game and implement it for both cases:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'trait Rules:\n val rankValues: String\n val wildcard: Option[Card]\n\nval standardRules = new Rules:\n val rankValues = "23456789TJQKA"\n val wildcard = None\n\nval jokerRules = new Rules:\n val rankValues = "J23456789TQKA"\n val wildcard = Some(\'J\')\n')),(0,i.kt)("h3",{id:"comparison-1"},"Comparison"),(0,i.kt)("p",null,"We then need to change the hand type estimation logic to take the rules into account:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"object HandType:\n def apply(hand: Hand)(using rules: Rules): HandType =\n val cardCounts: Map[Card, Int] =\n hand.groupBy(identity).mapValues(_.length).toMap\n\n val cardGroups: List[Int] = rules.wildcard match\n case Some(card) if cardCounts.keySet.contains(card) =>\n val wildcardCount = cardCounts(card)\n val cardGroupsNoWildcard = cardCounts.removed(card).values.toList.sorted.reverse\n cardGroupsNoWildcard match\n case Nil => List(wildcardCount)\n case _ => cardGroupsNoWildcard.head + wildcardCount :: cardGroupsNoWildcard.tail\n case _ => cardCounts.values.toList.sorted.reverse\n\n cardGroups match\n case 5 :: _ => HandType.FiveOfAKind\n case 4 :: _ => HandType.FourOfAKind\n case 3 :: 2 :: Nil => HandType.FullHouse\n case 3 :: _ => HandType.ThreeOfAKind\n case 2 :: 2 :: _ => HandType.TwoPair\n case 2 :: _ => HandType.OnePair\n case _ => HandType.HighCard\n end apply\nend HandType\n")),(0,i.kt)("p",null,"The logic is the same as in the Part 1 solution, except that now we need to take the wildcard into account. If the wildcard is present in the hand, we need to calculate the hand type as if the wildcard was not present, and then add the wildcard count to the largest group of cards. If the wildcard is not present, we calculate the hand type as before. We also handle the case when the hand is composed entirely of wildcards."),(0,i.kt)("p",null,"We then need to change the card comparison logic to also depend on the rules:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"given cardOrdering(using rules: Rules): Ordering[Card] = Ordering.by(rules.rankValues.indexOf(_))\n")),(0,i.kt)("p",null,"The rest of the orderings stay the same, except we need to make them also depend on the ",(0,i.kt)("inlineCode",{parentName:"p"},"Rules")," as they all use ",(0,i.kt)("inlineCode",{parentName:"p"},"cardOrdering")," in some way:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"given handOrdering(using Rules): Ordering[Hand] = (h1: Hand, h2: Hand) =>\n val h1Type = HandType(h1)\n val h2Type = HandType(h2)\n if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal\n else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)\ngiven betOrdering(using Rules): Ordering[Bet] = Ordering.by(_.hand)\n")),(0,i.kt)("h3",{id:"calculating-the-winnings-1"},"Calculating the winnings"),(0,i.kt)("p",null,"The winnings calculation also stays the same, except for the addition of the ",(0,i.kt)("inlineCode",{parentName:"p"},"Rules")," parameter, which is required for sorting the bets."),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"def calculateWinnings(bets: List[Bet])(using Rules): Int =\n bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum\n")),(0,i.kt)("p",null,"Finally, we can calculate the winnings as before while specifying the rules under which to do the calculation:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"def part2(input: String): Int =\n calculateWinnings(parse(input))(using jokerRules)\n")),(0,i.kt)("h2",{id:"complete-code"},"Complete Code"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'type Card = Char\ntype Hand = String\n\ncase class Bet(hand: Hand, bid: Int)\nobject Bet:\n def apply(s: String): Bet = Bet(s.take(5), s.drop(6).toInt)\n\nenum HandType:\n case HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind\nobject HandType:\n def apply(hand: Hand)(using rules: Rules): HandType =\n val cardCounts: Map[Card, Int] =\n hand.groupBy(identity).mapValues(_.length).toMap\n\n val cardGroups: List[Int] = rules.wildcard match\n case Some(card) if cardCounts.keySet.contains(card) =>\n val wildcardCount = cardCounts(card)\n val cardGroupsNoWildcard = cardCounts.removed(card).values.toList.sorted.reverse\n cardGroupsNoWildcard match\n case Nil => List(wildcardCount)\n case _ => cardGroupsNoWildcard.head + wildcardCount :: cardGroupsNoWildcard.tail\n case _ => cardCounts.values.toList.sorted.reverse\n\n cardGroups match\n case 5 :: _ => HandType.FiveOfAKind\n case 4 :: _ => HandType.FourOfAKind\n case 3 :: 2 :: Nil => HandType.FullHouse\n case 3 :: _ => HandType.ThreeOfAKind\n case 2 :: 2 :: _ => HandType.TwoPair\n case 2 :: _ => HandType.OnePair\n case _ => HandType.HighCard\n end apply\nend HandType\n\ntrait Rules:\n val rankValues: String\n val wildcard: Option[Card]\n\nval standardRules = new Rules:\n val rankValues = "23456789TJQKA"\n val wildcard = None\n\nval jokerRules = new Rules:\n val rankValues = "J23456789TQKA"\n val wildcard = Some(\'J\')\n\n\ngiven cardOrdering(using rules: Rules): Ordering[Card] = Ordering.by(rules.rankValues.indexOf(_))\ngiven handOrdering(using Rules): Ordering[Hand] = (h1: Hand, h2: Hand) =>\n val h1Type = HandType(h1)\n val h2Type = HandType(h2)\n if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal\n else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)\ngiven betOrdering(using Rules): Ordering[Bet] = Ordering.by(_.hand)\n\ndef calculateWinnings(bets: List[Bet])(using Rules): Int =\n bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum\n\ndef parse(input: String): List[Bet] =\n input.linesIterator.toList.map(Bet(_))\n\ndef part1(input: String): Int =\n println(calculateWinnings(parse(input))(using standardRules))\n\ndef part2(input: String): Int =\n println(calculateWinnings(parse(input))(using jokerRules))\n')),(0,i.kt)("h2",{id:"solutions-from-the-community"},"Solutions from the community"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/spamegg1/advent-of-code-2023-scala/blob/solutions/07.worksheet.sc#L132"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/spamegg1"},"Spamegg")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/prinsniels/AdventOfCode2023/blob/main/src/main/scala/solutions/day07.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/prinsniels"},"Niels Prins")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/lenguyenthanh/aoc-2023/blob/main/Day07.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/lenguyenthanh"},"Thanh Le")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/xRuiAlves/advent-of-code-2023/blob/main/Day7.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/xRuiAlves/"},"Rui Alves")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/Philippus/adventofcode/blob/main/src/main/scala/adventofcode2023/Day07.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/philippus"},"Philippus Baalman")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/GrigoriiBerezin/advent_code_2023/tree/master/task07/src/main/scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/GrigoriiBerezin"},"g.berezin")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/bishabosha/advent-of-code-2023/blob/main/2023-day07.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/bishabosha"},"Jamie Thompson")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/alexandru/advent-of-code/blob/main/scala3/2023/src/main/scala/day7.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/alexandru/"},"Alexandru Nedelcu")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/guycastle/advent_of_code_2023/blob/main/src/main/scala/days/day07/DaySeven.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/guycastle/"},"Guillaume Vandecasteele")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/jnclt/adventofcode2023/blob/main/day07/camel-cards.sc"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/jnclt"},"jnclt")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://gist.github.com/thanhbv/38bd6676d3348334db58e4926add0a11"},"Solution")," by thanhbv")),(0,i.kt)("p",null,"Share your solution to the Scala community by editing this page. (You can even write the whole article!)"))}p.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[1220],{5961:(e,a,n)=>{n.r(a),n.d(a,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>r,metadata:()=>s,toc:()=>o});var t=n(7462),i=(n(7294),n(3905));n(6340);const r={},l="Day 7: Camel Cards",s={unversionedId:"2023/puzzles/day07",id:"2023/puzzles/day07",title:"Day 7: Camel Cards",description:"by @anatoliykmetyuk",source:"@site/target/mdoc/2023/puzzles/day07.md",sourceDirName:"2023/puzzles",slug:"/2023/puzzles/day07",permalink:"/scala-advent-of-code/2023/puzzles/day07",draft:!1,editUrl:"https://github.com/scalacenter/scala-advent-of-code/edit/website/docs/2023/puzzles/day07.md",tags:[],version:"current",frontMatter:{},sidebar:"adventOfCodeSidebar",previous:{title:"Day 6: Wait For It",permalink:"/scala-advent-of-code/2023/puzzles/day06"},next:{title:"Day 8: Haunted Wasteland",permalink:"/scala-advent-of-code/2023/puzzles/day08"}},d={},o=[{value:"Puzzle description",id:"puzzle-description",level:2},{value:"Part 1 Solution",id:"part-1-solution",level:2},{value:"Domain",id:"domain",level:3},{value:"Comparison",id:"comparison",level:3},{value:"Calculating the winnings",id:"calculating-the-winnings",level:3},{value:"Part 2 Solution",id:"part-2-solution",level:2},{value:"Rules",id:"rules",level:3},{value:"Comparison",id:"comparison-1",level:3},{value:"Calculating the winnings",id:"calculating-the-winnings-1",level:3},{value:"Complete Code",id:"complete-code",level:2},{value:"Solutions from the community",id:"solutions-from-the-community",level:2}],c={toc:o};function p(e){let{components:a,...n}=e;return(0,i.kt)("wrapper",(0,t.Z)({},c,n,{components:a,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"day-7-camel-cards"},"Day 7: Camel Cards"),(0,i.kt)("p",null,"by ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/anatoliykmetyuk"},"@anatoliykmetyuk")),(0,i.kt)("h2",{id:"puzzle-description"},"Puzzle description"),(0,i.kt)("p",null,(0,i.kt)("a",{parentName:"p",href:"https://adventofcode.com/2023/day/7"},"https://adventofcode.com/2023/day/7")),(0,i.kt)("h2",{id:"part-1-solution"},"Part 1 Solution"),(0,i.kt)("p",null,"The problem, in its essence, is a simplified version of the classic poker problem where you are required to compare poker hands according to certain rules."),(0,i.kt)("h3",{id:"domain"},"Domain"),(0,i.kt)("p",null,"We'll start by defining the domain of the problem:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"type Card = Char\ntype Hand = String\ncase class Bet(hand: Hand, bid: Int)\nenum HandType:\n case HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind\n")),(0,i.kt)("p",null,"We can then define the constructors to create a ",(0,i.kt)("inlineCode",{parentName:"p"},"Bet")," and a ",(0,i.kt)("inlineCode",{parentName:"p"},"HandType"),":"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"object Bet:\n def apply(s: String): Bet = Bet(s.take(5), s.drop(6).toInt)\n\nobject HandType:\n def apply(hand: Hand): HandType =\n val cardCounts: List[Int] =\n hand.groupBy(identity).values.toList.map(_.length).sorted.reverse\n\n cardGroups match\n case 5 :: _ => HandType.FiveOfAKind\n case 4 :: _ => HandType.FourOfAKind\n case 3 :: 2 :: Nil => HandType.FullHouse\n case 3 :: _ => HandType.ThreeOfAKind\n case 2 :: 2 :: _ => HandType.TwoPair\n case 2 :: _ => HandType.OnePair\n case _ => HandType.HighCard\n end apply\n")),(0,i.kt)("p",null,"A ",(0,i.kt)("inlineCode",{parentName:"p"},"Bet")," is created from a ",(0,i.kt)("inlineCode",{parentName:"p"},"String")," e.g. ",(0,i.kt)("inlineCode",{parentName:"p"},'"5678A 364"')," - that is, the hand and the bid amount."),(0,i.kt)("p",null,"A ",(0,i.kt)("inlineCode",{parentName:"p"},"HandType")," is a bit more complicated: it is calculated from ",(0,i.kt)("inlineCode",{parentName:"p"},"Hand")," - e.g. ",(0,i.kt)("inlineCode",{parentName:"p"},'"5678A"')," - according to the rules specified in the challenge. Since the essence of hand scoring lies in how many occurrences of a given card there are in the hand, we utilize Scala's declarative collection capabilities to group the cards and calculate their occurrences. We can then use a ",(0,i.kt)("inlineCode",{parentName:"p"},"match")," expression to look for the occurrences patterns as specified in the challenge, in descending order of value."),(0,i.kt)("h3",{id:"comparison"},"Comparison"),(0,i.kt)("p",null,"The objective of the challenge is to sort bets and calculate the final winnings. Let's address the sorting part. Scala collections are good enough at sorting, so we don't need to implement the sorting proper. But for Scala to do its job, it needs to know the ordering function of the elements. We need to define how to compare two bets:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'val ranks = "23456789TJQKA"\ngiven cardOrdering: Ordering[Card] = Ordering.by(ranks.indexOf(_))\ngiven handOrdering: Ordering[Hand] = (h1: Hand, h2: Hand) =>\n val h1Type = HandType(h1)\n val h2Type = HandType(h2)\n if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal\n else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)\ngiven betOrdering: Ordering[Bet] = Ordering.by(_.hand)\n')),(0,i.kt)("p",null,"We define three orderings: one for cards, one for hands, and one for bets."),(0,i.kt)("p",null,"The card ordering is simple: we compare the cards according to their rank. The hand ordering is implemented according to the spec of the challenge: we first compare the hand types, and if they are equal, we compare the individual cards of the hands."),(0,i.kt)("p",null,"The bet ordering is then defined in terms of hand ordering."),(0,i.kt)("h3",{id:"calculating-the-winnings"},"Calculating the winnings"),(0,i.kt)("p",null,"Given the work we've done so far, calculating the winnings is a matter of sorting the bets and calculating the winnings for each:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"def calculateWinnings(bets: List[Bet]): Int =\n bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum\n\ndef parse(input: String): List[Bet] =\n input.linesIterator.toList.map(Bet(_))\n\ndef part1(input: String): Int =\n calculateWinnings(parse(input))\n")),(0,i.kt)("p",null,"We read the bets from the input string, sort them, and calculate the winnings for each bet."),(0,i.kt)("h2",{id:"part-2-solution"},"Part 2 Solution"),(0,i.kt)("p",null,"The second part of the challenge changes the meaning of the ",(0,i.kt)("inlineCode",{parentName:"p"},"J")," card. Now it's a Joker, which can be used as any card to produce the best hand possible. In practice, it means determining the prevailing card of the hand and becoming that card: such is the winning strategy of using the Joker. Another change in the rules is that now ",(0,i.kt)("inlineCode",{parentName:"p"},"J")," is the weakest card when used in tiebreaking comparisons."),(0,i.kt)("p",null,"We can re-use most of the logic of the Part 1 solution. However because of the different set of rules, we need to create an abstraction to describe the rules for each part, then change the hand scoring logic to take the rules abstraction into account."),(0,i.kt)("h3",{id:"rules"},"Rules"),(0,i.kt)("p",null,"We define a ",(0,i.kt)("inlineCode",{parentName:"p"},"Rules")," trait that encapsulates the rules of the game and implement it for both cases:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'trait Rules:\n val rankValues: String\n val wildcard: Option[Card]\n\nval standardRules = new Rules:\n val rankValues = "23456789TJQKA"\n val wildcard = None\n\nval jokerRules = new Rules:\n val rankValues = "J23456789TQKA"\n val wildcard = Some(\'J\')\n')),(0,i.kt)("h3",{id:"comparison-1"},"Comparison"),(0,i.kt)("p",null,"We then need to change the hand type estimation logic to take the rules into account:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"object HandType:\n def apply(hand: Hand)(using rules: Rules): HandType =\n val cardCounts: Map[Card, Int] =\n hand.groupBy(identity).mapValues(_.length).toMap\n\n val cardGroups: List[Int] = rules.wildcard match\n case Some(card) if cardCounts.keySet.contains(card) =>\n val wildcardCount = cardCounts(card)\n val cardGroupsNoWildcard = cardCounts.removed(card).values.toList.sorted.reverse\n cardGroupsNoWildcard match\n case Nil => List(wildcardCount)\n case _ => cardGroupsNoWildcard.head + wildcardCount :: cardGroupsNoWildcard.tail\n case _ => cardCounts.values.toList.sorted.reverse\n\n cardGroups match\n case 5 :: _ => HandType.FiveOfAKind\n case 4 :: _ => HandType.FourOfAKind\n case 3 :: 2 :: Nil => HandType.FullHouse\n case 3 :: _ => HandType.ThreeOfAKind\n case 2 :: 2 :: _ => HandType.TwoPair\n case 2 :: _ => HandType.OnePair\n case _ => HandType.HighCard\n end apply\nend HandType\n")),(0,i.kt)("p",null,"The logic is the same as in the Part 1 solution, except that now we need to take the wildcard into account. If the wildcard is present in the hand, we need to calculate the hand type as if the wildcard was not present, and then add the wildcard count to the largest group of cards. If the wildcard is not present, we calculate the hand type as before. We also handle the case when the hand is composed entirely of wildcards."),(0,i.kt)("p",null,"We then need to change the card comparison logic to also depend on the rules:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"given cardOrdering(using rules: Rules): Ordering[Card] = Ordering.by(rules.rankValues.indexOf(_))\n")),(0,i.kt)("p",null,"The rest of the orderings stay the same, except we need to make them also depend on the ",(0,i.kt)("inlineCode",{parentName:"p"},"Rules")," as they all use ",(0,i.kt)("inlineCode",{parentName:"p"},"cardOrdering")," in some way:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"given handOrdering(using Rules): Ordering[Hand] = (h1: Hand, h2: Hand) =>\n val h1Type = HandType(h1)\n val h2Type = HandType(h2)\n if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal\n else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)\ngiven betOrdering(using Rules): Ordering[Bet] = Ordering.by(_.hand)\n")),(0,i.kt)("h3",{id:"calculating-the-winnings-1"},"Calculating the winnings"),(0,i.kt)("p",null,"The winnings calculation also stays the same, except for the addition of the ",(0,i.kt)("inlineCode",{parentName:"p"},"Rules")," parameter, which is required for sorting the bets."),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"def calculateWinnings(bets: List[Bet])(using Rules): Int =\n bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum\n")),(0,i.kt)("p",null,"Finally, we can calculate the winnings as before while specifying the rules under which to do the calculation:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"def part2(input: String): Int =\n calculateWinnings(parse(input))(using jokerRules)\n")),(0,i.kt)("h2",{id:"complete-code"},"Complete Code"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'type Card = Char\ntype Hand = String\n\ncase class Bet(hand: Hand, bid: Int)\nobject Bet:\n def apply(s: String): Bet = Bet(s.take(5), s.drop(6).toInt)\n\nenum HandType:\n case HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind\nobject HandType:\n def apply(hand: Hand)(using rules: Rules): HandType =\n val cardCounts: Map[Card, Int] =\n hand.groupBy(identity).mapValues(_.length).toMap\n\n val cardGroups: List[Int] = rules.wildcard match\n case Some(card) if cardCounts.keySet.contains(card) =>\n val wildcardCount = cardCounts(card)\n val cardGroupsNoWildcard = cardCounts.removed(card).values.toList.sorted.reverse\n cardGroupsNoWildcard match\n case Nil => List(wildcardCount)\n case _ => cardGroupsNoWildcard.head + wildcardCount :: cardGroupsNoWildcard.tail\n case _ => cardCounts.values.toList.sorted.reverse\n\n cardGroups match\n case 5 :: _ => HandType.FiveOfAKind\n case 4 :: _ => HandType.FourOfAKind\n case 3 :: 2 :: Nil => HandType.FullHouse\n case 3 :: _ => HandType.ThreeOfAKind\n case 2 :: 2 :: _ => HandType.TwoPair\n case 2 :: _ => HandType.OnePair\n case _ => HandType.HighCard\n end apply\nend HandType\n\ntrait Rules:\n val rankValues: String\n val wildcard: Option[Card]\n\nval standardRules = new Rules:\n val rankValues = "23456789TJQKA"\n val wildcard = None\n\nval jokerRules = new Rules:\n val rankValues = "J23456789TQKA"\n val wildcard = Some(\'J\')\n\n\ngiven cardOrdering(using rules: Rules): Ordering[Card] = Ordering.by(rules.rankValues.indexOf(_))\ngiven handOrdering(using Rules): Ordering[Hand] = (h1: Hand, h2: Hand) =>\n val h1Type = HandType(h1)\n val h2Type = HandType(h2)\n if h1Type != h2Type then h1Type.ordinal - h2Type.ordinal\n else h1.zip(h2).find(_ != _).map( (c1, c2) => cardOrdering.compare(c1, c2) ).getOrElse(0)\ngiven betOrdering(using Rules): Ordering[Bet] = Ordering.by(_.hand)\n\ndef calculateWinnings(bets: List[Bet])(using Rules): Int =\n bets.sorted.zipWithIndex.map { case (bet, index) => bet.bid * (index + 1) }.sum\n\ndef parse(input: String): List[Bet] =\n input.linesIterator.toList.map(Bet(_))\n\ndef part1(input: String): Int =\n println(calculateWinnings(parse(input))(using standardRules))\n\ndef part2(input: String): Int =\n println(calculateWinnings(parse(input))(using jokerRules))\n')),(0,i.kt)("h2",{id:"solutions-from-the-community"},"Solutions from the community"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/spamegg1/advent-of-code-2023-scala/blob/solutions/07.worksheet.sc#L132"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/spamegg1"},"Spamegg")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/prinsniels/AdventOfCode2023/blob/main/src/main/scala/solutions/day07.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/prinsniels"},"Niels Prins")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/lenguyenthanh/aoc-2023/blob/main/Day07.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/lenguyenthanh"},"Thanh Le")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/xRuiAlves/advent-of-code-2023/blob/main/Day7.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/xRuiAlves/"},"Rui Alves")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/Philippus/adventofcode/blob/main/src/main/scala/adventofcode2023/Day07.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/philippus"},"Philippus Baalman")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/GrigoriiBerezin/advent_code_2023/tree/master/task07/src/main/scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/GrigoriiBerezin"},"g.berezin")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/bishabosha/advent-of-code-2023/blob/main/2023-day07.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/bishabosha"},"Jamie Thompson")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/alexandru/advent-of-code/blob/main/scala3/2023/src/main/scala/day7.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/alexandru/"},"Alexandru Nedelcu")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/guycastle/advent_of_code_2023/blob/main/src/main/scala/days/day07/DaySeven.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/guycastle/"},"Guillaume Vandecasteele")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/YannMoisan/advent-of-code/blob/master/2023/src/main/scala/Day7.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/YannMoisan"},"Yann Moisan")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/jnclt/adventofcode2023/blob/main/day07/camel-cards.sc"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/jnclt"},"jnclt")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://gist.github.com/thanhbv/38bd6676d3348334db58e4926add0a11"},"Solution")," by thanhbv")),(0,i.kt)("p",null,"Share your solution to the Scala community by editing this page. (You can even write the whole article!)"))}p.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.fd86aa88.js b/assets/js/runtime~main.baddc3a1.js similarity index 98% rename from assets/js/runtime~main.fd86aa88.js rename to assets/js/runtime~main.baddc3a1.js index a6f0d9554..674a195a8 100644 --- a/assets/js/runtime~main.fd86aa88.js +++ b/assets/js/runtime~main.baddc3a1.js @@ -1 +1 @@ -(()=>{"use strict";var e,c,a,f,d,b={},t={};function r(e){var c=t[e];if(void 0!==c)return c.exports;var a=t[e]={exports:{}};return b[e].call(a.exports,a,a.exports,r),a.exports}r.m=b,e=[],r.O=(c,a,f,d)=>{if(!a){var b=1/0;for(i=0;i=d)&&Object.keys(r.O).every((e=>r.O[e](a[o])))?a.splice(o--,1):(t=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[a,f,d]},r.n=e=>{var c=e&&e.__esModule?()=>e.default:()=>e;return r.d(c,{a:c}),c},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);r.r(d);var b={};c=c||[null,a({}),a([]),a(a)];for(var t=2&f&&e;"object"==typeof t&&!~c.indexOf(t);t=a(t))Object.getOwnPropertyNames(t).forEach((c=>b[c]=()=>e[c]));return b.default=()=>e,r.d(d,b),d},r.d=(e,c)=>{for(var a in c)r.o(c,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:c[a]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((c,a)=>(r.f[a](e,c),c)),[])),r.u=e=>"assets/js/"+({1:"06fa13a8",53:"935f2afb",341:"62432f74",578:"4cf3b8de",738:"e2b8a71b",771:"ba72e685",912:"d22639e6",1078:"ff5f3c5c",1220:"de026f3e",1295:"a7c819ed",1655:"3f15e8e2",1866:"ef006d58",1935:"89cb3c3f",2014:"d5cd8a03",2255:"a6e610da",2288:"e0cc4b4f",2402:"5a9b63f1",2678:"8ebe7983",2863:"2e8f18da",2888:"4496c5bc",2967:"559dca7a",3159:"0a673ff4",3240:"950c6b93",3298:"4e95716f",3360:"e7ad7ee9",3414:"4fe84b5f",3760:"8790bf4c",3856:"1bbb7d86",3866:"bd59612b",4052:"73b9919f",4120:"f4f7cb3a",4159:"738d623f",4195:"c4f5d8e4",4204:"18ed2f59",4404:"53a8669b",4459:"fe0d56d5",4480:"1d39ce0c",4760:"0f5219f4",4871:"003324e5",5002:"1077c9b3",5099:"66145d18",5481:"212d8bd4",5511:"b0cdce56",5557:"4337dc21",5692:"3c0fac00",6030:"1d58c4bc",6091:"fe537a1c",6279:"63b7595f",6324:"338c8fb2",6712:"95444edb",6754:"1d76bc70",7111:"1279ac73",7238:"94550ecd",7333:"cf8e1c86",7399:"507065a6",7508:"7c5e7584",7522:"6c07b263",7546:"18e96609",7684:"6cdb47d2",7784:"dd8a65aa",7918:"17896441",8119:"adcb9b5c",8243:"ae238375",8592:"common",8960:"c702c251",9185:"b468346a",9468:"9f228057",9514:"1be78505",9898:"2301f76b",9965:"f89f89b6"}[e]||e)+"."+{1:"96e40d9d",53:"9bcc869b",341:"b10e5ec0",578:"a90d5870",738:"79714bbb",771:"ec57f95e",912:"c1afc57f",1078:"cc64e407",1220:"d21d9c34",1295:"e4fd7c14",1655:"ecbc7af9",1866:"9de0e10f",1935:"20272596",2014:"f6d63f96",2255:"7a9dfa0b",2288:"aeb2a318",2402:"e803504d",2678:"93e8309d",2863:"2bd28422",2888:"fe8c1a54",2967:"9ca0cd35",3159:"2e6b2e28",3240:"8dcb6957",3298:"32e28888",3360:"5f787026",3414:"b170d47a",3760:"a5b39a59",3856:"8bdec007",3866:"d6385552",4052:"27fab747",4120:"6085543b",4159:"59f85ab6",4195:"80ab070c",4204:"0e536163",4404:"3e2072d3",4459:"ab6c1654",4480:"1fd666d6",4760:"d11772f2",4871:"618eee13",4972:"57117b7b",5002:"88a86fe4",5099:"9c685604",5481:"d0d8890a",5511:"599b912e",5557:"6480befd",5692:"38dd3e38",6030:"599e4426",6091:"f770afb8",6279:"30803325",6324:"a904ee48",6712:"08722476",6754:"432628c2",7111:"451788af",7238:"71309634",7333:"28a7932d",7399:"63f84413",7508:"e413fbbf",7522:"c1cb7405",7546:"c03abba7",7684:"a7099c75",7784:"9469e964",7918:"0df78363",8119:"c436e4a6",8243:"cee506b9",8592:"ad0c3e14",8960:"9959e1fa",9185:"37df79b0",9468:"ab734704",9514:"0cc6328f",9898:"92cdaf4b",9965:"53205d2b"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,c)=>Object.prototype.hasOwnProperty.call(e,c),f={},d="website:",r.l=(e,c,a,b)=>{if(f[e])f[e].push(c);else{var t,o;if(void 0!==a)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],t.parentNode&&t.parentNode.removeChild(t),d&&d.forEach((e=>e(a))),c)return c(a)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/scala-advent-of-code/",r.gca=function(e){return e={17896441:"7918","06fa13a8":"1","935f2afb":"53","62432f74":"341","4cf3b8de":"578",e2b8a71b:"738",ba72e685:"771",d22639e6:"912",ff5f3c5c:"1078",de026f3e:"1220",a7c819ed:"1295","3f15e8e2":"1655",ef006d58:"1866","89cb3c3f":"1935",d5cd8a03:"2014",a6e610da:"2255",e0cc4b4f:"2288","5a9b63f1":"2402","8ebe7983":"2678","2e8f18da":"2863","4496c5bc":"2888","559dca7a":"2967","0a673ff4":"3159","950c6b93":"3240","4e95716f":"3298",e7ad7ee9:"3360","4fe84b5f":"3414","8790bf4c":"3760","1bbb7d86":"3856",bd59612b:"3866","73b9919f":"4052",f4f7cb3a:"4120","738d623f":"4159",c4f5d8e4:"4195","18ed2f59":"4204","53a8669b":"4404",fe0d56d5:"4459","1d39ce0c":"4480","0f5219f4":"4760","003324e5":"4871","1077c9b3":"5002","66145d18":"5099","212d8bd4":"5481",b0cdce56:"5511","4337dc21":"5557","3c0fac00":"5692","1d58c4bc":"6030",fe537a1c:"6091","63b7595f":"6279","338c8fb2":"6324","95444edb":"6712","1d76bc70":"6754","1279ac73":"7111","94550ecd":"7238",cf8e1c86:"7333","507065a6":"7399","7c5e7584":"7508","6c07b263":"7522","18e96609":"7546","6cdb47d2":"7684",dd8a65aa:"7784",adcb9b5c:"8119",ae238375:"8243",common:"8592",c702c251:"8960",b468346a:"9185","9f228057":"9468","1be78505":"9514","2301f76b":"9898",f89f89b6:"9965"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(c,a)=>{var f=r.o(e,c)?e[c]:void 0;if(0!==f)if(f)a.push(f[2]);else if(/^(1303|532)$/.test(c))e[c]=0;else{var d=new Promise(((a,d)=>f=e[c]=[a,d]));a.push(f[2]=d);var b=r.p+r.u(c),t=new Error;r.l(b,(a=>{if(r.o(e,c)&&(0!==(f=e[c])&&(e[c]=void 0),f)){var d=a&&("load"===a.type?"missing":a.type),b=a&&a.target&&a.target.src;t.message="Loading chunk "+c+" failed.\n("+d+": "+b+")",t.name="ChunkLoadError",t.type=d,t.request=b,f[1](t)}}),"chunk-"+c,c)}},r.O.j=c=>0===e[c];var c=(c,a)=>{var f,d,b=a[0],t=a[1],o=a[2],n=0;if(b.some((c=>0!==e[c]))){for(f in t)r.o(t,f)&&(r.m[f]=t[f]);if(o)var i=o(r)}for(c&&c(a);n{"use strict";var e,c,a,f,d,b={},t={};function r(e){var c=t[e];if(void 0!==c)return c.exports;var a=t[e]={exports:{}};return b[e].call(a.exports,a,a.exports,r),a.exports}r.m=b,e=[],r.O=(c,a,f,d)=>{if(!a){var b=1/0;for(i=0;i=d)&&Object.keys(r.O).every((e=>r.O[e](a[o])))?a.splice(o--,1):(t=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[a,f,d]},r.n=e=>{var c=e&&e.__esModule?()=>e.default:()=>e;return r.d(c,{a:c}),c},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);r.r(d);var b={};c=c||[null,a({}),a([]),a(a)];for(var t=2&f&&e;"object"==typeof t&&!~c.indexOf(t);t=a(t))Object.getOwnPropertyNames(t).forEach((c=>b[c]=()=>e[c]));return b.default=()=>e,r.d(d,b),d},r.d=(e,c)=>{for(var a in c)r.o(c,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:c[a]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((c,a)=>(r.f[a](e,c),c)),[])),r.u=e=>"assets/js/"+({1:"06fa13a8",53:"935f2afb",341:"62432f74",578:"4cf3b8de",738:"e2b8a71b",771:"ba72e685",912:"d22639e6",1078:"ff5f3c5c",1220:"de026f3e",1295:"a7c819ed",1655:"3f15e8e2",1866:"ef006d58",1935:"89cb3c3f",2014:"d5cd8a03",2255:"a6e610da",2288:"e0cc4b4f",2402:"5a9b63f1",2678:"8ebe7983",2863:"2e8f18da",2888:"4496c5bc",2967:"559dca7a",3159:"0a673ff4",3240:"950c6b93",3298:"4e95716f",3360:"e7ad7ee9",3414:"4fe84b5f",3760:"8790bf4c",3856:"1bbb7d86",3866:"bd59612b",4052:"73b9919f",4120:"f4f7cb3a",4159:"738d623f",4195:"c4f5d8e4",4204:"18ed2f59",4404:"53a8669b",4459:"fe0d56d5",4480:"1d39ce0c",4760:"0f5219f4",4871:"003324e5",5002:"1077c9b3",5099:"66145d18",5481:"212d8bd4",5511:"b0cdce56",5557:"4337dc21",5692:"3c0fac00",6030:"1d58c4bc",6091:"fe537a1c",6279:"63b7595f",6324:"338c8fb2",6712:"95444edb",6754:"1d76bc70",7111:"1279ac73",7238:"94550ecd",7333:"cf8e1c86",7399:"507065a6",7508:"7c5e7584",7522:"6c07b263",7546:"18e96609",7684:"6cdb47d2",7784:"dd8a65aa",7918:"17896441",8119:"adcb9b5c",8243:"ae238375",8592:"common",8960:"c702c251",9185:"b468346a",9468:"9f228057",9514:"1be78505",9898:"2301f76b",9965:"f89f89b6"}[e]||e)+"."+{1:"96e40d9d",53:"9bcc869b",341:"b10e5ec0",578:"a90d5870",738:"79714bbb",771:"ec57f95e",912:"c1afc57f",1078:"cc64e407",1220:"19ddd6d8",1295:"e4fd7c14",1655:"ecbc7af9",1866:"9de0e10f",1935:"20272596",2014:"f6d63f96",2255:"7a9dfa0b",2288:"aeb2a318",2402:"e803504d",2678:"93e8309d",2863:"2bd28422",2888:"fe8c1a54",2967:"9ca0cd35",3159:"2e6b2e28",3240:"8dcb6957",3298:"32e28888",3360:"5f787026",3414:"b170d47a",3760:"a5b39a59",3856:"8bdec007",3866:"d6385552",4052:"27fab747",4120:"6085543b",4159:"59f85ab6",4195:"80ab070c",4204:"0e536163",4404:"3e2072d3",4459:"ab6c1654",4480:"1fd666d6",4760:"d11772f2",4871:"618eee13",4972:"57117b7b",5002:"88a86fe4",5099:"9c685604",5481:"d0d8890a",5511:"599b912e",5557:"6480befd",5692:"38dd3e38",6030:"599e4426",6091:"f770afb8",6279:"30803325",6324:"a904ee48",6712:"08722476",6754:"432628c2",7111:"451788af",7238:"71309634",7333:"28a7932d",7399:"63f84413",7508:"e413fbbf",7522:"c1cb7405",7546:"c03abba7",7684:"a7099c75",7784:"9469e964",7918:"0df78363",8119:"c436e4a6",8243:"cee506b9",8592:"ad0c3e14",8960:"9959e1fa",9185:"37df79b0",9468:"ab734704",9514:"0cc6328f",9898:"92cdaf4b",9965:"53205d2b"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,c)=>Object.prototype.hasOwnProperty.call(e,c),f={},d="website:",r.l=(e,c,a,b)=>{if(f[e])f[e].push(c);else{var t,o;if(void 0!==a)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],t.parentNode&&t.parentNode.removeChild(t),d&&d.forEach((e=>e(a))),c)return c(a)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/scala-advent-of-code/",r.gca=function(e){return e={17896441:"7918","06fa13a8":"1","935f2afb":"53","62432f74":"341","4cf3b8de":"578",e2b8a71b:"738",ba72e685:"771",d22639e6:"912",ff5f3c5c:"1078",de026f3e:"1220",a7c819ed:"1295","3f15e8e2":"1655",ef006d58:"1866","89cb3c3f":"1935",d5cd8a03:"2014",a6e610da:"2255",e0cc4b4f:"2288","5a9b63f1":"2402","8ebe7983":"2678","2e8f18da":"2863","4496c5bc":"2888","559dca7a":"2967","0a673ff4":"3159","950c6b93":"3240","4e95716f":"3298",e7ad7ee9:"3360","4fe84b5f":"3414","8790bf4c":"3760","1bbb7d86":"3856",bd59612b:"3866","73b9919f":"4052",f4f7cb3a:"4120","738d623f":"4159",c4f5d8e4:"4195","18ed2f59":"4204","53a8669b":"4404",fe0d56d5:"4459","1d39ce0c":"4480","0f5219f4":"4760","003324e5":"4871","1077c9b3":"5002","66145d18":"5099","212d8bd4":"5481",b0cdce56:"5511","4337dc21":"5557","3c0fac00":"5692","1d58c4bc":"6030",fe537a1c:"6091","63b7595f":"6279","338c8fb2":"6324","95444edb":"6712","1d76bc70":"6754","1279ac73":"7111","94550ecd":"7238",cf8e1c86:"7333","507065a6":"7399","7c5e7584":"7508","6c07b263":"7522","18e96609":"7546","6cdb47d2":"7684",dd8a65aa:"7784",adcb9b5c:"8119",ae238375:"8243",common:"8592",c702c251:"8960",b468346a:"9185","9f228057":"9468","1be78505":"9514","2301f76b":"9898",f89f89b6:"9965"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(c,a)=>{var f=r.o(e,c)?e[c]:void 0;if(0!==f)if(f)a.push(f[2]);else if(/^(1303|532)$/.test(c))e[c]=0;else{var d=new Promise(((a,d)=>f=e[c]=[a,d]));a.push(f[2]=d);var b=r.p+r.u(c),t=new Error;r.l(b,(a=>{if(r.o(e,c)&&(0!==(f=e[c])&&(e[c]=void 0),f)){var d=a&&("load"===a.type?"missing":a.type),b=a&&a.target&&a.target.src;t.message="Loading chunk "+c+" failed.\n("+d+": "+b+")",t.name="ChunkLoadError",t.type=d,t.request=b,f[1](t)}}),"chunk-"+c,c)}},r.O.j=c=>0===e[c];var c=(c,a)=>{var f,d,b=a[0],t=a[1],o=a[2],n=0;if(b.some((c=>0!==e[c]))){for(f in t)r.o(t,f)&&(r.m[f]=t[f]);if(o)var i=o(r)}for(c&&c(a);n Scala Center Advent of Code | Scala Center Advent of Code - +

Scala Advent of Code byScala Center

Credit to https://github.com/OlegIlyenko/scala-icon

Learn Scala 3

A simpler, safer and more concise version of Scala, the famous object-oriented and functional programming language.

Solve Advent of Code puzzles

Challenge your programming skills by solving Advent of Code puzzles.

Share with the community

Get or give support to the community. Share your solutions with the community.

- + \ No newline at end of file diff --git a/introduction/index.html b/introduction/index.html index 0f2c6c04f..2c67e73f8 100644 --- a/introduction/index.html +++ b/introduction/index.html @@ -5,7 +5,7 @@ Introduction | Scala Center Advent of Code - + @@ -18,7 +18,7 @@ We will strive to only use the Scala standard library to solve the puzzles, so that no specific knowledge of external libraries is necessary to follow along.

Participate

In addition, if you have your own solution for a puzzle and would like to share it, you can add a link to a gist in the solution page of the puzzles.

If you would like to discuss the puzzles with other developers, or discuss our solutions on the following day, drop by the the Scala Discord server, in the #advent-of-code channel. There you can also find a pinned message with the invite code for a private leaderboard including those from the Scala community for some friendly competition.

Do you want to get your hands dirty and solve the Advent of Code puzzles in Scala? Read ahead to the Setup page!

- + \ No newline at end of file diff --git a/puzzles/day1/index.html b/puzzles/day1/index.html index 0390105b7..e0743b2cf 100644 --- a/puzzles/day1/index.html +++ b/puzzles/day1/index.html @@ -5,7 +5,7 @@ Day 1: Sonar Sweep | Scala Center Advent of Code - + @@ -16,7 +16,7 @@ For example, the sliding window of size 3 of Seq(10, 20, 30, 40, 50) is:

Seq(Seq(10, 20, 30), Seq(20, 30, 40), Seq(30, 40, 50)).

We can generalize this procedure in a method that compute a sliding window of some size n on any sequence of elements. Such a method already exists in the Scala standard library under the name sliding. It returns an iterator of arrays.

$ Seq(10, 20, 30, 40, 50).sliding(3).toSeq
Seq(Array(10, 20, 30), Array(20, 30, 40), Array(30, 40, 50))

We can use the sliding method to make our code shorter and faster.

Final solution

def part1(input: String): Int =
val depths = input.linesIterator.map(_.toInt)
val pairs = depths.sliding(2).map(arr => (arr(0), arr(1)))
pairs.count((prev, next) => prev < next)

def part2(input: String): Int =
val depths = input.linesIterator.map(_.toInt)
val sums = depths.sliding(3).map(_.sum)
val pairs = sums.sliding(2).map(arr => (arr(0), arr(1)))
pairs.count((prev, next) => prev < next)

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

The you can run it with scala-cli:

$ scala-cli 2021 -M day1.part1
The answer is 1559

$ scala-cli 2021 -M template1.part2
The answer is 1600

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

Run it in the browser

Part 1

Part 2

Bonus

There is a trick to make the solution of part 2 even smaller.

Indeed comparing depths(i) + depths(i + 1) + depths(i + 2) with depths(i + 1) + depths(i + 2) + depths(i + 3) is the same as comparing depths(i) with depths(i + 3). So the second part of the puzzle is almost the same as the first part of the puzzle.

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day10/index.html b/puzzles/day10/index.html index 41fc7e32b..c20e6d47d 100644 --- a/puzzles/day10/index.html +++ b/puzzles/day10/index.html @@ -5,7 +5,7 @@ Day 10: Syntax Scoring | Scala Center Advent of Code - + @@ -47,7 +47,7 @@ the element in the middle:

def part2(input: String): BigInt =
val rows: LazyList[List[Symbol]] =
input.linesIterator
.to(LazyList)
.map(parseRow)

val scores =
rows.map(checkChunks)
.collect { case incomplete: CheckResult.Incomplete => incomplete.score }
.toVector
.sorted

scores(scores.length / 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 day10.part1
The solution is 367059
$ scala-cli 2021 -M day10.part2
The solution is 1952146692

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day11/index.html b/puzzles/day11/index.html index 6e48af663..103209635 100644 --- a/puzzles/day11/index.html +++ b/puzzles/day11/index.html @@ -5,13 +5,13 @@ Day 11: Dumbo Octopus | Scala Center Advent of Code - +

Day 11: Dumbo Octopus

by @tgodzik

Puzzle description

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

Final Solution

trait Step:
def increment: Step
def addFlashes(f: Int): Step
def shouldStop: Boolean
def currentFlashes: Int
def stepNumber: Int

case class MaxIterStep(currentFlashes: Int, stepNumber: Int, max: Int) extends Step:
def increment = this.copy(stepNumber = stepNumber + 1)
def addFlashes(f: Int) = this.copy(currentFlashes = currentFlashes + f)
def shouldStop = stepNumber == max

case class SynchronizationStep(
currentFlashes: Int,
stepNumber: Int,
maxChange: Int,
lastFlashes: Int
) extends Step:
def increment = this.copy(stepNumber = stepNumber + 1)
def addFlashes(f: Int) =
this.copy(currentFlashes = currentFlashes + f, lastFlashes = currentFlashes)
def shouldStop = currentFlashes - lastFlashes == maxChange

case class Point(x: Int, y: Int)
case class Octopei(inputMap: Map[Point, Int]):

@tailrec
private def propagate(
toVisit: Queue[Point],
alreadyFlashed: Set[Point],
currentSituation: Map[Point, Int]
): Map[Point, Int] =
toVisit.dequeueOption match
case None => currentSituation
case Some((point, dequeued)) =>
currentSituation.get(point) match
case Some(value) if value > 9 && !alreadyFlashed(point) =>
val propagated =
Seq(
point.copy(x = point.x + 1),
point.copy(x = point.x - 1),
point.copy(y = point.y + 1),
point.copy(y = point.y - 1),
point.copy(x = point.x + 1, y = point.y + 1),
point.copy(x = point.x + 1, y = point.y - 1),
point.copy(x = point.x - 1, y = point.y + 1),
point.copy(x = point.x - 1, y = point.y - 1)
)
val newSituation = propagated.foldLeft(currentSituation) {
case (map, point) =>
map.get(point) match
case Some(value) => map.updated(point, value + 1)
case _ => map
}
propagate(
dequeued.appendedAll(propagated),
alreadyFlashed + point,
newSituation
)
case _ =>
propagate(dequeued, alreadyFlashed, currentSituation)
end propagate

def simulate(step: Step) = simulateIter(step, inputMap)

@tailrec
private def simulateIter(
step: Step,
currentSituation: Map[Point, Int]
): Step =
if step.shouldStop then step
else
val incremented = currentSituation.map { case (point, value) =>
(point, value + 1)
}
val flashes = incremented.collect {
case (point, value) if value > 9 => point
}.toList
val propagated = propagate(Queue(flashes*), Set.empty, incremented)
val newFlashes = propagated.collect {
case (point, value) if value > 9 => 1
}.sum
val zeroed = propagated.map {
case (point, value) if value > 9 => (point, 0)
case same => same
}
simulateIter(step.increment.addFlashes(newFlashes), zeroed)
end simulateIter

end Octopei

def part1(input: String) =
val octopei = parse(input)
val step = MaxIterStep(0, 0, 100)
octopei.simulate(step).currentFlashes

def part2(input: String) =
val octopei = parse(input)
val step = SynchronizationStep(0, 0, octopei.inputMap.size, 0)
octopei.simulate(step).stepNumber

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 day11.part1
The answer is: 1673

$ scala-cli 2021 -M day11.part2
The answer is: 279

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day12/index.html b/puzzles/day12/index.html index bcadb61f7..a51712ff5 100644 --- a/puzzles/day12/index.html +++ b/puzzles/day12/index.html @@ -5,13 +5,13 @@ Day 12: Passage Pathing | Scala Center Advent of Code - +
- + \ No newline at end of file diff --git a/puzzles/day13/index.html b/puzzles/day13/index.html index 459c92efc..8a7d074a8 100644 --- a/puzzles/day13/index.html +++ b/puzzles/day13/index.html @@ -5,7 +5,7 @@ Day 13: Transparent Origami | Scala Center Advent of Code - + @@ -22,7 +22,7 @@ Finally we convert this double array to a String with .map(_.mkString).mkString('\n').

def part2(input: String): String =
val (dots, folds) = parseInstructions(input)
val foldedDots = folds.foldLeft(dots)((dots, fold) => dots.map(fold.apply))

val (width, height) = (foldedDots.map(_.x).max + 1, foldedDots.map(_.y).max + 1)
val paper = Array.fill(height, width)('.')
for dot <- foldedDots do paper(dot.y)(dot.x) = '#'

paper.map(_.mkString).mkString("\n")

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 day13.part1
The answer is: 788

$ scala-cli 2021 -M day10.part2
The answer is:
#..#...##.###..#..#.####.#..#.###...##.
#.#.....#.#..#.#.#..#....#..#.#..#.#..#
##......#.###..##...###..#..#.###..#...
#.#.....#.#..#.#.#..#....#..#.#..#.#.##
#.#..#..#.#..#.#.#..#....#..#.#..#.#..#
#..#..##..###..#..#.####..##..###...###

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day14/index.html b/puzzles/day14/index.html index d18370696..f062499c3 100644 --- a/puzzles/day14/index.html +++ b/puzzles/day14/index.html @@ -5,7 +5,7 @@ Day 14: Extended Polymerization | Scala Center Advent of Code - + @@ -25,7 +25,7 @@ We can use the sum of multisets, noted with ++ and Σ\Sigma, which accumulates counts.

With the definitions above, we can proceed with solving our problem.

For any string longer than 2 chars, we have the following property:

S(x1x2x3xp,n)=Σi=1p1(S(xixi+1,n))S(x_1 x_2 x_3 \cdots x_p, n) = \Sigma_{i=1}^{p-1} (S(x_i x_{i+1}, n))

In other words, SS for a string is the sum (a multiset sum) of SS for all the adjacent pairs in the string, with the same number of iterations nn. That is because each initial pair of letters (such as NN, NC and CB) expands independently of the others. Each initial char is counted exactly once in the final frequencies because it is counted as part of the expansion of the pair on its left, and not the expansion of the pair on its right (we always exclude the first char).

As a particular case of the above, for a string of 3 chars xzyxzy, we have

S(xzy,n)=S(xz,n)+S(zy,n)S(xzy, n) = S(xz, n) + S(zy, n)

For strings of length 2, we have two cases: n=0n = 0 and n>0n > 0.

Base case: a pair xyxy, and n=0n = 0

S(xy,0)={y1} for all x,y (by definition)S(xy, 0) = \{ y \rightarrow 1 \} \text{ for all } x, y \text{ (by definition)}

Inductive case: a pair xyxy, and n>0n > 0

S(xy,n)=S(xzy,n1) where z is the insertion char for the pair xy (by definition)=S(xz,n1)+S(zy,n1) – the particular case of 3 chars above\begin{aligned} S(xy, n) & = S(xzy, n-1) \text{ where $z$ is the insertion char for the pair $xy$ (by definition)} \\ & = S(xz, n-1) + S(zy, n-1) \text{ -- the particular case of 3 chars above} \end{aligned}

This means that the frequencies of letters after xyxy has produced its final polymer are equal to the sum of frequencies that xzyxzy produces after 1 fewer steps.

Now that we have an inductive definition of S(xy,n)S(xy, n) for all pairs, we can write an iterative algorithm that computes that for all possible pairs xyxy, for n[0,40]n \in \lbrack 0, 40 \rbrack :

// S : (charPair, n) -> frequencies
val S = mutable.Map.empty[(CharPair, Int), Frequencies]

// Base case: S(xy, 0) = {y -> 1} for all x, y
for (pair @ (first, second), insert) <- insertionRules do
S((pair, 0)) = Map(second -> 1L)

// Recursive case S(xy, n) = S(xz, n - 1) + S(zy, n - 1) with z = insertionRules(xy)
for n <- 1 to 40 do
for (pair, insert) <- insertionRules do
val (x, y) = pair
val z = insertionRules(pair)
S((pair, n)) = addFrequencies(S((x, z), n - 1), S((z, y), n - 1))

where addFrequencies implements the multiset sum of two bags of frequencies:

def addFrequencies(a: Frequencies, b: Frequencies): Frequencies =
b.foldLeft(a) { case (prev, (char, frequency)) =>
prev + (char -> (prev.getOrElse(char, 0L) + frequency))
}

Using the initial property of SS for strings longer than 2 chars, we can compute S(initialPolymer,40)S(\text{initialPolymer}, 40) from the compute S(xy,40)S(xy, 40):

// S(polymer, 40) = ∑(S(pair, 40))
val pairsInPolymer = initialPolymer.zip(initialPolymer.tail)
val polymerS = (for pair <- pairsInPolymer yield S(pair, 40)).reduce(addFrequencies)

And finally, we add the very first character, once, to compute the full frequencies:

// We have to add the very first char to get all the frequencies
val frequencies = addFrequencies(polymerS, Map(initialPolymer.head -> 1L))

After which we have all the pieces for part 2.

Final code for part 2

def part2(input: String): Long =
val (initialPolymer, insertionRules) = parseInput(input)

// S : (charPair, n) -> frequencies of everything but the first char after n iterations from charPair
val S = mutable.Map.empty[(CharPair, Int), Frequencies]

// Base case: S(xy, 0) = {y -> 1} for all x, y
for (pair @ (first, second), insert) <- insertionRules do
S((pair, 0)) = Map(second -> 1L)

// Recursive case S(xy, n) = S(xz, n - 1) + S(zy, n - 1) with z = insertionRules(xy)
for n <- 1 to 40 do
for (pair, insert) <- insertionRules do
val (x, y) = pair
val z = insertionRules(pair)
S((pair, n)) = addFrequencies(S((x, z), n - 1), S((z, y), n - 1))

// S(polymer, 40) = ∑(S(pair, 40))
val pairsInPolymer = initialPolymer.zip(initialPolymer.tail)
val polymerS = (for pair <- pairsInPolymer yield S(pair, 40)).reduce(addFrequencies)

// We have to add the very first char to get all the frequencies
val frequencies = addFrequencies(polymerS, Map(initialPolymer.head -> 1L))

// Finally, we can finish the computation as in part 1
val max = frequencies.values.max
val min = frequencies.values.min
max - min

def addFrequencies(a: Frequencies, b: Frequencies): Frequencies =
b.foldLeft(a) { case (prev, (char, frequency)) =>
prev + (char -> (prev.getOrElse(char, 0L) + frequency))
}

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 day14.part1
The answer is: 3306

$ scala-cli 2021 -M day14.part2
The answer is: 3760312702877

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day15/index.html b/puzzles/day15/index.html index e6827cb0b..bf22ded83 100644 --- a/puzzles/day15/index.html +++ b/puzzles/day15/index.html @@ -5,13 +5,13 @@ Day 15: Chiton | Scala Center Advent of Code - +

Day 15: Chiton

By @anatoliykmetyuk

Puzzle description

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

Problem

The problem in its essence is that of finding the least-costly path through a graph. This problem is solved by Dijkstra's algorithm, nicely explained in this Computerphile video.

Domain Model

The two domain entities we are working with are the game map and an individual cell of that map. In presence of the game map, a cell is fully described by a pair of its coordinates.

type Coord = (Int, Int)

The game map contains all the cells from the challenge input. It also defines the neighbours of a given cell, which we need to know for Dijkstra's algorithm. Finally, it defines a function to get the cost of entering a given cell.

class GameMap(cells: IndexedSeq[IndexedSeq[Int]]):
val maxRow = cells.length - 1
val maxCol = cells.head.length - 1

def neighboursOf(c: Coord): List[Coord] =
val (row, col) = c
val lb = mutable.ListBuffer.empty[Coord]
if row < maxRow then lb.append((row+1, col))
if row > 0 then lb.append((row-1, col))
if col < maxCol then lb.append((row, col+1))
if col > 0 then lb.append((row, col-1))
lb.toList

def costOf(c: Coord): Int = c match
case (row, col) => cells(row)(col)
end GameMap

IndexedSeq in the cells type is important for this algorithm since we are doing a lot of index-based accesses, so we need to use a data structure optimized for that.

Algorithm – Part 1

We start the solution by defining three data structures for the algorithm:

val visited = mutable.Set.empty[Coord]
val dist = mutable.Map[Coord, Int]((0, 0) -> 0)
val queue = java.util.PriorityQueue[Coord](Ordering.by(dist))
queue.add((0, 0))

The first one is a Set of all visited nodes – the ones the algorithm will not look at again. The second one is a Map of distances containing the smallest currently known distance from the top-left corner of the map to the given cell. Finally, the third one is a java.util.PriorityQueue that defines in which order to examine cells. We are using Java's PriorityQueue, not the Scala's one since the Java PriorityQueue implementation defines the remove operation on the queue which is necessary for efficient implementation and which the Scala queue lacks.

We also initialize the queue with the first node we are going to examine – the top-left corner of the map.

Once we have the data structures, there's a loop which runs Dijkstra's algorithm on those structures:

while queue.peek() != null do
val c = queue.poll()
visited += c
val newNodes: List[Coord] = gameMap.neighboursOf(c).filterNot(visited)
val cDist = dist(c)
for n <- newNodes do
val newDist = cDist + gameMap.costOf(n)
if !dist.contains(n) || dist(n) > newDist then
dist(n) = newDist
queue.remove(n)
queue.add(n)
dist((gameMap.maxRow, gameMap.maxCol))

We use queue.remove(n) followed by queue.add(n) here – this is to recompute the position of n in the queue following the change in the ordering of the queue (that is, the mutation of dist). Ideally, you would need a decreaseKey operation on the priority queue for the best performance – but that would require writing a dedicated data structure, which is out of scope for this solution.

Part 2

Part 2 is like Part 1 but 25 times larger. The Part 1 algorithm is capable of dealing with scale, and so the only challenge is to construct the game map for part 2.

We generate the Part 2 game map from the Part 1 map using three nested loops:

val seedTile = readInput()
val gameMap = GameMap(
(0 until 5).flatMap { tileIdVertical =>
for row <- seedTile yield
for
tileIdHorizontal <- 0 until 5
cell <- row
yield (cell + tileIdHorizontal + tileIdVertical - 1) % 9 + 1
}
)

The innermost loop generates individual cells according to the challenge spec. The second-level loop pads the 100x100 tiles of the map horizontally, starting from the seedTile (the one used in Part 1). Finally, the outermost loop pads the tiles vertically.

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day16/index.html b/puzzles/day16/index.html index a94fbbf8a..b7adf0eb9 100644 --- a/puzzles/day16/index.html +++ b/puzzles/day16/index.html @@ -5,7 +5,7 @@ Day 16: Packet Decoder | Scala Center Advent of Code - + @@ -54,7 +54,7 @@ that will calculate the equation. We can do it similarly to the versionsSum function in the previous part:

  def value: Long =
this match
case Sum(version, exprs) => exprs.map(_.value).sum
case Product(version, exprs) => exprs.map(_.value).reduce(_ * _)
case Minimum(version, exprs) => exprs.map(_.value).min
case Maximum(version, exprs) => exprs.map(_.value).max
case Literal(version, value) => value
case GreaterThan(version, lhs, rhs) => if lhs.value > rhs.value then 1 else 0
case LesserThan(version, lhs, rhs) => if lhs.value < rhs.value then 1 else 0
case Equals(version, lhs, rhs) => if lhs.value == rhs.value then 1 else 0

Full solution

package day16

import scala.util.Using
import scala.io.Source
import scala.annotation.tailrec

@main def part1(): Unit =
println(s"The solution is ${part1(readInput())}")

@main def part2(): Unit =
println(s"The solution is ${part2(readInput())}")

def readInput(): String =
Using.resource(Source.fromFile("input/day16"))(_.mkString)

val hexadecimalMapping =
Map(
'0' -> "0000",
'1' -> "0001",
'2' -> "0010",
'3' -> "0011",
'4' -> "0100",
'5' -> "0101",
'6' -> "0110",
'7' -> "0111",
'8' -> "1000",
'9' -> "1001",
'A' -> "1010",
'B' -> "1011",
'C' -> "1100",
'D' -> "1101",
'E' -> "1110",
'F' -> "1111"
)

/*
* Structures for all possible operators
*/
enum Packet(version: Int, typeId: Int):
case Sum(version: Int, exprs: List[Packet]) extends Packet(version, 0)
case Product(version: Int, exprs: List[Packet]) extends Packet(version, 1)
case Minimum(version: Int, exprs: List[Packet]) extends Packet(version, 2)
case Maximum(version: Int, exprs: List[Packet]) extends Packet(version, 3)
case Literal(version: Int, literalValue: Long) extends Packet(version, 4)
case GreaterThan(version: Int, lhs: Packet, rhs: Packet) extends Packet(version, 5)
case LesserThan(version: Int, lhs: Packet, rhs: Packet) extends Packet(version, 6)
case Equals(version: Int, lhs: Packet, rhs: Packet) extends Packet(version, 7)

def versionSum: Int =
this match
case Sum(version, exprs) => version + exprs.map(_.versionSum).sum
case Product(version, exprs) => version + exprs.map(_.versionSum).sum
case Minimum(version, exprs) => version + exprs.map(_.versionSum).sum
case Maximum(version, exprs) => version + exprs.map(_.versionSum).sum
case Literal(version, value) => version
case GreaterThan(version, lhs, rhs) => version + lhs.versionSum + rhs.versionSum
case LesserThan(version, lhs, rhs) => version + lhs.versionSum + rhs.versionSum
case Equals(version, lhs, rhs) => version + lhs.versionSum + rhs.versionSum

def value: Long =
this match
case Sum(version, exprs) => exprs.map(_.value).sum
case Product(version, exprs) => exprs.map(_.value).reduce(_ * _)
case Minimum(version, exprs) => exprs.map(_.value).min
case Maximum(version, exprs) => exprs.map(_.value).max
case Literal(version, value) => value
case GreaterThan(version, lhs, rhs) => if lhs.value > rhs.value then 1 else 0
case LesserThan(version, lhs, rhs) => if lhs.value < rhs.value then 1 else 0
case Equals(version, lhs, rhs) => if lhs.value == rhs.value then 1 else 0
end Packet

type BinaryData = List[Char]

inline def toInt(chars: BinaryData): Int =
Integer.parseInt(chars.mkString, 2)

inline def toLong(chars: BinaryData): Long =
java.lang.Long.parseLong(chars.mkString, 2)

@tailrec
def readLiteralBody(tail: BinaryData, numAcc: BinaryData): (Long, BinaryData) =
val (num, rest) = tail.splitAt(5)
if num(0) == '1' then readLiteralBody(rest, numAcc.appendedAll(num.drop(1)))
else
val bits = numAcc.appendedAll(num.drop(1))
(toLong(bits), rest)
end readLiteralBody

def readOperatorBody(current: BinaryData): (List[Packet], BinaryData) =
val (lenId, rest) = current.splitAt(1)

@tailrec
def readMaxBits(
current: BinaryData,
remaining: Int,
acc: List[Packet]
): (List[Packet], BinaryData) =
if remaining == 0 then (acc, current)
else
val (newExpr, rest) = decodePacket(current)
readMaxBits(rest, remaining - (current.size - rest.size), acc :+ newExpr)

@tailrec
def readMaxPackets(
current: BinaryData,
remaining: Int,
acc: List[Packet]
): (List[Packet], BinaryData) =
if remaining == 0 then (acc, current)
else
val (newExpr, rest) = decodePacket(current)
readMaxPackets(rest, remaining - 1, acc :+ newExpr)

lenId match
// read based on length
case List('0') =>
val (size, packets) = rest.splitAt(15)
readMaxBits(packets, toInt(size), Nil)

// read based on number of packages
case _ =>
val (size, packets) = rest.splitAt(11)
readMaxPackets(packets, toInt(size), Nil)
end match
end readOperatorBody

def decodePacket(packet: BinaryData): (Packet, BinaryData) =
val (versionBits, rest) = packet.splitAt(3)
val version = toInt(versionBits)
val (typeBits, body) = rest.splitAt(3)
val tpe = toInt(typeBits)

tpe match
case 4 =>
val (value, remaining) = readLiteralBody(body, Nil)
(Packet.Literal(version, value), remaining)
case otherTpe =>
val (values, remaining) = readOperatorBody(body)
otherTpe match
case 0 => (Packet.Sum(version, values), remaining)
case 1 => (Packet.Product(version, values), remaining)
case 2 => (Packet.Minimum(version, values), remaining)
case 3 => (Packet.Maximum(version, values), remaining)
case 5 => (Packet.GreaterThan(version, values(0), values(1)), remaining)
case 6 => (Packet.LesserThan(version, values(0), values(1)), remaining)
case 7 => (Packet.Equals(version, values(0), values(1)), remaining)
end match
end decodePacket

def parse(input: String) =
val number = input.toList.flatMap(hex => hexadecimalMapping(hex).toCharArray)
val (operator, _) = decodePacket(number)
operator

def part1(input: String) =
val packet = parse(input)
packet.versionSum

def part2(input: String) =
val packet = parse(input)
packet.value
end part2

You might have noticed that we had to slightly modify the versionsSum function to work with our new structure.

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day17/index.html b/puzzles/day17/index.html index 9d94ec417..3f91f17af 100644 --- a/puzzles/day17/index.html +++ b/puzzles/day17/index.html @@ -5,7 +5,7 @@ Day 17: Trick Shot | Scala Center Advent of Code - + @@ -102,7 +102,7 @@ edge of the target (to travel to the furthest edge in one step).

We adapt allMaxHeights with this new rule:

def allMaxHeights(target: Target)(positiveOnly: Boolean): Seq[Int] =
val Target(xs, ys) = target
val upperBoundX = xs.max
val upperBoundY = -ys.min -1
val lowerBoundY = if positiveOnly then 0 else ys.min
for
vx <- 0 to upperBoundX
vy <- lowerBoundY to upperBoundY
maxy <- simulate(Probe(initial, Velocity(vx, vy)), target)
yield
maxy

Computing the Solution

As our input has not changed, we can update part 1 and give the code for part 2 as follows:

def part1(input: String) =
allMaxHeights(Input(input.trim))(positiveOnly = true).max

def part2(input: String) =
allMaxHeights(Input(input.trim))(positiveOnly = false).size

Notice that in part 2 we only need the number of possible max heights, rather than find the highest.

Final Code

case class Target(xs: Range, ys: Range)

case class Velocity(x: Int, y: Int)

case class Position(x: Int, y: Int)

val initial = Position(x = 0, y = 0)

case class Probe(position: Position, velocity: Velocity)

def step(probe: Probe): Probe =
val Probe(Position(px, py), Velocity(vx, vy)) = probe
Probe(Position(px + vx, py + vy), Velocity(vx - vx.sign, vy - 1))

def collides(probe: Probe, target: Target): Boolean =
val Probe(Position(px, py), _) = probe
val Target(xs, ys) = target
xs.contains(px) && ys.contains(py)

def beyond(probe: Probe, target: Target): Boolean =
val Probe(Position(px, py), Velocity(vx, vy)) = probe
val Target(xs, ys) = target
val beyondX = (vx == 0 && px < xs.min) || px > xs.max
val beyondY = vy < 0 && py < ys.min
beyondX || beyondY

def simulate(probe: Probe, target: Target): Option[Int] =
LazyList
.iterate((probe, 0))((probe, maxY) => (step(probe), maxY `max` probe.position.y))
.dropWhile((probe, _) => !collides(probe, target) && !beyond(probe, target))
.headOption
.collect { case (probe, maxY) if collides(probe, target) => maxY }

def allMaxHeights(target: Target)(positiveOnly: Boolean): Seq[Int] =
val upperBoundX = target.xs.max
val upperBoundY = target.ys.min.abs
val lowerBoundY = if positiveOnly then 0 else -upperBoundY
for
vx <- 0 to upperBoundX
vy <- lowerBoundY to upperBoundY
maxy <- simulate(Probe(initial, Velocity(vx, vy)), target)
yield
maxy

type Parser[A] = PartialFunction[String, A]

val IntOf: Parser[Int] =
case s if s.matches(raw"-?\d+") => s.toInt

val RangeOf: Parser[Range] =
case s"${IntOf(begin)}..${IntOf(end)}" => begin to end

val Input: Parser[Target] =
case s"target area: x=${RangeOf(xs)}, y=${RangeOf(ys)}" => Target(xs, ys)

def part1(input: String) =
allMaxHeights(Input(input.trim))(positiveOnly = true).max

def part2(input: String) =
allMaxHeights(Input(input.trim))(positiveOnly = false).size

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 advent-of-code

You can run it with scala-cli.

$ scala-cli 2021 -M day17.part1
The answer is: 4851

$ scala-cli 2021 -M day17.part2
The answer is: 1739

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day18/index.html b/puzzles/day18/index.html index d9296d5c9..2c2f16ba7 100644 --- a/puzzles/day18/index.html +++ b/puzzles/day18/index.html @@ -5,13 +5,13 @@ Day 18: Snailfish | Scala Center Advent of Code - +
- + \ No newline at end of file diff --git a/puzzles/day19/index.html b/puzzles/day19/index.html index d4713b8da..a653af833 100644 --- a/puzzles/day19/index.html +++ b/puzzles/day19/index.html @@ -5,13 +5,13 @@ Day 19: Beacon Scanner | Scala Center Advent of Code - +
- + \ No newline at end of file diff --git a/puzzles/day2/index.html b/puzzles/day2/index.html index 8bed5b4ec..0d2b77f8a 100644 --- a/puzzles/day2/index.html +++ b/puzzles/day2/index.html @@ -5,7 +5,7 @@ Day 2: Dive! | Scala Center Advent of Code - + @@ -19,7 +19,7 @@ and then add a method move that will translate the puzzle's rules to a position.

case class Position(horizontal: Int, depth: Int):
def move(p: Command): Position =
p match
case Command.Forward(x) => Position(horizontal + x, depth)
case Command.Down(x) => Position(horizontal, depth + x)
case Command.Up(x) => Position(horizontal, depth - x)

To apply all the commands from the input file, we use foldLeft

val firstPosition = Position(0, 0)
val lastPosition = entries.foldLeft(firstPosition)((position, command) => position.move(command))

foldLeft is a method from the standard library on iterable collections: Seq, List, Iterator...

It's a super convenient method that allows to iterate from left to right on a list.

The signature of foldLeft is:

def foldLeft[B](initialElement: B)(op: (B, A) => B): B

Let's see an example:

// Implementing a sum on a List
List(1, 3, 2, 4).foldLeft(0)((accumulator, current) => accumulator + current) // 10

It is the same as:

(((0 + 1) + 3) + 2) + 4

Final code for part 1

def part1(input: String): Int =
val entries = input.linesIterator.map(Command.from)
val firstPosition = Position(0, 0)
// we iterate on each entry and move it following the received command
val lastPosition = entries.foldLeft(firstPosition)((position, command) => position.move(command))
lastPosition.result

case class Position(horizontal: Int, depth: Int):
def move(p: Command): Position =
p match
case Command.Forward(x) => Position(horizontal + x, depth)
case Command.Down(x) => Position(horizontal, depth + x)
case Command.Up(x) => Position(horizontal, depth - x)

def result = horizontal * depth

enum Command:
case Forward(x: Int)
case Down(x: Int)
case Up(x: Int)

object Command:
def from(s: String): Command =
s match
case s"forward $x" if x.toIntOption.isDefined => Forward(x.toInt)
case s"up $x" if x.toIntOption.isDefined => Up(x.toInt)
case s"down $x" if x.toIntOption.isDefined => Down(x.toInt)
case _ => throw new Exception(s"value $s is not valid command")

Solution of Part 2

The part 2 introduces new rules to move the sonar. So we need a new position that takes into account the aim and a new method move with the new rules. The remaining code remains the same.

Moving the sonar part 2

case class PositionWithAim(horizontal: Int, depth: Int, aim: Int):
def move(p: Command): PositionWithAim =
p match
case Command.Forward(x) => PositionWithAim(horizontal + x, depth + x * aim, aim)
case Command.Down(x) => PositionWithAim(horizontal, depth, aim + x)
case Command.Up(x) => PositionWithAim(horizontal, depth, aim - x)

Final code for part 2

case class PositionWithAim(horizontal: Int, depth: Int, aim: Int):
def move(p: Command): PositionWithAim =
p match
case Command.Forward(x) => PositionWithAim(horizontal + x, depth + x * aim, aim)
case Command.Down(x) => PositionWithAim(horizontal, depth, aim + x)
case Command.Up(x) => PositionWithAim(horizontal, depth, aim - x)

def result = horizontal * depth

enum Command:
case Forward(x: Int)
case Down(x: Int)
case Up(x: Int)

object Command:
def from(s: String): Command =
s match
case s"forward $x" if x.toIntOption.isDefined => Forward(x.toInt)
case s"up $x" if x.toIntOption.isDefined => Up(x.toInt)
case s"down $x" if x.toIntOption.isDefined => Down(x.toInt)
case _ => throw new Exception(s"value $s is not valid command")

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

The you can run it with scala-cli:

$ scala-cli 2021 -M day2.part1
The answer is 2070300

$ scala-cli 2021 -M day2.part2
The answer is 2078985210

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day20/index.html b/puzzles/day20/index.html index b8958ee5a..c00f7adf1 100644 --- a/puzzles/day20/index.html +++ b/puzzles/day20/index.html @@ -5,7 +5,7 @@ Day 20: Trench Map | Scala Center Advent of Code - + @@ -52,7 +52,7 @@ 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. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day21/index.html b/puzzles/day21/index.html index 42de020fd..143dce2e2 100644 --- a/puzzles/day21/index.html +++ b/puzzles/day21/index.html @@ -5,7 +5,7 @@ Day 21: Dirac Dice | Scala Center Advent of Code - + @@ -23,7 +23,7 @@ There are only 7 different outcomes to the roll of three dice, with most of them occurring several times. The rest of the game is not affected by anything but the sum, although it will happen in several universes, which we need to count. We can implement that by remembering in how many universes the current state of the game gets played, and add that amount to the number of times player 1 or 2 wins.

We first compute how many times each outcome happens:

/** For each 3-die throw, how many of each total sum do we have? */
val dieCombinations: List[(Int, Long)] =
val possibleRolls: List[Int] =
for
die1 <- List(1, 2, 3)
die2 <- List(1, 2, 3)
die3 <- List(1, 2, 3)
yield
die1 + die2 + die3
possibleRolls.groupMapReduce(identity)(_ => 1L)(_ + _).toList

Then, we add a parameter inHowManyUniverses to playWithDiracDie, and multiply it in the recursive calls by the number of times that each outcome happens:

def playWithDiracDie(players: Players, player1Turn: Boolean, wins: Wins, inHowManyUniverses: Long): Unit =
for (diesValue, count) <- dieCombinations do
val newInHowManyUniverses = inHowManyUniverses * count
val player = players(0)
val newCell = (player.cell + diesValue) % 10
val newScore = player.score + (newCell + 1)
if newScore >= 21 then
if player1Turn then
wins.player1Wins += newInHowManyUniverses
else
wins.player2Wins += newInHowManyUniverses
else
val newPlayer = Player(newCell, newScore)
playWithDiracDie((players(1), newPlayer), !player1Turn, wins, newInHowManyUniverses)
end for

We start with 1 universe, so the initial call to playWithDiracDie is:

playWithDiracDie(players, player1Turn = true, wins, inHowManyUniverses = 1L)

The reduction of the branching factor from 27 to 7 is enough to simulate all the possible universes in seconds, whereas I stopped waiting for the naive solution after a few minutes.

Solution for part 2

Here is the full code for part 2:

final class Wins(var player1Wins: Long, var player2Wins: Long)

def part2(input: String): Long =
val players = parseInput(input)
val wins = new Wins(0L, 0L)
playWithDiracDie(players, player1Turn = true, wins, inHowManyUniverses = 1L)
Math.max(wins.player1Wins, wins.player2Wins)

/** For each 3-die throw, how many of each total sum do we have? */
val dieCombinations: List[(Int, Long)] =
val possibleRolls: List[Int] =
for
die1 <- List(1, 2, 3)
die2 <- List(1, 2, 3)
die3 <- List(1, 2, 3)
yield
die1 + die2 + die3
possibleRolls.groupMapReduce(identity)(_ => 1L)(_ + _).toList

def playWithDiracDie(players: Players, player1Turn: Boolean, wins: Wins, inHowManyUniverses: Long): Unit =
for (diesValue, count) <- dieCombinations do
val newInHowManyUniverses = inHowManyUniverses * count
val player = players(0)
val newCell = (player.cell + diesValue) % 10
val newScore = player.score + (newCell + 1)
if newScore >= 21 then
if player1Turn then
wins.player1Wins += newInHowManyUniverses
else
wins.player2Wins += newInHowManyUniverses
else
val newPlayer = Player(newCell, newScore)
playWithDiracDie((players(1), newPlayer), !player1Turn, wins, newInHowManyUniverses)
end for

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 day21.part1
The answer is: 855624

$ scala-cli 2021 -M day21.part2
The answer is: 187451244607486

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day22/index.html b/puzzles/day22/index.html index 5c8d0cf86..1a8c72972 100644 --- a/puzzles/day22/index.html +++ b/puzzles/day22/index.html @@ -5,7 +5,7 @@ Day 22: Reactor Reboot | Scala Center Advent of Code - + @@ -68,7 +68,7 @@ only while they fit the initialisation sequence, and then summarise the set of cuboids:

def part1(input: String): BigInt =
val steps = input.linesIterator.map(StepOf)
summary(run(steps.takeWhile(s => isInit(s.cuboid))))

Solution of Part 2

Part 2 is identical to part 1, except that we run all steps, not just the initialisation sequence:

def part2(input: String): BigInt =
summary(run(input.linesIterator.map(StepOf)))

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 day22.part1
The answer is: 647062

$ scala-cli 2021 -M day22.part2
The answer is: 1319618626668022

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day23/index.html b/puzzles/day23/index.html index cb2d1804a..08f67725c 100644 --- a/puzzles/day23/index.html +++ b/puzzles/day23/index.html @@ -5,7 +5,7 @@ Day 23: Amphipod | Scala Center Advent of Code - + @@ -14,7 +14,7 @@ Our intuition here is that the puzzle can be modeled as a graph and solved using Dijkstra's algorithm.

A graph of situations

We can think of the puzzle as a graph of situations, where a node is an instance of Situation and an edge is an amphipod's move whose weight is the energy cost of the move.

In such a graph, two situations are connected if there is an amphipod move that transform the first situation into the second.

Implementing the Dijkstra's solver

We want to find the minimal energy cost to go from the initial situation to the final situation, where all amphipods are located in their destination room. This is the energy cost of the shortest path between the two situations in the graph described above. We can use Dijkstra's algorithm to find it.

Here is our implementation:

class DijkstraSolver(initialSituation: Situation):
private val bestSituations = mutable.Map(initialSituation -> 0)
private val situationsToExplore =
mutable.PriorityQueue((initialSituation, 0))(Ordering.by((_, energy) => -energy))

@tailrec
final def solve(): Energy =
val (situation, energy) = situationsToExplore.dequeue
if situation.isFinal then energy
else if bestSituations(situation) < energy then solve()
else
for
(nextSituation, consumedEnergy) <- situation.moveAllAmphipodsOnce
nextEnergy = energy + consumedEnergy
knownEnergy = bestSituations.getOrElse(nextSituation, Int.MaxValue)
if nextEnergy < knownEnergy
do
bestSituations.update(nextSituation, nextEnergy)
situationsToExplore.enqueue((nextSituation, nextEnergy))
solve()

At the beginning we only know the cost of the initial situation which is 0.

The solve method is recursive:

  1. First we dequeue the best known situation in the situationToExplore queue.
  2. If it is the final situation, we return the associated energy cost.
  3. If it is not:
  • We compute all the situations connected to it by moving all amphipods once.
  • For each of these new situations, we check if the energy cost is better than before and if so we add it into the queue.
  • We recurse by calling solve again.

Final solution

// using scala 3.1.0

package day23

import scala.util.Using
import scala.io.Source
import scala.annotation.tailrec
import scala.collection.mutable


@main def part1(): Unit =
val answer = part1(readInput())
println(s"The answer is: $answer")

@main def part2(): Unit =
val answer = part2(readInput())
println(s"The answer is: $answer")

def readInput(): String =
Using.resource(Source.fromFile("input/day23"))(_.mkString)

case class Position(x: Int, y: Int)

enum Room(val x: Int):
case A extends Room(3)
case B extends Room(5)
case C extends Room(7)
case D extends Room(9)

type Energy = Int

enum Amphipod(val energy: Energy, val destination: Room):
case A extends Amphipod(1, Room.A)
case B extends Amphipod(10, Room.B)
case C extends Amphipod(100, Room.C)
case D extends Amphipod(1000, Room.D)

object Amphipod:
def tryParse(input: Char): Option[Amphipod] =
input match
case 'A' => Some(Amphipod.A)
case 'B' => Some(Amphipod.B)
case 'C' => Some(Amphipod.C)
case 'D' => Some(Amphipod.D)
case _ => None

val hallwayStops: Seq[Position] = Seq(
Position(1, 1),
Position(2, 1),
Position(4, 1),
Position(6, 1),
Position(8, 1),
Position(10, 1),
Position(11, 1)
)

case class Situation(positions: Map[Position, Amphipod], roomSize: Int):
def moveAllAmphipodsOnce: Seq[(Situation, Energy)] =
for
(start, amphipod) <- positions.toSeq
stop <- nextStops(amphipod, start)
path = getPath(start, stop)
if path.forall(isEmpty)
yield
val newPositions = positions - start + (stop -> amphipod)
val energy = path.size * amphipod.energy
(copy(positions = newPositions), energy)

def isFinal =
positions.forall((position, amphipod) => position.x == amphipod.destination.x)

/**
* Return a list of positions to which an amphipod at position `from` can go:
* - If the amphipod is in its destination room and the room is free it must not go anywhere.
* - If the amphipod is in its destination room and the room is not free it can go to the hallway.
* - If the amphipod is in the hallway it can only go to its destination.
* - Otherwise it can go to the hallway.
*/
private def nextStops(amphipod: Amphipod, from: Position): Seq[Position] =
from match
case Position(x, y) if x == amphipod.destination.x =>
if isDestinationFree(amphipod) then Seq.empty
else hallwayStops
case Position(_, 1) =>
if isDestinationFree(amphipod) then
(roomSize + 1).to(2, step = -1)
.map(y => Position(amphipod.destination.x, y))
.find(isEmpty)
.toSeq
else Seq.empty
case _ => hallwayStops


private def isDestinationFree(amphipod: Amphipod): Boolean =
2.to(roomSize + 1)
.flatMap(y => positions.get(Position(amphipod.destination.x, y)))
.forall(_ == amphipod)

// Build the path to go from `start` to `stop`
private def getPath(start: Position, stop: Position): Seq[Position] =
val hallway =
if start.x < stop.x
then (start.x + 1).to(stop.x).map(Position(_, 1))
else (start.x - 1).to(stop.x, step = -1).map(Position(_, 1))
val startRoom = (start.y - 1).to(1, step = -1).map(Position(start.x, _))
val stopRoom = 2.to(stop.y).map(Position(stop.x, _))
startRoom ++ hallway ++ stopRoom

private def isEmpty(position: Position) =
!positions.contains(position)

object Situation:
def parse(input: String, roomSize: Int): Situation =
val positions =
for
(line, y) <- input.linesIterator.zipWithIndex
(char, x) <- line.zipWithIndex
amphipod <- Amphipod.tryParse(char)
yield Position(x, y) -> amphipod
Situation(positions.toMap, roomSize)

class DijkstraSolver(initialSituation: Situation):
private val bestSituations = mutable.Map(initialSituation -> 0)
private val situationsToExplore =
mutable.PriorityQueue((initialSituation, 0))(Ordering.by((_, energy) => -energy))

@tailrec
final def solve(): Energy =
val (situation, energy) = situationsToExplore.dequeue
if situation.isFinal then energy
else if bestSituations(situation) < energy then solve()
else
for
(nextSituation, consumedEnergy) <- situation.moveAllAmphipodsOnce
nextEnergy = energy + consumedEnergy
knownEnergy = bestSituations.getOrElse(nextSituation, Int.MaxValue)
if nextEnergy < knownEnergy
do
bestSituations.update(nextSituation, nextEnergy)
situationsToExplore.enqueue((nextSituation, nextEnergy))
solve()

def part1(input: String): Energy =
val initialSituation = Situation.parse(input, roomSize = 2)
DijkstraSolver(initialSituation).solve()

def part2(input: String): Energy =
val lines = input.linesIterator
val unfoldedInput = (lines.take(3) ++ Seq(" #D#C#B#A#", " #D#B#A#C#") ++ lines.take(2)).mkString("\n")
val initialSituation = Situation.parse(unfoldedInput, roomSize = 4)
DijkstraSolver(initialSituation).solve()

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 day21.part1
The answer is: 855624

$ scala-cli 2021 -M day21.part2
The answer is: 187451244607486

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day24/index.html b/puzzles/day24/index.html index 09cb4f9af..3bcf573e2 100644 --- a/puzzles/day24/index.html +++ b/puzzles/day24/index.html @@ -5,13 +5,13 @@ Day 24: Arithmetic Logic Unit | Scala Center Advent of Code - +
- + \ No newline at end of file diff --git a/puzzles/day25/index.html b/puzzles/day25/index.html index df6fbdc6c..7f72c73a8 100644 --- a/puzzles/day25/index.html +++ b/puzzles/day25/index.html @@ -5,13 +5,13 @@ Day 25: Sea Cucumber | Scala Center Advent of Code - +

Day 25: Sea Cucumber

by @Sporarum, student at EPFL, and @adpi2

Puzzle description

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

Solution of Part 1

enum SeaCucumber:
case Empty, East, South

object SeaCucumber:
def fromChar(c: Char) = c match
case '.' => Empty
case '>' => East
case 'v' => South

type Board = Seq[Seq[SeaCucumber]]

def part1(input: String): Int =
val board: Board = input.linesIterator.map(_.map(SeaCucumber.fromChar(_))).toSeq
fixedPoint(board)

def fixedPoint(board: Board, step: Int = 1): Int =
val next = move(board)
if board == next then step else fixedPoint(next, step + 1)

def move(board: Board) = moveSouth(moveEast(board))
def moveEast(board: Board) = moveImpl(board, SeaCucumber.East)
def moveSouth(board: Board) = moveImpl(board.transpose, SeaCucumber.South).transpose

def moveImpl(board: Board, cucumber: SeaCucumber): Board =
board.map { l =>
zip3(l.last +: l.init, l, (l.tail :+ l.head)).map{
case (`cucumber`, SeaCucumber.Empty, _) => `cucumber`
case (_, `cucumber`, SeaCucumber.Empty) => SeaCucumber.Empty
case (_, curr, _) => curr
}
}

def zip3[A,B,C](l1: Seq[A], l2: Seq[B], l3: Seq[C]): Seq[(A,B,C)] =
l1.zip(l2).zip(l3).map { case ((a, b), c) => (a,b,c) }

Run it in the browser

Part 1

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 day25.part1
The answer is: 435

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day3/index.html b/puzzles/day3/index.html index 501657c26..85374f8fa 100644 --- a/puzzles/day3/index.html +++ b/puzzles/day3/index.html @@ -5,7 +5,7 @@ Day 3: Binary Diagnostic | Scala Center Advent of Code - + @@ -34,7 +34,7 @@ Here is an example of partition that separates odd numbers from even numbers:

val numbers = List(4, 6, 5, 12, 75, 3, 10)
val (oddNumbers, evenNumbers) = numbers.partition(x => x % 2 != 0)
// oddNumbers = List(5, 75, 3)
// evenNumbers = List(4, 6, 12, 10)

We use it as follows to separate our lines in two lists:

val (bitLinesWithOne, bitLinesWithZero) =
bitLines.partition(line => line(bitPosition) == 1)

We can determine whether there are more 1s than 0s (or a tie) by comparing the size of the two lists. Comparing the sizes of two collections is best done with sizeCompare:

val onesAreMostCommon = bitLinesWithOne.sizeCompare(bitLinesWithZero) >= 0

Finally, we decide which list we keep to go further:

val bitLinesToKeep =
if onesAreMostCommon then
if keepMostCommon then bitLinesWithOne else bitLinesWithZero
else
if keepMostCommon then bitLinesWithZero else bitLinesWithOne
recursiveFilter(bitLinesToKeep, bitPosition + 1, keepMostCommon)

(The two tests could be combined as if onesAreMostCommon == keepMostCommon, but I found that less readable.)

Final code for part 2

def part2(input: String): Int =
val bitLines: List[BitLine] = input.linesIterator.map(parseBitLine).toList

val oxygenGeneratorRatingLine: BitLine =
recursiveFilter(bitLines, 0, keepMostCommon = true)
val oxygenGeneratorRating = bitLineToInt(oxygenGeneratorRatingLine)

val co2ScrubberRatingLine: BitLine =
recursiveFilter(bitLines, 0, keepMostCommon = false)
val co2ScrubberRating = bitLineToInt(co2ScrubberRatingLine)

oxygenGeneratorRating * co2ScrubberRating

@scala.annotation.tailrec
def recursiveFilter(bitLines: List[BitLine], bitPosition: Int,
keepMostCommon: Boolean): BitLine =
bitLines match
case Nil =>
throw new AssertionError("this shouldn't have happened")
case lastRemainingLine :: Nil =>
lastRemainingLine
case _ =>
val (bitLinesWithOne, bitLinesWithZero) =
bitLines.partition(line => line(bitPosition) == 1)
val onesAreMostCommon = bitLinesWithOne.sizeCompare(bitLinesWithZero) >= 0
val bitLinesToKeep =
if onesAreMostCommon then
if keepMostCommon then bitLinesWithOne else bitLinesWithZero
else
if keepMostCommon then bitLinesWithZero else bitLinesWithOne
recursiveFilter(bitLinesToKeep, bitPosition + 1, keepMostCommon)

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. Since today's solution is written in Scala.js, you will need a local setup of Node.js to run it.

$ scala-cli 2021 -M day3.part1 --js-module-kind commonjs
The answer is 1025636

$ scala-cli 2021 -M day3.part2 --js-module-kind commonjs
The answer is 793873

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day4/index.html b/puzzles/day4/index.html index 5ed9df6dd..e361e510e 100644 --- a/puzzles/day4/index.html +++ b/puzzles/day4/index.html @@ -5,7 +5,7 @@ Day 4: Giant Squid | Scala Center Advent of Code - + @@ -24,7 +24,7 @@ We filter them with lines.filter(_ > turn).

However, only taking the sum would be wrong, as we are using the turns, and not the original numbers! We thus need to map them to their original values:

val sumNumsNotDrawn = board.lines.map{ line =>
line.filter(_ > turn).map(turnToNumber(_)).sum
}.sum

The score is then:

turnToNumber(turn) * sumUnmarkedNums

Solution of Part 1

In part one, we have to compute the score of the first board to win. This is the board whith the smallest winning turn.

val (winnerBoard, winnerTurn) = winningTurns.minBy((_, turn) => turn)

And so the score is:

val winnerScore = score(winnerBoard, winnerTurn)

Solution of Part 2

In part two, we have to find the score of the last board to win, so we swap the minBy by a maxBy to get our result:

val (loserBoard, loserTurn) = winningTurns.maxBy((_, turn) => turn)
val loserScore = score(loserBoard, loserTurn)

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 day4.run
The answer of part 1 is 14093.
The answer of part 2 is 17388.

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day5/index.html b/puzzles/day5/index.html index f3e8767fe..50465419f 100644 --- a/puzzles/day5/index.html +++ b/puzzles/day5/index.html @@ -5,7 +5,7 @@ Day 5: Hydrothermal Venture | Scala Center Advent of Code - + @@ -24,7 +24,7 @@ both x and y positions increment by 1 at each step of the range. So we can add additional condition to our solution:

else for (px, py) <- rangex.zip(rangey) do update(Point(px, py))

We can just use the 2 previously defined ranges for this. So the full method will look like this:

def findDangerousPoints(vents: Seq[Vent]) =
val map = mutable.Map[Point, Int]().withDefaultValue(0)
def update(p: Point) =
val current = map(p)
map.update(p, current + 1)

for vent <- vents do
def rangex =
val stepx = if vent.end.x > vent.start.x then 1 else -1
vent.start.x.to(vent.end.x, stepx)
def rangey =
val stepy = if vent.end.y > vent.start.y then 1 else -1
vent.start.y.to(vent.end.y, stepy)
// vent is horizontal
if vent.start.x == vent.end.x then
for py <- rangey do update(Point(vent.start.x, py))
// vent is vertical
else if vent.start.y == vent.end.y then
for px <- rangex do update(Point(px, vent.start.y))
// vent is diagonal
else for (px, py) <- rangex.zip(rangey) do update(Point(px, py))
end for

map.count { case (_, v) => v > 1 }
end findDangerousPoints

Run solution 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 day5.part1
The answer is: 7674

$ scala-cli 2021 -M day5.part2
The answer is: 20898

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day6/index.html b/puzzles/day6/index.html index 257d27477..13d20a582 100644 --- a/puzzles/day6/index.html +++ b/puzzles/day6/index.html @@ -5,7 +5,7 @@ Day 6: Lanternfish | Scala Center Advent of Code - + @@ -68,7 +68,7 @@ achieves this by summing the groups of fish: the method values returns a collection of groups of fish (each containing the number of fish in that group), finally the method sum sums up the groups.

Final code for part 2

// "How many lanternfish would there be after 256 days?"
def part2(input: String): BigInt =
simulate(
days = 256,
Fish.parseSeveral(input).groupMapReduce(_.timer)(_ => BigInt(1))(_ + _)
)

def simulate(days: Int, initialPopulation: Map[Int, BigInt]): BigInt =
(1 to days)
.foldLeft(initialPopulation)((population, _) => tick(population))
.values
.sum

def tick(population: Map[Int, BigInt]): Map[Int, BigInt] =
def countPopulation(daysLeft: Int): BigInt = population.getOrElse(daysLeft, BigInt(0))
Map(
0 -> countPopulation(1),
1 -> countPopulation(2),
2 -> countPopulation(3),
3 -> countPopulation(4),
4 -> countPopulation(5),
5 -> countPopulation(6),
6 -> (countPopulation(7) + countPopulation(0)),
7 -> countPopulation(8),
8 -> countPopulation(0)
)

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 day6.part1
The solution is 345793

$ scala-cli 2021 -M day6.part2
The solution is 1572643095893

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day7/index.html b/puzzles/day7/index.html index 0eb021c0a..01d843f68 100644 --- a/puzzles/day7/index.html +++ b/puzzles/day7/index.html @@ -5,7 +5,7 @@ Day 7: The Treachery of Whales | Scala Center Advent of Code - + @@ -40,7 +40,7 @@ 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. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day8/index.html b/puzzles/day8/index.html index f6a3f8fbc..b5d33102b 100644 --- a/puzzles/day8/index.html +++ b/puzzles/day8/index.html @@ -5,7 +5,7 @@ Day 8: Seven Segment Search | Scala Center Advent of Code - + @@ -82,7 +82,7 @@ Each display has 4 digits, so after decoding the digits we will have a sequence of 4 Digit.

To convert a sequence of Digit to an integer value, we can convert each digit to its corresponding integer representation by calling .ordinal, and then we can accumulate a sum by (from the left), multiplying the current total by 10 for each new digit, and then adding the current digit:

def digitsToInt(digits: Seq[Digit]): Int =
digits.foldLeft(0)((acc, d) => acc * 10 + d.ordinal)

Final Result

Finally, we use our digitsToInt function to convert each solution to an integer value, and sum the result:

solutions.map(digitsToInt).sum

Final Code

The final code for part 2 can be appended to the code of part 1:

import Digit.*

def part2(input: String): Int =

def parseSegmentsSeq(segments: String): Seq[Segments] =
segments.trim.split(" ").toSeq.map(Segment.parseSegments)

def splitParts(line: String): (Seq[Segments], Seq[Segments]) =
val Array(cipher, plaintext) = line.split('|').map(parseSegmentsSeq)
(cipher, plaintext)

def digitsToInt(digits: Seq[Digit]): Int =
digits.foldLeft(0)((acc, d) => acc * 10 + d.ordinal)

val problems = input.linesIterator.map(splitParts)

val solutions = problems.map((cipher, plaintext) =>
plaintext.map(substitutions(cipher))
)

solutions.map(digitsToInt).sum

end part2

def substitutions(cipher: Seq[Segments]): Map[Segments, Digit] =

def lookup(section: Seq[Segments], withSegments: Segments): (Segments, Seq[Segments]) =
val (Seq(uniqueMatch), remaining) = section.partition(withSegments.subsetOf)
(uniqueMatch, remaining)

val uniques: Map[Digit, Segments] =
Map.from(
for
segments <- cipher
digit <- Digit.lookupUnique(segments)
yield
digit -> segments
)

val ofSizeFive = cipher.filter(_.sizeIs == 5)
val ofSizeSix = cipher.filter(_.sizeIs == 6)

val one = uniques(One)
val four = uniques(Four)
val seven = uniques(Seven)
val eight = uniques(Eight)
val (three, remainingFives) = lookup(ofSizeFive, withSegments = one)
val (nine, remainingSixes) = lookup(ofSizeSix, withSegments = three)
val (zero, Seq(six)) = lookup(remainingSixes, withSegments = seven)
val (five, Seq(two)) = lookup(remainingFives, withSegments = four &~ one)

val decode: Map[Segments, Digit] =
Seq(zero, one, two, three, four, five, six, seven, eight, nine)
.zip(Digit.index)
.toMap

decode
end substitutions

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 day8.part1
The solution is 521

$ scala-cli 2021 -M day8.part2
The solution is 1016804

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/puzzles/day9/index.html b/puzzles/day9/index.html index 13179df35..79fc661b5 100644 --- a/puzzles/day9/index.html +++ b/puzzles/day9/index.html @@ -5,7 +5,7 @@ Day 9: Smoke Basin | Scala Center Advent of Code - + @@ -37,7 +37,7 @@ retrieve neighbors of neighbors, I add the cells that still need to be processed in the queue. The algorithm stops when there are no more cells to visit:

def basin(lowPoint: Position, heightMap: Heightmap): Set[Position] =
@scala.annotation.tailrec
def iter(visited: Set[Position], toVisit: Queue[Position], basinAcc: Set[Position]): Set[Position] =
// No cells to visit, we are done
if toVisit.isEmpty then basinAcc
else
// Select next cell to visit
val (currentPos, remaining) = toVisit.dequeue
// Collect the neighboring cells that should be part of the basin
val newNodes = heightMap.neighborsOf(currentPos).toList.collect {
case (pos, height) if !visited(currentPos) && height != 9 => pos
}
// Continue to next neighbor
iter(visited + currentPos, remaining ++ newNodes, basinAcc ++ newNodes)

iter(Set.empty, Queue(lowPoint), Set(lowPoint))

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 day9.part1
The solution is 448
$ scala-cli 2021 -M day9.part2
The solution is 1417248

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

Solutions from the community

Share your solution to the Scala community by editing this page. (You can even write the whole article!)

- + \ No newline at end of file diff --git a/setup/index.html b/setup/index.html index 5c5e67039..76008285e 100644 --- a/setup/index.html +++ b/setup/index.html @@ -5,7 +5,7 @@ Setup | Scala Center Advent of Code - + @@ -18,7 +18,7 @@ It supports an incredible number of languages through its extension system.

Its more popular extension for Scala is called Metals. We will use VS Code and Metals to write and navigate Scala code.

VS Code

Download the right VS Code for your operating system on the download page of VS Code and then install it.

Install Metals

1. Open VS Code and Click the extensions icon in the left bar

Open Extensions

2. Search metals and click the Scala (Metals) extension and click the Install button

Install Metals

- + \ No newline at end of file