From 4610c9d2de5c90cba388802950f920d469f6d46c Mon Sep 17 00:00:00 2001 From: Docusaurus bot Date: Sat, 21 Dec 2024 05:30:00 +0000 Subject: [PATCH] Deploy website - based on 682ff4cc2859883b6967e11a9542e2db58dc7dad --- 2021/index.html | 10 +++++----- 2022/index.html | 10 +++++----- 2022/puzzles/day01/index.html | 10 +++++----- 2022/puzzles/day02/index.html | 10 +++++----- 2022/puzzles/day03/index.html | 10 +++++----- 2022/puzzles/day04/index.html | 10 +++++----- 2022/puzzles/day05/index.html | 10 +++++----- 2022/puzzles/day06/index.html | 10 +++++----- 2022/puzzles/day07/index.html | 10 +++++----- 2022/puzzles/day08/index.html | 10 +++++----- 2022/puzzles/day09/index.html | 10 +++++----- 2022/puzzles/day10/index.html | 10 +++++----- 2022/puzzles/day11/index.html | 10 +++++----- 2022/puzzles/day12/index.html | 10 +++++----- 2022/puzzles/day13/index.html | 10 +++++----- 2022/puzzles/day14/index.html | 10 +++++----- 2022/puzzles/day15/index.html | 10 +++++----- 2022/puzzles/day16/index.html | 10 +++++----- 2022/puzzles/day17/index.html | 10 +++++----- 2022/puzzles/day18/index.html | 10 +++++----- 2022/puzzles/day19/index.html | 10 +++++----- 2022/puzzles/day20/index.html | 10 +++++----- 2022/puzzles/day21/index.html | 10 +++++----- 2022/puzzles/day22/index.html | 10 +++++----- 2022/puzzles/day23/index.html | 10 +++++----- 2022/puzzles/day24/index.html | 10 +++++----- 2022/puzzles/day25/index.html | 10 +++++----- 2023/index.html | 10 +++++----- 2023/puzzles/day01/index.html | 12 ++++++------ 2023/puzzles/day02/index.html | 10 +++++----- 2023/puzzles/day03/index.html | 10 +++++----- 2023/puzzles/day04/index.html | 10 +++++----- 2023/puzzles/day05/index.html | 10 +++++----- 2023/puzzles/day06/index.html | 10 +++++----- 2023/puzzles/day07/index.html | 10 +++++----- 2023/puzzles/day08/index.html | 10 +++++----- 2023/puzzles/day09/index.html | 10 +++++----- 2023/puzzles/day10/index.html | 10 +++++----- 2023/puzzles/day11/index.html | 10 +++++----- 2023/puzzles/day12/index.html | 10 +++++----- 2023/puzzles/day13/index.html | 10 +++++----- 2023/puzzles/day14/index.html | 10 +++++----- 2023/puzzles/day15/index.html | 10 +++++----- 2023/puzzles/day16/index.html | 10 +++++----- 2023/puzzles/day17/index.html | 10 +++++----- 2023/puzzles/day18/index.html | 10 +++++----- 2023/puzzles/day19/index.html | 10 +++++----- 2023/puzzles/day20/index.html | 10 +++++----- 2023/puzzles/day21/index.html | 10 +++++----- 2023/puzzles/day22/index.html | 10 +++++----- 2023/puzzles/day23/index.html | 10 +++++----- 2023/puzzles/day24/index.html | 10 +++++----- 2023/puzzles/day25/index.html | 10 +++++----- 2024/index.html | 10 +++++----- 2024/puzzles/day0/index.html | 10 +++++----- 2024/puzzles/day01/index.html | 10 +++++----- 2024/puzzles/day02/index.html | 10 +++++----- 2024/puzzles/day03/index.html | 10 +++++----- 2024/puzzles/day04/index.html | 10 +++++----- 2024/puzzles/day05/index.html | 10 +++++----- 2024/puzzles/day06/index.html | 10 +++++----- 2024/puzzles/day07/index.html | 10 +++++----- 2024/puzzles/day08/index.html | 10 +++++----- 2024/puzzles/day09/index.html | 10 +++++----- 2024/puzzles/day10/index.html | 10 +++++----- 2024/puzzles/day11/index.html | 10 +++++----- 2024/puzzles/day12/index.html | 10 +++++----- 2024/puzzles/day13/index.html | 10 +++++----- 2024/puzzles/day14/index.html | 10 +++++----- 2024/puzzles/day15/index.html | 10 +++++----- 2024/puzzles/day16/index.html | 10 +++++----- 2024/puzzles/day17/index.html | 10 +++++----- 2024/puzzles/day18/index.html | 10 +++++----- 2024/puzzles/day19/index.html | 10 +++++----- 2024/puzzles/day20/index.html | 12 ++++++------ 2024/puzzles/day21/index.html | 18 ++++++++++++++++++ 404.html | 10 +++++----- assets/js/73b9919f.33a91d3e.js | 1 + assets/js/73b9919f.d577c60d.js | 1 - assets/js/935f2afb.4524d548.js | 1 - assets/js/935f2afb.ac7730e4.js | 1 + assets/js/980a4c0d.4c069f5d.js | 1 + assets/js/980a4c0d.b5e51a7d.js | 1 - assets/js/f5509088.998a04b7.js | 1 + assets/js/main.c82ce0e7.js | 2 -- assets/js/main.dfe73e1e.js | 2 ++ ...ICENSE.txt => main.dfe73e1e.js.LICENSE.txt} | 0 assets/js/runtime~main.68500a6b.js | 1 + assets/js/runtime~main.6dd29f7f.js | 1 - index.html | 10 +++++----- introduction/index.html | 10 +++++----- puzzles/day1/index.html | 10 +++++----- puzzles/day10/index.html | 10 +++++----- puzzles/day11/index.html | 10 +++++----- puzzles/day12/index.html | 10 +++++----- puzzles/day13/index.html | 10 +++++----- puzzles/day14/index.html | 10 +++++----- puzzles/day15/index.html | 10 +++++----- puzzles/day16/index.html | 10 +++++----- puzzles/day17/index.html | 10 +++++----- puzzles/day18/index.html | 10 +++++----- puzzles/day19/index.html | 10 +++++----- puzzles/day2/index.html | 10 +++++----- puzzles/day20/index.html | 10 +++++----- puzzles/day21/index.html | 10 +++++----- puzzles/day22/index.html | 10 +++++----- puzzles/day23/index.html | 10 +++++----- puzzles/day24/index.html | 10 +++++----- puzzles/day25/index.html | 10 +++++----- puzzles/day3/index.html | 10 +++++----- puzzles/day4/index.html | 10 +++++----- puzzles/day5/index.html | 10 +++++----- puzzles/day6/index.html | 10 +++++----- puzzles/day7/index.html | 10 +++++----- puzzles/day8/index.html | 10 +++++----- puzzles/day9/index.html | 10 +++++----- setup/index.html | 10 +++++----- sitemap.xml | 2 +- 118 files changed, 548 insertions(+), 529 deletions(-) create mode 100644 2024/puzzles/day21/index.html create mode 100644 assets/js/73b9919f.33a91d3e.js delete mode 100644 assets/js/73b9919f.d577c60d.js delete mode 100644 assets/js/935f2afb.4524d548.js create mode 100644 assets/js/935f2afb.ac7730e4.js create mode 100644 assets/js/980a4c0d.4c069f5d.js delete mode 100644 assets/js/980a4c0d.b5e51a7d.js create mode 100644 assets/js/f5509088.998a04b7.js delete mode 100644 assets/js/main.c82ce0e7.js create mode 100644 assets/js/main.dfe73e1e.js rename assets/js/{main.c82ce0e7.js.LICENSE.txt => main.dfe73e1e.js.LICENSE.txt} (100%) create mode 100644 assets/js/runtime~main.68500a6b.js delete mode 100644 assets/js/runtime~main.6dd29f7f.js diff --git a/2021/index.html b/2021/index.html index c135a3073..fd60746d8 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.

- - +
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 85159c657..6c2e5c4dd 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.

- - +
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 09cc6cb85..f1c65a638 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 +

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.

- - + + \ No newline at end of file diff --git a/2022/puzzles/day02/index.html b/2022/puzzles/day02/index.html index 892d9874e..99e1504c8 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day03/index.html b/2022/puzzles/day03/index.html index bd513ad0f..c4f7d589d 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day04/index.html b/2022/puzzles/day04/index.html index 8b87eb044..0a4533e81 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day05/index.html b/2022/puzzles/day05/index.html index 79638ea33..642d68bdb 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day06/index.html b/2022/puzzles/day06/index.html index d55063068..d6b87479e 100644 --- a/2022/puzzles/day06/index.html +++ b/2022/puzzles/day06/index.html @@ -5,12 +5,12 @@ Day 6: Tuning Trouble | Scala Center Advent of Code - - + +
-

Day 6: Tuning Trouble

Code by Jan Boerman, and Jamie Thompson. +

Day 6: Tuning Trouble

Code by Jan Boerman, and Jamie Thompson. Article by Quentin Bernet and Jamie Thompson

Puzzle description

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

Solution

The goal today is to find the first spot of the input with 4 consecutive characters that are all different.

There are thus three steps: look at chunks of 4 consecutive characters, check if they are all different, and find the first index among those.

To look at windows of 4 characters, we can use the sliding method on Strings with 4 as the size:

  val windows = input.sliding(4)

To check if characters in a string are all different, a nice trick is to first convert it to a Set, and then testing if the size is the same as the original: myString.toSet.size == myString.size. In this case we know the size will always be 4, because sliding(4) always returns strings of length 4, so we can write:

  def allDifferent(s: String): Boolean = s.toSet.size == 4

The last piece of the puzzle is to find the first index where a condition is true, again the standard library has something for us: indexWhere.

  val firstIndex = windows.indexWhere(allDifferent)

We can now assemble everything:

def part1(input: String): Int =
val windows = input.sliding(4)
def allDifferent(s: String): Boolean = s.toSet.size == 4
val firstIndex = windows.indexWhere(allDifferent)
firstIndex + 4

You'll notice we have to add 4 to the final answer, that's because firstIndex tells us the index of the first character of the window, and we want the last one.

That was only the solution for the first part, but the only difference for part 2 is that the sequences need to be of 14 characters instead of 4!

So we can just extract our logic into a nice function:

def findIndex(input: String, n: Int): Int =
val windows = input.sliding(n)
def allDifferent(s: String): Boolean = s.toSet.size == n
val firstIndex = windows.indexWhere(allDifferent)
firstIndex + n

And inline the intermediate results:

def findIndex(input: String, n: Int): Int =
input.sliding(n).indexWhere(_.toSet.size == n) + n

There we have it, a one-line solution!

P.S: sliding, toSet, and indexWhere are not only available for Strings but for almost all collections!

Final Code

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

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

def findIndex(input: String, n: Int): Int =
input.sliding(n).indexWhere(_.toSet.size == n) + n

Run it in the browser

Part 1

Part 2

Optimising the Code

The code shown so far is very concise, however it is not optimal for very large input strings. There will be many intermediate objects created, such as the strings in the sliding window, and the sets on each string in the window.

This can make pressure on the garbage collector, slowing down the program.

We can optimise this in two ways:

  • avoid allocating intermediate strings for each window,
  • reusing a mutable set to record which characters are in the window.

For the optimised solution, you can reuse a single, mutable set. As you advance the window by 1 index, you should remove the first element of the previous window, and add the last element of the current window.

There is a problem however, an ordinary set is not enough, you need a multiset to record how many times each character @@ -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.

- - + + \ No newline at end of file diff --git a/2022/puzzles/day07/index.html b/2022/puzzles/day07/index.html index 20d39cee8..0dedf2aad 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day08/index.html b/2022/puzzles/day08/index.html index 458118aa0..f394bbf4f 100644 --- a/2022/puzzles/day08/index.html +++ b/2022/puzzles/day08/index.html @@ -5,12 +5,12 @@ Day 8: Treetop Tree House | Scala Center Advent of Code - - + +
-

Day 8: Treetop Tree House

code and article by Quentin Bernet

Puzzle description

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

Solution

Part 1

As always, we have to start by parsing the puzzle input. We can convert the string into a list of lines by splitting at the /n character: input.split('\n').toList. +

Day 8: Treetop Tree House

code and article by Quentin Bernet

Puzzle description

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

Solution

Part 1

As always, we have to start by parsing the puzzle input. We can convert the string into a list of lines by splitting at the /n character: input.split('\n').toList. If we now focus on a single line, String behaves like a list of Chars so we can use map on it! And then the individual Chars can be converted to Int with asDigit: line.map(char => char.asDigit).toList.

Note: char.toInt would return the ascii value, so for example '0'.toInt == 48.

Putting this all together, we get:

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

Oh what's HeightField ? We'll manipulate a lot of List[List[something]], so it's useful to create a type alias for it:

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

And a HeightField is well; a field of heights! @@ -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.

- - + + \ No newline at end of file diff --git a/2022/puzzles/day09/index.html b/2022/puzzles/day09/index.html index 6b222301e..40e2b5014 100644 --- a/2022/puzzles/day09/index.html +++ b/2022/puzzles/day09/index.html @@ -5,12 +5,12 @@ Day 9: Rope Bridge | Scala Center Advent of Code - - + +
-

Day 9: Rope Bridge

code by Jamie Thompson

Puzzle description

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

Solution

Today's goal is to find the unique positions occupied by the final knot of a rope. For part 1 the rope has 2 knots, and +

Day 9: Rope Bridge

code by Jamie Thompson

Puzzle description

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

Solution

Today's goal is to find the unique positions occupied by the final knot of a rope. For part 1 the rope has 2 knots, and for part 2, it has 3.

To model a position, you can use a case class to store x and y coordinates:

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

The front knot, or head of the rope can be translated in one of four directions U, D, L, R; modelled as an enum:

enum Direction:
case U, D, L, R

Reading the challenge description, we know that the head can translate in any direction by multiple steps. You can model a single step by the following method moveOne on Position:

import Direction.*

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)

Then using the rules described in the challenge description, one knot follows another knot, by translating 1 position in the vector between it and the previous knot. This is modelled by another method, follow on Position:

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

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
info

Its important to note that while the head of the rope can move in only 1 of the four directions, a trailing knot can @@ -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.

- - + + \ No newline at end of file diff --git a/2022/puzzles/day10/index.html b/2022/puzzles/day10/index.html index e69b8faea..7459ec023 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 - - + +
-

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: +

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.

- - + + \ No newline at end of file diff --git a/2022/puzzles/day11/index.html b/2022/puzzles/day11/index.html index 85d0805b1..ebe9e134a 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day12/index.html b/2022/puzzles/day12/index.html index 99dd38ecf..de9e7e348 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day13/index.html b/2022/puzzles/day13/index.html index f09f3a398..7e1ba9afa 100644 --- a/2022/puzzles/day13/index.html +++ b/2022/puzzles/day13/index.html @@ -5,14 +5,14 @@ Day 13: Distress Signal | Scala Center Advent of Code - - + +
-

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. +

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! See here for the expected format

- - + + \ No newline at end of file diff --git a/2022/puzzles/day14/index.html b/2022/puzzles/day14/index.html index 1165c24f4..d760eccde 100644 --- a/2022/puzzles/day14/index.html +++ b/2022/puzzles/day14/index.html @@ -5,14 +5,14 @@ Day 14: Regolith Reservoir | Scala Center Advent of Code - - + +
-

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. +

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! See here for the expected format

- - + + \ No newline at end of file diff --git a/2022/puzzles/day15/index.html b/2022/puzzles/day15/index.html index 5eea4f7cb..06ce4bdbf 100644 --- a/2022/puzzles/day15/index.html +++ b/2022/puzzles/day15/index.html @@ -5,15 +5,15 @@ Day 15: Beacon Exclusion Zone | Scala Center Advent of Code - - + +
-

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. +

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! See here for the expected format

- - + + \ No newline at end of file diff --git a/2022/puzzles/day16/index.html b/2022/puzzles/day16/index.html index 894d6fa89..13a9a2b9c 100644 --- a/2022/puzzles/day16/index.html +++ b/2022/puzzles/day16/index.html @@ -5,14 +5,14 @@ Day 16: Proboscidea Volcanium | Scala Center Advent of Code - - + +
-

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. +

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! See here for the expected format

- - + + \ No newline at end of file diff --git a/2022/puzzles/day17/index.html b/2022/puzzles/day17/index.html index fc64dda90..cc03dfcfc 100644 --- a/2022/puzzles/day17/index.html +++ b/2022/puzzles/day17/index.html @@ -5,14 +5,14 @@ Day 17: Pyroclastic Flow | Scala Center Advent of Code - - + +
-

Day 17: Pyroclastic Flow

Puzzle description

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

Solutions from the community

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

- - + + \ No newline at end of file diff --git a/2022/puzzles/day18/index.html b/2022/puzzles/day18/index.html index 8b25d914c..b910fc272 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day19/index.html b/2022/puzzles/day19/index.html index 9715c70c6..f002b3619 100644 --- a/2022/puzzles/day19/index.html +++ b/2022/puzzles/day19/index.html @@ -5,14 +5,14 @@ Day 19: Not Enough Minerals | Scala Center Advent of Code - - + +
-

Day 19: Not Enough Minerals

Puzzle description

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

Solutions from the community

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

- - + + \ No newline at end of file diff --git a/2022/puzzles/day20/index.html b/2022/puzzles/day20/index.html index c89a2764c..4b418a7d7 100644 --- a/2022/puzzles/day20/index.html +++ b/2022/puzzles/day20/index.html @@ -5,14 +5,14 @@ Day 20: Grove Positioning System | Scala Center Advent of Code - - + +
-

Day 20: Grove Positioning System

Puzzle description

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

Solutions from the community

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

- - + + \ No newline at end of file diff --git a/2022/puzzles/day21/index.html b/2022/puzzles/day21/index.html index f89069ae8..9c20e3925 100644 --- a/2022/puzzles/day21/index.html +++ b/2022/puzzles/day21/index.html @@ -5,14 +5,14 @@ Day 21: Monkey Math | Scala Center Advent of Code - - + +
-

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. +

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! See here for the expected format

- - + + \ No newline at end of file diff --git a/2022/puzzles/day22/index.html b/2022/puzzles/day22/index.html index afe6cb7e1..e6c9e7f18 100644 --- a/2022/puzzles/day22/index.html +++ b/2022/puzzles/day22/index.html @@ -5,14 +5,14 @@ Day 22: Monkey Map | Scala Center Advent of Code - - + +
-

Day 22: Monkey Map

Puzzle description

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

Solutions from the community

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

- - + + \ No newline at end of file diff --git a/2022/puzzles/day23/index.html b/2022/puzzles/day23/index.html index a151569e4..2a3b6ab25 100644 --- a/2022/puzzles/day23/index.html +++ b/2022/puzzles/day23/index.html @@ -5,14 +5,14 @@ Day 23: Unstable Diffusion | Scala Center Advent of Code - - + +
-

Day 23: Unstable Diffusion

Puzzle description

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

Solutions from the community

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

- - + + \ No newline at end of file diff --git a/2022/puzzles/day24/index.html b/2022/puzzles/day24/index.html index 382932c38..4863d7fad 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2022/puzzles/day25/index.html b/2022/puzzles/day25/index.html index bb22ea41d..5cec8491a 100644 --- a/2022/puzzles/day25/index.html +++ b/2022/puzzles/day25/index.html @@ -5,14 +5,14 @@ Day 25: Full of Hot Air | Scala Center Advent of Code - - + +
-

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. +

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! See here for the expected format

- - + + \ No newline at end of file diff --git a/2023/index.html b/2023/index.html index 778885e51..1d7d0f5a2 100644 --- a/2023/index.html +++ b/2023/index.html @@ -5,13 +5,13 @@ Scala Center Advent of Code | Scala Center Advent of Code - - + + - - +
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 8f0810eb9..e791eb113 100644 --- a/2023/puzzles/day01/index.html +++ b/2023/puzzles/day01/index.html @@ -5,12 +5,12 @@ Day 1: Trebuchet?! | Scala Center Advent of Code - - + +
-

Day 1: Trebuchet?!

by @sjrd

Puzzle description

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

Solution Summary

  1. Iterate over each line of the input.
  2. Convert each line into coordinates, using the appropriate mechanism for part1 and part2.
  3. Sum the coordinates.

Part 1

Our main driver iterates over the lines, converts each line into coordinates, then sums them. +

Day 1: Trebuchet?!

by @sjrd

Puzzle description

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

Solution Summary

  1. Iterate over each line of the input.
  2. Convert each line into coordinates, using the appropriate mechanism for part1 and part2.
  3. Sum the coordinates.

Part 1

Our main driver iterates over the lines, converts each line into coordinates, then sums them. It therefore looks like:

def part1(input: String): String =
// Convert one line into the appropriate coordinates
def lineToCoordinates(line: String): Int =
???

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

In order to convert a line into coordinates, we find the first and last digits in the line. We then put them next to each other in a string to interpret them as coordinates, as asked.

// 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

Part 2

The main driver is the same as for part 1. What changes is how we convert each line into coordinates.

We first build a hard-coded map of string representations to numeric values:

/** The textual representation of digits. */
val stringDigitReprs: Map[String, Int] = 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: Map[String, Int] =
stringDigitReprs ++ (1 to 9).map(i => i.toString() -> i)

We will now have to find the first and last string representation in the line. @@ -21,8 +21,8 @@ It therefore returns 2, 9, and one, but misses the eight that overlaps with one.

There is no built-in function to handle overlapping matches, nor to find the last match of a regex in a string. 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.

- - +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 f19584c5c..eee414807 100644 --- a/2023/puzzles/day02/index.html +++ b/2023/puzzles/day02/index.html @@ -5,16 +5,16 @@ Day 2: Cube Conundrum | Scala Center Advent of Code - - + +
-

Day 2: Cube Conundrum

by @bishabosha

Puzzle description

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

Solution Summary

  1. Iterate over each line of the input.
  2. Parse each line into a game
  3. Summarise each game (using the appropriate summary function for part1 or part2)
  • part1 requires to check first if any hand in a game (by removing cubes) will cause a negative cube count, compared to the initial configuration of "possible" cubes. If there are no negative counts, then the game is possible and summarise as the game's id, otherwise summarise as zero.
  • part2 requires to find the maximum cube count of each color in any given hand, and then summarise as the product of those cube counts.
  1. Sum the total of summaries

Part 1

Framework

The main driver for solving will be the solution function. +

Day 2: Cube Conundrum

by @bishabosha

Puzzle description

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

Solution Summary

  1. Iterate over each line of the input.
  2. Parse each line into a game
  3. Summarise each game (using the appropriate summary function for part1 or part2)
  • part1 requires to check first if any hand in a game (by removing cubes) will cause a negative cube count, compared to the initial configuration of "possible" cubes. If there are no negative counts, then the game is possible and summarise as the game's id, otherwise summarise as zero.
  • part2 requires to find the maximum cube count of each color in any given hand, and then summarise as the product of those cube counts.
  1. Sum the total of summaries

Part 1

Framework

The main driver for solving will be the solution function. In a single pass over the puzzle input it will:

  • iterate through each line,
  • parse each line into a game,
  • summarise each game as an Int,
  • sum the total of summaries.
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.

- - + + \ No newline at end of file diff --git a/2023/puzzles/day03/index.html b/2023/puzzles/day03/index.html index 1ef9d742d..7beefb570 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 - - + +
-

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 sparse 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 whether 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.

- - +

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 sparse 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 whether 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.

+ + \ No newline at end of file diff --git a/2023/puzzles/day04/index.html b/2023/puzzles/day04/index.html index 083d8d5fe..21005bb34 100644 --- a/2023/puzzles/day04/index.html +++ b/2023/puzzles/day04/index.html @@ -5,12 +5,12 @@ Day 4: Scratchcards | Scala Center Advent of Code - - + +
-

Day 4: Scratchcards

by @shardulc

Puzzle description

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

Solution summary

First, we note that both parts rely on counting how many winning numbers there +

Day 4: Scratchcards

by @shardulc

Puzzle description

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

Solution summary

First, we note that both parts rely on counting how many winning numbers there are on a card, so we write the helper function countWinning. Then, part 1 converts each card's winning numbers count to a number of points and adds them up. Part 2 cannot be expressed as a map over the winning counts because the @@ -33,7 +33,7 @@ card. (It is summed up in numCards in the accumulator.)

Why track by relative index instead of absolute?

  • We don't have to parse or store the card numbers.
  • We can discard information as soon as it is no longer needed, and keep only limited information about the future, in this case bounded by the maximum possible number of winning numbers.
  • (Personal opinion) It makes for a nicer, purely functional solution!

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.

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

Day 5: If You Give A Seed A Fertilizer

by @g.berezin and @bishabosha

Puzzle description

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

Solution summary

Data Structures

First and foremost, the data must be parsed from the file into the following classes:

  • A data structure representing the pairing of a resource kind and the range it occupies:
// in the first task you can use the same model but set `end = start`
final case class Resource(
start: Long, end: Long, kind: ResourceKind)
  • An enumeration for storing the kind of resource:
enum ResourceKind:
case Seed, Soil, Fertilizer, Water,
Light, Temperature, Humidity, Location
  • A schema for converting from one resource type to another:
final case class ResourceMap(
from: ResourceKind,
to: ResourceKind,
properties: Seq[Property] // sorted by `sourceStart`
)

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

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

Parts 1 and 2

It helps to consider parts 1 and 2 together when explaining the solution. +

Day 5: If You Give A Seed A Fertilizer

by @g.berezin and @bishabosha

Puzzle description

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

Solution summary

Data Structures

First and foremost, the data must be parsed from the file into the following classes:

  • A data structure representing the pairing of a resource kind and the range it occupies:
// in the first task you can use the same model but set `end = start`
final case class Resource(
start: Long, end: Long, kind: ResourceKind)
  • An enumeration for storing the kind of resource:
enum ResourceKind:
case Seed, Soil, Fertilizer, Water,
Light, Temperature, Humidity, Location
  • A schema for converting from one resource type to another:
final case class ResourceMap(
from: ResourceKind,
to: ResourceKind,
properties: Seq[Property] // sorted by `sourceStart`
)

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

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

Parts 1 and 2

It helps to consider parts 1 and 2 together when explaining the solution. In part 1, you are required to convert each Seed to a Location, using a chain of ResourceMap. In most inputs you likely have about 20 seeds to consider, so this is not an expensive operation. However in the second half of the solution, you must actually consider 10 ranges of seeds, where each range can be billions of elements long, so it is not practical to consider individual seeds.

Considering the number of seeds in the input data, so you should manipulate the resources in the form of intervals, and when passing through the properties of a ResourceMap, you divide an input resource interval into semi-intervals depending on how the interval intersects with an individual Property.

Conveniently for part 1, a single seed can be considered as an interval with one element, so we can reuse the same code for both parts.

type ParseSeeds = String => Seq[Resource]

def calculate(seeds: Seq[Resource], maps: Seq[ResourceMap]): Long = ???

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)

The calculation proceeds as follows, iterate through the initial seeds, @@ -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.

- - + + \ No newline at end of file diff --git a/2023/puzzles/day06/index.html b/2023/puzzles/day06/index.html index 69ff6faa2..deb5c5370 100644 --- a/2023/puzzles/day06/index.html +++ b/2023/puzzles/day06/index.html @@ -5,12 +5,12 @@ Day 6: Wait For It | Scala Center Advent of Code - - + +
-

Day 6: Wait For It

by Spamegg

Puzzle description

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

Solution summary

We are given a time value, t, and a distance record, d. +

Day 6: Wait For It

by Spamegg

Puzzle description

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

Solution summary

We are given a time value, t, and a distance record, d. Say, we hold down the button for x milliseconds. This determines our speed. Then our boat travels for t-x seconds. So our total distance traveled is: x * (t-x). @@ -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.

- - + + \ No newline at end of file diff --git a/2023/puzzles/day07/index.html b/2023/puzzles/day07/index.html index ae68a1e17..86648f6b2 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 - - + +
-

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.

- - +

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.

+ + \ No newline at end of file diff --git a/2023/puzzles/day08/index.html b/2023/puzzles/day08/index.html index 4fa545c35..1ba0006aa 100644 --- a/2023/puzzles/day08/index.html +++ b/2023/puzzles/day08/index.html @@ -5,16 +5,16 @@ Day 8: Haunted Wasteland | Scala Center Advent of Code - - + +
-

Day 8: Haunted Wasteland

by @prinsniels

Puzzle description

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

Initial setup

In its most basic form, we are required to count the number of instructions to follow on a network to reach a desired state. In the example given, we start at AAA and are required to reach ZZZ. To model this problem I have done the following;

/** Describes the Node we are at */
type State = String

/**
* Describes how to get from a Starting State
* to a New State, given an instruction
*/
type Transition = (State, Instr) => State

/** The possible instructions given */
enum Instr:
case GoLeft, GoRight

/**
* The puzzle describes that the input instructions are infinite,
* meaning that if there a no instructions left, we start with
* the first instruction again. To model this I have used
* a `LazyList[Instr]`. This allows for an infinite stream
* of instructions.
*/
object Instr:
def parse(inp: String): LazyList[Instr] =
inp
.map {
case 'L' => Instr.GoLeft
case 'R' => Instr.GoRight
}
.to(LazyList) #::: Instr.parse(inp)

/**
* convert a List of strings (e.g. `"AAA = (BBB, CCC)"`)
* to a map of entries, (e.g. `"AAA" -> Vector("BBB", "CCC")`)
*/
def parseNetwork(inp: List[String]): Map[String, Vector[String]] =
inp.map {
case s"$a = ($b, $c)" => (a -> Vector(b, c))
}.toMap

/**
* Count function.
* Check if the predicate is met.
* If true, return the number of steps taken,
* if false transition into the next state from the current state,
* given the first instruction.
*/
@tailrec
def countStepsUntil(
state: State, instrs: LazyList[Instr], trans: Transition,
count: Int, pred: State => Boolean): Int =
if pred(state) then count
else
countStepsUntil(
trans(state, instrs.head), instrs.tail, trans, count + 1, pred)

Part one solution

Part one simply asks to count the number of steps taken to reach a desired state. To model this we need to define the predicate and transition function. +

Day 8: Haunted Wasteland

by @prinsniels

Puzzle description

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

Initial setup

In its most basic form, we are required to count the number of instructions to follow on a network to reach a desired state. In the example given, we start at AAA and are required to reach ZZZ. To model this problem I have done the following;

/** Describes the Node we are at */
type State = String

/**
* Describes how to get from a Starting State
* to a New State, given an instruction
*/
type Transition = (State, Instr) => State

/** The possible instructions given */
enum Instr:
case GoLeft, GoRight

/**
* The puzzle describes that the input instructions are infinite,
* meaning that if there a no instructions left, we start with
* the first instruction again. To model this I have used
* a `LazyList[Instr]`. This allows for an infinite stream
* of instructions.
*/
object Instr:
def parse(inp: String): LazyList[Instr] =
inp
.map {
case 'L' => Instr.GoLeft
case 'R' => Instr.GoRight
}
.to(LazyList) #::: Instr.parse(inp)

/**
* convert a List of strings (e.g. `"AAA = (BBB, CCC)"`)
* to a map of entries, (e.g. `"AAA" -> Vector("BBB", "CCC")`)
*/
def parseNetwork(inp: List[String]): Map[String, Vector[String]] =
inp.map {
case s"$a = ($b, $c)" => (a -> Vector(b, c))
}.toMap

/**
* Count function.
* Check if the predicate is met.
* If true, return the number of steps taken,
* if false transition into the next state from the current state,
* given the first instruction.
*/
@tailrec
def countStepsUntil(
state: State, instrs: LazyList[Instr], trans: Transition,
count: Int, pred: State => Boolean): Int =
if pred(state) then count
else
countStepsUntil(
trans(state, instrs.head), instrs.tail, trans, count + 1, pred)

Part one solution

Part one simply asks to count the number of steps taken to reach a desired state. To model this we need to define the predicate and transition function. The transition function needs to know the network it is operating on. To be a bit more flexible I decided to create a function that returns the transition function based on a given network.

def transitions(network: Map[String, Vector[String]]): Transition =
(n, d) =>
d match
case Instr.GoLeft => network(n)(0)
case Instr.GoRight => network(n)(1)

For the predicate tell the function to stop when STATE == "ZZZ"

def part1(input: String): Int =
val inpL = input.split("\n\n")
val instructions = Instr.parse(inpL.head)
val network = parseNetwork(inpL.tail.head.split("\n").toList)
val trans = transitions(network)

countStepsUntil("AAA", instructions, trans, 0, _ == "ZZZ")

Part two solution

The second part is a bit trickier. We are required to find the number of steps to take, until all nodes in the state end with a Z. One can try to brute force this, by changing the transition function to (Set[String], Instr) => Set[String] but this takes way to much processing time. Key insight comes from the realization that all states in the starting Set[Sate] move on their own independent path and keep repeating themselves. By knowing this we can use an LCM to get to the correct answer.

def part2(input: String): Long =
// ... reuse parsing from part 1
def lcm(a: Long, b: Long): Long =
a * b / gcd(a, b)

def gcd(a: Long, b: Long): Long =
if b == 0 then a else gcd(b, a % b)

// get all the starting states
val starts: Set[State] = network.keySet.filter(_.endsWith("A"))

starts
.map(state =>
// for each state find the cycle time
countStepsUntil(
state, instructions, trans, 0, _.endsWith("Z")).toLong)
.reduce(lcm)

final code

import scala.annotation.tailrec

type State = String

type Transition = (State, Instr) => State

enum Instr:
case GoLeft, GoRight

object Instr:
def parse(inp: String): LazyList[Instr] =
inp
.map {
case 'L' => Instr.GoLeft
case 'R' => Instr.GoRight
}
.to(LazyList) #::: Instr.parse(inp)

def parseNetwork(inp: List[String]): Map[String, Vector[String]] =
inp.map {
case s"$a = ($b, $c)" => (a -> Vector(b, c))
}.toMap

def transitions(network: Map[String, Vector[String]]): Transition =
(n, d) =>
d match
case Instr.GoLeft => network(n)(0)
case Instr.GoRight => network(n)(1)

@tailrec
def countStepsUntil(
state: State, instrs: LazyList[Instr], trans: Transition,
count: Int, pred: State => Boolean): Int =
if pred(state) then count
else
countStepsUntil(
trans(state, instrs.head), instrs.tail, trans, count + 1, pred)

def part1(input: String): Int =
val inpL = input.split("\n\n")
val instructions = Instr.parse(inpL.head)
val network = parseNetwork(inpL.tail.head.split("\n").toList)
val trans = transitions(network)

countStepsUntil("AAA", instructions, trans, 0, _ == "ZZZ")

def part2(input: String): Long =
val inpL = input.split("\n\n")
val instructions = Instr.parse(inpL.head)
val network = parseNetwork(inpL.tail.head.split("\n").toList)
val trans = transitions(network)

val starts: Set[State] = network.keySet.filter(_.endsWith("A"))

def lcm(a: Long, b: Long): Long =
a * b / gcd(a, b)

def gcd(a: Long, b: Long): Long =
if b == 0 then a else gcd(b, a % b)

starts
.map(state =>
countStepsUntil(
state, instructions, trans, 0, _.endsWith("Z")).toLong)
.reduce(lcm)

Solutions from the community

Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

- - + + \ No newline at end of file diff --git a/2023/puzzles/day09/index.html b/2023/puzzles/day09/index.html index 2de2dc8eb..f28db1628 100644 --- a/2023/puzzles/day09/index.html +++ b/2023/puzzles/day09/index.html @@ -5,12 +5,12 @@ Day 9: Mirage Maintenance | Scala Center Advent of Code - - + +
-

Day 9: Mirage Maintenance

by @SethTisue

Puzzle description

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

Background

This method of predicting the next number in a sequence is an example +

- - + + \ No newline at end of file diff --git a/2023/puzzles/day10/index.html b/2023/puzzles/day10/index.html index 072f2f5aa..8828d7ace 100644 --- a/2023/puzzles/day10/index.html +++ b/2023/puzzles/day10/index.html @@ -5,12 +5,12 @@ Day 10: Pipe Maze | Scala Center Advent of Code - - + +
-

Day 10: Pipe Maze

by @EugeneFlesselle

Puzzle description

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

Solution Summary

We can keep the grid as provided in the input, a 2-dimensional array of characters for all intents and purposes. +

Day 10: Pipe Maze

by @EugeneFlesselle

Puzzle description

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

Solution Summary

We can keep the grid as provided in the input, a 2-dimensional array of characters for all intents and purposes. We will also keep track of tiles throughout the problem as identified by their indexes in the grid.

def parse(input: String) = input.linesIterator.toSeq
val grid: Seq[String] = parse(input)

Part 1

We first implement connected, a function returning the tiles connected to a given point p, as specified in the problem description, and in no particular order. For the starting position 'S' in particular, as we do not know its direction, @@ -30,7 +30,7 @@ and only increase the count in enclosed portions for tiles which are not part of the loop. Finally, we obtain the total number of enclosed tiles by iterating over all lines.

def part2(input: String): Int =
val grid = parse(input)
val inLoop = findLoop(grid).toSet

def connectsNorth(i: Int, j: Int): Boolean =
connected(grid)(i,j).contains(i-1, j)

def enclosedInLine(i: Int): Int =
val (_, count) = grid(i).indices.foldLeft((false, 0)):
case ((enclosed, count), j) if inLoop(i, j) =>
(enclosed ^ connectsNorth(i, j), count)
case ((true, count), j) =>
(true, count + 1)
case ((false, count), j) =>
(false, count)
count

grid.indices.map(enclosedInLine).sum

Final Code

def parse(input: String) = input.linesIterator.toSeq

/** The tiles connected to point `p` in the `grid` */
def connected(grid: Seq[String])(p: (Int, Int)): Set[(Int, Int)] =
val (i, j) = p
grid(i)(j) match
case '|' => Set((i - 1, j), (i + 1, j))
case '-' => Set((i, j - 1), (i, j + 1))
case 'L' => Set((i - 1, j), (i, j + 1))
case 'J' => Set((i - 1, j), (i, j - 1))
case '7' => Set((i + 1, j), (i, j - 1))
case 'F' => Set((i + 1, j), (i, j + 1))
case '.' => Set()
case 'S' => Set((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1))
.filter((i, j) => grid.isDefinedAt(i) && grid(i).isDefinedAt(j))
.filter(connected(grid)(_).contains(i, j))
end connected

/** The loop starting from 'S' in the grid */
def findLoop(grid: Seq[String]): Seq[(Int, Int)] =
val start =
val startI = grid.indexWhere(_.contains('S'))
(startI, grid(startI).indexOf('S'))

val initial = (start, connected(grid)(start).head)

/** List of connected points starting from 'S'
* e.g. `(y0, x0) :: (y1, x1) :: (y2, x2) :: ...`
*/
val loop = LazyList.iterate(initial): (prev, curr) =>
val next = connected(grid)(curr) - prev
(curr, next.head)

start +: loop.map(_._2).takeWhile(_ != start)
end findLoop

def part1(input: String): String =
val grid = parse(input)
val loop = findLoop(grid)
(loop.length / 2).toString
end part1

def part2(input: String): String =
val grid = parse(input)
val inLoop = findLoop(grid).toSet

/** True iff `grid(i)(j)` is a pipe connecting to the north */
def connectsNorth(i: Int, j: Int): Boolean =
connected(grid)(i, j).contains(i - 1, j)

/** Number of tiles enclosed by the loop in `grid(i)` */
def enclosedInLine(i: Int): Int =
val (_, count) = grid(i).indices.foldLeft((false, 0)):
case ((enclosed, count), j) if inLoop(i, j) =>
(enclosed ^ connectsNorth(i, j), count)
case ((true, count), j) =>
(true, count + 1)
case ((false, count), j) =>
(false, count)
count

grid.indices.map(enclosedInLine).sum.toString
end part2

Solutions from the community

Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

- - + + \ No newline at end of file diff --git a/2023/puzzles/day11/index.html b/2023/puzzles/day11/index.html index fc3e2c4ff..a91c02fa6 100644 --- a/2023/puzzles/day11/index.html +++ b/2023/puzzles/day11/index.html @@ -5,12 +5,12 @@ Day 11: Cosmic Expansion | Scala Center Advent of Code - - + +
-

Day 11: Cosmic Expansion

by @natsukagami

Puzzle description

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

Puzzle Summary

We are given a grid of . and #. We would like to find the sum of distances between all pairs of # in the grid. +

Day 11: Cosmic Expansion

by @natsukagami

Puzzle description

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

Puzzle Summary

We are given a grid of . and #. We would like to find the sum of distances between all pairs of # in the grid. The distance between two # is defined as the number of vertical and horizontal steps to go from one to the other. One caveat: each row and each column that has no # actually represents k empty rows/columns respectively.

  • In part 1, k = 2.
  • In part 2, k = 1_000_000.

Solution Summary

by @natsukagami

We start by parsing the input into a board structure (a Seq[String], with each string representing a row).

val board = readInput().linesIterator.toSeq

First, it is clear to us that the distance we are looking for is the Manhattan Distance between two # in the grid. @@ -32,7 +32,7 @@ Same with columns. Can we still keep the counting linear?

  • (Squared) Euclidean distance: what if our distance is the square of the actual distance between the #s (i.e. (a.x - b.x)^2 + (a.y - b.y)^2)? We should be able to still keep the counting algorithm linear with some math!
  • Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day12/index.html b/2023/puzzles/day12/index.html index 725edfd60..2016dc41b 100644 --- a/2023/puzzles/day12/index.html +++ b/2023/puzzles/day12/index.html @@ -5,14 +5,14 @@ Day 12: Hot Springs | Scala Center Advent of Code - - + +
    -

    Day 12: Hot Springs

    by @mbovel

    Puzzle description

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

    Scaffold

    Let's create a folder with a file day12.scala to hold the core of our code.

    We start by writing two examples and defining three functions countAll, countRow, and count that we will implement later:

    //> using scala 3.3.1

    import scala.io.Source

    /** The example puzzle from the problem description. */
    val examplePuzzle = IArray(
    "???.### 1,1,3",
    ".??..??...?##. 1,1,3",
    "?#?#?#?#?#?#?#? 1,3,1,6",
    "????.#...#... 4,1,1",
    "????.######..#####. 1,6,5",
    "###???????? 3,2,1"
    )

    /** Our personal puzzle input. */
    val personalPuzzle = Source.fromFile("input.txt").mkString.trim()

    /** Entry point for part 1. */
    @main def part1(): Unit = println(countAll(personalPuzzle))

    /** Sums `countRow` over all rows in `input`. */
    def countAll(input: String): Long = ???

    /** Counts all of the different valid arrangements of
    * operational and broken springs in the given row.
    */
    def countRow(input: String): Long = ???

    /** Helper recursive function for `countRow` that does
    * the actual work.
    *
    * @param input
    * the remaining input to process
    * @param ds
    * a list of the numbers of damaged springs remaining to be placed
    */
    def count(input: List[Char], ds: List[Int]): Long = ???

    Thanks to scala-cli we can run this file with:

    $ scala-cli -M part1 .

    Tests

    In the same folder, we create a file day12.test.scala to hold our tests. We write one test for each individual row of the example from the instructions, a test for the whole example, and a test for our personal puzzle input:

    //> using scala 3.3.1
    //> using test.dep org.scalameta::munit::1.0.0-M10

    class Day12Test extends munit.FunSuite:
    test("example row 1"):
    assertEquals(countRow(examplePuzzle(0)), 1L)

    test("example row 2"):
    assertEquals(countRow(examplePuzzle(1)), 4L)

    test("example row 3"):
    assertEquals(countRow(examplePuzzle(2)), 1L)

    test("example row 4"):
    assertEquals(countRow(examplePuzzle(3)), 1L)

    test("example row 5"):
    assertEquals(countRow(examplePuzzle(4)), 4L)

    test("example row 6"):
    assertEquals(countRow(examplePuzzle(5)), 10L)

    test("example"):
    assertEquals(countAll(examplePuzzle.mkString("\n")), 21L)

    test("puzzle input"):
    assertEquals(countAll(personalPuzzle), 7118L)

    We can run the tests with:

    $ scala-cli test .

    Part 1

    Implementation of countAll and countRow

    countAll and countRow can be implemented concisely using split and map:

    def countAll(input: String): Long = input.split("\n").map(countRow).sum

    def countRow(input: String): Long =
    val Array(conditions, damagedCounts) = input.split(" ")
    count(
    conditions.toList,
    damagedCounts.split(",").map(_.toInt).toList
    )

    Character-level implementation of count

    For our first implementation, we'll iterate through the input string character by character, and we'll use an additional parameter d to keep track of the number of consecutive damaged springs seen so far:

    /** Helper recursive function for `countRow` that does the actual work.
    *
    * @param input
    * the remaining input to process
    * @param ds
    * a list of the numbers of damaged springs remaining to be placed
    * @param d
    * the number of consecutive damaged springs seen so far
    */
    def count(input: List[Char], ds: List[Int], d: Int = 0): Long =
    // We've reached the end of the input.
    if input.isEmpty then
    // This is a valid arrangement if there are no sequences of
    // damaged springs left to place (ds.isEmpty) and we're
    // not currently in a sequence of damaged springs (d == 0).
    if ds.isEmpty && d == 0 then 1L
    // This is also a valid arrangement if there is one sequence
    // of damaged springs left to place (ds.length == 1) and its
    // size is d (ds.head == d).
    else if ds.length == 1 && ds.head == d then 1L
    // Otherwise, this is not a valid arrangement.
    else 0
    else
    def operationalCase() =
    // If we're not currently in a sequence of damaged springs,
    // then we can consume an operational spring.
    if d == 0 then count(input.tail, ds, 0)
    // We are currently in a sequence of damaged springs,
    // which this operational spring ends. If the length
    // of the damaged sequence is the expected one, the we can
    // continue with the next damaged sequence.
    else if !ds.isEmpty && ds.head == d then
    count(input.tail, ds.tail, 0)
    // Otherwise, this is not a valid arrangement.
    else 0L
    def damagedCase() =
    // If no damaged springs are expected, then this is not a valid
    // arrangement.
    if ds.isEmpty then 0L
    // Optimization: no need to recurse if d becomes greater than the
    // expected damaged sequence length.
    else if d == ds.head then 0L
    // Otherwise, we can consume a damaged spring.
    else count(input.tail, ds, d + 1)
    input.head match
    // If we encounter a question mark, this position can have
    // either an operational or a damaged spring.
    case '?' => operationalCase() + damagedCase()
    // If we encounter a dot, this position has an operational
    // spring.
    case '.' => operationalCase()
    // If we encounter a hash, this position has damaged spring.
    case '#' => damagedCase()

    Counting calls

    The implementation above is correct, but it has an exponential run time complexity: it calls itself up to two times at each step, so the number of calls grows exponentially with the length of the input.

    To demonstrate this, we will add a counter ops that counts the number of calls to count:

    var ops = 0
    private def count(input: List[Char], d: Long, ds: List[Long]): Long =
    ops += 1
    // ... same as before ...

    And consider the following example puzzle in addition to our two existing examples:

    val slowPuzzleSize = 16
    val slowPuzzle =
    ("??." * slowPuzzleSize) + " " + ("1," * (slowPuzzleSize - 1)) + "1"

    To see how many times count is called for our example puzzles, we add the following function:

    @main def countOps =
    val puzzles =
    IArray(
    ("example", examplePuzzle.mkString("\n")),
    ("personal", personalPuzzle),
    ("slow", slowPuzzle)
    )
    for (name, input) <- puzzles do
    ops = 0
    val start = System.nanoTime()
    val result = countAll(input)
    val end = System.nanoTime()
    val elapsed = (end - start) / 1_000_000
    println(f"$name%8s: $result%5d ($ops%9d calls, $elapsed%4d ms)")

    Running this code gives us the following output:

     example:    21 (      305 calls,   24 ms)
    personal: 7118 ( 149712 calls, 37 ms)
    slow: 65536 (172186881 calls, 1415 ms)

    Memoization

    Many of the calls to count are redundant: they are made with the same arguments as previous calls. A quick way to improve algorithmic complexity and the performance of this function is to memoize it: we can cache the results of previous calls to count and reuse them when the same arguments are passed again.

    We can use a mutable.Map to store the results of previous calls. We use tuples containing the arguments (input, ds, d) as keys, and we use the getOrElseUpdate method to either retrieve the cached result or compute it and store it in the map.

    Here is the memoized version of count:

    import scala.collection.mutable

    val cache = mutable.Map.empty[(List[Char], List[Int], Long), Long]
    private def count(input: List[Char], ds: List[Int], d: Int = 0): Long =
    cache.getOrElseUpdate((input, ds, d), countUncached(input, ds, d))

    var ops = 0
    def countUncached(input: List[Char], ds: List[Int], d: Int = 0): Long =
    ops += 1
    // ... same as before ...

    Running countOps again, we now get the following output:

     example:    21 (      169 calls,   32 ms)
    personal: 7118 ( 38382 calls, 74 ms)
    slow: 65536 ( 679 calls, 1 ms)

    That's much better! The number of operations is lower, and the running time is faster in the pathological slow puzzle case.

    info

    The number of operations is a good primary metric here, because it is completely deterministic, stable across runs and is a good proxy for the complexity of the algorithm.

    We also measure the actual running time of the function as an indicator, but a naive measurement like this is not accurate and can vary a lot between runs. For a more accurate measurement, one could use a benchmarking library such as JMH (for example with the JMH SBT plugin) or ScalaMeter.

    Part 2

    Oh, there is a second part to this puzzle!

    Implementation

    The only change needed to implement the second part is to unfold the input rows before counting them. We add the unfoldRow function to do that, and call it from countAllUnfolded:

    /** Entry point for part 2 */
    @main def part2(): Unit =
    println(countAllUnfolded(personalPuzzle))

    def countAllUnfolded(input: String): Long =
    input.split("\n").map(unfoldRow).map(countRow).sum

    def unfoldRow(input: String): String =
    val Array(conditions, damagedCounts) =
    input.split(" ")
    val conditionsUnfolded =
    (0 until 5).map(_ => conditions).mkString("?")
    val damagedCountUnfolded =
    (0 until 5).map(_ => damagedCounts).mkString(",")
    f"$conditionsUnfolded $damagedCountUnfolded"

    Executing part2 with my personal input puzzle runs in ~800 ms on my machine, and countUncached is called 681'185:

              example:            21 (   169 calls,  31 ms)
    personal: 7118 ( 38382 calls, 74 ms)
    slow: 65536 ( 679 calls, 1 ms)
    personal unfolded: 7030194981795 (681185 calls, 815 ms)

    Can we do better?

    Group-level implementation of count

    Our first implementation of count works. Recursing character by character through the input string looks like a natural way to solve this problem. But we can simplify the implementation and improve its performance by considering groups instead of individual characters.

    To know if a group of damaged springs of length nn can be at a given position, we can consume the next nn characters of the input and check if they can all be damaged springs (i.e. none of them is a .), and if the following character can be an operational spring (i.e. it is not a #).

    import scala.collection.mutable

    extension (b: Boolean) private inline def toLong: Long =
    if b then 1L else 0L

    val cache2 = mutable.Map.empty[(List[Char], List[Int]), Long]

    private def count2(input: List[Char], ds: List[Int]): Long =
    cache2.getOrElseUpdate((input, ds), count2Uncached(input, ds))

    def count2Uncached(input: List[Char], ds: List[Int]): Long =
    ops += 1
    // We've seen all expected damaged sequences. The arrangement
    // is therefore valid only if the input does not contain
    // damaged springs.
    if ds.isEmpty then input.forall(_ != '#').toLong
    // The input is empty but we expected some damaged springs,
    // so this is not a valid arrangement.
    else if input.isEmpty then 0L
    else
    def operationalCase(): Long =
    // We can consume all following operational springs.
    count2(input.tail.dropWhile(_ == '.'), ds)
    def damagedCase(): Long =
    // If the length of the input is less than the expected
    // length of the damaged sequence, then this is not a
    // valid arrangement.
    if input.length < ds.head then 0L
    else
    // Split the input into a group of length ds.head and
    // the rest.
    val (group, rest) = input.splitAt(ds.head)
    // If the group contains any operational springs, then
    // this is not a a group of damaged springs, so this
    // is not a valid arrangement.
    if !group.forall(_ != '.') then 0L
    // If the rest of the input is empty, then this is a
    // valid arrangement only if the damaged sequence is
    // the last one expected.
    else if rest.isEmpty then ds.tail.isEmpty.toLong
    // If we now have a damaged spring, then this is not
    // the end of a damaged sequence as expected, and
    // therefore not a valid arrangement.
    else if rest.head == '#' then 0L
    // Otherwise, we can continue with the rest of the
    // input and the next expected damaged sequence.
    else count2(rest.tail, ds.tail)
    input.head match
    case '?' => operationalCase() + damagedCase()
    case '.' => operationalCase()
    case '#' => damagedCase()

    I find this implementation simpler and easier to understand than the previous one. Do you agree?

    It naturally results in less calls, and the running time is improved:

              example:            21 (    69 calls,   36 ms)
    personal: 7118 ( 12356 calls, 74 ms)
    slow: 65536 ( 404 calls, 1 ms)
    personal unfolded: 7030194981795 (235829 calls, 497 ms)

    Local cache

    We implemented memoization by using a global mutable map. What happens if we use a local, distinct one for each call to count instead?

    import scala.collection.mutable

    def count2(input: List[Char], ds: List[Int]): Long =
    val cache2 = mutable.Map.empty[(List[Char], List[Int]), Long]

    def count2Cached(input: List[Char], ds: List[Int]): Long =
    cache2.getOrElseUpdate((input, ds), count2Uncached(input, ds))

    def count2Uncached(input: List[Char], ds: List[Int]): Long =
    // ... same as before ...
    // (but calling count2Cached instead of count2)

    Even though this results to more calls to count2Uncached, this actually improves the performance of the unfolded version, down to ~400 ms on my machine:

              example:            21 (       71 calls,   32 ms)
    personal: 7118 ( 18990 calls, 67 ms)
    slow: 65536 ( 425 calls, 3 ms)
    personal unfolded: 7030194981795 ( 260272 calls, 407 ms)

    Simplify cache keys

    Because we will always consider the same sublists of input and ds for the lifetime of the cache, we can just use the lengths of these lists as keys:

    import scala.collection.mutable

    def count2(input: List[Char], ds: List[Int]): Long =
    val cache2 = mutable.Map.empty[(Int, Int), Long]

    def count2Cached(input: List[Char], ds: List[Int]): Long =
    val key = (input.length, ds.length)
    cache2.getOrElseUpdate(key, count2Uncached(input, ds))

    // ... def count2Uncached as before

    Which further reduces the running time of the unfolded version to ~320 ms on my machine:

              example:            21 (       71 calls,   33 ms)
    personal: 7118 ( 18990 calls, 66 ms)
    slow: 65536 ( 425 calls, 0 ms)
    personal unfolded: 7030194981795 ( 260272 calls, 320 ms)

    Simplify the cache structure

    Our cache key is now just a pair of integers, so we don't need a Map; an Array can do the job just as well.

    def count2(input: List[Char], ds: List[Int]): Long =
    val dim1 = input.length + 1
    val dim2 = ds.length + 1
    val cache = Array.fill(dim1 * dim2)(-1L)

    def count2Cached(input: List[Char], ds: List[Int]): Long =
    val key = input.length * dim2 + ds.length
    val result = cache(key)
    if result == -1L then
    val result = count2Uncached(input, ds)
    cache(key) = result
    result
    else result

    // ... def count2Uncached as before

    This reduces the running time of the unfolded version down to ~200 ms on my machine:

              example:            21 (       71 calls,   27 ms)
    personal: 7118 ( 18990 calls, 47 ms)
    slow: 65536 ( 425 calls, 0 ms)
    personal unfolded: 7030194981795 ( 260272 calls, 201 ms)

    Inline helper functions

    We have used helper functions to structure the implementation of count2. To avoid the calls overhead, we can use Scala 3's inline keyword.

    After adding the inline modifier to count2Cached, operationalCase and damagedCase, the running time of the unfolded version is reduced to ~140 ms on my machine:

              example:            21 (       71 calls,   28 ms)
    personal: 7118 ( 18990 calls, 50 ms)
    slow: 65536 ( 425 calls, 0 ms)
    personal unfolded: 7030194981795 ( 260272 calls, 137 ms)

    Further optimizations

    • Using a different data structure for the input and the damaged sequence, for example an IArray instead of a List, and indexing into it instead of using splitAt, tail or head methods would probably improve the performance further, but would be more verbose and less idiomatic.
    • Parallelizing countAllUnfolded did not result in any performance improvement on my machine. It might on larger inputs.

    Can you think of other optimizations that could improve the performance of this code without sacrificing readability?

    Final Code

    /** Entry point for part 1. */
    def part1(input: String): Unit =
    println(countAll(input))

    /** Sums `countRow` over all rows in `input`. */
    def countAll(input: String): Long =
    input.split("\n").map(countRow).sum

    /** Counts all of the different valid arrangements
    * of operational and broken springs in the given row.
    */
    def countRow(input: String): Long =
    val Array(conditions, damagedCounts) = input.split(" ")
    count2(
    conditions.toList,
    damagedCounts.split(",").map(_.toInt).toList
    )

    extension (b: Boolean) private inline def toLong: Long =
    if b then 1L else 0L

    def count2(input: List[Char], ds: List[Int]): Long =
    val dim1 = input.length + 1
    val dim2 = ds.length + 1
    val cache = Array.fill(dim1 * dim2)(-1L)

    inline def count2Cached(input: List[Char], ds: List[Int]): Long =
    val key = input.length * dim2 + ds.length
    val result = cache(key)
    if result == -1L then
    val result = count2Uncached(input, ds)
    cache(key) = result
    result
    else result

    def count2Uncached(input: List[Char], ds: List[Int]): Long =
    // We've seen all expected damaged sequences.
    // The arrangement is therefore valid only if the
    // input does not contain damaged springs.
    if ds.isEmpty then input.forall(_ != '#').toLong
    // The input is empty but we expected some damaged springs,
    // so this is not a valid arrangement.
    else if input.isEmpty then 0L
    else
    inline def operationalCase(): Long =
    // Operational case: we can consume all operational
    // springs to get to the next choice.
    count2Cached(input.tail.dropWhile(_ == '.'), ds)
    inline def damagedCase(): Long =
    // If the length of the input is less than the expected
    // length of the damaged sequence, then this is not a
    // valid arrangement.
    if input.length < ds.head then 0L
    else
    // Split the input into a group of length ds.head and
    // the rest.
    val (group, rest) = input.splitAt(ds.head)
    // If the group contains any operational springs, then
    // this is not a a group of damaged springs, so this
    // is not a valid arrangement.
    if !group.forall(_ != '.') then 0L
    // If the rest of the input is empty, then this is a
    // valid arrangement only if the damaged sequence
    // is the last one expected.
    else if rest.isEmpty then ds.tail.isEmpty.toLong
    // If we now have a damaged spring, then this is not the
    // end of a damaged sequence as expected, and therefore
    // not a valid arrangement.
    else if rest.head == '#' then 0L
    // Otherwise, we can continue with the rest of the input
    // and the next expected damaged sequence.
    else count2Cached(rest.tail, ds.tail)
    input.head match
    case '?' => operationalCase() + damagedCase()
    case '.' => operationalCase()
    case '#' => damagedCase()

    count2Cached(input, ds)
    end count2

    /** Entry point for part 2 */
    def part2(input: String): Unit =
    println(countAllUnfolded(input))

    def countAllUnfolded(input: String): Long =
    input.split("\n").map(unfoldRow).map(countRow).sum

    def unfoldRow(input: String): String =
    val Array(conditions, damagedCounts) =
    input.split(" ")
    val conditionsUnfolded =
    (0 until 5).map(_ => conditions).mkString("?")
    val damagedCountUnfolded =
    (0 until 5).map(_ => damagedCounts).mkString(",")
    f"$conditionsUnfolded $damagedCountUnfolded"

    Solutions from the community

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

    Day 12: Hot Springs

    by @mbovel

    Puzzle description

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

    Scaffold

    Let's create a folder with a file day12.scala to hold the core of our code.

    We start by writing two examples and defining three functions countAll, countRow, and count that we will implement later:

    //> using scala 3.3.1

    import scala.io.Source

    /** The example puzzle from the problem description. */
    val examplePuzzle = IArray(
    "???.### 1,1,3",
    ".??..??...?##. 1,1,3",
    "?#?#?#?#?#?#?#? 1,3,1,6",
    "????.#...#... 4,1,1",
    "????.######..#####. 1,6,5",
    "###???????? 3,2,1"
    )

    /** Our personal puzzle input. */
    val personalPuzzle = Source.fromFile("input.txt").mkString.trim()

    /** Entry point for part 1. */
    @main def part1(): Unit = println(countAll(personalPuzzle))

    /** Sums `countRow` over all rows in `input`. */
    def countAll(input: String): Long = ???

    /** Counts all of the different valid arrangements of
    * operational and broken springs in the given row.
    */
    def countRow(input: String): Long = ???

    /** Helper recursive function for `countRow` that does
    * the actual work.
    *
    * @param input
    * the remaining input to process
    * @param ds
    * a list of the numbers of damaged springs remaining to be placed
    */
    def count(input: List[Char], ds: List[Int]): Long = ???

    Thanks to scala-cli we can run this file with:

    $ scala-cli -M part1 .

    Tests

    In the same folder, we create a file day12.test.scala to hold our tests. We write one test for each individual row of the example from the instructions, a test for the whole example, and a test for our personal puzzle input:

    //> using scala 3.3.1
    //> using test.dep org.scalameta::munit::1.0.0-M10

    class Day12Test extends munit.FunSuite:
    test("example row 1"):
    assertEquals(countRow(examplePuzzle(0)), 1L)

    test("example row 2"):
    assertEquals(countRow(examplePuzzle(1)), 4L)

    test("example row 3"):
    assertEquals(countRow(examplePuzzle(2)), 1L)

    test("example row 4"):
    assertEquals(countRow(examplePuzzle(3)), 1L)

    test("example row 5"):
    assertEquals(countRow(examplePuzzle(4)), 4L)

    test("example row 6"):
    assertEquals(countRow(examplePuzzle(5)), 10L)

    test("example"):
    assertEquals(countAll(examplePuzzle.mkString("\n")), 21L)

    test("puzzle input"):
    assertEquals(countAll(personalPuzzle), 7118L)

    We can run the tests with:

    $ scala-cli test .

    Part 1

    Implementation of countAll and countRow

    countAll and countRow can be implemented concisely using split and map:

    def countAll(input: String): Long = input.split("\n").map(countRow).sum

    def countRow(input: String): Long =
    val Array(conditions, damagedCounts) = input.split(" ")
    count(
    conditions.toList,
    damagedCounts.split(",").map(_.toInt).toList
    )

    Character-level implementation of count

    For our first implementation, we'll iterate through the input string character by character, and we'll use an additional parameter d to keep track of the number of consecutive damaged springs seen so far:

    /** Helper recursive function for `countRow` that does the actual work.
    *
    * @param input
    * the remaining input to process
    * @param ds
    * a list of the numbers of damaged springs remaining to be placed
    * @param d
    * the number of consecutive damaged springs seen so far
    */
    def count(input: List[Char], ds: List[Int], d: Int = 0): Long =
    // We've reached the end of the input.
    if input.isEmpty then
    // This is a valid arrangement if there are no sequences of
    // damaged springs left to place (ds.isEmpty) and we're
    // not currently in a sequence of damaged springs (d == 0).
    if ds.isEmpty && d == 0 then 1L
    // This is also a valid arrangement if there is one sequence
    // of damaged springs left to place (ds.length == 1) and its
    // size is d (ds.head == d).
    else if ds.length == 1 && ds.head == d then 1L
    // Otherwise, this is not a valid arrangement.
    else 0
    else
    def operationalCase() =
    // If we're not currently in a sequence of damaged springs,
    // then we can consume an operational spring.
    if d == 0 then count(input.tail, ds, 0)
    // We are currently in a sequence of damaged springs,
    // which this operational spring ends. If the length
    // of the damaged sequence is the expected one, the we can
    // continue with the next damaged sequence.
    else if !ds.isEmpty && ds.head == d then
    count(input.tail, ds.tail, 0)
    // Otherwise, this is not a valid arrangement.
    else 0L
    def damagedCase() =
    // If no damaged springs are expected, then this is not a valid
    // arrangement.
    if ds.isEmpty then 0L
    // Optimization: no need to recurse if d becomes greater than the
    // expected damaged sequence length.
    else if d == ds.head then 0L
    // Otherwise, we can consume a damaged spring.
    else count(input.tail, ds, d + 1)
    input.head match
    // If we encounter a question mark, this position can have
    // either an operational or a damaged spring.
    case '?' => operationalCase() + damagedCase()
    // If we encounter a dot, this position has an operational
    // spring.
    case '.' => operationalCase()
    // If we encounter a hash, this position has damaged spring.
    case '#' => damagedCase()

    Counting calls

    The implementation above is correct, but it has an exponential run time complexity: it calls itself up to two times at each step, so the number of calls grows exponentially with the length of the input.

    To demonstrate this, we will add a counter ops that counts the number of calls to count:

    var ops = 0
    private def count(input: List[Char], d: Long, ds: List[Long]): Long =
    ops += 1
    // ... same as before ...

    And consider the following example puzzle in addition to our two existing examples:

    val slowPuzzleSize = 16
    val slowPuzzle =
    ("??." * slowPuzzleSize) + " " + ("1," * (slowPuzzleSize - 1)) + "1"

    To see how many times count is called for our example puzzles, we add the following function:

    @main def countOps =
    val puzzles =
    IArray(
    ("example", examplePuzzle.mkString("\n")),
    ("personal", personalPuzzle),
    ("slow", slowPuzzle)
    )
    for (name, input) <- puzzles do
    ops = 0
    val start = System.nanoTime()
    val result = countAll(input)
    val end = System.nanoTime()
    val elapsed = (end - start) / 1_000_000
    println(f"$name%8s: $result%5d ($ops%9d calls, $elapsed%4d ms)")

    Running this code gives us the following output:

     example:    21 (      305 calls,   24 ms)
    personal: 7118 ( 149712 calls, 37 ms)
    slow: 65536 (172186881 calls, 1415 ms)

    Memoization

    Many of the calls to count are redundant: they are made with the same arguments as previous calls. A quick way to improve algorithmic complexity and the performance of this function is to memoize it: we can cache the results of previous calls to count and reuse them when the same arguments are passed again.

    We can use a mutable.Map to store the results of previous calls. We use tuples containing the arguments (input, ds, d) as keys, and we use the getOrElseUpdate method to either retrieve the cached result or compute it and store it in the map.

    Here is the memoized version of count:

    import scala.collection.mutable

    val cache = mutable.Map.empty[(List[Char], List[Int], Long), Long]
    private def count(input: List[Char], ds: List[Int], d: Int = 0): Long =
    cache.getOrElseUpdate((input, ds, d), countUncached(input, ds, d))

    var ops = 0
    def countUncached(input: List[Char], ds: List[Int], d: Int = 0): Long =
    ops += 1
    // ... same as before ...

    Running countOps again, we now get the following output:

     example:    21 (      169 calls,   32 ms)
    personal: 7118 ( 38382 calls, 74 ms)
    slow: 65536 ( 679 calls, 1 ms)

    That's much better! The number of operations is lower, and the running time is faster in the pathological slow puzzle case.

    info

    The number of operations is a good primary metric here, because it is completely deterministic, stable across runs and is a good proxy for the complexity of the algorithm.

    We also measure the actual running time of the function as an indicator, but a naive measurement like this is not accurate and can vary a lot between runs. For a more accurate measurement, one could use a benchmarking library such as JMH (for example with the JMH SBT plugin) or ScalaMeter.

    Part 2

    Oh, there is a second part to this puzzle!

    Implementation

    The only change needed to implement the second part is to unfold the input rows before counting them. We add the unfoldRow function to do that, and call it from countAllUnfolded:

    /** Entry point for part 2 */
    @main def part2(): Unit =
    println(countAllUnfolded(personalPuzzle))

    def countAllUnfolded(input: String): Long =
    input.split("\n").map(unfoldRow).map(countRow).sum

    def unfoldRow(input: String): String =
    val Array(conditions, damagedCounts) =
    input.split(" ")
    val conditionsUnfolded =
    (0 until 5).map(_ => conditions).mkString("?")
    val damagedCountUnfolded =
    (0 until 5).map(_ => damagedCounts).mkString(",")
    f"$conditionsUnfolded $damagedCountUnfolded"

    Executing part2 with my personal input puzzle runs in ~800 ms on my machine, and countUncached is called 681'185:

              example:            21 (   169 calls,  31 ms)
    personal: 7118 ( 38382 calls, 74 ms)
    slow: 65536 ( 679 calls, 1 ms)
    personal unfolded: 7030194981795 (681185 calls, 815 ms)

    Can we do better?

    Group-level implementation of count

    Our first implementation of count works. Recursing character by character through the input string looks like a natural way to solve this problem. But we can simplify the implementation and improve its performance by considering groups instead of individual characters.

    To know if a group of damaged springs of length nn can be at a given position, we can consume the next nn characters of the input and check if they can all be damaged springs (i.e. none of them is a .), and if the following character can be an operational spring (i.e. it is not a #).

    import scala.collection.mutable

    extension (b: Boolean) private inline def toLong: Long =
    if b then 1L else 0L

    val cache2 = mutable.Map.empty[(List[Char], List[Int]), Long]

    private def count2(input: List[Char], ds: List[Int]): Long =
    cache2.getOrElseUpdate((input, ds), count2Uncached(input, ds))

    def count2Uncached(input: List[Char], ds: List[Int]): Long =
    ops += 1
    // We've seen all expected damaged sequences. The arrangement
    // is therefore valid only if the input does not contain
    // damaged springs.
    if ds.isEmpty then input.forall(_ != '#').toLong
    // The input is empty but we expected some damaged springs,
    // so this is not a valid arrangement.
    else if input.isEmpty then 0L
    else
    def operationalCase(): Long =
    // We can consume all following operational springs.
    count2(input.tail.dropWhile(_ == '.'), ds)
    def damagedCase(): Long =
    // If the length of the input is less than the expected
    // length of the damaged sequence, then this is not a
    // valid arrangement.
    if input.length < ds.head then 0L
    else
    // Split the input into a group of length ds.head and
    // the rest.
    val (group, rest) = input.splitAt(ds.head)
    // If the group contains any operational springs, then
    // this is not a a group of damaged springs, so this
    // is not a valid arrangement.
    if !group.forall(_ != '.') then 0L
    // If the rest of the input is empty, then this is a
    // valid arrangement only if the damaged sequence is
    // the last one expected.
    else if rest.isEmpty then ds.tail.isEmpty.toLong
    // If we now have a damaged spring, then this is not
    // the end of a damaged sequence as expected, and
    // therefore not a valid arrangement.
    else if rest.head == '#' then 0L
    // Otherwise, we can continue with the rest of the
    // input and the next expected damaged sequence.
    else count2(rest.tail, ds.tail)
    input.head match
    case '?' => operationalCase() + damagedCase()
    case '.' => operationalCase()
    case '#' => damagedCase()

    I find this implementation simpler and easier to understand than the previous one. Do you agree?

    It naturally results in less calls, and the running time is improved:

              example:            21 (    69 calls,   36 ms)
    personal: 7118 ( 12356 calls, 74 ms)
    slow: 65536 ( 404 calls, 1 ms)
    personal unfolded: 7030194981795 (235829 calls, 497 ms)

    Local cache

    We implemented memoization by using a global mutable map. What happens if we use a local, distinct one for each call to count instead?

    import scala.collection.mutable

    def count2(input: List[Char], ds: List[Int]): Long =
    val cache2 = mutable.Map.empty[(List[Char], List[Int]), Long]

    def count2Cached(input: List[Char], ds: List[Int]): Long =
    cache2.getOrElseUpdate((input, ds), count2Uncached(input, ds))

    def count2Uncached(input: List[Char], ds: List[Int]): Long =
    // ... same as before ...
    // (but calling count2Cached instead of count2)

    Even though this results to more calls to count2Uncached, this actually improves the performance of the unfolded version, down to ~400 ms on my machine:

              example:            21 (       71 calls,   32 ms)
    personal: 7118 ( 18990 calls, 67 ms)
    slow: 65536 ( 425 calls, 3 ms)
    personal unfolded: 7030194981795 ( 260272 calls, 407 ms)

    Simplify cache keys

    Because we will always consider the same sublists of input and ds for the lifetime of the cache, we can just use the lengths of these lists as keys:

    import scala.collection.mutable

    def count2(input: List[Char], ds: List[Int]): Long =
    val cache2 = mutable.Map.empty[(Int, Int), Long]

    def count2Cached(input: List[Char], ds: List[Int]): Long =
    val key = (input.length, ds.length)
    cache2.getOrElseUpdate(key, count2Uncached(input, ds))

    // ... def count2Uncached as before

    Which further reduces the running time of the unfolded version to ~320 ms on my machine:

              example:            21 (       71 calls,   33 ms)
    personal: 7118 ( 18990 calls, 66 ms)
    slow: 65536 ( 425 calls, 0 ms)
    personal unfolded: 7030194981795 ( 260272 calls, 320 ms)

    Simplify the cache structure

    Our cache key is now just a pair of integers, so we don't need a Map; an Array can do the job just as well.

    def count2(input: List[Char], ds: List[Int]): Long =
    val dim1 = input.length + 1
    val dim2 = ds.length + 1
    val cache = Array.fill(dim1 * dim2)(-1L)

    def count2Cached(input: List[Char], ds: List[Int]): Long =
    val key = input.length * dim2 + ds.length
    val result = cache(key)
    if result == -1L then
    val result = count2Uncached(input, ds)
    cache(key) = result
    result
    else result

    // ... def count2Uncached as before

    This reduces the running time of the unfolded version down to ~200 ms on my machine:

              example:            21 (       71 calls,   27 ms)
    personal: 7118 ( 18990 calls, 47 ms)
    slow: 65536 ( 425 calls, 0 ms)
    personal unfolded: 7030194981795 ( 260272 calls, 201 ms)

    Inline helper functions

    We have used helper functions to structure the implementation of count2. To avoid the calls overhead, we can use Scala 3's inline keyword.

    After adding the inline modifier to count2Cached, operationalCase and damagedCase, the running time of the unfolded version is reduced to ~140 ms on my machine:

              example:            21 (       71 calls,   28 ms)
    personal: 7118 ( 18990 calls, 50 ms)
    slow: 65536 ( 425 calls, 0 ms)
    personal unfolded: 7030194981795 ( 260272 calls, 137 ms)

    Further optimizations

    • Using a different data structure for the input and the damaged sequence, for example an IArray instead of a List, and indexing into it instead of using splitAt, tail or head methods would probably improve the performance further, but would be more verbose and less idiomatic.
    • Parallelizing countAllUnfolded did not result in any performance improvement on my machine. It might on larger inputs.

    Can you think of other optimizations that could improve the performance of this code without sacrificing readability?

    Final Code

    /** Entry point for part 1. */
    def part1(input: String): Unit =
    println(countAll(input))

    /** Sums `countRow` over all rows in `input`. */
    def countAll(input: String): Long =
    input.split("\n").map(countRow).sum

    /** Counts all of the different valid arrangements
    * of operational and broken springs in the given row.
    */
    def countRow(input: String): Long =
    val Array(conditions, damagedCounts) = input.split(" ")
    count2(
    conditions.toList,
    damagedCounts.split(",").map(_.toInt).toList
    )

    extension (b: Boolean) private inline def toLong: Long =
    if b then 1L else 0L

    def count2(input: List[Char], ds: List[Int]): Long =
    val dim1 = input.length + 1
    val dim2 = ds.length + 1
    val cache = Array.fill(dim1 * dim2)(-1L)

    inline def count2Cached(input: List[Char], ds: List[Int]): Long =
    val key = input.length * dim2 + ds.length
    val result = cache(key)
    if result == -1L then
    val result = count2Uncached(input, ds)
    cache(key) = result
    result
    else result

    def count2Uncached(input: List[Char], ds: List[Int]): Long =
    // We've seen all expected damaged sequences.
    // The arrangement is therefore valid only if the
    // input does not contain damaged springs.
    if ds.isEmpty then input.forall(_ != '#').toLong
    // The input is empty but we expected some damaged springs,
    // so this is not a valid arrangement.
    else if input.isEmpty then 0L
    else
    inline def operationalCase(): Long =
    // Operational case: we can consume all operational
    // springs to get to the next choice.
    count2Cached(input.tail.dropWhile(_ == '.'), ds)
    inline def damagedCase(): Long =
    // If the length of the input is less than the expected
    // length of the damaged sequence, then this is not a
    // valid arrangement.
    if input.length < ds.head then 0L
    else
    // Split the input into a group of length ds.head and
    // the rest.
    val (group, rest) = input.splitAt(ds.head)
    // If the group contains any operational springs, then
    // this is not a a group of damaged springs, so this
    // is not a valid arrangement.
    if !group.forall(_ != '.') then 0L
    // If the rest of the input is empty, then this is a
    // valid arrangement only if the damaged sequence
    // is the last one expected.
    else if rest.isEmpty then ds.tail.isEmpty.toLong
    // If we now have a damaged spring, then this is not the
    // end of a damaged sequence as expected, and therefore
    // not a valid arrangement.
    else if rest.head == '#' then 0L
    // Otherwise, we can continue with the rest of the input
    // and the next expected damaged sequence.
    else count2Cached(rest.tail, ds.tail)
    input.head match
    case '?' => operationalCase() + damagedCase()
    case '.' => operationalCase()
    case '#' => damagedCase()

    count2Cached(input, ds)
    end count2

    /** Entry point for part 2 */
    def part2(input: String): Unit =
    println(countAllUnfolded(input))

    def countAllUnfolded(input: String): Long =
    input.split("\n").map(unfoldRow).map(countRow).sum

    def unfoldRow(input: String): String =
    val Array(conditions, damagedCounts) =
    input.split(" ")
    val conditionsUnfolded =
    (0 until 5).map(_ => conditions).mkString("?")
    val damagedCountUnfolded =
    (0 until 5).map(_ => damagedCounts).mkString(",")
    f"$conditionsUnfolded $damagedCountUnfolded"

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day13/index.html b/2023/puzzles/day13/index.html index 4fe657f30..6cb7b0ba5 100644 --- a/2023/puzzles/day13/index.html +++ b/2023/puzzles/day13/index.html @@ -5,16 +5,16 @@ Day 13: Point of Incidence | Scala Center Advent of Code - - + +
    -

    Day 13: Point of Incidence

    by @adpi2

    Puzzle description

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

    Solution summary

    • Convert the input to a sequence of patterns.
    • Iterate over each pattern to detect its reflection. If there is no reflection on a pattern we try to find it in the transposition of the pattern.
    • Sum up all the reflection numbers.

    Parsing of the input

    We defines the model of the problem using type aliases:

    type Tile = '.' | '#'
    type Line = Seq[Tile]
    type Pattern = Seq[Line]

    To parse the input we split the full input by empty lines, (expressed with the regex \R\R, which works multiplatform). +

    Day 13: Point of Incidence

    by @adpi2

    Puzzle description

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

    Solution summary

    • Convert the input to a sequence of patterns.
    • Iterate over each pattern to detect its reflection. If there is no reflection on a pattern we try to find it in the transposition of the pattern.
    • Sum up all the reflection numbers.

    Parsing of the input

    We defines the model of the problem using type aliases:

    type Tile = '.' | '#'
    type Line = Seq[Tile]
    type Pattern = Seq[Line]

    To parse the input we split the full input by empty lines, (expressed with the regex \R\R, which works multiplatform). This gives a sequence of strings representing the patterns. Then split each pattern by a new line (regex \R). Then for each line we assert that all the characters are valid tiles.

    def parseInput(input: String): Seq[Pattern] =
    val patterns = input.split(raw"\R\R").toSeq
    patterns.map: patternStr =>
    patternStr.split(raw"\R").toSeq.map: lineStr =>
    lineStr.collect[Tile] { case tile: Tile => tile }
    info

    In Scala we tend to prefer a declarative style. An alternative imperative option would be to iterate over each line, accumulating lines into a buffer as we encounter them, and then at each empty line transfer all the accumulated lines as a group to a separate pattern buffer.

    Part 1: detecting pure reflection

    To detect the reflection line in a pattern:

    • We iterate over the index of the lines from 1 to the size of the pattern
    • We split the patterns in two at the given index
    • We invert the first part, zip it with the second part and compare line by line.

    The resulting code is:

    def findReflection(pattern: Pattern): Option[Int] =
    (1 until pattern.size).find: i =>
    val (leftPart, rightPart) = pattern.splitAt(i)
    leftPart.reverse.zip(rightPart).forall(_ == _)

    If we cannot find a line of reflection, then we transpose the pattern and try to find a column of reflection:

    findReflection(pattern).map(100 * _).orElse(findReflection(pattern.transpose))

    Part 2: detecting the reflection with a unique smudge

    The second part is almost identical to the first part. But, instead of comparing the lines based on equality, we count the number of different characters. We keep the index of the line or column which contains only a single smudge.

    The inner part of findReflection becomes:

    val (leftPart, rightPart) = pattern.splitAt(i)
    val smudges = leftPart.reverse
    .zip(rightPart)
    .map((l1, l2) => l1.zip(l2).count(_ != _))
    .sum
    smudges == 1

    Final code

    type Tile = '.' | '#'
    type Line = Seq[Tile]
    type Pattern = Seq[Line]

    def part1(input: String): Int =
    parseInput(input)
    .flatMap: pattern =>
    findReflection(pattern).map(100 * _).orElse(findReflection(pattern.transpose))
    .sum

    def part2(input: String) =
    parseInput(input)
    .flatMap: pattern =>
    findReflectionWithSmudge(pattern).map(100 * _)
    .orElse(findReflectionWithSmudge(pattern.transpose))
    .sum

    def parseInput(input: String): Seq[Pattern] =
    val patterns = input.split(raw"\R\R").toSeq
    patterns.map: patternStr =>
    patternStr.split(raw"\R").toSeq.map: lineStr =>
    lineStr.collect[Tile] { case tile: Tile => tile }

    def findReflection(pattern: Pattern): Option[Int] =
    (1 until pattern.size).find: i =>
    val (leftPart, rightPart) = pattern.splitAt(i)
    leftPart.reverse.zip(rightPart).forall(_ == _)

    def findReflectionWithSmudge(pattern: Pattern): Option[Int] =
    (1 until pattern.size).find: i =>
    val (leftPart, rightPart) = pattern.splitAt(i)
    val smudges = leftPart.reverse
    .zip(rightPart)
    .map((l1, l2) => l1.zip(l2).count(_ != _))
    .sum
    smudges == 1

    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/day14/index.html b/2023/puzzles/day14/index.html index 81764f4ab..856df3c09 100644 --- a/2023/puzzles/day14/index.html +++ b/2023/puzzles/day14/index.html @@ -5,14 +5,14 @@ Day 14: Parabolic Reflector Dish | Scala Center Advent of Code - - + +
    -

    Day 14: Parabolic Reflector Dish

    by @anatoliykmetyuk

    Puzzle description

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

    Part 1 Solution

    Part 1 of the puzzle features a square field where two types of objects can reside: round rocks 'O' and square rocks '#'. The objective is to "tilt" the field north, so that the round rocks roll as far as they can in that direction, while square rocks stay in place. Then, we need to compute the "loading" metric on the northern side of the field which is calculated as a function of how many round rocks there are on the field and how close they are to the northern edge.

    To solve Part 1, we do not even need to simulate the movement of the rocks - that is, we do not need to compute the state of the model of the platform after the tilting was performed. We can iterate from the top (north) of the field to the bottom, one line at a time, and use caching to remember where the rocks can fall. The algorithm is as follows:

    def totalLoading(lines: List[String]): Int =
    var loading = 0
    val whereCanIFall = collection.mutable.Map.empty[Int, Int]
    val totalRows = lines.size
    for
    (line, row) <- lines.zipWithIndex
    (char, col) <- line.zipWithIndex
    do char match
    case 'O' =>
    val fallRow = whereCanIFall.getOrElseUpdate(col, 0)
    loading += totalRows - fallRow
    whereCanIFall(col) = fallRow + 1
    case '#' =>
    whereCanIFall(col) = row + 1
    case '.' =>
    loading

    Part 2 Solution

    Part 2 is much more involved. Here, we are required to tilt the field in all four directions - North, West, South and East - in turn, in many cycles. The objective is to calculate the same loading metric after 1 billion cycles.

    The approach from Part 1 doesn't work here: we now need to modify our model state. The reason we didn't have to do it in Part 1 is that our model evolves in only a single step, and we only need one metric from the end state, so we don't have to calculate the entire final state. In Part 2, however, the model evolves in many steps, and each successive step depends on the previous step. So, we need to calculate the entire final state.

    The Model

    Actually, we already know how to do the Northern tilt - similarly to Part 1. We do not want to re-implement the tilts for the other directions - instead, we want to use the Northern tilt as a building block for the other tilts. So, we need to be able to rotate our model in all four directions, and then tilt it North.

    We can represent the model as a 2D array of chars:

    class Model(state: String):
    private var dataN: Array[Array[Char]] = Array.empty // The array is indexed as dataN(x)(y), where x is the column and y - the row.
    setState(state)

    def setState(newState: String): Unit =
    dataN = newState.split('\n').map(_.toArray).transpose

    We'll need to change the model a lot in a succession of many steps, so we build it with mutability in mind. We parse the input String into the dataN array (N for "North" - the default orientation of the model, against which all the views will be calculated).

    Rotations

    We do not want to actually rotate the model, as in changing the coordinates of the stones in the model array. It is computationally expensive (and this challenge is all about optimization). Instead, we need the ability to calculate a lightweight view of our model, so that the rotation operation is cheap.

    We are going to take a page from computer graphics here. In computer graphics, all transformations of images are defined as matrices, which, when applied to the coordinates of the pixels, produce a new image with changed coordinates. In a computer game, when a player turns, the game doesn't actually rotate the world: instead, it changes the camera transformation matrix, and applies it to the world, thereby calculating what the player sees.

    We are not going to define actual matrices here, but we are going to define a transformation function for each rotation we will be working against. These functions will take a pair of coordinates and will return a new pair of coordinates in a rotated coordinate system:

    type CoordTransform = (Int, Int) => (Int, Int)

    enum Direction:
    case N, W, S, E
    def isVertical: Boolean = this match
    case N | S => true
    case W | E => false

    def mkTransform(direction: Direction, offsetX: Int, offsetY: Int): CoordTransform = direction match
    case Direction.N => (x, y) => (x, y)
    case Direction.W => (x, y) => (offsetY-y, x)
    case Direction.S => (x, y) => (offsetX-x, offsetY-y)
    case Direction.E => (x, y) => (y, offsetX-x)

    def mkInverse(direction: Direction, offsetX: Int, offsetY: Int): CoordTransform = direction match
    case Direction.N => mkTransform(Direction.N, offsetX, offsetY)
    case Direction.W => mkTransform(Direction.E, offsetX, offsetY)
    case Direction.S => mkTransform(Direction.S, offsetX, offsetY)
    case Direction.E => mkTransform(Direction.W, offsetX, offsetY)

    The Direction enum represents the directions in which we are going to rotate the platform. But, if we naively rotate the coordinate system against the origin, we'll end up with some of the coordinates ending up to be negative. It's a design decision really, but in this case, we decide to keep our coordinates positive for better interop with array indexing, the array being the underlying implementation of our model.

    So, not only do we rotate the coordinate system, but we also offset it so that the coordinates are always positive. This is what mkTransform does. mkInverse is a helper function that allows us to rotate the coordinate system back to the original, northern, orientation.

    Then, we are going to teach our model to work with the rotated coordinate system:

    // class Model:
    var rotation = Direction.N
    def extentX = if rotation.isVertical then dataN(0).length else dataN.length
    def extentY = if rotation.isVertical then dataN.length else dataN(0).length
    private def toNorth: CoordTransform = mkInverse(rotation, extentX-1, extentY-1)

    def apply(x: Int, y: Int): Char =
    val (xN, yN) = toNorth(x, y)
    dataN(xN)(yN)

    def update(x: Int, y: Int, char: Char): Unit =
    val (xN, yN) = toNorth(x, y)
    dataN(xN)(yN) = char

    override def toString =
    val sb = new StringBuilder
    for y <- (0 until extentY).toIterator do
    for x <- (0 until extentX).toIterator do
    sb.append(this(x, y))
    sb.append('\n')
    sb.toString

    The rotation of the coordinate system in which the model works is done by assigning the public variable rotation. The extentX and extentY functions return the width and height of the model in the rotated coordinate system. The toNorth function returns a transformation function that converts coordinates from the rotated coordinate system back to the original, northern, coordinate system. This is needed because the dataN array is always in the northern coordinate system.

    The apply and update functions are the getters and setters of the model. They receive coordinates in the rotated coordinate system, and convert them to the northern coordinate system before accessing the dataN array.

    Think of apply and update as optical lenses: the configuration of the lenses (in our case, the rotation variable) can change what you see without changing the object you're looking at.

    Tilting and calculating the loading metric

    Now that we have the ability to rotate the model, we can implement the tilting operation. We only implement it once and it will be usable for all 4 directions:

    def cellsIterator(model: Model): Iterator[(Int, Int, Char)] =
    for
    x <- (0 until model.extentX).toIterator
    y <- (0 until model.extentY).toIterator
    yield (x, y, model(x, y))

    def rollUp(model: Model): Unit =
    val whereCanIFall = collection.mutable.Map.empty[Int, Int]
    for (x, y, c) <- cellsIterator(model) do c match
    case 'O' =>
    val fallY = whereCanIFall.getOrElseUpdate(x, 0)
    model(x, y) = '.'
    model(x, fallY) = 'O'
    whereCanIFall(x) = fallY + 1
    case '#' =>
    whereCanIFall(x) = y + 1
    case '.' =>
    end rollUp

    So, rollUp will always roll the stones up the platform - but the "up" is relative to the rotation we've set! So, if we set the rotation to Direction.W, the stones will roll to the left when we invoke rollUp - although in the current view of the model, it will look like they roll up. If we set the rotation to Direction.S, the stones will roll down. And so on. E.g., to roll left:

    model.rotation = Direction.W
    rollUp(model)

    This is achieved by the cellsIterator method which, under the hood, uses model.extentX, model.extentY and model(x, y) - the dimensions and the model accessor that are relative to the model rotation.

    We can also calculate the model loading as follows:

    def totalLoading(model: Model): Int =
    model.rotation = Direction.N
    var loading = 0
    for (_, y, c) <- cellsIterator(model) do c match
    case 'O' => loading += model.extentY - y
    case _ =>
    loading

    Since the model loading needs to always be calculated against the northern side of the model, we always set the rotation to Direction.N before calculating the loading.

    Naive cycling

    The challenge requires us to cycle through the North-West-South-East tilts for a billion cycles. Let's implement the simplest possible cycling function:

    def cycle(model: Model, times: Int): Unit =
    for i <- 1 to times do
    for cse <- Direction.values do
    model.rotation = cse
    rollUp(model)
    model.rotation = Direction.N

    This simple loop repeats a required number of times, and for each loop, we have a nested loop that iterates over all the directions (in order) and performs the rollup for each of those directions. At the end, the rotation is reset to North.

    Performant cycling

    This solution looks good but won't get to the billion cycles in a reasonable time. The rollups take way too long.

    To optimize, we will use the intent of the operation: we cycle for so many times to eventually converge to a certain state. As the challenge puts it, "This process [cycling repeatedly] should work if you leave it running long enough, but you're still worried about the north support beams...".

    So, there's a possibility that the desired state will be achieved earlier than the billion cycles. Looks like a good case for a dynamic programming approach: we need to remember the states of the model we've seen before and what they look like after one cycle. And if the current state is in our cache, no need to compute the next state again.

    Furthermore, since the transitions between states are deterministic (a state A always leads to state B), the moment one state in our cache is followed by another, previously encountered, state from that cache, we can stop cycling and just calculate the final state from the cache.

    import scala.util.boundary, boundary.break

    def cycle(model: Model, times: Int): Unit =
    val chain = collection.mutable.ListBuffer.empty[String]
    var currentState = model.toString
    boundary:
    for cyclesDone <- 0 until times do
    if chain.contains(currentState) then
    val cycleStart = chain.indexOf(currentState)
    val cycleLength = chain.length - cycleStart
    val cycleIndex = (times - cyclesDone) % cycleLength
    currentState = chain(cycleIndex + cycleStart)
    model.setState(currentState)
    break()

    chain += currentState
    for cse <- Direction.values do
    model.rotation = cse
    rollUp(model)
    currentState = model.toString

    Complete Code

    //> using scala "3.3.1"

    import scala.util.boundary, boundary.break

    type CoordTransform = (Int, Int) => (Int, Int)

    enum Direction:
    case N, W, S, E
    def isVertical: Boolean = this match
    case N | S => true
    case W | E => false

    def mkTransform(direction: Direction, offsetX: Int, offsetY: Int): CoordTransform = direction match
    case Direction.N => (x, y) => (x, y)
    case Direction.W => (x, y) => (offsetY-y, x)
    case Direction.S => (x, y) => (offsetX-x, offsetY-y)
    case Direction.E => (x, y) => (y, offsetX-x)

    def mkInverse(direction: Direction, offsetX: Int, offsetY: Int): CoordTransform = direction match
    case Direction.N => mkTransform(Direction.N, offsetX, offsetY)
    case Direction.W => mkTransform(Direction.E, offsetX, offsetY)
    case Direction.S => mkTransform(Direction.S, offsetX, offsetY)
    case Direction.E => mkTransform(Direction.W, offsetX, offsetY)

    class Model(state: String):
    private var dataN: Array[Array[Char]] = Array.empty // The array is indexed as dataN(x)(y), where x is the column and y - the row.
    setState(state)

    var rotation = Direction.N
    def extentX = if rotation.isVertical then dataN(0).length else dataN.length
    def extentY = if rotation.isVertical then dataN.length else dataN(0).length
    private def toNorth: CoordTransform = mkInverse(rotation, extentX-1, extentY-1)

    def apply(x: Int, y: Int): Char =
    val (xN, yN) = toNorth(x, y)
    dataN(xN)(yN)

    def update(x: Int, y: Int, char: Char): Unit =
    val (xN, yN) = toNorth(x, y)
    dataN(xN)(yN) = char

    def setState(newState: String): Unit =
    dataN = newState.split('\n').map(_.toArray).transpose

    override def toString =
    val sb = new StringBuilder
    for y <- (0 until extentY).toIterator do
    for x <- (0 until extentX).toIterator do
    sb.append(dataN(x)(y))
    sb.append('\n')
    sb.toString
    end Model

    def cellsIterator(model: Model): Iterator[(Int, Int, Char)] =
    for
    x <- (0 until model.extentX).toIterator
    y <- (0 until model.extentY).toIterator
    yield (x, y, model(x, y))

    def rollUp(model: Model): Unit =
    val whereCanIFall = collection.mutable.Map.empty[Int, Int]
    for (x, y, c) <- cellsIterator(model) do c match
    case 'O' =>
    val fallY = whereCanIFall.getOrElseUpdate(x, 0)
    model(x, y) = '.'
    model(x, fallY) = 'O'
    whereCanIFall(x) = fallY + 1
    case '#' =>
    whereCanIFall(x) = y + 1
    case '.' =>
    end rollUp

    def cycle(model: Model, times: Int): Unit =
    val chain = collection.mutable.ListBuffer.empty[String]
    var currentState = model.toString
    boundary:
    for cyclesDone <- 0 until times do
    if chain.contains(currentState) then
    val cycleStart = chain.indexOf(currentState)
    val cycleLength = chain.length - cycleStart
    val cycleIndex = (times - cyclesDone) % cycleLength
    currentState = chain(cycleIndex + cycleStart)
    model.setState(currentState)
    break()

    chain += currentState
    for cse <- Direction.values do
    model.rotation = cse
    rollUp(model)
    currentState = model.toString

    def debug(model: Model): Unit =
    println(s"=== ${model.rotation}; W: ${ model.extentX }, H: ${ model.extentY } ===")
    println(model)

    def totalLoading(model: Model): Int =
    model.rotation = Direction.N
    var loading = 0
    for (_, y, c) <- cellsIterator(model) do c match
    case 'O' => loading += model.extentY - y
    case _ =>
    loading

    def part1(input: String): Int =
    val model = Model(input)
    rollUp(model)
    totalLoading(model)

    def part2(input: String): Int =
    val model = Model(input)
    cycle(model, 1_000_000_000)
    totalLoading(model)

    Solutions from the community

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

    Day 14: Parabolic Reflector Dish

    by @anatoliykmetyuk

    Puzzle description

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

    Part 1 Solution

    Part 1 of the puzzle features a square field where two types of objects can reside: round rocks 'O' and square rocks '#'. The objective is to "tilt" the field north, so that the round rocks roll as far as they can in that direction, while square rocks stay in place. Then, we need to compute the "loading" metric on the northern side of the field which is calculated as a function of how many round rocks there are on the field and how close they are to the northern edge.

    To solve Part 1, we do not even need to simulate the movement of the rocks - that is, we do not need to compute the state of the model of the platform after the tilting was performed. We can iterate from the top (north) of the field to the bottom, one line at a time, and use caching to remember where the rocks can fall. The algorithm is as follows:

    def totalLoading(lines: List[String]): Int =
    var loading = 0
    val whereCanIFall = collection.mutable.Map.empty[Int, Int]
    val totalRows = lines.size
    for
    (line, row) <- lines.zipWithIndex
    (char, col) <- line.zipWithIndex
    do char match
    case 'O' =>
    val fallRow = whereCanIFall.getOrElseUpdate(col, 0)
    loading += totalRows - fallRow
    whereCanIFall(col) = fallRow + 1
    case '#' =>
    whereCanIFall(col) = row + 1
    case '.' =>
    loading

    Part 2 Solution

    Part 2 is much more involved. Here, we are required to tilt the field in all four directions - North, West, South and East - in turn, in many cycles. The objective is to calculate the same loading metric after 1 billion cycles.

    The approach from Part 1 doesn't work here: we now need to modify our model state. The reason we didn't have to do it in Part 1 is that our model evolves in only a single step, and we only need one metric from the end state, so we don't have to calculate the entire final state. In Part 2, however, the model evolves in many steps, and each successive step depends on the previous step. So, we need to calculate the entire final state.

    The Model

    Actually, we already know how to do the Northern tilt - similarly to Part 1. We do not want to re-implement the tilts for the other directions - instead, we want to use the Northern tilt as a building block for the other tilts. So, we need to be able to rotate our model in all four directions, and then tilt it North.

    We can represent the model as a 2D array of chars:

    class Model(state: String):
    private var dataN: Array[Array[Char]] = Array.empty // The array is indexed as dataN(x)(y), where x is the column and y - the row.
    setState(state)

    def setState(newState: String): Unit =
    dataN = newState.split('\n').map(_.toArray).transpose

    We'll need to change the model a lot in a succession of many steps, so we build it with mutability in mind. We parse the input String into the dataN array (N for "North" - the default orientation of the model, against which all the views will be calculated).

    Rotations

    We do not want to actually rotate the model, as in changing the coordinates of the stones in the model array. It is computationally expensive (and this challenge is all about optimization). Instead, we need the ability to calculate a lightweight view of our model, so that the rotation operation is cheap.

    We are going to take a page from computer graphics here. In computer graphics, all transformations of images are defined as matrices, which, when applied to the coordinates of the pixels, produce a new image with changed coordinates. In a computer game, when a player turns, the game doesn't actually rotate the world: instead, it changes the camera transformation matrix, and applies it to the world, thereby calculating what the player sees.

    We are not going to define actual matrices here, but we are going to define a transformation function for each rotation we will be working against. These functions will take a pair of coordinates and will return a new pair of coordinates in a rotated coordinate system:

    type CoordTransform = (Int, Int) => (Int, Int)

    enum Direction:
    case N, W, S, E
    def isVertical: Boolean = this match
    case N | S => true
    case W | E => false

    def mkTransform(direction: Direction, offsetX: Int, offsetY: Int): CoordTransform = direction match
    case Direction.N => (x, y) => (x, y)
    case Direction.W => (x, y) => (offsetY-y, x)
    case Direction.S => (x, y) => (offsetX-x, offsetY-y)
    case Direction.E => (x, y) => (y, offsetX-x)

    def mkInverse(direction: Direction, offsetX: Int, offsetY: Int): CoordTransform = direction match
    case Direction.N => mkTransform(Direction.N, offsetX, offsetY)
    case Direction.W => mkTransform(Direction.E, offsetX, offsetY)
    case Direction.S => mkTransform(Direction.S, offsetX, offsetY)
    case Direction.E => mkTransform(Direction.W, offsetX, offsetY)

    The Direction enum represents the directions in which we are going to rotate the platform. But, if we naively rotate the coordinate system against the origin, we'll end up with some of the coordinates ending up to be negative. It's a design decision really, but in this case, we decide to keep our coordinates positive for better interop with array indexing, the array being the underlying implementation of our model.

    So, not only do we rotate the coordinate system, but we also offset it so that the coordinates are always positive. This is what mkTransform does. mkInverse is a helper function that allows us to rotate the coordinate system back to the original, northern, orientation.

    Then, we are going to teach our model to work with the rotated coordinate system:

    // class Model:
    var rotation = Direction.N
    def extentX = if rotation.isVertical then dataN(0).length else dataN.length
    def extentY = if rotation.isVertical then dataN.length else dataN(0).length
    private def toNorth: CoordTransform = mkInverse(rotation, extentX-1, extentY-1)

    def apply(x: Int, y: Int): Char =
    val (xN, yN) = toNorth(x, y)
    dataN(xN)(yN)

    def update(x: Int, y: Int, char: Char): Unit =
    val (xN, yN) = toNorth(x, y)
    dataN(xN)(yN) = char

    override def toString =
    val sb = new StringBuilder
    for y <- (0 until extentY).toIterator do
    for x <- (0 until extentX).toIterator do
    sb.append(this(x, y))
    sb.append('\n')
    sb.toString

    The rotation of the coordinate system in which the model works is done by assigning the public variable rotation. The extentX and extentY functions return the width and height of the model in the rotated coordinate system. The toNorth function returns a transformation function that converts coordinates from the rotated coordinate system back to the original, northern, coordinate system. This is needed because the dataN array is always in the northern coordinate system.

    The apply and update functions are the getters and setters of the model. They receive coordinates in the rotated coordinate system, and convert them to the northern coordinate system before accessing the dataN array.

    Think of apply and update as optical lenses: the configuration of the lenses (in our case, the rotation variable) can change what you see without changing the object you're looking at.

    Tilting and calculating the loading metric

    Now that we have the ability to rotate the model, we can implement the tilting operation. We only implement it once and it will be usable for all 4 directions:

    def cellsIterator(model: Model): Iterator[(Int, Int, Char)] =
    for
    x <- (0 until model.extentX).toIterator
    y <- (0 until model.extentY).toIterator
    yield (x, y, model(x, y))

    def rollUp(model: Model): Unit =
    val whereCanIFall = collection.mutable.Map.empty[Int, Int]
    for (x, y, c) <- cellsIterator(model) do c match
    case 'O' =>
    val fallY = whereCanIFall.getOrElseUpdate(x, 0)
    model(x, y) = '.'
    model(x, fallY) = 'O'
    whereCanIFall(x) = fallY + 1
    case '#' =>
    whereCanIFall(x) = y + 1
    case '.' =>
    end rollUp

    So, rollUp will always roll the stones up the platform - but the "up" is relative to the rotation we've set! So, if we set the rotation to Direction.W, the stones will roll to the left when we invoke rollUp - although in the current view of the model, it will look like they roll up. If we set the rotation to Direction.S, the stones will roll down. And so on. E.g., to roll left:

    model.rotation = Direction.W
    rollUp(model)

    This is achieved by the cellsIterator method which, under the hood, uses model.extentX, model.extentY and model(x, y) - the dimensions and the model accessor that are relative to the model rotation.

    We can also calculate the model loading as follows:

    def totalLoading(model: Model): Int =
    model.rotation = Direction.N
    var loading = 0
    for (_, y, c) <- cellsIterator(model) do c match
    case 'O' => loading += model.extentY - y
    case _ =>
    loading

    Since the model loading needs to always be calculated against the northern side of the model, we always set the rotation to Direction.N before calculating the loading.

    Naive cycling

    The challenge requires us to cycle through the North-West-South-East tilts for a billion cycles. Let's implement the simplest possible cycling function:

    def cycle(model: Model, times: Int): Unit =
    for i <- 1 to times do
    for cse <- Direction.values do
    model.rotation = cse
    rollUp(model)
    model.rotation = Direction.N

    This simple loop repeats a required number of times, and for each loop, we have a nested loop that iterates over all the directions (in order) and performs the rollup for each of those directions. At the end, the rotation is reset to North.

    Performant cycling

    This solution looks good but won't get to the billion cycles in a reasonable time. The rollups take way too long.

    To optimize, we will use the intent of the operation: we cycle for so many times to eventually converge to a certain state. As the challenge puts it, "This process [cycling repeatedly] should work if you leave it running long enough, but you're still worried about the north support beams...".

    So, there's a possibility that the desired state will be achieved earlier than the billion cycles. Looks like a good case for a dynamic programming approach: we need to remember the states of the model we've seen before and what they look like after one cycle. And if the current state is in our cache, no need to compute the next state again.

    Furthermore, since the transitions between states are deterministic (a state A always leads to state B), the moment one state in our cache is followed by another, previously encountered, state from that cache, we can stop cycling and just calculate the final state from the cache.

    import scala.util.boundary, boundary.break

    def cycle(model: Model, times: Int): Unit =
    val chain = collection.mutable.ListBuffer.empty[String]
    var currentState = model.toString
    boundary:
    for cyclesDone <- 0 until times do
    if chain.contains(currentState) then
    val cycleStart = chain.indexOf(currentState)
    val cycleLength = chain.length - cycleStart
    val cycleIndex = (times - cyclesDone) % cycleLength
    currentState = chain(cycleIndex + cycleStart)
    model.setState(currentState)
    break()

    chain += currentState
    for cse <- Direction.values do
    model.rotation = cse
    rollUp(model)
    currentState = model.toString

    Complete Code

    //> using scala "3.3.1"

    import scala.util.boundary, boundary.break

    type CoordTransform = (Int, Int) => (Int, Int)

    enum Direction:
    case N, W, S, E
    def isVertical: Boolean = this match
    case N | S => true
    case W | E => false

    def mkTransform(direction: Direction, offsetX: Int, offsetY: Int): CoordTransform = direction match
    case Direction.N => (x, y) => (x, y)
    case Direction.W => (x, y) => (offsetY-y, x)
    case Direction.S => (x, y) => (offsetX-x, offsetY-y)
    case Direction.E => (x, y) => (y, offsetX-x)

    def mkInverse(direction: Direction, offsetX: Int, offsetY: Int): CoordTransform = direction match
    case Direction.N => mkTransform(Direction.N, offsetX, offsetY)
    case Direction.W => mkTransform(Direction.E, offsetX, offsetY)
    case Direction.S => mkTransform(Direction.S, offsetX, offsetY)
    case Direction.E => mkTransform(Direction.W, offsetX, offsetY)

    class Model(state: String):
    private var dataN: Array[Array[Char]] = Array.empty // The array is indexed as dataN(x)(y), where x is the column and y - the row.
    setState(state)

    var rotation = Direction.N
    def extentX = if rotation.isVertical then dataN(0).length else dataN.length
    def extentY = if rotation.isVertical then dataN.length else dataN(0).length
    private def toNorth: CoordTransform = mkInverse(rotation, extentX-1, extentY-1)

    def apply(x: Int, y: Int): Char =
    val (xN, yN) = toNorth(x, y)
    dataN(xN)(yN)

    def update(x: Int, y: Int, char: Char): Unit =
    val (xN, yN) = toNorth(x, y)
    dataN(xN)(yN) = char

    def setState(newState: String): Unit =
    dataN = newState.split('\n').map(_.toArray).transpose

    override def toString =
    val sb = new StringBuilder
    for y <- (0 until extentY).toIterator do
    for x <- (0 until extentX).toIterator do
    sb.append(dataN(x)(y))
    sb.append('\n')
    sb.toString
    end Model

    def cellsIterator(model: Model): Iterator[(Int, Int, Char)] =
    for
    x <- (0 until model.extentX).toIterator
    y <- (0 until model.extentY).toIterator
    yield (x, y, model(x, y))

    def rollUp(model: Model): Unit =
    val whereCanIFall = collection.mutable.Map.empty[Int, Int]
    for (x, y, c) <- cellsIterator(model) do c match
    case 'O' =>
    val fallY = whereCanIFall.getOrElseUpdate(x, 0)
    model(x, y) = '.'
    model(x, fallY) = 'O'
    whereCanIFall(x) = fallY + 1
    case '#' =>
    whereCanIFall(x) = y + 1
    case '.' =>
    end rollUp

    def cycle(model: Model, times: Int): Unit =
    val chain = collection.mutable.ListBuffer.empty[String]
    var currentState = model.toString
    boundary:
    for cyclesDone <- 0 until times do
    if chain.contains(currentState) then
    val cycleStart = chain.indexOf(currentState)
    val cycleLength = chain.length - cycleStart
    val cycleIndex = (times - cyclesDone) % cycleLength
    currentState = chain(cycleIndex + cycleStart)
    model.setState(currentState)
    break()

    chain += currentState
    for cse <- Direction.values do
    model.rotation = cse
    rollUp(model)
    currentState = model.toString

    def debug(model: Model): Unit =
    println(s"=== ${model.rotation}; W: ${ model.extentX }, H: ${ model.extentY } ===")
    println(model)

    def totalLoading(model: Model): Int =
    model.rotation = Direction.N
    var loading = 0
    for (_, y, c) <- cellsIterator(model) do c match
    case 'O' => loading += model.extentY - y
    case _ =>
    loading

    def part1(input: String): Int =
    val model = Model(input)
    rollUp(model)
    totalLoading(model)

    def part2(input: String): Int =
    val model = Model(input)
    cycle(model, 1_000_000_000)
    totalLoading(model)

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day15/index.html b/2023/puzzles/day15/index.html index c1af74c09..b5e2e8414 100644 --- a/2023/puzzles/day15/index.html +++ b/2023/puzzles/day15/index.html @@ -5,12 +5,12 @@ Day 15: Lens Library | Scala Center Advent of Code - - + +
    -

    Day 15: Lens Library

    by @sjrd

    Puzzle description

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

    Solution Summary

    1. Parse the input into a list of sequences.
    2. Implement the HASH function.
    3. Follow the given algorithm almost 1 for 1.

    Part 1

    For Part 1, we are asked to implement a string hashing called HASH. +

    Day 15: Lens Library

    by @sjrd

    Puzzle description

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

    Solution Summary

    1. Parse the input into a list of sequences.
    2. Implement the HASH function.
    3. Follow the given algorithm almost 1 for 1.

    Part 1

    For Part 1, we are asked to implement a string hashing called HASH. While it is specified in a very imperative way, it lends itself to a straightforward fold in a functional style:

    /** The `HASH` function. */
    def hash(sequence: String): Int =
    sequence.foldLeft(0) { (prev, c) =>
    ((prev + c.toInt) * 17) % 256
    }
    end hash

    We also have to parse the input into comma-separated sequences. We are told to ignore newline characters:

    /** Parses the input into a list of sequences. */
    def inputToSequences(input: String): List[String] =
    input.filter(_ != '\n').split(',').toList

    Now we wire things together and we are done:

    def part1(input: String): String =
    val sequences = inputToSequences(input)
    val result = sequences.map(hash(_)).sum
    println(result)
    result.toString()
    end part1

    Part 2

    Part 2 is the real stuff. The first part was basically unit-testing our HASH function. @@ -18,7 +18,7 @@ We then define our boxes as an array of 256 lists of LabeledLenses:

    /** A labeled lens, as found in the boxes. */
    final case class LabeledLens(label: String, focalLength: Int)

    val boxes = Array.fill[List[LabeledLens]](256)(Nil)

    We then implement the logical operations removeLens and addLens, corresponding to the - and = steps.

    // Remove the lens with the given label from the box it belongs to
    def removeLens(label: String): Unit =
    val boxIndex = hash(label)
    boxes(boxIndex) = boxes(boxIndex).filter(_.label != label)

    // Add a lens in the contents of a box; replace an existing label or add to the end
    def addLensToList(lens: LabeledLens, list: List[LabeledLens]): List[LabeledLens] =
    list match
    case Nil => lens :: Nil // add to the end
    case LabeledLens(lens.label, _) :: tail => lens :: tail // replace
    case head :: tail => head :: addLensToList(lens, tail) // keep looking

    // Add a lens with the given label and focal length into the box it belongs to, in the right place
    def addLens(label: String, focalLength: Int): Unit =
    val lens = LabeledLens(label, focalLength)
    val boxIndex = hash(label)
    boxes(boxIndex) = addLensToList(lens, boxes(boxIndex))

    Finally, we use our trust s extractor to parse and "execute" each step of the initialization sequence:

    // Parse and execute the steps
    for step <- steps do
    step match
    case s"$label-" => removeLens(label)
    case s"$label=$focalLength" => addLens(label, focalLength.toInt)

    To prove to our hash table follows the correct algorithm, we are asked to compute the focusing power of our boxes. Here again, we follow the definition of the problem 1 to 1:

    // Focusing power of a lens in a given box and at a certain position within that box
    def focusingPower(boxIndex: Int, lensIndex: Int, lens: LabeledLens): Int =
    (boxIndex + 1) * (lensIndex + 1) * lens.focalLength

    // Focusing power of all the lenses
    val focusingPowers =
    for
    (box, boxIndex) <- boxes.zipWithIndex
    (lens, lensIndex) <- box.zipWithIndex
    yield
    focusingPower(boxIndex, lensIndex, lens)

    // Sum it up
    val result = focusingPowers.sum

    Final Code

    /** The `HASH` function. */
    def hash(sequence: String): Int =
    sequence.foldLeft(0) { (prev, c) =>
    ((prev + c.toInt) * 17) % 256
    }
    end hash

    /** Parses the input into a list of sequences. */
    def inputToSequences(input: String): List[String] =
    input.filter(_ != '\n').split(',').toList

    def part1(input: String): String =
    val sequences = inputToSequences(input)
    val result = sequences.map(hash(_)).sum
    println(result)
    result.toString()
    end part1

    /** A labeled lens, as found in the boxes. */
    final case class LabeledLens(label: String, focalLength: Int)

    def part2(input: String): String =
    val steps = inputToSequences(input)

    val boxes = Array.fill[List[LabeledLens]](256)(Nil)

    // --- Processing all the steps --------------------

    // Remove the lens with the given label from the box it belongs to
    def removeLens(label: String): Unit =
    val boxIndex = hash(label)
    boxes(boxIndex) = boxes(boxIndex).filter(_.label != label)

    // Add a lens in the contents of a box; replace an existing label or add to the end
    def addLensToList(lens: LabeledLens, list: List[LabeledLens]): List[LabeledLens] =
    list match
    case Nil => lens :: Nil // add to the end
    case LabeledLens(lens.label, _) :: tail => lens :: tail // replace
    case head :: tail => head :: addLensToList(lens, tail) // keep looking

    // Add a lens with the given label and focal length into the box it belongs to, in the right place
    def addLens(label: String, focalLength: Int): Unit =
    val lens = LabeledLens(label, focalLength)
    val boxIndex = hash(label)
    boxes(boxIndex) = addLensToList(lens, boxes(boxIndex))

    // Parse and execute the steps
    for step <- steps do
    step match
    case s"$label-" => removeLens(label)
    case s"$label=$focalLength" => addLens(label, focalLength.toInt)

    // --- Computing the focusing power --------------------

    // Focusing power of a lens in a given box and at a certain position within that box
    def focusingPower(boxIndex: Int, lensIndex: Int, lens: LabeledLens): Int =
    (boxIndex + 1) * (lensIndex + 1) * lens.focalLength

    // Focusing power of all the lenses
    val focusingPowers =
    for
    (box, boxIndex) <- boxes.zipWithIndex
    (lens, lensIndex) <- box.zipWithIndex
    yield
    focusingPower(boxIndex, lensIndex, lens)

    // Sum it up
    val result = focusingPowers.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. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day16/index.html b/2023/puzzles/day16/index.html index 78712b784..f75bce382 100644 --- a/2023/puzzles/day16/index.html +++ b/2023/puzzles/day16/index.html @@ -5,14 +5,14 @@ Day 16: The Floor Will Be Lava | Scala Center Advent of Code - - + +
    -

    Day 16: The Floor Will Be Lava

    By @iusildra

    Puzzle description

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

    Solution summary

    The solution models the input as a grid with 3 types of cells:

    • Empty cells, which are traversable
    • Mirror cells, redirecting the lava flow in a 90° angle
    • Splitter cells, redirecting the lava flow only if it comes from a specific direction, otherwise it just flows through

    Then once we have the model with some helper functions, we can solve the problem by simulating the lava flow.

    1. We start by defining the origin of the lava flow
    2. Then we find the next cell the lava will flow to
      1. If the cell is empty, we move the lava there
      2. If the cell is a mirror, we redirect the lava flow
      3. If the cell is a splitter, we split the flow if necessary
    3. With the new lava flow (and its new direction), we repeat step 2 until we every path hits a wall

    Detailed solution explanation

    Global model

    We start by defining the direction of the lava flow, which is a simple enum, and the coordinates of a cell (a case class):

    enum Direction:
    case Up, Right, Down, Left

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

    Then, we model the 3 kinds of cells. We need at least a position and a method to compute the next direction(s). For convenience, we'll also add methods to calculate the path to another cell / coordinate.

    Even though a mirror can only "create" 1 new direction, because of splitters, we'll return a list of directions to limit code duplication.

    sealed abstract class Element:
    val pos: Coord
    def nextDirection(comingFrom: Direction): List[Direction]
    def pathTo(coord: Coord): Seq[Coord] =
    if pos.x == coord.x then
    if pos.y < coord.y then (pos.y to coord.y).map(Coord(pos.x, _))
    else (coord.y to pos.y).map(Coord(pos.x, _))
    else if (pos.x < coord.x) then (pos.x to coord.x).map(Coord(_, pos.y))
    else (coord.x to pos.x).map(Coord(_, pos.y))

    object Element:
    def apply(sym: Char, x: Int, y: Int) =
    sym match
    case '\\' => BackslashMirror(Coord(x, y))
    case '/' => SlashMirror(Coord(x, y))
    case '|' => VSplitter(Coord(x, y))
    case '-' => HSplitter(Coord(x, y))
    case _ => throw new IllegalArgumentException

    A mirror redirects the lava flow by 90°, so we need to know where the lava is coming to to know where it will go next. (A lava flow coming to the right will go up with a /-mirror...)

    case class SlashMirror(override val pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case Direction.Up => List(Direction.Right)
    case Direction.Left => List(Direction.Down)
    case Direction.Right => List(Direction.Up)
    case Direction.Down => List(Direction.Left)

    case class BackslashMirror(override val pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case Direction.Up => List(Direction.Left)
    case Direction.Right => List(Direction.Down)
    case Direction.Down => List(Direction.Right)
    case Direction.Left => List(Direction.Up)

    A splitter redirects the lava flow only if it encounters perpendicularly. Otherwise, it just lets the lava flow through.

    case class VSplitter(pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case d @ (Direction.Up | Direction.Down) => List(d)
    case _ => List(Direction.Up, Direction.Down)
    case class HSplitter(pos: Coord) extends Element:
    def nextDirection(comingFrom: Direction) =
    comingFrom match
    case d @ (Direction.Left | Direction.Left) => List(d)
    case _ => List(Direction.Left, Direction.Right)

    Finally, an empty cell has no behavior and shouldn't be traversed in this implementation.

    case class Empty(override val pos: Coord) extends Element:
    def nextDirection(comingFrom: Direction): Nothing =
    throw new UnsupportedOperationException

    Now that we have the model, we can parse the input and create a sparse grid of cells.

    info

    Beware of terminology, a sparse collection is a collection that is optimised for representing a few non-empty elements in a mostly empty space. A sparse collection is useful because when searching for the next cell, we can just look at the next/previous element in the collection instead of iterating and skipping-over empty elements.

    To do so, we need to map over each line with their index (to get the y coordinate) and for each character of a line, if it is not an empty cell, we create the corresponding element.

    def findElements(source: Array[String]): Array[IndexedSeq[Element]] =
    source.zipWithIndex
    .map: (line, y) =>
    line.zipWithIndex
    .filter(_._1 != '.')
    .map { (sym, x) => Element(sym, x, y) }

    Now we have everything we need to solve the problem. Until the end, every piece of code will be in a single method called solution, for convenience (I don't need to pass several arguments to my helper functions). The solver needs to know the input, but also the starting point of the lava flow as well as its direction (which can be ambiguous if it starts on a corner).

    def solution(input: Array[String], origin: Coord, dir: Direction) =

    Then, we'll use some more memory to have faster access to the elements and avoid recomputing the same path several times.

    • elements is a sparse grid of elements (only used as an intermediate step)
    • elementsByRow is a map of y coordinates to the elements on that row, to quickly to find the next cell in the same row
    • elementsByColumn is a map of x coordinates to the elements on that column, to quickly to find the next cell in the same column
    • Since we have a sparse collection, the coordinates of the elements to not match the coordinates of the input, so we need to find the min/max x and y values of the elements to know when to stop the simulation
    • activated is a grid of booleans to know if a cell has already been activated by the lava flow. Note: Array is a mutable type
      // still in the solution method
    val elements = findElements(input)
    val elementsByRow = elements.flatten.groupBy(_.pos.y)
    val elementsByColumn = elements.flatten.groupBy(_.pos.x)
    val minY = elementsByColumn.map((k, v) => (k, v(0).pos.y))
    val maxY = elementsByColumn.map((k, v) => (k, v.last.pos.y))
    val minX = elementsByRow.map((k, v) => (k, v(0).pos.x))
    val maxX = elementsByRow.map((k, v) => (k, v.last.pos.x))
    val activated = Array.fill(input.length)(Array.fill(input(0).length())(false))

    To find the next element in the lava flow, we only need the current element and the direction of the lava flow. But since we are using sparse collections, we cannot just check if x > 0 or x < line.size. An input's line can have 10 elements but only 4 non-Empty ones, so calling the 5-th element would crash with an IndexOutOfBoundsExceptions.

    Yet, this constraint comes with a benefit, we can just check if the next element is in the collection or not, and "jump" to it if it is. If it is not, we can just return an Empty cell (which will later be used to stop the simulation)

      // still in the solution method
    def findNext(
    elem: Element,
    goingTo: Direction
    ): Element =
    goingTo match
    case Direction.Left if elem.pos.x > minX(elem.pos.y) =>
    val byRow = elementsByRow(elem.pos.y)
    byRow(byRow.indexOf(elem) - 1)
    case Direction.Left =>
    Empty(Coord(0, elem.pos.y))
    case Direction.Right if elem.pos.x < maxX(elem.pos.y) =>
    val byRow = elementsByRow(elem.pos.y)
    byRow(byRow.indexOf(elem) + 1)
    case Direction.Right =>
    Empty(Coord(input(0).length() - 1, elem.pos.y))
    case Direction.Up if elem.pos.y > minY(elem.pos.x) =>
    val byCol = elementsByColumn(elem.pos.x)
    byCol(byCol.indexOf(elem) - 1)
    case Direction.Up =>
    Empty(Coord(elem.pos.x, 0))
    case Direction.Down if elem.pos.y < maxY(elem.pos.x) =>
    val byCol = elementsByColumn(elem.pos.x)
    byCol(byCol.indexOf(elem) + 1)
    case Direction.Down =>
    Empty(Coord(elem.pos.x, input.length - 1))

    Also, we might use a method to activate the cells:

      // still in the solution method
    def activate(from: Element, to: Coord) =
    from
    .pathTo(to)
    .foreach:
    case Coord(x, y) => activated(y)(x) = true

    Time to simulate the lava flow. We'll use a recursive method, but there are some caveats:

    • We'll use tail-recursion call to be stack-safe, but with one splitters giving multiple directions, we need to go over one, then over the second etc... which it not tail-recursive.
    • We need to keep a record of all the cells we've visited, and from where we came from, to avoid recomputing the same path several times. (Also called memoization)

    The first caveat can easily be solved by using a Queue (same idea as in the breadth-first search algorithm) to store the next cells to visit. This way when encountering an element giving us multiple directions, we'll just enqueue them and visit them later.

    The second one is a bit less straightforward. We need to be sure that our store won't prevent us from visiting a cell. For instance, with the following input:

    ...vv...
    ...vv...
    ../vv/..
    ...vv...

    Coming from the top of the right / mirror, we must be able to reach the left / mirror. One solution is to store the tuple of source cell and destination cell using a Set (for efficient search among unindexed elements)

    The resulting method looks like this:

      // still in the solution method
    @tailrec // to let the compiler warn us if it's not tail-recursive
    def loop(
    elems: Queue[(Element, Direction)],
    memo: Set[(Coord, Coord)]
    ): Unit =
    if elems.isEmpty then ()
    else elems.dequeue match
    case ((_: Empty, _), _) => throw new UnsupportedOperationException
    case ((elem, goingTo), rest) =>
    val nextElems =
    elem
    .nextDirection(goingTo)
    .foldLeft((rest, memo)): (acc, dir) =>
    val followup = findNext(elem, dir)
    if (memo.contains((elem.pos, followup.pos))) then acc
    else
    activate(elem, followup.pos)
    followup match
    case Empty(pos) => (acc._1, acc._2 + ((elem.pos, pos)))
    case next =>
    (acc._1.enqueue(next -> dir), acc._2 + ((elem.pos, followup.pos)))
    loop(nextElems._1, nextElems._2)
    end loop

    As long as there are elements in the queue, we dequeue and look for the next direction(s). The foldLeft allows us to activate & enqueue the next cells, and to update the memo before passing to the next direction. Once every direction has been explored, we call the method again with the new elements to visit

    Finally, we need to make the first call to the loop method in the solution method. The first element to visit can be computed based on the starting point and the direction of the lava flow. Then we activate the cells on the path and call the loop method.

      // still in the solution method
    val starting = dir match
    case Direction.Right => elementsByRow(origin.y)(0)
    case Direction.Down => elementsByColumn(origin.x)(0)
    case Direction.Left => elementsByRow(origin.y).last
    case Direction.Up => elementsByColumn(origin.x).last

    activate(starting, origin)
    loop(Queue(starting -> dir), Set())

    // println(activated.zipWithIndex.map((line, i) => f"$i%03d " + line.map(if _ then '#' else '.').mkString).mkString("\n"))
    activated.flatten.count(identity)
    end solution

    Once the simulation is done, and all the cells have been activated, we just need to count the number of activated cells. (there is a println commented out to see the lava flow)

    Part 1

    For the first part, we just need to call the solution method with the starting point and the direction of the lava flow

    def part1(input: String) =
    solution(input.split("\n"), Coord(0, 0), Direction.Right)

    Part 2

    Here we need to find the starting point and direction that maximize the number of activated cells. To do so, we'll just try every possible combination and keep the best one.

    def part2(input: String) =
    val lines = input.split("\n")
    val horizontal = (0 until lines.length).flatMap: i =>
    List(
    (Coord(0, i), Direction.Right),
    (Coord(lines(0).length() - 1, i), Direction.Left)
    )
    val vertical = (0 until lines(0).length()).flatMap: i =>
    List(
    (Coord(i, 0), Direction.Down),
    (Coord(i, lines.length - 1), Direction.Up)
    )
    val borders = horizontal ++ vertical
    borders.map((coord, dir) => solution(lines, coord, dir)).max

    Full code

    import scala.annotation.tailrec
    import scala.collection.immutable.Queue
    import scala.collection.mutable.Buffer

    /* -------------------------------------------------------------------------- */
    /* Global */
    /* -------------------------------------------------------------------------- */
    enum Direction:
    case Up, Right, Down, Left

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

    sealed abstract class Element:
    val pos: Coord
    def nextDirection(comingFrom: Direction): List[Direction]
    def pathTo(coord: Coord): Seq[Coord] =
    if pos.x == coord.x then
    if pos.y < coord.y then (pos.y to coord.y).map(Coord(pos.x, _))
    else (coord.y to pos.y).map(Coord(pos.x, _))
    else if (pos.x < coord.x) then (pos.x to coord.x).map(Coord(_, pos.y))
    else (coord.x to pos.x).map(Coord(_, pos.y))

    object Element:
    def apply(sym: Char, x: Int, y: Int) =
    sym match
    case '\\' => BackslashMirror(Coord(x, y))
    case '/' => SlashMirror(Coord(x, y))
    case '|' => VSplitter(Coord(x, y))
    case '-' => HSplitter(Coord(x, y))
    case _ => throw new IllegalArgumentException

    case class SlashMirror(override val pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case Direction.Up => List(Direction.Right)
    case Direction.Left => List(Direction.Down)
    case Direction.Right => List(Direction.Up)
    case Direction.Down => List(Direction.Left)

    case class BackslashMirror(override val pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case Direction.Up => List(Direction.Left)
    case Direction.Right => List(Direction.Down)
    case Direction.Down => List(Direction.Right)
    case Direction.Left => List(Direction.Up)

    case class VSplitter(pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case d @ (Direction.Up | Direction.Down) => List(d)
    case _ => List(Direction.Up, Direction.Down)
    case class HSplitter(pos: Coord) extends Element:
    def nextDirection(comingFrom: Direction) =
    comingFrom match
    case d @ (Direction.Left | Direction.Left) => List(d)
    case _ => List(Direction.Left, Direction.Right)

    case class Empty(pos: Coord) extends Element:
    def nextDirection(comingFrom: Direction): Nothing =
    throw new UnsupportedOperationException

    def findElements(source: Array[String]) =
    source.zipWithIndex
    .map: (line, y) =>
    line.zipWithIndex
    .filter(_._1 != '.')
    .map { (sym, x) => Element(sym, x, y) }

    def solution(input: Array[String], origin: Coord, dir: Direction) =
    val elements = findElements(input)
    val elementsByRow = elements.flatten.groupBy(_.pos.y)
    val elementsByColumn = elements.flatten.groupBy(_.pos.x)
    val minY = elementsByColumn.map((k, v) => (k, v(0).pos.y))
    val maxY = elementsByColumn.map((k, v) => (k, v.last.pos.y))
    val minX = elementsByRow.map((k, v) => (k, v(0).pos.x))
    val maxX = elementsByRow.map((k, v) => (k, v.last.pos.x))
    val activated = Array.fill(input.length)(Array.fill(input(0).length())(false))
    // val memo = Set.empty[(Coord, Coord)]
    def findNext(
    elem: Element,
    goingTo: Direction
    ): Element =
    goingTo match
    case Direction.Left if elem.pos.x > minX(elem.pos.y) =>
    val byRow = elementsByRow(elem.pos.y)
    byRow(byRow.indexOf(elem) - 1)
    case Direction.Left =>
    Empty(Coord(0, elem.pos.y))
    case Direction.Right if elem.pos.x < maxX(elem.pos.y) =>
    val byRow = elementsByRow(elem.pos.y)
    byRow(byRow.indexOf(elem) + 1)
    case Direction.Right =>
    Empty(Coord(input(0).length() - 1, elem.pos.y))
    case Direction.Up if elem.pos.y > minY(elem.pos.x) =>
    val byCol = elementsByColumn(elem.pos.x)
    byCol(byCol.indexOf(elem) - 1)
    case Direction.Up =>
    Empty(Coord(elem.pos.x, 0))
    case Direction.Down if elem.pos.y < maxY(elem.pos.x) =>
    val byCol = elementsByColumn(elem.pos.x)
    byCol(byCol.indexOf(elem) + 1)
    case Direction.Down =>
    Empty(Coord(elem.pos.x, input.length - 1))

    def activate(from: Element, to: Coord) =
    from
    .pathTo(to)
    .foreach:
    case Coord(x, y) => activated(y)(x) = true

    @tailrec
    def loop(
    elems: Queue[(Element, Direction)],
    memo: Set[(Coord, Coord)]
    ): Unit =
    if elems.isEmpty then ()
    else
    elems.dequeue match
    case ((_: Empty, _), _) => throw new UnsupportedOperationException
    case ((elem, goingTo), rest) =>
    val nextElems =
    elem
    .nextDirection(goingTo)
    .foldLeft((rest, memo)): (acc, dir) =>
    val followup = findNext(elem, dir)
    if (memo.contains((elem.pos, followup.pos))) then acc
    else
    activate(elem, followup.pos)
    followup match
    case Empty(pos) => (acc._1, acc._2 + ((elem.pos, pos)))
    case next =>
    (acc._1.enqueue(next -> dir), acc._2 + ((elem.pos, followup.pos)))
    loop(nextElems._1, nextElems._2)
    end loop

    val starting = dir match
    case Direction.Right => elementsByRow(origin.y)(0)
    case Direction.Down => elementsByColumn(origin.x)(0)
    case Direction.Left => elementsByRow(origin.y).last
    case Direction.Up => elementsByColumn(origin.x).last

    activate(starting, origin)
    loop(Queue(starting -> dir), Set())

    // println(activated.zipWithIndex.map((line, i) => f"$i%03d " + line.map(if _ then '#' else '.').mkString).mkString("\n"))
    activated.flatten.count(identity)

    /* -------------------------------------------------------------------------- */
    /* Part I */
    /* -------------------------------------------------------------------------- */
    def part1(input: String) =
    solution(input.split("\n"), Coord(0, 0), Direction.Right)

    /* -------------------------------------------------------------------------- */
    /* Part II */
    /* -------------------------------------------------------------------------- */
    def part2(input: String) =
    val lines = input.split("\n")
    val horizontal = (0 until lines.length).flatMap: i =>
    List(
    (Coord(0, i), Direction.Right),
    (Coord(lines(0).length() - 1, i), Direction.Left)
    )
    val vertical = (0 until lines(0).length()).flatMap: i =>
    List(
    (Coord(i, 0), Direction.Down),
    (Coord(i, lines.length - 1), Direction.Up)
    )
    val borders = horizontal ++ vertical
    borders.map((coord, dir) => solution(lines, coord, dir)).max

    Solutions from the community

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

    Day 16: The Floor Will Be Lava

    By @iusildra

    Puzzle description

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

    Solution summary

    The solution models the input as a grid with 3 types of cells:

    • Empty cells, which are traversable
    • Mirror cells, redirecting the lava flow in a 90° angle
    • Splitter cells, redirecting the lava flow only if it comes from a specific direction, otherwise it just flows through

    Then once we have the model with some helper functions, we can solve the problem by simulating the lava flow.

    1. We start by defining the origin of the lava flow
    2. Then we find the next cell the lava will flow to
      1. If the cell is empty, we move the lava there
      2. If the cell is a mirror, we redirect the lava flow
      3. If the cell is a splitter, we split the flow if necessary
    3. With the new lava flow (and its new direction), we repeat step 2 until we every path hits a wall

    Detailed solution explanation

    Global model

    We start by defining the direction of the lava flow, which is a simple enum, and the coordinates of a cell (a case class):

    enum Direction:
    case Up, Right, Down, Left

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

    Then, we model the 3 kinds of cells. We need at least a position and a method to compute the next direction(s). For convenience, we'll also add methods to calculate the path to another cell / coordinate.

    Even though a mirror can only "create" 1 new direction, because of splitters, we'll return a list of directions to limit code duplication.

    sealed abstract class Element:
    val pos: Coord
    def nextDirection(comingFrom: Direction): List[Direction]
    def pathTo(coord: Coord): Seq[Coord] =
    if pos.x == coord.x then
    if pos.y < coord.y then (pos.y to coord.y).map(Coord(pos.x, _))
    else (coord.y to pos.y).map(Coord(pos.x, _))
    else if (pos.x < coord.x) then (pos.x to coord.x).map(Coord(_, pos.y))
    else (coord.x to pos.x).map(Coord(_, pos.y))

    object Element:
    def apply(sym: Char, x: Int, y: Int) =
    sym match
    case '\\' => BackslashMirror(Coord(x, y))
    case '/' => SlashMirror(Coord(x, y))
    case '|' => VSplitter(Coord(x, y))
    case '-' => HSplitter(Coord(x, y))
    case _ => throw new IllegalArgumentException

    A mirror redirects the lava flow by 90°, so we need to know where the lava is coming to to know where it will go next. (A lava flow coming to the right will go up with a /-mirror...)

    case class SlashMirror(override val pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case Direction.Up => List(Direction.Right)
    case Direction.Left => List(Direction.Down)
    case Direction.Right => List(Direction.Up)
    case Direction.Down => List(Direction.Left)

    case class BackslashMirror(override val pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case Direction.Up => List(Direction.Left)
    case Direction.Right => List(Direction.Down)
    case Direction.Down => List(Direction.Right)
    case Direction.Left => List(Direction.Up)

    A splitter redirects the lava flow only if it encounters perpendicularly. Otherwise, it just lets the lava flow through.

    case class VSplitter(pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case d @ (Direction.Up | Direction.Down) => List(d)
    case _ => List(Direction.Up, Direction.Down)
    case class HSplitter(pos: Coord) extends Element:
    def nextDirection(comingFrom: Direction) =
    comingFrom match
    case d @ (Direction.Left | Direction.Left) => List(d)
    case _ => List(Direction.Left, Direction.Right)

    Finally, an empty cell has no behavior and shouldn't be traversed in this implementation.

    case class Empty(override val pos: Coord) extends Element:
    def nextDirection(comingFrom: Direction): Nothing =
    throw new UnsupportedOperationException

    Now that we have the model, we can parse the input and create a sparse grid of cells.

    info

    Beware of terminology, a sparse collection is a collection that is optimised for representing a few non-empty elements in a mostly empty space. A sparse collection is useful because when searching for the next cell, we can just look at the next/previous element in the collection instead of iterating and skipping-over empty elements.

    To do so, we need to map over each line with their index (to get the y coordinate) and for each character of a line, if it is not an empty cell, we create the corresponding element.

    def findElements(source: Array[String]): Array[IndexedSeq[Element]] =
    source.zipWithIndex
    .map: (line, y) =>
    line.zipWithIndex
    .filter(_._1 != '.')
    .map { (sym, x) => Element(sym, x, y) }

    Now we have everything we need to solve the problem. Until the end, every piece of code will be in a single method called solution, for convenience (I don't need to pass several arguments to my helper functions). The solver needs to know the input, but also the starting point of the lava flow as well as its direction (which can be ambiguous if it starts on a corner).

    def solution(input: Array[String], origin: Coord, dir: Direction) =

    Then, we'll use some more memory to have faster access to the elements and avoid recomputing the same path several times.

    • elements is a sparse grid of elements (only used as an intermediate step)
    • elementsByRow is a map of y coordinates to the elements on that row, to quickly to find the next cell in the same row
    • elementsByColumn is a map of x coordinates to the elements on that column, to quickly to find the next cell in the same column
    • Since we have a sparse collection, the coordinates of the elements to not match the coordinates of the input, so we need to find the min/max x and y values of the elements to know when to stop the simulation
    • activated is a grid of booleans to know if a cell has already been activated by the lava flow. Note: Array is a mutable type
      // still in the solution method
    val elements = findElements(input)
    val elementsByRow = elements.flatten.groupBy(_.pos.y)
    val elementsByColumn = elements.flatten.groupBy(_.pos.x)
    val minY = elementsByColumn.map((k, v) => (k, v(0).pos.y))
    val maxY = elementsByColumn.map((k, v) => (k, v.last.pos.y))
    val minX = elementsByRow.map((k, v) => (k, v(0).pos.x))
    val maxX = elementsByRow.map((k, v) => (k, v.last.pos.x))
    val activated = Array.fill(input.length)(Array.fill(input(0).length())(false))

    To find the next element in the lava flow, we only need the current element and the direction of the lava flow. But since we are using sparse collections, we cannot just check if x > 0 or x < line.size. An input's line can have 10 elements but only 4 non-Empty ones, so calling the 5-th element would crash with an IndexOutOfBoundsExceptions.

    Yet, this constraint comes with a benefit, we can just check if the next element is in the collection or not, and "jump" to it if it is. If it is not, we can just return an Empty cell (which will later be used to stop the simulation)

      // still in the solution method
    def findNext(
    elem: Element,
    goingTo: Direction
    ): Element =
    goingTo match
    case Direction.Left if elem.pos.x > minX(elem.pos.y) =>
    val byRow = elementsByRow(elem.pos.y)
    byRow(byRow.indexOf(elem) - 1)
    case Direction.Left =>
    Empty(Coord(0, elem.pos.y))
    case Direction.Right if elem.pos.x < maxX(elem.pos.y) =>
    val byRow = elementsByRow(elem.pos.y)
    byRow(byRow.indexOf(elem) + 1)
    case Direction.Right =>
    Empty(Coord(input(0).length() - 1, elem.pos.y))
    case Direction.Up if elem.pos.y > minY(elem.pos.x) =>
    val byCol = elementsByColumn(elem.pos.x)
    byCol(byCol.indexOf(elem) - 1)
    case Direction.Up =>
    Empty(Coord(elem.pos.x, 0))
    case Direction.Down if elem.pos.y < maxY(elem.pos.x) =>
    val byCol = elementsByColumn(elem.pos.x)
    byCol(byCol.indexOf(elem) + 1)
    case Direction.Down =>
    Empty(Coord(elem.pos.x, input.length - 1))

    Also, we might use a method to activate the cells:

      // still in the solution method
    def activate(from: Element, to: Coord) =
    from
    .pathTo(to)
    .foreach:
    case Coord(x, y) => activated(y)(x) = true

    Time to simulate the lava flow. We'll use a recursive method, but there are some caveats:

    • We'll use tail-recursion call to be stack-safe, but with one splitters giving multiple directions, we need to go over one, then over the second etc... which it not tail-recursive.
    • We need to keep a record of all the cells we've visited, and from where we came from, to avoid recomputing the same path several times. (Also called memoization)

    The first caveat can easily be solved by using a Queue (same idea as in the breadth-first search algorithm) to store the next cells to visit. This way when encountering an element giving us multiple directions, we'll just enqueue them and visit them later.

    The second one is a bit less straightforward. We need to be sure that our store won't prevent us from visiting a cell. For instance, with the following input:

    ...vv...
    ...vv...
    ../vv/..
    ...vv...

    Coming from the top of the right / mirror, we must be able to reach the left / mirror. One solution is to store the tuple of source cell and destination cell using a Set (for efficient search among unindexed elements)

    The resulting method looks like this:

      // still in the solution method
    @tailrec // to let the compiler warn us if it's not tail-recursive
    def loop(
    elems: Queue[(Element, Direction)],
    memo: Set[(Coord, Coord)]
    ): Unit =
    if elems.isEmpty then ()
    else elems.dequeue match
    case ((_: Empty, _), _) => throw new UnsupportedOperationException
    case ((elem, goingTo), rest) =>
    val nextElems =
    elem
    .nextDirection(goingTo)
    .foldLeft((rest, memo)): (acc, dir) =>
    val followup = findNext(elem, dir)
    if (memo.contains((elem.pos, followup.pos))) then acc
    else
    activate(elem, followup.pos)
    followup match
    case Empty(pos) => (acc._1, acc._2 + ((elem.pos, pos)))
    case next =>
    (acc._1.enqueue(next -> dir), acc._2 + ((elem.pos, followup.pos)))
    loop(nextElems._1, nextElems._2)
    end loop

    As long as there are elements in the queue, we dequeue and look for the next direction(s). The foldLeft allows us to activate & enqueue the next cells, and to update the memo before passing to the next direction. Once every direction has been explored, we call the method again with the new elements to visit

    Finally, we need to make the first call to the loop method in the solution method. The first element to visit can be computed based on the starting point and the direction of the lava flow. Then we activate the cells on the path and call the loop method.

      // still in the solution method
    val starting = dir match
    case Direction.Right => elementsByRow(origin.y)(0)
    case Direction.Down => elementsByColumn(origin.x)(0)
    case Direction.Left => elementsByRow(origin.y).last
    case Direction.Up => elementsByColumn(origin.x).last

    activate(starting, origin)
    loop(Queue(starting -> dir), Set())

    // println(activated.zipWithIndex.map((line, i) => f"$i%03d " + line.map(if _ then '#' else '.').mkString).mkString("\n"))
    activated.flatten.count(identity)
    end solution

    Once the simulation is done, and all the cells have been activated, we just need to count the number of activated cells. (there is a println commented out to see the lava flow)

    Part 1

    For the first part, we just need to call the solution method with the starting point and the direction of the lava flow

    def part1(input: String) =
    solution(input.split("\n"), Coord(0, 0), Direction.Right)

    Part 2

    Here we need to find the starting point and direction that maximize the number of activated cells. To do so, we'll just try every possible combination and keep the best one.

    def part2(input: String) =
    val lines = input.split("\n")
    val horizontal = (0 until lines.length).flatMap: i =>
    List(
    (Coord(0, i), Direction.Right),
    (Coord(lines(0).length() - 1, i), Direction.Left)
    )
    val vertical = (0 until lines(0).length()).flatMap: i =>
    List(
    (Coord(i, 0), Direction.Down),
    (Coord(i, lines.length - 1), Direction.Up)
    )
    val borders = horizontal ++ vertical
    borders.map((coord, dir) => solution(lines, coord, dir)).max

    Full code

    import scala.annotation.tailrec
    import scala.collection.immutable.Queue
    import scala.collection.mutable.Buffer

    /* -------------------------------------------------------------------------- */
    /* Global */
    /* -------------------------------------------------------------------------- */
    enum Direction:
    case Up, Right, Down, Left

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

    sealed abstract class Element:
    val pos: Coord
    def nextDirection(comingFrom: Direction): List[Direction]
    def pathTo(coord: Coord): Seq[Coord] =
    if pos.x == coord.x then
    if pos.y < coord.y then (pos.y to coord.y).map(Coord(pos.x, _))
    else (coord.y to pos.y).map(Coord(pos.x, _))
    else if (pos.x < coord.x) then (pos.x to coord.x).map(Coord(_, pos.y))
    else (coord.x to pos.x).map(Coord(_, pos.y))

    object Element:
    def apply(sym: Char, x: Int, y: Int) =
    sym match
    case '\\' => BackslashMirror(Coord(x, y))
    case '/' => SlashMirror(Coord(x, y))
    case '|' => VSplitter(Coord(x, y))
    case '-' => HSplitter(Coord(x, y))
    case _ => throw new IllegalArgumentException

    case class SlashMirror(override val pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case Direction.Up => List(Direction.Right)
    case Direction.Left => List(Direction.Down)
    case Direction.Right => List(Direction.Up)
    case Direction.Down => List(Direction.Left)

    case class BackslashMirror(override val pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case Direction.Up => List(Direction.Left)
    case Direction.Right => List(Direction.Down)
    case Direction.Down => List(Direction.Right)
    case Direction.Left => List(Direction.Up)

    case class VSplitter(pos: Coord) extends Element:
    def nextDirection(goingTo: Direction) =
    goingTo match
    case d @ (Direction.Up | Direction.Down) => List(d)
    case _ => List(Direction.Up, Direction.Down)
    case class HSplitter(pos: Coord) extends Element:
    def nextDirection(comingFrom: Direction) =
    comingFrom match
    case d @ (Direction.Left | Direction.Left) => List(d)
    case _ => List(Direction.Left, Direction.Right)

    case class Empty(pos: Coord) extends Element:
    def nextDirection(comingFrom: Direction): Nothing =
    throw new UnsupportedOperationException

    def findElements(source: Array[String]) =
    source.zipWithIndex
    .map: (line, y) =>
    line.zipWithIndex
    .filter(_._1 != '.')
    .map { (sym, x) => Element(sym, x, y) }

    def solution(input: Array[String], origin: Coord, dir: Direction) =
    val elements = findElements(input)
    val elementsByRow = elements.flatten.groupBy(_.pos.y)
    val elementsByColumn = elements.flatten.groupBy(_.pos.x)
    val minY = elementsByColumn.map((k, v) => (k, v(0).pos.y))
    val maxY = elementsByColumn.map((k, v) => (k, v.last.pos.y))
    val minX = elementsByRow.map((k, v) => (k, v(0).pos.x))
    val maxX = elementsByRow.map((k, v) => (k, v.last.pos.x))
    val activated = Array.fill(input.length)(Array.fill(input(0).length())(false))
    // val memo = Set.empty[(Coord, Coord)]
    def findNext(
    elem: Element,
    goingTo: Direction
    ): Element =
    goingTo match
    case Direction.Left if elem.pos.x > minX(elem.pos.y) =>
    val byRow = elementsByRow(elem.pos.y)
    byRow(byRow.indexOf(elem) - 1)
    case Direction.Left =>
    Empty(Coord(0, elem.pos.y))
    case Direction.Right if elem.pos.x < maxX(elem.pos.y) =>
    val byRow = elementsByRow(elem.pos.y)
    byRow(byRow.indexOf(elem) + 1)
    case Direction.Right =>
    Empty(Coord(input(0).length() - 1, elem.pos.y))
    case Direction.Up if elem.pos.y > minY(elem.pos.x) =>
    val byCol = elementsByColumn(elem.pos.x)
    byCol(byCol.indexOf(elem) - 1)
    case Direction.Up =>
    Empty(Coord(elem.pos.x, 0))
    case Direction.Down if elem.pos.y < maxY(elem.pos.x) =>
    val byCol = elementsByColumn(elem.pos.x)
    byCol(byCol.indexOf(elem) + 1)
    case Direction.Down =>
    Empty(Coord(elem.pos.x, input.length - 1))

    def activate(from: Element, to: Coord) =
    from
    .pathTo(to)
    .foreach:
    case Coord(x, y) => activated(y)(x) = true

    @tailrec
    def loop(
    elems: Queue[(Element, Direction)],
    memo: Set[(Coord, Coord)]
    ): Unit =
    if elems.isEmpty then ()
    else
    elems.dequeue match
    case ((_: Empty, _), _) => throw new UnsupportedOperationException
    case ((elem, goingTo), rest) =>
    val nextElems =
    elem
    .nextDirection(goingTo)
    .foldLeft((rest, memo)): (acc, dir) =>
    val followup = findNext(elem, dir)
    if (memo.contains((elem.pos, followup.pos))) then acc
    else
    activate(elem, followup.pos)
    followup match
    case Empty(pos) => (acc._1, acc._2 + ((elem.pos, pos)))
    case next =>
    (acc._1.enqueue(next -> dir), acc._2 + ((elem.pos, followup.pos)))
    loop(nextElems._1, nextElems._2)
    end loop

    val starting = dir match
    case Direction.Right => elementsByRow(origin.y)(0)
    case Direction.Down => elementsByColumn(origin.x)(0)
    case Direction.Left => elementsByRow(origin.y).last
    case Direction.Up => elementsByColumn(origin.x).last

    activate(starting, origin)
    loop(Queue(starting -> dir), Set())

    // println(activated.zipWithIndex.map((line, i) => f"$i%03d " + line.map(if _ then '#' else '.').mkString).mkString("\n"))
    activated.flatten.count(identity)

    /* -------------------------------------------------------------------------- */
    /* Part I */
    /* -------------------------------------------------------------------------- */
    def part1(input: String) =
    solution(input.split("\n"), Coord(0, 0), Direction.Right)

    /* -------------------------------------------------------------------------- */
    /* Part II */
    /* -------------------------------------------------------------------------- */
    def part2(input: String) =
    val lines = input.split("\n")
    val horizontal = (0 until lines.length).flatMap: i =>
    List(
    (Coord(0, i), Direction.Right),
    (Coord(lines(0).length() - 1, i), Direction.Left)
    )
    val vertical = (0 until lines(0).length()).flatMap: i =>
    List(
    (Coord(i, 0), Direction.Down),
    (Coord(i, lines.length - 1), Direction.Up)
    )
    val borders = horizontal ++ vertical
    borders.map((coord, dir) => solution(lines, coord, dir)).max

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day17/index.html b/2023/puzzles/day17/index.html index 05a1190b0..0cc733ebb 100644 --- a/2023/puzzles/day17/index.html +++ b/2023/puzzles/day17/index.html @@ -5,19 +5,19 @@ Day 17: Clumsy Crucible | Scala Center Advent of Code - - + +
    -

    Day 17: Clumsy Crucible

    by @stewSquared

    Puzzle description

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

    Solution Summary

    This is a classic search problem with an interesting restriction on state transformations.

    We will solve this using Dijkstra's Algorithm to find a path through the grid, using the heat loss of each position as our node weights. However, the states in our priority queue will need to include more than just position and accumulated heat loss, since the streak of forward movements in a given direction affects which positions are accessible from a given state.

    Since the restrictions on state transformations differ in part 1 and part 2, we'll model them separately from the base state transformations.

    Framework

    First, we will need a Grid class to represent the possible positions, and store the heat at each position. +

    Day 17: Clumsy Crucible

    by @stewSquared

    Puzzle description

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

    Solution Summary

    This is a classic search problem with an interesting restriction on state transformations.

    We will solve this using Dijkstra's Algorithm to find a path through the grid, using the heat loss of each position as our node weights. However, the states in our priority queue will need to include more than just position and accumulated heat loss, since the streak of forward movements in a given direction affects which positions are accessible from a given state.

    Since the restrictions on state transformations differ in part 1 and part 2, we'll model them separately from the base state transformations.

    Framework

    First, we will need a Grid class to represent the possible positions, and store the heat at each position. It will be represented by a 2D vector:

    case class Grid(grid: Vector[Vector[Int]]):
    val xRange = grid.head.indices
    val yRange = grid.indices

    We can parse the input and store it in the Grid class. Each line is treated as a row, and each character in the row is treated as a single column, and required to be a digit:

    def loadGrid(input: String): Grid =
    Grid:
    Vector.from:
    for line <- input.split("\n")
    yield line.map(_.asDigit).toVector

    We can define some accessors to make it more convenient to work with a Grid that is available in the context.

    def grid(using Grid) = summon[Grid].grid
    def xRange(using Grid) = summon[Grid].xRange
    def yRange(using Grid) = summon[Grid].yRange

    Second, for convenience, let's introduce a class for presenting direction:

    enum Dir:
    case N, S, E, W

    def turnRight = this match
    case Dir.N => E
    case Dir.E => S
    case Dir.S => W
    case Dir.W => N

    def turnLeft = this match
    case Dir.N => W
    case Dir.W => S
    case Dir.S => E
    case Dir.E => N

    Since moving forward, turning left, and turning right are common operations, convenience methods for each are included here.

    Third, a class for position:

    case class Point(x: Int, y: Int):
    def inBounds(using Grid) =
    xRange.contains(x) && yRange.contains(y)

    def heatLoss(using Grid) =
    if inBounds then grid(y)(x) else 0

    def move(dir: Dir) = dir match
    case Dir.N => copy(y = y - 1)
    case Dir.S => copy(y = y + 1)
    case Dir.E => copy(x = x + 1)
    case Dir.W => copy(x = x - 1)

    Here we provide some convenience methods for checking if a point is inBounds on the grid, and the heatLoss of a point on the grid.

    Search State

    Now we want to be able to model our state as we're searching. The state will track our position (pos). To know what transitions are possible, we need to keep track of our streak of movements in a given direction (dir). Later, we'll also keep track of the heat lost while getting to a state.

    case class State(pos: Point, dir: Dir, streak: Int):

    Next let's define some methods for transitioning to new states. We know that we can chose to move forward, turn left, or turn right. For now, we won't consider the restrictions from Part 1 or Part 2 on whether or not you can move forward:

      def straight: State =
    State(pos.move(dir), dir, streak + 1)

    def turnLeft: State =
    val newDir = dir.turnLeft
    State(pos.move(newDir), newDir, 1)

    def turnRight: State =
    val newDir = dir.turnRight
    State(pos.move(newDir), newDir, 1)

    Note that the streak resets to one when we turn right or turn left, since we also move the position forward in that new direction.

    Dijkstra's Algorithm

    Finally, let's lay the groundwork for an implementation of Dijkstra's algorithm.

    Since our valid state transformations vary between part 1 and part 2, let's parameterize our search method by a function:

    import collection.mutable.{PriorityQueue, Map}

    type StateTransform = Grid ?=> State => List[State]

    def search(next: StateTransform)(using Grid): Int =

    The algorithm uses Map to track the minimum total heat loss for each state, and a Priority Queue prioritizing by this heatloss to choose the next state to visit:

      val minHeatLoss = Map.empty[State, Int]

    given Ordering[State] = Ordering.by(minHeatLoss)
    val pq = PriorityQueue.empty[State].reverse

    var visiting = State(Point(0, 0), Dir.E, 0)
    minHeatLoss(visiting) = 0

    As we generate new states to add to the priority Queue, we need to make sure not to add suboptimal states. The first time we visit any state, it will be with a minimum possible cost, because we're visiting this new state from an adjacent minimum heatloss state in our priority queue. So any state we've already visited will be discarded. This is what our loop will look like:

      val end = Point(xRange.max, yRange.max)
    while visiting.pos != end do
    val states = next(visiting).filterNot(minHeatLoss.contains)
    states.foreach: s =>
    minHeatLoss(s) = minHeatLoss(visiting) + s.pos.heatLoss
    pq.enqueue(s)
    visiting = pq.dequeue()

    minHeatLoss(visiting)

    Notice how minHeatLoss is always updated to the minimum of the state we're visiting from plus the incremental heatloss of the new state we're adding to the queue.

    We can then provide a framework for calling the search function using the input with solve. It parses the input to a Grid, defining it as a given instance.

    def solve(input: String, next: StateTransform): Int =
    given Grid = loadGrid(input)
    search(next)

    Part 1

    Now we need to model our state transformation restrictions for Part 1. We can typically move straight, left, and right, but we need to make sure our streak while moving straight never exceeds 3:

    // Inside case class State:
    def nextStates(using Grid): List[State] =
    List(straight, turnLeft, turnRight).filter: s =>
    s.pos.inBounds && s.streak <= 3

    This will only ever filter out the forward movement, since moving to the left or right resets the streak to 1.

    We can then call solve with nextStates from our entry point for part1:

    def part1(input: String): Int =
    solve(input, _.nextStates)

    Part 2

    Part 2 is similar, but our streak limit increases to 10. Furthermore, while the streak is less than four, only a forward movement is possible:

    // Inside case class State:
    def nextStates2(using Grid): List[State] =
    if streak < 4 then List(straight)
    else List(straight, turnLeft, turnRight).filter: s =>
    s.pos.inBounds && s.streak <= 10

    And we call solve with nextStates2 to solve part2:

    def part2(input: String): Int =
    solve(input, _.nextStates2)

    Final Code

    import collection.mutable.{PriorityQueue, Map}

    def part1(input: String): Int =
    solve(input, _.nextStates)

    def part2(input: String): Int =
    solve(input, _.nextStates2)

    def loadGrid(input: String): Grid =
    Grid:
    Vector.from:
    for line <- input.split("\n")
    yield line.map(_.asDigit).toVector

    case class Grid(grid: Vector[Vector[Int]]):
    val xRange = grid.head.indices
    val yRange = grid.indices

    enum Dir:
    case N, S, E, W

    def turnRight = this match
    case Dir.N => E
    case Dir.E => S
    case Dir.S => W
    case Dir.W => N

    def turnLeft = this match
    case Dir.N => W
    case Dir.W => S
    case Dir.S => E
    case Dir.E => N

    def grid(using Grid) = summon[Grid].grid
    def xRange(using Grid) = summon[Grid].xRange
    def yRange(using Grid) = summon[Grid].yRange

    case class Point(x: Int, y: Int):
    def inBounds(using Grid) =
    xRange.contains(x) && yRange.contains(y)

    def heatLoss(using Grid) =
    if inBounds then grid(y)(x) else 0

    def move(dir: Dir) = dir match
    case Dir.N => copy(y = y - 1)
    case Dir.S => copy(y = y + 1)
    case Dir.E => copy(x = x + 1)
    case Dir.W => copy(x = x - 1)

    case class State(pos: Point, dir: Dir, streak: Int):
    def straight: State =
    State(pos.move(dir), dir, streak + 1)

    def turnLeft: State =
    val newDir = dir.turnLeft
    State(pos.move(newDir), newDir, 1)

    def turnRight: State =
    val newDir = dir.turnRight
    State(pos.move(newDir), newDir, 1)

    def nextStates(using Grid): List[State] =
    List(straight, turnLeft, turnRight).filter: s =>
    s.pos.inBounds && s.streak <= 3

    def nextStates2(using Grid): List[State] =
    if streak < 4 then List(straight)
    else List(straight, turnLeft, turnRight).filter: s =>
    s.pos.inBounds && s.streak <= 10

    type StateTransform = Grid ?=> State => List[State]

    def solve(input: String, next: StateTransform): Int =
    given Grid = loadGrid(input)
    search(next)

    def search(next: StateTransform)(using Grid): Int =

    val minHeatLoss = Map.empty[State, Int]

    given Ordering[State] = Ordering.by(minHeatLoss)
    val pq = PriorityQueue.empty[State].reverse

    var visiting = State(Point(0, 0), Dir.E, 0)
    minHeatLoss(visiting) = 0

    val end = Point(xRange.max, yRange.max)
    while visiting.pos != end do
    val states = next(visiting).filterNot(minHeatLoss.contains)
    states.foreach: s =>
    minHeatLoss(s) = minHeatLoss(visiting) + s.pos.heatLoss
    pq.enqueue(s)
    visiting = pq.dequeue()

    minHeatLoss(visiting)

    Run it in the browser

    Run Part 1

    Run Part 2

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day18/index.html b/2023/puzzles/day18/index.html index 84aba3f28..c2e1012d3 100644 --- a/2023/puzzles/day18/index.html +++ b/2023/puzzles/day18/index.html @@ -5,12 +5,12 @@ Day 18: Lavaduct Lagoon | Scala Center Advent of Code - - + +
    -

    Day 18: Lavaduct Lagoon

    by @EugeneFlesselle

    Puzzle description

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

    Solution Summary

    Assume we have a given digPlan: Seq[Trench] for which to compute the area, +

    Day 18: Lavaduct Lagoon

    by @EugeneFlesselle

    Puzzle description

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

    Solution Summary

    Assume we have a given digPlan: Seq[Trench] for which to compute the area, and let the following classes:

    enum Direction:
    case Up, Down, Left, Right

    case class Trench(dir: Direction, length: Int)

    We can go through the dig plan keeping track of the current position, by starting from (x = 0, y = 0), increasing x when going right, increasing y when going down, and so on.

    Provided our current position, we can then keep track of the lagoon area as follows:

    • When going Right: we count all the points in the line we cover, i.e the length of the trench.
    • When going Down: we count all the points which we leave on the left (or pass over), i.e. the length of the downwards trench times our current x coordinate, @@ -32,7 +32,7 @@ its direction from the last digit, and its length by converting from the hexadecimal encoding of the remaining digits.

      object Direction:
      def fromInt(i: Char): Direction = i match
      case '0' => Right case '1' => Down case '2' => Left case '3' => Up

      val digPlan = for
      case s"$_ $_ (#$color)" <- input.linesIterator
      dir = Direction.fromInt(color.last)
      len = BigInt(x = color.init, radix = 16)
      yield Trench(dir, len.toInt)

      Final code

      enum Direction:
      case Up, Down, Left, Right
      object Direction:
      def fromChar(c: Char): Direction = c match
      case 'U' => Up case 'D' => Down case 'L' => Left case 'R' => Right
      def fromInt(i: Char): Direction = i match
      case '0' => Right case '1' => Down case '2' => Left case '3' => Up
      import Direction.*

      case class Trench(dir: Direction, length: Int)

      def area(digPlan: Seq[Trench]): Long =
      val (_, area) = digPlan.foldLeft((0, 0), 1L):
      case (((x, y), area), Trench(dir, len)) => dir match
      case Right => ((x + len, y), area + len)
      case Down => ((x, y + len), area + (x + 1) * len.toLong)
      case Left => ((x - len, y), area)
      case Up => ((x, y - len), area - x * len.toLong)
      area

      def part1(input: String): String =
      val digPlan = for
      case s"$dirC $len (#$_)" <- input.linesIterator
      dir = Direction.fromChar(dirC.head)
      yield Trench(dir, len.toInt)

      area(digPlan.toSeq).toString

      def part2(input: String): String =
      val digPlan = for
      case s"$_ $_ (#$color)" <- input.linesIterator
      dir = Direction.fromInt(color.last)
      len = BigInt(x = color.init, radix = 16)
      yield Trench(dir, len.toInt)

      area(digPlan.toSeq).toString

      Solutions from the community

      Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day19/index.html b/2023/puzzles/day19/index.html index 0687b3701..32db09a7b 100644 --- a/2023/puzzles/day19/index.html +++ b/2023/puzzles/day19/index.html @@ -5,14 +5,14 @@ Day 19: Aplenty | Scala Center Advent of Code - - + +
    -

    Day 19: Aplenty

    by @mbovel

    Puzzle description

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

    Data structures

    We define the following data structures for today's puzzle:

    enum Channel:
    case X, M, A, S

    enum Operator:
    case LessThan, GreaterThan

    enum Result:
    case Reject, Accept

    enum Instruction:
    case IfThenElse(
    channel: Channel,
    operator: Operator,
    value: Int,
    thenBranch: GoTo | Return,
    elseBranch: Instruction
    )
    case Return(result: Result)
    case GoTo(target: String)

    import Instruction.*

    type Workflow = Map[String, Instruction]

    case class Part(x: Int, m: Int, a: Int, s: Int)

    Parsing

    Then, we write the associated parsing methods, using String's split method and regular expression patterns

    object Channel:
    def parse(input: String): Channel =
    input match
    case "x" => Channel.X
    case "m" => Channel.M
    case "a" => Channel.A
    case "s" => Channel.S
    case _ => throw Exception(s"Invalid channel: $input")

    object Operator:
    def parse(input: String): Operator =
    input match
    case "<" => Operator.LessThan
    case ">" => Operator.GreaterThan
    case _ => throw Exception(s"Invalid operator: $input")

    object Result:
    def parse(input: String): Result =
    input match
    case "R" => Result.Reject
    case "A" => Result.Accept
    case _ => throw Exception(s"Invalid result: $input")

    object Instruction:
    private val IfThenElseRegex = """([xmas])([<>])(\d+):(\w+),(.*)""".r
    private val ReturnRegex = """([RA])""".r
    private val GoToRegex = """(\w+)""".r
    def parse(input: String): Instruction =
    input match
    case IfThenElseRegex(channel, operator, value, thenBranch, elseBranch) =>
    Instruction.parse(thenBranch) match
    case thenBranch: (GoTo | Return) =>
    IfThenElse(
    Channel.parse(channel),
    Operator.parse(operator),
    value.toInt,
    thenBranch,
    Instruction.parse(elseBranch)
    )
    case _ => throw Exception(s"Invalid then branch: $thenBranch")
    case ReturnRegex(result) => Return(Result.parse(result))
    case GoToRegex(target) => GoTo(target)
    case _ => throw Exception(s"Invalid instruction: $input")

    object Workflow:
    def parse(input: String): Workflow =
    input.split("\n").map(parseBlock).toMap

    private val BlockRegex = """(\w+)\{(.*?)\}""".r
    private def parseBlock(input: String): (String, Instruction) =
    input match
    case BlockRegex(label, body) =>
    (label, Instruction.parse(body))

    object Part:
    val PartRegex = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)\}""".r
    def parse(input: String): Part =
    input match
    case PartRegex(x, m, a, s) => Part(x.toInt, m.toInt, a.toInt, s.toInt)
    case _ => throw Exception(s"Invalid part: $input")

    Part 1 – Evaluation

    These helpers allow us to implement the core logic succinctly:

    def part1(input: String): Int =
    val Array(workflowLines, partLines) = input.split("\n\n")
    val workflow = Workflow.parse(workflowLines.trim())
    val parts = partLines.trim().split("\n").map(Part.parse)

    def eval(part: Part, instruction: Instruction): Result =
    instruction match
    case IfThenElse(channel, operator, value, thenBranch, elseBranch) =>
    val channelValue = channel match
    case Channel.X => part.x
    case Channel.M => part.m
    case Channel.A => part.a
    case Channel.S => part.s
    val result = operator match
    case Operator.LessThan => channelValue < value
    case Operator.GreaterThan => channelValue > value
    if result then eval(part, thenBranch) else eval(part, elseBranch)
    case Return(result) => result
    case GoTo(target) => eval(part, workflow(target))

    parts
    .collect: part =>
    eval(part, workflow("in")) match
    case Result.Reject => 0
    case Result.Accept => part.x + part.m + part.a + part.s
    .sum

    Part 2 – Symbolic execution

    To solve the second part efficiently, we use symbolic execution to count the number of executions of the workflow that lead to an Accept result.

    We represent symbolic values with the AbstractPart structure, where the value associated to each channel is not a number, but a range of possible values:

    case class Range(from: Long, until: Long):
    assert(from < until)
    def count() = until - from

    object Range:
    def safe(from: Long, until: Long): Option[Range] =
    if from < until then Some(Range(from, until)) else None

    case class AbstractPart(x: Range, m: Range, a: Range, s: Range):
    def count() = x.count() * m.count() * a.count() * s.count()

    def withChannel(channel: Channel, newRange: Range) =
    channel match
    case Channel.X => copy(x = newRange)
    case Channel.M => copy(m = newRange)
    case Channel.A => copy(a = newRange)
    case Channel.S => copy(s = newRange)

    def getChannel(channel: Channel) =
    channel match
    case Channel.X => x
    case Channel.M => m
    case Channel.A => a
    case Channel.S => s

    We will start the evaluation with abstract parts that contain all possible values for each channel: four ranges from 1 until 4001.

    When we evaluate an IfThenElse instruction, we split the argument AbstractPart into two parts, one that contains only the values that satisfy the condition, and one that contains only the values that do not satisfy the condition.

    This is achieved with the split method of AbstractPart:

      def split(
    channel: Channel,
    value: Int
    ): (Option[AbstractPart], Option[AbstractPart]) =
    val currentRange = getChannel(channel)
    (
    Range.safe(currentRange.from, value).map(withChannel(channel, _)),
    Range.safe(value, currentRange.until).map(withChannel(channel, _))
    )

    Using these helpers, we can implement the abstract evaluator as follows:

    def part2(input: String): Long = combinations(input, 4001)

    extension [T](part: (T, T)) private inline def swap: (T, T) = (part._2, part._1)

    def combinations(input: String, until: Long): Long =
    val Array(workflowLines, _) = input.split("\n\n")
    val workflow = Workflow.parse(workflowLines.trim())

    def count(part: AbstractPart, instruction: Instruction): Long =
    instruction match
    case IfThenElse(channel, operator, value, thenBranch, elseBranch) =>
    val (trueValues, falseValues) =
    operator match
    case Operator.LessThan => part.split(channel, value)
    case Operator.GreaterThan => part.split(channel, value + 1).swap
    trueValues.map(count(_, thenBranch)).getOrElse(0L)
    + falseValues.map(count(_, elseBranch)).getOrElse(0L)
    case Return(Result.Accept) => part.count()
    case Return(Result.Reject) => 0L
    case GoTo(target) => count(part, workflow(target))

    count(
    AbstractPart(
    Range(1, until),
    Range(1, until),
    Range(1, until),
    Range(1, until)
    ),
    workflow("in")
    )

    Solutions from the community

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

    Day 19: Aplenty

    by @mbovel

    Puzzle description

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

    Data structures

    We define the following data structures for today's puzzle:

    enum Channel:
    case X, M, A, S

    enum Operator:
    case LessThan, GreaterThan

    enum Result:
    case Reject, Accept

    enum Instruction:
    case IfThenElse(
    channel: Channel,
    operator: Operator,
    value: Int,
    thenBranch: GoTo | Return,
    elseBranch: Instruction
    )
    case Return(result: Result)
    case GoTo(target: String)

    import Instruction.*

    type Workflow = Map[String, Instruction]

    case class Part(x: Int, m: Int, a: Int, s: Int)

    Parsing

    Then, we write the associated parsing methods, using String's split method and regular expression patterns

    object Channel:
    def parse(input: String): Channel =
    input match
    case "x" => Channel.X
    case "m" => Channel.M
    case "a" => Channel.A
    case "s" => Channel.S
    case _ => throw Exception(s"Invalid channel: $input")

    object Operator:
    def parse(input: String): Operator =
    input match
    case "<" => Operator.LessThan
    case ">" => Operator.GreaterThan
    case _ => throw Exception(s"Invalid operator: $input")

    object Result:
    def parse(input: String): Result =
    input match
    case "R" => Result.Reject
    case "A" => Result.Accept
    case _ => throw Exception(s"Invalid result: $input")

    object Instruction:
    private val IfThenElseRegex = """([xmas])([<>])(\d+):(\w+),(.*)""".r
    private val ReturnRegex = """([RA])""".r
    private val GoToRegex = """(\w+)""".r
    def parse(input: String): Instruction =
    input match
    case IfThenElseRegex(channel, operator, value, thenBranch, elseBranch) =>
    Instruction.parse(thenBranch) match
    case thenBranch: (GoTo | Return) =>
    IfThenElse(
    Channel.parse(channel),
    Operator.parse(operator),
    value.toInt,
    thenBranch,
    Instruction.parse(elseBranch)
    )
    case _ => throw Exception(s"Invalid then branch: $thenBranch")
    case ReturnRegex(result) => Return(Result.parse(result))
    case GoToRegex(target) => GoTo(target)
    case _ => throw Exception(s"Invalid instruction: $input")

    object Workflow:
    def parse(input: String): Workflow =
    input.split("\n").map(parseBlock).toMap

    private val BlockRegex = """(\w+)\{(.*?)\}""".r
    private def parseBlock(input: String): (String, Instruction) =
    input match
    case BlockRegex(label, body) =>
    (label, Instruction.parse(body))

    object Part:
    val PartRegex = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)\}""".r
    def parse(input: String): Part =
    input match
    case PartRegex(x, m, a, s) => Part(x.toInt, m.toInt, a.toInt, s.toInt)
    case _ => throw Exception(s"Invalid part: $input")

    Part 1 – Evaluation

    These helpers allow us to implement the core logic succinctly:

    def part1(input: String): Int =
    val Array(workflowLines, partLines) = input.split("\n\n")
    val workflow = Workflow.parse(workflowLines.trim())
    val parts = partLines.trim().split("\n").map(Part.parse)

    def eval(part: Part, instruction: Instruction): Result =
    instruction match
    case IfThenElse(channel, operator, value, thenBranch, elseBranch) =>
    val channelValue = channel match
    case Channel.X => part.x
    case Channel.M => part.m
    case Channel.A => part.a
    case Channel.S => part.s
    val result = operator match
    case Operator.LessThan => channelValue < value
    case Operator.GreaterThan => channelValue > value
    if result then eval(part, thenBranch) else eval(part, elseBranch)
    case Return(result) => result
    case GoTo(target) => eval(part, workflow(target))

    parts
    .collect: part =>
    eval(part, workflow("in")) match
    case Result.Reject => 0
    case Result.Accept => part.x + part.m + part.a + part.s
    .sum

    Part 2 – Symbolic execution

    To solve the second part efficiently, we use symbolic execution to count the number of executions of the workflow that lead to an Accept result.

    We represent symbolic values with the AbstractPart structure, where the value associated to each channel is not a number, but a range of possible values:

    case class Range(from: Long, until: Long):
    assert(from < until)
    def count() = until - from

    object Range:
    def safe(from: Long, until: Long): Option[Range] =
    if from < until then Some(Range(from, until)) else None

    case class AbstractPart(x: Range, m: Range, a: Range, s: Range):
    def count() = x.count() * m.count() * a.count() * s.count()

    def withChannel(channel: Channel, newRange: Range) =
    channel match
    case Channel.X => copy(x = newRange)
    case Channel.M => copy(m = newRange)
    case Channel.A => copy(a = newRange)
    case Channel.S => copy(s = newRange)

    def getChannel(channel: Channel) =
    channel match
    case Channel.X => x
    case Channel.M => m
    case Channel.A => a
    case Channel.S => s

    We will start the evaluation with abstract parts that contain all possible values for each channel: four ranges from 1 until 4001.

    When we evaluate an IfThenElse instruction, we split the argument AbstractPart into two parts, one that contains only the values that satisfy the condition, and one that contains only the values that do not satisfy the condition.

    This is achieved with the split method of AbstractPart:

      def split(
    channel: Channel,
    value: Int
    ): (Option[AbstractPart], Option[AbstractPart]) =
    val currentRange = getChannel(channel)
    (
    Range.safe(currentRange.from, value).map(withChannel(channel, _)),
    Range.safe(value, currentRange.until).map(withChannel(channel, _))
    )

    Using these helpers, we can implement the abstract evaluator as follows:

    def part2(input: String): Long = combinations(input, 4001)

    extension [T](part: (T, T)) private inline def swap: (T, T) = (part._2, part._1)

    def combinations(input: String, until: Long): Long =
    val Array(workflowLines, _) = input.split("\n\n")
    val workflow = Workflow.parse(workflowLines.trim())

    def count(part: AbstractPart, instruction: Instruction): Long =
    instruction match
    case IfThenElse(channel, operator, value, thenBranch, elseBranch) =>
    val (trueValues, falseValues) =
    operator match
    case Operator.LessThan => part.split(channel, value)
    case Operator.GreaterThan => part.split(channel, value + 1).swap
    trueValues.map(count(_, thenBranch)).getOrElse(0L)
    + falseValues.map(count(_, elseBranch)).getOrElse(0L)
    case Return(Result.Accept) => part.count()
    case Return(Result.Reject) => 0L
    case GoTo(target) => count(part, workflow(target))

    count(
    AbstractPart(
    Range(1, until),
    Range(1, until),
    Range(1, until),
    Range(1, until)
    ),
    workflow("in")
    )

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day20/index.html b/2023/puzzles/day20/index.html index d20f831b6..a562f7535 100644 --- a/2023/puzzles/day20/index.html +++ b/2023/puzzles/day20/index.html @@ -5,12 +5,12 @@ Day 20: Pulse Propagation | Scala Center Advent of Code - - + +
    -

    Day 20: Pulse Propagation

    by @merlinorg

    Puzzle description

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

    Summary

    Day 20 involves executing a machine that is operated by pushing a button to +

    Day 20: Pulse Propagation

    by @merlinorg

    Puzzle description

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

    Summary

    Day 20 involves executing a machine that is operated by pushing a button to send pulses to various modules. These modules update their internal state according to their type and the pulse information, and then send further pulses through the machine.

    It is tempting to implement the machine using mutable state, however a @@ -84,7 +84,7 @@ button press count at that point.

    extension (self: Problem2FSM)
    inline def +(state: MachineFSM): Problem2FSM =
    import self.*
    state.queue.headOption match
    case Some(Pulse(src, _, true)) if cycles.get(src).contains(0L) =>
    copy(cycles = cycles + (src -> state.presses))
    case _ => self

    Part 2 Solution

    Part 2 is solved identically to part 1, combining the state machines and iterating until we reach a solution.

    def part2(input: String): Long =
    val machine = parse(input)
    Iterator
    .iterate(MachineFSM(machine))(nextState)
    .scanLeft(Problem2FSM.from(machine))(_ + _)
    .findMap(Problem2FSM.solution)

    Final Code

    The complete, rather lengthy solution follows:

    import scala.annotation.tailrec
    import scala.collection.immutable.Queue

    type ModuleName = String

    // Pulses are the messages of our primary state machine. They are either low
    // (false) or high (true) and travel from a source to a destination module
    final case class Pulse(
    source: ModuleName,
    destination: ModuleName,
    level: Boolean,
    )

    object Pulse:
    final val ButtonPress = Pulse("button", "broadcaster", false)

    // The modules include pass-throughs which simply forward pulses, flip flips
    // which toggle state and emit when they receive a low pulse, and conjunctions
    // which emit a low signal when all inputs are high.

    sealed trait Module:
    def name: ModuleName
    def destinations: Vector[ModuleName]
    // Generate pulses for all the destinations of this module
    def pulses(level: Boolean): Vector[Pulse] =
    destinations.map(Pulse(name, _, level))
    end Module

    final case class PassThrough(
    name: ModuleName,
    destinations: Vector[ModuleName],
    ) extends Module

    final case class FlipFlop(
    name: ModuleName,
    destinations: Vector[ModuleName],
    state: Boolean,
    ) extends Module

    final case class Conjunction(
    name: ModuleName,
    destinations: Vector[ModuleName],
    // The source modules that most-recently sent a high pulse
    state: Set[ModuleName],
    ) extends Module

    // The machine comprises a collection of named modules and a map that gathers
    // which modules serve as sources for each module in the machine.

    final case class Machine(
    modules: Map[ModuleName, Module],
    sources: Map[ModuleName, Set[ModuleName]]
    )

    object Machine:
    val Initial = Machine(Map.empty, Map.empty)

    extension (self: Machine)
    inline def +(module: Module): Machine =
    import self.*
    copy(
    modules = modules.updated(module.name, module),
    sources = module.destinations.foldLeft(sources): (sources, destination) =>
    sources.updatedWith(destination):
    case None => Some(Set(module.name))
    case Some(values) => Some(values + module.name)
    )

    val Initial = Machine(Map.empty, Map.empty)

    // To parse the input we first parse all of the modules and then fold them
    // into a new machine
    def parse(input: String): Machine =
    val modules = input.linesIterator.map:
    case s"%$name -> $targets" =>
    FlipFlop(name, targets.split(", ").toVector, false)
    case s"&$name -> $targets" =>
    Conjunction(name, targets.split(", ").toVector, Set.empty)
    case s"$name -> $targets" =>
    PassThrough(name, targets.split(", ").toVector)
    modules.foldLeft(Machine.Initial)(_ + _)

    // The primary state machine state comprises the machine itself, the number of
    // button presses and a queue of outstanding pulses.

    final case class MachineFSM(
    machine: Machine,
    presses: Long = 0,
    queue: Queue[Pulse] = Queue.empty,
    )

    def nextState(fsm: MachineFSM): MachineFSM =
    import fsm.*

    queue.dequeueOption match
    case None =>
    copy(presses = presses + 1, queue = Queue(Pulse.ButtonPress))

    case Some((Pulse(source, destination, level), tail)) =>
    machine.modules.get(destination) match
    case Some(passThrough: PassThrough) =>
    copy(queue = tail ++ passThrough.pulses(level))

    case Some(flipFlop: FlipFlop) if !level =>
    val flipFlop2 = flipFlop.copy(state = !flipFlop.state)
    copy(
    machine = machine + flipFlop2,
    queue = tail ++ flipFlop2.pulses(flipFlop2.state)
    )

    case Some(conjunction: Conjunction) =>
    val conjunction2 = conjunction.copy(
    state = if level then conjunction.state + source
    else conjunction.state - source
    )
    val active = machine.sources(conjunction2.name) == conjunction2.state
    copy(
    machine = machine + conjunction2,
    queue = tail ++ conjunction2.pulses(!active)
    )

    case _ =>
    copy(queue = tail)
    end nextState

    // An unruly and lawless find-map-get
    extension [A](self: Iterator[A])
    def findMap[B](f: A => Option[B]): B = self.flatMap(f).next()

    // The problem 1 state machine comprises the number of low and high pulses
    // processed, and whether the problem is complete (after 1000 presses). This
    // state machine gets updated by each state of the primary state machine.

    final case class Problem1FSM(
    lows: Long,
    highs: Long,
    complete: Boolean,
    )

    object Problem1FSM:
    final val Initial = Problem1FSM(0, 0, false)

    // The result is the product of lows and highs
    def solution(fsm: Problem1FSM): Option[Long] =
    import fsm.*
    Option.when(complete)(lows * highs)

    // If the head of the pulse queue is a low or high pulse then update the
    // low/high count. If the pulse queue is empty and the button has been pressed
    // 1000 times then complete.

    extension (self: Problem1FSM)
    inline def +(state: MachineFSM): Problem1FSM =
    import self.*
    state.queue.headOption match
    case Some(Pulse(_, _, false)) => copy(lows = lows + 1)
    case Some(Pulse(_, _, true)) => copy(highs = highs + 1)
    case None if state.presses == 1000 => copy(complete = true)
    case None => self

    // Part 1 is solved by first constructing the primary state machine that
    // executes the pulse machinery. Each state of this machine is then fed to a
    // second problem 1 state machine. We then run the combined state machines to
    // completion.

    def part1(input: String): Long =
    val machine = parse(input)
    Iterator
    .iterate(MachineFSM(machine))(nextState)
    .scanLeft(Problem1FSM.Initial)(_ + _)
    .findMap(Problem1FSM.solution)

    // The problem is characterized by a terminal module ("rx") that is fed by
    // several subgraphs so we look to see which are the sources of the terminal
    // module; these are the subgraphs whose cycle lengths we need to count.
    def subgraphs(machine: Machine): Set[ModuleName] =
    val terminal = (machine.sources.keySet -- machine.modules.keySet).head
    machine.sources(machine.sources(terminal).head)

    // The problem 2 state machine is looking for the least common multiple of the
    // cycle lengths of the subgraphs that feed into the output "rx" module. When it
    // observes a high pulse from the final module of one these subgraphs, it
    // records the number of button presses to reach this state.

    final case class Problem2FSM(
    cycles: Map[ModuleName, Long],
    )

    object Problem2FSM:

    def from(machine: Machine): Problem2FSM =
    Problem2FSM(subgraphs(machine).map(_ -> 0L).toMap)

    private def lcm(list: Iterable[Long]): Long =
    list.foldLeft(1L)((a, b) => b * a / gcd(a, b))

    @tailrec
    private def gcd(x: Long, y: Long): Long =
    if y == 0 then x else gcd(y, x % y)

    def solution(fsm: Problem2FSM): Option[Long] =
    import fsm.cycles
    Option.when(cycles.values.forall(_ > 0))(lcm(cycles.values))

    extension (self: Problem2FSM)
    inline def +(state: MachineFSM): Problem2FSM =
    import self.*
    state.queue.headOption match
    case Some(Pulse(src, _, true)) if cycles.get(src).contains(0L) =>
    copy(cycles = cycles + (src -> state.presses))
    case _ => self

    // Part 2 is solved by first constructing the primary state machine that
    // executes the pulse machinery. Each state of this machine is then fed to a
    // second problem 2 state machine. We then run the combined state machines to
    // completion.

    def part2(input: String): Long =
    val machine = parse(input)
    Iterator
    .iterate(MachineFSM(machine))(nextState)
    .scanLeft(Problem2FSM.from(machine))(_ + _)
    .findMap(Problem2FSM.solution)

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day21/index.html b/2023/puzzles/day21/index.html index 10d6ff773..0ca939393 100644 --- a/2023/puzzles/day21/index.html +++ b/2023/puzzles/day21/index.html @@ -5,14 +5,14 @@ Day 21: Step Counter | Scala Center Advent of Code - - + +
    -

    Day 21: Step Counter

    Puzzle description

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

    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/day22/index.html b/2023/puzzles/day22/index.html index f65c5d060..b165bf87d 100644 --- a/2023/puzzles/day22/index.html +++ b/2023/puzzles/day22/index.html @@ -5,12 +5,12 @@ Day 22: Sand Slabs | Scala Center Advent of Code - - + +
    -

    Day 22: Sand Slabs

    by Paweł Cembaluk

    Puzzle description

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

    Model

    Before delving into the solution, let's familiarize ourselves with the representation of bricks.. We have two case +

    Day 22: Sand Slabs

    by Paweł Cembaluk

    Puzzle description

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

    Model

    Before delving into the solution, let's familiarize ourselves with the representation of bricks.. We have two case classes: Coordinate to denote a point in the three-dimensional space, and Brick to define a brick with starting and ending coordinates.

    case class Coordinate(x: Int, y: Int, z: Int)

    case class Brick(start: Coordinate, end: Coordinate)

    Part 1

    Parsing the input

    The parse method employs pattern matching and string interpolation to extract starting and ending coordinates from a multi-line string representation of bricks. It iterates over each line, deconstructs it into coordinate values using @@ -34,7 +34,7 @@ a Set underneath. By converting it to a list, we ensure that each count is preserved independently. Without this conversion, if multiple bricks have the same falling chain count, some counts may be lost.

    By combining these individual chain counts and summing them up, we arrive at the answer for Part 2:

    def part2(input: String): Int = {
    val bricks = parse(input)
    val brickToSupportingBricks = dropBricks(bricks)
    val fallingChainCounts = brickToSupportingBricks.keys.toList.map(
    countFallingChain(brickToSupportingBricks)
    )
    fallingChainCounts.sum
    }

    Final code

    case class Coordinate(x: Int, y: Int, z: Int)

    case class Brick(start: Coordinate, end: Coordinate) {

    lazy val moveDown: Brick =
    copy(
    start = start.copy(z = start.z - 1),
    end = end.copy(z = end.z - 1)
    )

    def collidesWith(other: Brick): Boolean =
    axisOverlaps(other)(_.x) &&
    axisOverlaps(other)(_.y) &&
    axisOverlaps(other)(_.z)

    private def axisOverlaps(other: Brick)(axis: Coordinate => Int) = {
    val min = axis(start) min axis(end)
    val max = axis(start) max axis(end)
    val otherMin = axis(other.start) min axis(other.end)
    val otherMax = axis(other.start) max axis(other.end)
    max >= otherMin && otherMax >= min
    }
    }

    def parse(input: String): Seq[Brick] =
    for s"$x1,$y1,$z1~$x2,$y2,$z2" <- input.split('\n')
    yield
    val start = Coordinate(x1.toInt, y1.toInt, z1.toInt)
    val end = Coordinate(x2.toInt, y2.toInt, z2.toInt)
    Brick(start, end)

    import scala.collection.mutable

    def dropBricks(bricks: Seq[Brick]): Map[Brick, Set[Brick]] = {
    val bricksByZAsc = bricks.sortBy(brick => (brick.start.z) min (brick.end.z))
    val remainingBricks = mutable.Stack.from(bricksByZAsc)
    val droppedBricks = mutable.Map[Brick, Set[Brick]]()

    while (remainingBricks.nonEmpty) {
    val brick = remainingBricks.pop()
    val brickMovedDown = brick.moveDown
    val collidingBricks =
    droppedBricks.keys.filter(brickMovedDown.collidesWith).toSet
    if (collidesWithGround(brickMovedDown) || collidingBricks.nonEmpty)
    droppedBricks.put(brick, collidingBricks)
    else
    remainingBricks.push(brickMovedDown)
    }

    droppedBricks.toMap
    }

    def collidesWithGround(brick: Brick): Boolean =
    brick.start.z == 0 || brick.end.z == 0

    def getDisintegrableBricks(brickToSupportingBricks: Map[Brick, Set[Brick]]): Set[Brick] = {
    val nonDisintegrableBricks = brickToSupportingBricks.values.collect {
    case supporting if supporting.sizeIs == 1 =>
    supporting.head // the only brick that holds the brick above
    }.toSet
    brickToSupportingBricks.keySet diff nonDisintegrableBricks
    }

    def part1(input: String): Int = {
    val bricks = parse(input)
    val brickToSupportingBricks = dropBricks(bricks)
    val disintegrableBricks = getDisintegrableBricks(brickToSupportingBricks)
    disintegrableBricks.size
    }

    def countFallingChain(brickToSupportingBricks: Map[Brick, Set[Brick]])(brick: Brick): Int = {
    val disintegratedBricks = mutable.Set[Brick](brick)
    var remainingBricks = brickToSupportingBricks.removed(brick)
    var isChainReactionFinished = false

    while (!isChainReactionFinished) {
    val (newDisintegratedBricks, newRemainingBricks) = remainingBricks
    .partition { (_, supportingBricks) =>
    supportingBricks.nonEmpty && supportingBricks.subsetOf(
    disintegratedBricks
    )
    }
    if (newDisintegratedBricks.isEmpty)
    isChainReactionFinished = true
    else
    disintegratedBricks.addAll(newDisintegratedBricks.keySet)
    remainingBricks = newRemainingBricks
    }

    disintegratedBricks.size - 1 // don't include the initial brick
    }

    def part2(input: String): Int = {
    val bricks = parse(input)
    val brickToSupportingBricks = dropBricks(bricks)
    val fallingChainCounts = brickToSupportingBricks.keys.toList.map(
    countFallingChain(brickToSupportingBricks)
    )
    fallingChainCounts.sum
    }

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day23/index.html b/2023/puzzles/day23/index.html index 6208a76f2..373a093a9 100644 --- a/2023/puzzles/day23/index.html +++ b/2023/puzzles/day23/index.html @@ -5,15 +5,15 @@ Day 23: A Long Walk | Scala Center Advent of Code - - + +
    -

    Day 23: A Long Walk

    by @stewSquared

    Puzzle description

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

    Solution

    Overview and Observations

    The general problem of finding the longest path through a grid or a graph is NP-hard, so we won't be using any fancy algorithms or heuristics today; we'll use a depth-first backtracking search. For part 2, we'll need some optimizations (graph compression and bitmasking) that reduce the size of the search space and visited set so the algorithm can run in around 2 seconds.

    The general approach to finding the longest path via DFS is to maintain a set of visited positions alongside the current position. The next position can be any adjacent position that isn't in the visited set. We then recursively search from one of those adjacent positions until we find the end, note the path length, then try the other adjacent positions, keeping the longest path length we find.

    For both problems, it is worth noticing that the vast majority of path positions in the maze are only connected to two other paths, so when entering from one path, there is only one path from which we can exit. Some paths are connected to three or four other paths. These paths, we'll call junctions.

    Each junction might have two or three adjacent paths we can enter. When we exit a junction, we will inevitably reach another junction (or the end of the maze). Because of this, every path through the maze is fully determined by the sequence of junctions it enters. This allows us two optimizations:

    • We can compress the grid into an adjacency graph of vertices (the junctions) with weighted edges (the distance of the path between junctions) to other vertices. This allows us to have a distinctly smaller visited set, as there are only ~35 junctions in the puzzle input. This also avoids re-computing the distance between two junctions as we might in a cell-by-cell search of the grid. On my machine, this drops the run time for part 2 from 1 minute to ~10 seconds (90%).

    • For each iteration of the search through this graph, we check all adjacent junctions against the visited set. When using a hash set, this will result in computing hashes of position coordinates tens of millions of times. We can avoid this by giving each junction an index and using a BitSet of these indices as our visited set. Checking for membership in a BitSet only requires a bitshift and a bitwise AND mask. On my machine, this drops the run time from ~7 seconds to ~2 seconds (70%).

    For part 1, neither of these optimizations are necessary. To understand why, notice that every junction is surrounded by slopes. When a junction is surrounded by four slopes, as most of them are, two are incoming and two are outgoing. For part 1, these are arranged in such a way that the adjacency graph becomes a directed acyclic graph, with a greatly reduced search space. One way to notice this early on is to generate a visualization via GraphViz, such as the following:

    G00110->1159331->3138221->280443->472663->63342->4170552->531011114->11232994->91165->11184775->71126->938886->844131311->1374141411->147210109->10209->1418812127->122227->13668->103815158->15440181812->18144202012->2052013->18140171713->17202161610->16120191910->1922215->19184222215->2247214->1619614->17152232316->2394212116->2119819->226019->234018->2060242418->24146282820->2828017->2110217->2456252522->25124262623->2619023->25112272721->2716221->268224->2825024->27104292928->2958313127->3113227->29156303026->309826->3112825->30336333330->33188323231->328431->336229->32392343432->349233->34158353534->3549

    Framework

    First we define a Point case class for representing coordinates, and a Dir enum for representing direction. Direction will be used when "walking" on a path through the maze to calculate whether a slope blocks us and to prevent us from searching adjacent points that are backwards. Similar definitions show up in solutions to other Advent of Code problems:

    case class Point(x: Int, y: Int):
    def dist(p: Point) = math.abs(x - p.x) + math.abs(y - p.y)
    def adjacent = List(copy(x = x + 1), copy(x = x - 1), copy(y = y + 1), copy(y = y - 1))

    def move(dir: Dir) = dir match
    case Dir.N => copy(y = y - 1)
    case Dir.S => copy(y = y + 1)
    case Dir.E => copy(x = x + 1)
    case Dir.W => copy(x = x - 1)
    enum Dir:
    case N, S, E, W

    def turnRight = this match
    case Dir.N => E
    case Dir.E => S
    case Dir.S => W
    case Dir.W => N

    def turnLeft = this match
    case Dir.N => W
    case Dir.W => S
    case Dir.S => E
    case Dir.E => N

    Next we create a Maze class that will give us basic information about the maze from the raw data:

    case class Maze(grid: Vector[Vector[Char]]):

    def apply(p: Point): Char = grid(p.y)(p.x)

    val xRange: Range = grid.head.indices
    val yRange: Range = grid.indices

    def points: Iterator[Point] = for
    y <- yRange.iterator
    x <- xRange.iterator
    yield Point(x, y)

    So far we just have helper methods. The next few definitions are the things we'll really want to know about the maze in order to construct our solutions:

      val walkable: Set[Point] = points.filter(p => grid(p.y)(p.x) != '#').toSet
    val start: Point = walkable.minBy(_.y)
    val end: Point = walkable.maxBy(_.y)

    val junctions: Set[Point] = walkable.filter: p =>
    Dir.values.map(p.move).count(walkable) > 2
    .toSet + start + end

    Here we can populate which points are slopes by looking up a point with this.apply(p), shortened to this(p).

      val slopes: Map[Point, Dir] = Map.from:
    points.collect:
    case p if this(p) == '^' => p -> Dir.N
    case p if this(p) == 'v' => p -> Dir.S
    case p if this(p) == '>' => p -> Dir.E
    case p if this(p) == '<' => p -> Dir.W
    end Maze

    walkable gives us the set of points that are not walls, ie., they are paths or slopes.

    junctions gives us the junction points mentioned in the overview, ie., paths that have multiple entrances or exits. Notice that start and end are part of the set here. While these are not true junctions, we do want them to appear in our adjacency graph.

    slopes gives us the direction of each slope. For Part 1, these are the directions one must be travelling in order to progress past a slope position.

    Finding Connected Junctions

    Next, we need an algorithm for finding junctions that are connected to a given junction, while tracking the distance travelled to reach that junction. This is the heart of our solution, and is necessary for both parts 1 and 2:

    def connectedJunctions(pos: Point)(using maze: Maze): List[(Point, Int)] = List.from:
    assert(maze.junctions.contains(pos))

    This walk helper method attempts to move in a given direction from a given position, accounting for walls and slopes in the maze. This alternatively could have been defined as a method on Point itself.

      def walk(pos: Point, dir: Dir): Option[Point] =
    val p = pos.move(dir)
    Option.when(maze.walkable(p) && maze.slopes.get(p).forall(_ == dir))(p)

    This search helper method walks down a path from a junction while tracking the current direction and distance. adjacentSearch attempts to walk recursively in directions that don't go backwards. A LazyList is used here to prevent stack overflows. If there is only one adjacent path to walk too, we continue searching that path recursively until we reach a junction, otherwise, we have reached a dead end; None represents the fact that no new junctions are reachable down this path.

      def search(pos: Point, facing: Dir, dist: Int): Option[(Point, Int)] =
    if maze.junctions.contains(pos) then
    Some(pos, dist)
    else
    val adjacentSearch = for
    nextFacing <- LazyList(facing, facing.turnRight, facing.turnLeft)
    nextPos <- walk(pos, nextFacing)
    yield search(nextPos, nextFacing, dist + 1)

    if adjacentSearch.size == 1 then adjacentSearch.head else None

    Finally, we begin the search in each direction from our current junction, returning all the connected junctions found.

      for
    d <- Dir.values
    p <- walk(pos, d)
    junction <- search(p, d, 1)
    yield junction
    end connectedJunctions

    Part 1

    connectedJunctions is sufficient to solve Part 1 quickly:

    def part1(input: String): Int =
    given Maze = Maze(parseInput(input))
    longestDownhillHike

    def parseInput(fileContents: String): Vector[Vector[Char]] =
    Vector.from:
    fileContents.split("\n").map(_.toVector)

    def longestDownhillHike(using maze: Maze): Int =
    def search(pos: Point, dist: Int): Int =
    if pos == maze.end then
    dist
    else
    connectedJunctions(pos).foldLeft(0):
    case (max, (n, d)) => max.max(search(n, dist + d))

    search(maze.start, 0)
    end longestDownhillHike

    This uses a recursive helper method named search. Beginning with start, we recursively search for the longest path starting at each of the connected junctions.

    Part 2

    For part 2, we'll implement the optimization mentioned in the overview, namely, bitmasking and graph compression. Graph compression is partially implemented in connectedJunctions, but we'll want to avoid recomputation by storing the full graph as a map from a junction, to a list of connected junctions and the distances to each of those junctions.

    def part2(input: String): Int =
    given Maze = Maze(parseInput(input))
    longestHike

    def longestHike(using maze: Maze): Int =
    type Index = Int

    We begin by assigning indices to each of the junctions, by sorting them (in any way, as long as the ordering is well-defined) and zipping with an index:

      val indexOf: Map[Point, Index] =
    maze.junctions.toList.sortBy(_.dist(maze.start)).zipWithIndex.toMap

    Next, we define an adjacency graph. Since connectedJunctions takes slopes into account, and we no longer care about slopes for part 2, we add both the forward and reverse directions into our Map. Note how we translate the Point locations used by connectedJunctions into indices using indexOf, defined above:

      val adjacent: Map[Index, List[(Index, Int)]] =
    maze.junctions.toList
    .flatMap: p1 =>
    connectedJunctions(p1).flatMap: (p2, d) =>
    val forward = indexOf(p1) -> (indexOf(p2), d)
    val reverse = indexOf(p2) -> (indexOf(p1), d)
    List(forward, reverse)
    .groupMap(_(0))(_(1))

    Finally, we perform a depth-first search that is very similar to what we used in Part 1. +

    Day 23: A Long Walk

    by @stewSquared

    Puzzle description

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

    Solution

    Overview and Observations

    The general problem of finding the longest path through a grid or a graph is NP-hard, so we won't be using any fancy algorithms or heuristics today; we'll use a depth-first backtracking search. For part 2, we'll need some optimizations (graph compression and bitmasking) that reduce the size of the search space and visited set so the algorithm can run in around 2 seconds.

    The general approach to finding the longest path via DFS is to maintain a set of visited positions alongside the current position. The next position can be any adjacent position that isn't in the visited set. We then recursively search from one of those adjacent positions until we find the end, note the path length, then try the other adjacent positions, keeping the longest path length we find.

    For both problems, it is worth noticing that the vast majority of path positions in the maze are only connected to two other paths, so when entering from one path, there is only one path from which we can exit. Some paths are connected to three or four other paths. These paths, we'll call junctions.

    Each junction might have two or three adjacent paths we can enter. When we exit a junction, we will inevitably reach another junction (or the end of the maze). Because of this, every path through the maze is fully determined by the sequence of junctions it enters. This allows us two optimizations:

    • We can compress the grid into an adjacency graph of vertices (the junctions) with weighted edges (the distance of the path between junctions) to other vertices. This allows us to have a distinctly smaller visited set, as there are only ~35 junctions in the puzzle input. This also avoids re-computing the distance between two junctions as we might in a cell-by-cell search of the grid. On my machine, this drops the run time for part 2 from 1 minute to ~10 seconds (90%).

    • For each iteration of the search through this graph, we check all adjacent junctions against the visited set. When using a hash set, this will result in computing hashes of position coordinates tens of millions of times. We can avoid this by giving each junction an index and using a BitSet of these indices as our visited set. Checking for membership in a BitSet only requires a bitshift and a bitwise AND mask. On my machine, this drops the run time from ~7 seconds to ~2 seconds (70%).

    For part 1, neither of these optimizations are necessary. To understand why, notice that every junction is surrounded by slopes. When a junction is surrounded by four slopes, as most of them are, two are incoming and two are outgoing. For part 1, these are arranged in such a way that the adjacency graph becomes a directed acyclic graph, with a greatly reduced search space. One way to notice this early on is to generate a visualization via GraphViz, such as the following:

    G00110->1159331->3138221->280443->472663->63342->4170552->531011114->11232994->91165->11184775->71126->938886->844131311->1374141411->147210109->10209->1418812127->122227->13668->103815158->15440181812->18144202012->2052013->18140171713->17202161610->16120191910->1922215->19184222215->2247214->1619614->17152232316->2394212116->2119819->226019->234018->2060242418->24146282820->2828017->2110217->2456252522->25124262623->2619023->25112272721->2716221->268224->2825024->27104292928->2958313127->3113227->29156303026->309826->3112825->30336333330->33188323231->328431->336229->32392343432->349233->34158353534->3549

    Framework

    First we define a Point case class for representing coordinates, and a Dir enum for representing direction. Direction will be used when "walking" on a path through the maze to calculate whether a slope blocks us and to prevent us from searching adjacent points that are backwards. Similar definitions show up in solutions to other Advent of Code problems:

    case class Point(x: Int, y: Int):
    def dist(p: Point) = math.abs(x - p.x) + math.abs(y - p.y)
    def adjacent = List(copy(x = x + 1), copy(x = x - 1), copy(y = y + 1), copy(y = y - 1))

    def move(dir: Dir) = dir match
    case Dir.N => copy(y = y - 1)
    case Dir.S => copy(y = y + 1)
    case Dir.E => copy(x = x + 1)
    case Dir.W => copy(x = x - 1)
    enum Dir:
    case N, S, E, W

    def turnRight = this match
    case Dir.N => E
    case Dir.E => S
    case Dir.S => W
    case Dir.W => N

    def turnLeft = this match
    case Dir.N => W
    case Dir.W => S
    case Dir.S => E
    case Dir.E => N

    Next we create a Maze class that will give us basic information about the maze from the raw data:

    case class Maze(grid: Vector[Vector[Char]]):

    def apply(p: Point): Char = grid(p.y)(p.x)

    val xRange: Range = grid.head.indices
    val yRange: Range = grid.indices

    def points: Iterator[Point] = for
    y <- yRange.iterator
    x <- xRange.iterator
    yield Point(x, y)

    So far we just have helper methods. The next few definitions are the things we'll really want to know about the maze in order to construct our solutions:

      val walkable: Set[Point] = points.filter(p => grid(p.y)(p.x) != '#').toSet
    val start: Point = walkable.minBy(_.y)
    val end: Point = walkable.maxBy(_.y)

    val junctions: Set[Point] = walkable.filter: p =>
    Dir.values.map(p.move).count(walkable) > 2
    .toSet + start + end

    Here we can populate which points are slopes by looking up a point with this.apply(p), shortened to this(p).

      val slopes: Map[Point, Dir] = Map.from:
    points.collect:
    case p if this(p) == '^' => p -> Dir.N
    case p if this(p) == 'v' => p -> Dir.S
    case p if this(p) == '>' => p -> Dir.E
    case p if this(p) == '<' => p -> Dir.W
    end Maze

    walkable gives us the set of points that are not walls, ie., they are paths or slopes.

    junctions gives us the junction points mentioned in the overview, ie., paths that have multiple entrances or exits. Notice that start and end are part of the set here. While these are not true junctions, we do want them to appear in our adjacency graph.

    slopes gives us the direction of each slope. For Part 1, these are the directions one must be travelling in order to progress past a slope position.

    Finding Connected Junctions

    Next, we need an algorithm for finding junctions that are connected to a given junction, while tracking the distance travelled to reach that junction. This is the heart of our solution, and is necessary for both parts 1 and 2:

    def connectedJunctions(pos: Point)(using maze: Maze): List[(Point, Int)] = List.from:
    assert(maze.junctions.contains(pos))

    This walk helper method attempts to move in a given direction from a given position, accounting for walls and slopes in the maze. This alternatively could have been defined as a method on Point itself.

      def walk(pos: Point, dir: Dir): Option[Point] =
    val p = pos.move(dir)
    Option.when(maze.walkable(p) && maze.slopes.get(p).forall(_ == dir))(p)

    This search helper method walks down a path from a junction while tracking the current direction and distance. adjacentSearch attempts to walk recursively in directions that don't go backwards. A LazyList is used here to prevent stack overflows. If there is only one adjacent path to walk too, we continue searching that path recursively until we reach a junction, otherwise, we have reached a dead end; None represents the fact that no new junctions are reachable down this path.

      def search(pos: Point, facing: Dir, dist: Int): Option[(Point, Int)] =
    if maze.junctions.contains(pos) then
    Some(pos, dist)
    else
    val adjacentSearch = for
    nextFacing <- LazyList(facing, facing.turnRight, facing.turnLeft)
    nextPos <- walk(pos, nextFacing)
    yield search(nextPos, nextFacing, dist + 1)

    if adjacentSearch.size == 1 then adjacentSearch.head else None

    Finally, we begin the search in each direction from our current junction, returning all the connected junctions found.

      for
    d <- Dir.values
    p <- walk(pos, d)
    junction <- search(p, d, 1)
    yield junction
    end connectedJunctions

    Part 1

    connectedJunctions is sufficient to solve Part 1 quickly:

    def part1(input: String): Int =
    given Maze = Maze(parseInput(input))
    longestDownhillHike

    def parseInput(fileContents: String): Vector[Vector[Char]] =
    Vector.from:
    fileContents.split("\n").map(_.toVector)

    def longestDownhillHike(using maze: Maze): Int =
    def search(pos: Point, dist: Int): Int =
    if pos == maze.end then
    dist
    else
    connectedJunctions(pos).foldLeft(0):
    case (max, (n, d)) => max.max(search(n, dist + d))

    search(maze.start, 0)
    end longestDownhillHike

    This uses a recursive helper method named search. Beginning with start, we recursively search for the longest path starting at each of the connected junctions.

    Part 2

    For part 2, we'll implement the optimization mentioned in the overview, namely, bitmasking and graph compression. Graph compression is partially implemented in connectedJunctions, but we'll want to avoid recomputation by storing the full graph as a map from a junction, to a list of connected junctions and the distances to each of those junctions.

    def part2(input: String): Int =
    given Maze = Maze(parseInput(input))
    longestHike

    def longestHike(using maze: Maze): Int =
    type Index = Int

    We begin by assigning indices to each of the junctions, by sorting them (in any way, as long as the ordering is well-defined) and zipping with an index:

      val indexOf: Map[Point, Index] =
    maze.junctions.toList.sortBy(_.dist(maze.start)).zipWithIndex.toMap

    Next, we define an adjacency graph. Since connectedJunctions takes slopes into account, and we no longer care about slopes for part 2, we add both the forward and reverse directions into our Map. Note how we translate the Point locations used by connectedJunctions into indices using indexOf, defined above:

      val adjacent: Map[Index, List[(Index, Int)]] =
    maze.junctions.toList
    .flatMap: p1 =>
    connectedJunctions(p1).flatMap: (p2, d) =>
    val forward = indexOf(p1) -> (indexOf(p2), d)
    val reverse = indexOf(p2) -> (indexOf(p1), d)
    List(forward, reverse)
    .groupMap(_(0))(_(1))

    Finally, we perform a depth-first search that is very similar to what we used in Part 1. The main differences are that we now use indices of junctions rather than Points representing current position, and we now check adjacent junctions against a BitSet of visited points, which we now track as we search recursively.

      def search(junction: Index, visited: BitSet, totalDist: Int): Int =
    if junction == indexOf(maze.end) then
    totalDist
    else
    adjacent(junction).foldLeft(0):
    case (longest, (nextJunct, dist)) =>
    if visited(nextJunct) then longest else
    longest.max(search(nextJunct, visited + nextJunct, totalDist + dist))

    search(indexOf(maze.start), BitSet.empty, 0)
    end longestHike

    Final Code

    import collection.immutable.BitSet

    def part1(input: String): Int =
    given Maze = Maze(parseInput(input))
    longestDownhillHike

    def part2(input: String): Int =
    given Maze = Maze(parseInput(input))
    longestHike

    def parseInput(fileContents: String): Vector[Vector[Char]] =
    Vector.from:
    fileContents.split("\n").map(_.toVector)

    enum Dir:
    case N, S, E, W

    def turnRight = this match
    case Dir.N => E
    case Dir.E => S
    case Dir.S => W
    case Dir.W => N

    def turnLeft = this match
    case Dir.N => W
    case Dir.W => S
    case Dir.S => E
    case Dir.E => N

    case class Point(x: Int, y: Int):
    def dist(p: Point) = math.abs(x - p.x) + math.abs(y - p.y)
    def adjacent = List(copy(x = x + 1), copy(x = x - 1), copy(y = y + 1), copy(y = y - 1))

    def move(dir: Dir) = dir match
    case Dir.N => copy(y = y - 1)
    case Dir.S => copy(y = y + 1)
    case Dir.E => copy(x = x + 1)
    case Dir.W => copy(x = x - 1)

    case class Maze(grid: Vector[Vector[Char]]):

    def apply(p: Point): Char = grid(p.y)(p.x)

    val xRange: Range = grid.head.indices
    val yRange: Range = grid.indices

    def points: Iterator[Point] = for
    y <- yRange.iterator
    x <- xRange.iterator
    yield Point(x, y)

    val walkable: Set[Point] = points.filter(p => grid(p.y)(p.x) != '#').toSet
    val start: Point = walkable.minBy(_.y)
    val end: Point = walkable.maxBy(_.y)

    val junctions: Set[Point] = walkable.filter: p =>
    Dir.values.map(p.move).count(walkable) > 2
    .toSet + start + end

    val slopes: Map[Point, Dir] = Map.from:
    points.collect:
    case p if this(p) == '^' => p -> Dir.N
    case p if this(p) == 'v' => p -> Dir.S
    case p if this(p) == '>' => p -> Dir.E
    case p if this(p) == '<' => p -> Dir.W
    end Maze

    def connectedJunctions(pos: Point)(using maze: Maze): List[(Point, Int)] = List.from:
    def walk(pos: Point, dir: Dir): Option[Point] =
    val p = pos.move(dir)
    Option.when(maze.walkable(p) && maze.slopes.get(p).forall(_ == dir))(p)

    def search(pos: Point, facing: Dir, dist: Int): Option[(Point, Int)] =
    if maze.junctions.contains(pos) then
    Some(pos, dist)
    else
    val adjacentSearch = for
    nextFacing <- LazyList(facing, facing.turnRight, facing.turnLeft)
    nextPos <- walk(pos, nextFacing)
    yield search(nextPos, nextFacing, dist + 1)

    if adjacentSearch.size == 1 then adjacentSearch.head else None

    for
    d <- Dir.values
    p <- walk(pos, d)
    junction <- search(p, d, 1)
    yield junction
    end connectedJunctions

    def longestDownhillHike(using maze: Maze): Int =
    def search(pos: Point, dist: Int): Int =
    if pos == maze.end then
    dist
    else
    connectedJunctions(pos).foldLeft(0):
    case (max, (n, d)) => max.max(search(n, dist + d))

    search(maze.start, 0)
    end longestDownhillHike

    def longestHike(using maze: Maze): Int =
    type Index = Int

    val indexOf: Map[Point, Index] =
    maze.junctions.toList.sortBy(_.dist(maze.start)).zipWithIndex.toMap

    val adjacent: Map[Index, List[(Index, Int)]] =
    maze.junctions.toList
    .flatMap: p1 =>
    connectedJunctions(p1).flatMap: (p2, d) =>
    val forward = indexOf(p1) -> (indexOf(p2), d)
    val reverse = indexOf(p2) -> (indexOf(p1), d)
    List(forward, reverse)
    .groupMap(_(0))(_(1))

    def search(junction: Index, visited: BitSet, totalDist: Int): Int =
    if junction == indexOf(maze.end) then
    totalDist
    else
    adjacent(junction).foldLeft(0):
    case (longest, (nextJunct, dist)) =>
    if visited(nextJunct) then longest else
    longest.max(search(nextJunct, visited + nextJunct, totalDist + dist))

    search(indexOf(maze.start), BitSet.empty, 0)
    end longestHike

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day24/index.html b/2023/puzzles/day24/index.html index 09fceb2cb..27c2ed7c5 100644 --- a/2023/puzzles/day24/index.html +++ b/2023/puzzles/day24/index.html @@ -5,12 +5,12 @@ Day 24: Never Tell Me The Odds | Scala Center Advent of Code - - + +
    -

    Day 24: Never Tell Me The Odds

    by @merlinorg

    Puzzle description

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

    Summary

    Day 24 involves calculating the intersections of the lines that some hailstones +

    Day 24: Never Tell Me The Odds

    by @merlinorg

    Puzzle description

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

    Summary

    Day 24 involves calculating the intersections of the lines that some hailstones trace in 2D, and then determining the linear trajectory for a rock in 3D that will intersect the trajectories of all the hailstones in the model.

    Model

    Hail

    A hailstone has an initial location and velocity in 3D space.

    final case class Hail(x: Long, y: Long, z: Long, vx: Long, vy: Long, vz: Long)

    2D Hail

    The first part of this problem asks us to consider just the two-dimensional XY trajectory of the hailstone, so we add a model for this:

    final case class Hail2D(x: Long, y: Long, vx: Long, vy: Long)

    and then add a method xyProjection method to Hail to get a 2D projection:

    // Inside class Hail:
    def xyProjection: Hail2D = Hail2D(x, y, vx, vy)

    Parsing

    To parse the input we just pattern match each line. The sample input @@ -81,7 +81,7 @@ since the x velocity is already known, so we only need to test z candidates, but this is less effort.

    First add a method xzProjection method to Hail to get a 2D projection:

    // Inside class Hail:
    def xzProjection: Hail2D = Hail2D(x, z, vx, vz)

    Next, add a helper method to find a value in an iterator:

    // An unruly and lawless find-map-get
    extension [A](self: Iterator[A])
    def findMap[B](f: A => Option[B]): B = self.flatMap(f).next()

    Finally solve part 2

    def part2(input: String): Long =
    val hails = parseAll(input)

    val hailsXY = hails.map(_.xyProjection)
    val (x, y) = Iterator
    .iterate(Spiral.Start)(_.next)
    .findMap: spiral =>
    findRockOrigin(hailsXY, spiral.x, spiral.y)

    val hailsXZ = hails.map(_.xzProjection)
    val (_, z) = Iterator
    .iterate(Spiral.Start)(_.next)
    .findMap: spiral =>
    findRockOrigin(hailsXZ, spiral.x, spiral.y)

    x + y + z
    end part2

    Final Code

    The complete solution follows:

    final case class Hail(x: Long, y: Long, z: Long, vx: Long, vy: Long, vz: Long):
    def xyProjection: Hail2D = Hail2D(x, y, vx, vy)
    def xzProjection: Hail2D = Hail2D(x, z, vx, vz)

    def parseAll(input: String): Vector[Hail] =
    input.linesIterator.toVector.map:
    case s"$x, $y, $z @ $dx, $dy, $dz" =>
    Hail(x.trim.toLong, y.trim.toLong, z.trim.toLong,
    dx.trim.toLong, dy.trim.toLong, dz.trim.toLong)

    final case class Hail2D(x: Long, y: Long, vx: Long, vy: Long):
    private val a: BigDecimal = BigDecimal(vy)
    private val b: BigDecimal = BigDecimal(-vx)
    private val c: BigDecimal = BigDecimal(vx * y - vy * x)

    def deltaV(dvx: Long, dvy: Long): Hail2D = copy(vx = vx - dvx, vy = vy - dvy)

    // If the paths of these hailstones intersect, return the intersection
    def intersect(hail: Hail2D): Option[(BigDecimal, BigDecimal)] =
    val denominator = a * hail.b - hail.a * b
    Option.when(denominator != 0):
    ((b * hail.c - hail.b * c) / denominator,
    (c * hail.a - hail.c * a) / denominator)

    // Return the time at which this hail will intersect the given point
    def timeTo(posX: BigDecimal, posY: BigDecimal): BigDecimal =
    if vx == 0 then (posY - y) / vy else (posX - x) / vx
    end Hail2D

    extension [A](self: Vector[A])
    // all non-self element pairs
    def allPairs: Vector[(A, A)] = self.tails.toVector.tail.flatMap(self.zip)

    extension [A](self: Iterator[A])
    // An unruly and lawless find-map-get
    def findMap[B](f: A => Option[B]): B = self.flatMap(f).next()

    def intersections(
    hails: Vector[Hail2D],
    min: Long,
    max: Long
    ): Vector[(Hail2D, Hail2D)] =
    for
    (hail0, hail1) <- hails.allPairs
    (x, y) <- hail0.intersect(hail1)
    if x >= min && x <= max && y >= min && y <= max &&
    hail0.timeTo(x, y) >= 0 && hail1.timeTo(x, y) >= 0
    yield (hail0, hail1)
    end intersections

    def part1(input: String): Long =
    val hails = Hail.parseAll(input)
    val hailsXY = hails.map(_.xyProjection)
    intersections(hailsXY, 200000000000000L, 400000000000000L).size
    end part1

    def findRockOrigin(
    hails: Vector[Hail2D],
    vx: Long,
    vy: Long
    ): Option[(Long, Long)] =
    val hail0 +: hail1 +: hail2 +: _ = hails.map(_.deltaV(vx, vy)): @unchecked
    for
    (x0, y0) <- hail0.intersect(hail1)
    (x1, y1) <- hail0.intersect(hail2)
    if x0 == x1 && y0 == y1
    time = hail0.timeTo(x0, y0)
    yield (hail0.x + hail0.vx * time.longValue,
    hail0.y + hail0.vy * time.longValue)
    end findRockOrigin

    final case class Spiral(
    x: Long, y: Long,
    dx: Long, dy: Long,
    count: Long, limit: Long,
    ):
    def next: Spiral =
    if count > 0 then
    copy(x = x + dx, y = y + dy, count = count - 1)
    else if dy == 0 then
    copy(x = x + dx, y = y + dy, dy = dx, dx = -dy, count = limit)
    else
    copy(x = x + dx, y = y + dy, dy = dx, dx = -dy,
    count = limit + 1, limit = limit + 1)
    end next
    end Spiral

    object Spiral:
    final val Start = Spiral(0, 0, 1, 0, 0, 0)

    def part2(input: String): Long =
    val hails = Hail.parseAll(input)

    val hailsXY = hails.map(_.xyProjection)
    val (x, y) = Iterator
    .iterate(Spiral.Start)(_.next)
    .findMap: spiral =>
    findRockOrigin(hailsXY, spiral.x, spiral.y)

    val hailsXZ = hails.map(_.xzProjection)
    val (_, z) = Iterator
    .iterate(Spiral.Start)(_.next)
    .findMap: spiral =>
    findRockOrigin(hailsXZ, spiral.x, spiral.y)

    x + y + z
    end part2

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2023/puzzles/day25/index.html b/2023/puzzles/day25/index.html index d65b2d61a..da5456b3f 100644 --- a/2023/puzzles/day25/index.html +++ b/2023/puzzles/day25/index.html @@ -5,12 +5,12 @@ Day 25: Snowverload | Scala Center Advent of Code - - + +
    -

    Day 25: Snowverload

    by @bishabosha

    Puzzle description

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

    Solution Summary

    We are told that there are 3 connections that when removed will partition the components into two groups. +

    Day 25: Snowverload

    by @bishabosha

    Puzzle description

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

    Solution Summary

    We are told that there are 3 connections that when removed will partition the components into two groups. We then have to multiply the sizes of the two partitions. This is equivalent to finding a minimum cut in an undirected, unweighted graph, which can be solved with the Stoer-Wagner minimum cut algorithm.

    Naive Way

    You may be tempted to brute force the solution by testing all combinations of three edges to remove, and checking if the result makes two partitions. This works in reasonable time with the sample input. The real input is much larger however, and with about 3400\sim 3400 connections, which means there are @@ -34,7 +34,7 @@ We will have to do the following processing steps to build a suitable graph representation:

    • identify all the vertices, and generate a unique integer ID for each one,
    • generate an undirected adjacency matrix of weights. We must duplicate each edge from the original input to make an efficient lookup table. We will initialise each weight to 1 (remember that even though each edge is equal initially, when edges are merged, their weights must be combined).

    Here is the code:

    def readGraph(alist: Map[String, Set[String]]): Graph =
    val all = alist.flatMap((k, vs) => vs + k).toSet

    val (_, lookup) =
    // perfect hashing
    val initial = (0, Map.empty[String, Id])
    all.foldLeft(initial): (acc, s) =>
    val (id, seen) = acc
    (id + 1, seen + (s -> id))

    def asEdges(k: String, v: String) =
    val t = (lookup(k), lookup(v))
    t :: t.swap :: Nil

    val v = lookup.values.to(BitSet)
    val nodes = v.unsorted.map(id => id -> BitSet(id)).toMap
    val edges =
    for
    (k, vs) <- alist.toSet
    v <- vs
    e <- asEdges(k, v) // (k -> v) + (v -> k)
    yield
    e

    val w = edges
    .groupBy((v, _) => v)
    .view
    .mapValues: m =>
    m
    .groupBy((_, v) => v)
    .view
    .mapValues(_ => 1)
    .toMap
    .toMap
    Graph(v, nodes, w)

    The Solution

    Putting everything together, we can now solve the problem!

    def part1(input: String): Int =
    val alist = parse(input) // 1.
    val g = readGraph(alist) // 2.
    val (graph, cut) = minimumCut(g) // 3.
    val (out, in) = graph.partition(cut) // 4.
    in.size * out.size // 5.
    1. Parse the input into an adjacency list (note. the edges are directed)
    2. Convert the adjacency list to the Graph structure.
    3. Call the minimumCut function on the graph, storing the minimum cut, and the state of the graph when the cut was made.
    4. use the cut on the graph to get the partition of vertices.
    5. multiply the sizes of the partitions to get the final answer.

    Final Code

    import scala.collection.immutable.BitSet
    import scala.collection.immutable.TreeSet

    def part1(input: String): Int =
    val alist = parse(input)
    val g = readGraph(alist)
    val (graph, cut) = minimumCut(g)
    val (out, in) = graph.partition(cut)
    in.size * out.size

    type Id = Int
    type Vertices = BitSet
    type Weight = Map[Id, Map[Id, Int]]

    def parse(input: String): Map[String, Set[String]] =
    input
    .linesIterator
    .map:
    case s"$key: $values" => key -> values.split(" ").toSet
    .toMap

    def readGraph(alist: Map[String, Set[String]]): Graph =
    val all = alist.flatMap((k, vs) => vs + k).toSet

    val (_, lookup) =
    // perfect hashing
    val initial = (0, Map.empty[String, Id])
    all.foldLeft(initial): (acc, s) =>
    val (id, seen) = acc
    (id + 1, seen + (s -> id))

    def asEdges(k: String, v: String) =
    val t = (lookup(k), lookup(v))
    t :: t.swap :: Nil

    val v = lookup.values.to(BitSet)
    val nodes = v.unsorted.map(id => id -> BitSet(id)).toMap
    val edges =
    for
    (k, vs) <- alist.toSet
    v <- vs
    e <- asEdges(k, v)
    yield
    e

    val w = edges
    .groupBy((v, _) => v)
    .view
    .mapValues: m =>
    m
    .groupBy((_, v) => v)
    .view
    .mapValues(_ => 1)
    .toMap
    .toMap
    Graph(v, nodes, w)

    class MostConnected(
    totalWeights: Map[Id, Int],
    queue: TreeSet[MostConnected.Entry]
    ):

    def pop =
    val id = queue.head.id
    id -> MostConnected(totalWeights - id, queue.tail)

    def expand(z: Id, explore: Vertices, w: Weight) =
    val connectedEdges =
    w(z).view.filterKeys(explore)
    var totalWeights0 = totalWeights
    var queue0 = queue
    for (id, w) <- connectedEdges do
    val w1 = totalWeights0.getOrElse(id, 0) + w
    totalWeights0 += id -> w1
    queue0 += MostConnected.Entry(id, w1)
    MostConnected(totalWeights0, queue0)
    end expand

    end MostConnected

    object MostConnected:
    def empty = MostConnected(Map.empty, TreeSet.empty)
    given Ordering[Entry] = (e1, e2) =>
    val first = e2.weight.compareTo(e1.weight)
    if first == 0 then e2.id.compareTo(e1.id) else first
    class Entry(val id: Id, val weight: Int):
    override def hashCode: Int = id
    override def equals(that: Any): Boolean = that match
    case that: Entry => id == that.id
    case _ => false

    case class Graph(v: Vertices, nodes: Map[Id, Vertices], w: Weight):
    def cutOfThePhase(t: Id) = Graph.Cut(t = t, edges = w(t))

    def partition(cut: Graph.Cut): (Vertices, Vertices) =
    (nodes(cut.t), (v - cut.t).flatMap(nodes))

    def shrink(s: Id, t: Id): Graph =
    def fetch(x: Id) =
    w(x).view.filterKeys(y => y != s && y != t)

    val prunedW = (w - t).view.mapValues(_ - t).toMap

    val fromS = fetch(s).toMap
    val fromT = fetch(t).map: (y, w0) =>
    y -> (fromS.getOrElse(y, 0) + w0)
    val mergedWeights = fromS ++ fromT

    val reverseMerged = mergedWeights.view.map: (y, w0) =>
    y -> (prunedW(y) + (s -> w0))

    val v1 = v - t // 5.
    val w1 = prunedW + (s -> mergedWeights) ++ reverseMerged
    val nodes1 = nodes - t + (s -> (nodes(s) ++ nodes(t)))
    Graph(v1, nodes1, w1)
    end shrink

    object Graph:
    def emptyCut = Cut(t = -1, edges = Map.empty)

    case class Cut(t: Id, edges: Map[Id, Int]):
    lazy val weight: Int = edges.values.sum

    def minimumCutPhase(g: Graph) =
    val a = g.v.head
    var A = a :: Nil
    var explore = g.v - a
    var mostConnected =
    MostConnected.empty.expand(a, explore, g.w)
    while explore.nonEmpty do
    val (z, rest) = mostConnected.pop
    A ::= z
    explore -= z
    mostConnected = rest.expand(z, explore, g.w)
    val t :: s :: _ = A: @unchecked
    (g.shrink(s, t), g.cutOfThePhase(t))

    /** See Stoer-Wagner min cut algorithm
    * https://dl.acm.org/doi/pdf/10.1145/263867.263872
    */
    def minimumCut(g: Graph) =
    var g0 = g
    var min = (g, Graph.emptyCut)
    while g0.v.size > 1 do
    val (g1, cutOfThePhase) = minimumCutPhase(g0)
    if cutOfThePhase.weight < min(1).weight
    || min(1).weight == 0 // initial case
    then
    min = (g0, cutOfThePhase)
    g0 = g1
    min

    Run it in the browser

    Part 1

    Beware that Safari is not able to run this solution efficiently (Chrome and Firefox are ok)

    There is no part 2 for this day!

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/index.html b/2024/index.html index 09f37c599..da1353f55 100644 --- a/2024/index.html +++ b/2024/index.html @@ -5,13 +5,13 @@ Scala Center Advent of Code | Scala Center Advent of Code - - + + - - +
    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/2024/puzzles/day0/index.html b/2024/puzzles/day0/index.html index 36b0c5365..f202fa07c 100644 --- a/2024/puzzles/day0/index.html +++ b/2024/puzzles/day0/index.html @@ -5,13 +5,13 @@ Redirecting... - - + + - - +
    + + \ No newline at end of file diff --git a/2024/puzzles/day01/index.html b/2024/puzzles/day01/index.html index 2b4909056..e08ad221d 100644 --- a/2024/puzzles/day01/index.html +++ b/2024/puzzles/day01/index.html @@ -5,12 +5,12 @@ Day 1: Historian Hysteria | Scala Center Advent of Code - - + +
    -

    Day 1: Historian Hysteria

    by @spamegg1

    Puzzle description

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

    Solution Summary

    1. Parse the input to split it into two lists (left/right), each sorted in increasing order.
    2. Find the distance scores (for part1) and the similarity scores (for part2).
    3. Sum the scores.

    Parsing

    Our parser iterates over the lines, extracts the pair of numbers from each line, +

    Day 1: Historian Hysteria

    by @spamegg1

    Puzzle description

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

    Solution Summary

    1. Parse the input to split it into two lists (left/right), each sorted in increasing order.
    2. Find the distance scores (for part1) and the similarity scores (for part2).
    3. Sum the scores.

    Parsing

    Our parser iterates over the lines, extracts the pair of numbers from each line, then splits them into two lists (lefts and rights), and separately sorts the lists. Therefore it looks like this:

    def parse(input: String): (Seq[Long], Seq[Long]) =
    // Extract pairs of numbers from each line
    val pairs = input
    .linesIterator
    .map(line => line.split(" ").map(_.toLong))
    .toSeq

    // Group the left and right members from each pair, sort them
    val lefts = pairs.map(_.head).toSeq.sorted
    val rights = pairs.map(_.last).toSeq.sorted
    (lefts, rights)

    Part 1

    Now that the lefts and rights are sorted in increasing order, we can zip them, so that the first smallest on the left is paired with the first smallest on the right, @@ -20,7 +20,7 @@ then multiply that count by the number itself. Finally we sum the similarity scores of all the left numbers:

    def part2(input: String): Long =
    val (lefts, rights) = parse(input)
    lefts
    .map(left => rights.count(_ == left) * left) // similarity scores
    .sum
    end part2

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day02/index.html b/2024/puzzles/day02/index.html index 0b7db0bb4..7eff2a375 100644 --- a/2024/puzzles/day02/index.html +++ b/2024/puzzles/day02/index.html @@ -5,12 +5,12 @@ Day 2: Red-Nosed Reports | Scala Center Advent of Code - - + +
    -

    Day 2: Red-Nosed Reports

    by @spamegg1

    Puzzle description

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

    Solution summary

    • First we parse each line of the input into a Report case class: case class Report(levels: Seq[Long])
    • In each Report, we construct the sequence of consecutive pairs.
    • For part 1, we check if the pairs are all increasing or all decreasing, and if the difference is within the given limits (such a report is "safe").
    • For part 2, we construct new Reports obtained by dropping one entry in the original report, and check if there exists a safe Report among these.
    • In both parts, we simply count the number of Reports that are considered safe.

    Parsing

    Each line of input is a string of numbers separated by a single space. +

    Day 2: Red-Nosed Reports

    by @spamegg1

    Puzzle description

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

    Solution summary

    • First we parse each line of the input into a Report case class: case class Report(levels: Seq[Long])
    • In each Report, we construct the sequence of consecutive pairs.
    • For part 1, we check if the pairs are all increasing or all decreasing, and if the difference is within the given limits (such a report is "safe").
    • For part 2, we construct new Reports obtained by dropping one entry in the original report, and check if there exists a safe Report among these.
    • In both parts, we simply count the number of Reports that are considered safe.

    Parsing

    Each line of input is a string of numbers separated by a single space. Therefore parsing looks like this:

    case class Report(levels: Seq[Long])

    def parseLine(line: String): Report = Report(line.split(" ").map(_.toLong).toSeq)

    def parse(input: String): Seq[Report] = input
    .linesIterator
    .map(parseLine)
    .toSeq

    Part 1: methods of the Report case class

    We need to check consecutive pairs of numbers in each report in 3 ways:

    • to see if they are all increasing,
    • to see if they are all decreasing,
    • to see if their differences are within given bounds.

    So let's construct them only once, save it as a val, then reuse this value 3 times. It's not the most efficient way (like traversing only once and keeping track of everything), but it's very clean and simple:

    case class Report(levels: Seq[Long]):
    val pairs = levels.init.zip(levels.tail) // consecutive pairs
    def allIncr: Boolean = pairs.forall(_ < _)
    def allDecr: Boolean = pairs.forall(_ > _)
    def within(lower: Long, upper: Long): Boolean = pairs.forall: pair =>
    val diff = math.abs(pair._1 - pair._2)
    lower <= diff && diff <= upper
    def isSafe: Boolean = (allIncr || allDecr) && within(1L, 3L)

    Part 1 solver simply counts safe reports, so it looks like this:

    def part1(input: String): Int = parse(input).count(_.isSafe)

    Part 2

    Now we add new methods to Report. @@ -20,7 +20,7 @@ but our puzzle inputs are fairly short (there are at most 8 levels in each Report), so it's a simple approach that reuses the isSafe method from Part 1.

    Part 2 solver now counts the dampened safe reports:

    def part2(input: String): Int = parse(input).count(_.isDampenedSafe)

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day03/index.html b/2024/puzzles/day03/index.html index a04aeb57d..c4939339b 100644 --- a/2024/puzzles/day03/index.html +++ b/2024/puzzles/day03/index.html @@ -5,14 +5,14 @@ Day 3: Mull It Over | Scala Center Advent of Code - - + +
    -

    Day 3: Mull It Over

    by @makingthematrix

    Puzzle description

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

    Solution Summary

    1. Part 1:
      • Write a regex to find a string that looks like "mul(a,b)" where a and b are integers.
      • Run the regex over the input string and find all matches.
      • For each match extract a and b and multiply them.
      • Sum all the partial results.
    2. Part 2:
      • Write a regex that will look for either the exact string as in Part 1 or "do()" or "don't()".
      • Run the regex over the input string and find all matches.
      • Fold over the matches and use the regex from Part 1 to multiply and sum only from the "highlighted" parts of the input string.

    Part 1

    This task plays very well with Scala's strengths: pattern matching and the standard collections library. But first, we need to learn a bit of regular expressions. Personally, I believe everyone should learn them. They're scary only at the beginning, and then they become a very valuable tool in your pocket.

    The shopkeeper at the North Pole Toboggan Rental Shop asks us to go through the input string, find all substrings that look like calls to a multiplication method "mul(a,b)", perform the multiplications, and sum them together. To find such substrings, we will construct a regular expression that will look for the following:

    1. Each substring starts with a text "mul(". We need to escape the parenthesis in the regex form, so this will be mul\(.
    2. "mul(" is followed by the first positive integer. Since it's an integer and positive, we don't need to look for floating points or minuses; we can look for sequences of ciphers. In regex, one cipher is denoted by \d, and since we look for a sequence of one or more ciphers, it's \d+.
    3. After the first number there should be a comma, so ,.
    4. Then there should be a second number, so again \d+.
    5. And the substring should finish with the closing parentheses, escaped again, so \).

    That's it. Our whole regex looks like this:

    val mulPattern: Regex = """mul\((\d+),(\d+)\)""".r

    Given that our input string is already read into the value input, we can now find in it all occurrences of substrings that match our regex:

    val multiplications = mulPattern.findAllIn(input)

    Take note that multiplications is an instance of MatchIterator over the input string. If we turn it into a sequence, it will be a sequence of substrings - but what we actually need to do is extract the numbers from within each substring, multiply them, and then sum them all together. Fortunately, the collect method from the Scala standard collection library will be just perfect for that. What's more, we can reuse the same regex. In Scala, regular expressions can be seamlessly used in pattern matching, which makes working with strings a breeze.

    val result1 = multiplications.collect { case mulPattern(a, b) => a.toInt * b.toInt }.sum

    And here it is - the result for Part 1.

    Part 2

    The second half of the tasks adds a complication. Inside the input string are hidden two more method calls: "do()" and "don't()". The task is to recognize them and perform the multiplication and addition only on those parts of the input strings that are "highlighted". The input string is "highlighted" at the beginning and to the first "don't()". "don't()" turns the highlighting off until we encounter "do()" which turns the highlighting on again. And so on, until the end of the input string.

    To implement this logic, first, we need a slightly more complicated regular expression that matches either "mul(a,b)" or "do()" or "don't()" and can extract from the input string all occurrences of each of those substrings in the order they appear.

    1. We already have the most complicated part of the new regular expressions - it's the regular expression from Part 1, mul\((\d+)\,(\d+)\).
    2. Then, we need to be able to find a text "do()". As we know, we need to escape parentheses, so do\(\).
    3. And similarly, we need to be able to find a substring "don't()" - don't\(\).
    4. Finally, we must tell the compiler we are looking for substrings that match any of the three rules we made above. In regex, this is done by putting regular expressions in parentheses and separating them with a | sign, like this: (GROUP 1|GROUP 2|GROUP 3).

    Together, our new regular expression looks as follows:

    val allPattern: Regex = """(mul\((\d+),(\d+)\)|do\(\)|don't\(\))""".r

    As in Part 1 we can now use allPattern to find all occurences of "mul(a,b)", "do()", and "don't()" in the input string:

    val occurences = allPattern.findAllIn(input)

    But we can't just use collect this time. We need to interate over the sequence of occurences in the order as they appear in the input string, and multiply and add numbers only if they appear in the "highlighted" parts. Let's first go through how to do it using English instead of Scala:

    1. We need a flag that will start "on" (true) and switch it "off" (false) and "on" again as we encounter "don't()"s and "do()"s.
    2. We will also need a sum field — an integer starting at 0 — to which we will add the results of multiplications.
    3. We start in the "highlighted" mode.
    4. If the flag is "on" and we encounter "mul(a,b)", then we extract integers a and b, multiply them, and add them to the sum.
    5. If we encounter "don't()", we switch the flag to "off".
    6. If we encounter "do()", we switch the flag back to "on".
    7. If the flag is "off" and we encounter "mul(a,b)", or if for some reason we can't extract a and b, we do nothing.

    It is theoretically possible that we can encounter consecutive "don't()"s or "do()"s in the input string, but that doesn't change anything - for example, if the flag is "off", and we encounter "don't()", the flag stays "off".

    We can implement this logic with foldLeft. In the first parameter list, our foldLeft will have the initial values of the flag (true) and the sum (0). In the second parameter list, we will provide a function that takes the accumulator - a tuple of the flag and the sum - and each subsequent occurrence of a substring that matches our pattern regular expression. The function will parse the substring and return the updated flag and sum.

    occurences.foldLeft((true, 0)) { ((isHighlighted, sum), substring) => ... }

    But wait, we can use pattern matching again, as in Part 1. We can split that function from the second parameter list into cases, each case implementing one line of logic from our description above:

    occurences.foldLeft((true, 0)) {
    case ((true, sum), mulPattern(a, b)) => (true, sum + (a.toInt * b.toInt)) // line 4
    case ((_, sum), "don't()") => (false, sum) // line 5
    case ((_, sum), "do()") => (true, sum) // line 6
    case ((flag, sum), _) => (flag, sum) // line 7
    }

    That last part in line 7, "if for some reason we can't extract a and b", should never occur because allPattern already guards us against it. However, the Scala compiler does not know that, and without that part, it will warn us that the match may not be exhaustive. The compiler only sees that we use mulPattern in the first case but does not have proof that after checking for it, for "don't()", and for "do()", there are no other cases left.

    Now, the only thing left is to get back the sum after foldLeft iterates over all occurrences of found substrings. foldLeft will return a tuple with the final value of the flag and the sum. We don't need the flag anymore, so we can ignore it:

    val (_, result2) = occurences.foldLeft((true, 0)) { ... }

    Final Code

    import java.nio.file.{Files, Path}
    import scala.util.matching.Regex

    object DayThree:
    private def readInput: String = Files.readString(Path.of("resources/input3"))
    private val mulPattern: Regex = """mul\((\d+),(\d+)\)""".r
    private val allPattern: Regex = """(mul\((\d+),(\d+)\)|do\(\)|don't\(\))""".r

    @main def main(): Unit =
    val input = readInput
    // Part 1
    val res1 = mulPattern.findAllIn(input).collect { case mulPattern(a, b) => a.toInt * b.toInt }.sum
    println(res1)
    // Part 2
    val (_, res2) = allPattern.findAllIn(input).foldLeft((true, 0)) {
    case ((true, sum), mulPattern(a, b)) => (true, sum + (a.toInt * b.toInt))
    case ((_, sum), "don't()") => (false, sum)
    case ((_, sum), "do()") => (true, sum)
    case ((flag, sum), _) => (flag, sum)
    }
    println(res2)

    Solutions from the community

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

    Day 3: Mull It Over

    by @makingthematrix

    Puzzle description

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

    Solution Summary

    1. Part 1:
      • Write a regex to find a string that looks like "mul(a,b)" where a and b are integers.
      • Run the regex over the input string and find all matches.
      • For each match extract a and b and multiply them.
      • Sum all the partial results.
    2. Part 2:
      • Write a regex that will look for either the exact string as in Part 1 or "do()" or "don't()".
      • Run the regex over the input string and find all matches.
      • Fold over the matches and use the regex from Part 1 to multiply and sum only from the "highlighted" parts of the input string.

    Part 1

    This task plays very well with Scala's strengths: pattern matching and the standard collections library. But first, we need to learn a bit of regular expressions. Personally, I believe everyone should learn them. They're scary only at the beginning, and then they become a very valuable tool in your pocket.

    The shopkeeper at the North Pole Toboggan Rental Shop asks us to go through the input string, find all substrings that look like calls to a multiplication method "mul(a,b)", perform the multiplications, and sum them together. To find such substrings, we will construct a regular expression that will look for the following:

    1. Each substring starts with a text "mul(". We need to escape the parenthesis in the regex form, so this will be mul\(.
    2. "mul(" is followed by the first positive integer. Since it's an integer and positive, we don't need to look for floating points or minuses; we can look for sequences of ciphers. In regex, one cipher is denoted by \d, and since we look for a sequence of one or more ciphers, it's \d+.
    3. After the first number there should be a comma, so ,.
    4. Then there should be a second number, so again \d+.
    5. And the substring should finish with the closing parentheses, escaped again, so \).

    That's it. Our whole regex looks like this:

    val mulPattern: Regex = """mul\((\d+),(\d+)\)""".r

    Given that our input string is already read into the value input, we can now find in it all occurrences of substrings that match our regex:

    val multiplications = mulPattern.findAllIn(input)

    Take note that multiplications is an instance of MatchIterator over the input string. If we turn it into a sequence, it will be a sequence of substrings - but what we actually need to do is extract the numbers from within each substring, multiply them, and then sum them all together. Fortunately, the collect method from the Scala standard collection library will be just perfect for that. What's more, we can reuse the same regex. In Scala, regular expressions can be seamlessly used in pattern matching, which makes working with strings a breeze.

    val result1 = multiplications.collect { case mulPattern(a, b) => a.toInt * b.toInt }.sum

    And here it is - the result for Part 1.

    Part 2

    The second half of the tasks adds a complication. Inside the input string are hidden two more method calls: "do()" and "don't()". The task is to recognize them and perform the multiplication and addition only on those parts of the input strings that are "highlighted". The input string is "highlighted" at the beginning and to the first "don't()". "don't()" turns the highlighting off until we encounter "do()" which turns the highlighting on again. And so on, until the end of the input string.

    To implement this logic, first, we need a slightly more complicated regular expression that matches either "mul(a,b)" or "do()" or "don't()" and can extract from the input string all occurrences of each of those substrings in the order they appear.

    1. We already have the most complicated part of the new regular expressions - it's the regular expression from Part 1, mul\((\d+)\,(\d+)\).
    2. Then, we need to be able to find a text "do()". As we know, we need to escape parentheses, so do\(\).
    3. And similarly, we need to be able to find a substring "don't()" - don't\(\).
    4. Finally, we must tell the compiler we are looking for substrings that match any of the three rules we made above. In regex, this is done by putting regular expressions in parentheses and separating them with a | sign, like this: (GROUP 1|GROUP 2|GROUP 3).

    Together, our new regular expression looks as follows:

    val allPattern: Regex = """(mul\((\d+),(\d+)\)|do\(\)|don't\(\))""".r

    As in Part 1 we can now use allPattern to find all occurences of "mul(a,b)", "do()", and "don't()" in the input string:

    val occurences = allPattern.findAllIn(input)

    But we can't just use collect this time. We need to interate over the sequence of occurences in the order as they appear in the input string, and multiply and add numbers only if they appear in the "highlighted" parts. Let's first go through how to do it using English instead of Scala:

    1. We need a flag that will start "on" (true) and switch it "off" (false) and "on" again as we encounter "don't()"s and "do()"s.
    2. We will also need a sum field — an integer starting at 0 — to which we will add the results of multiplications.
    3. We start in the "highlighted" mode.
    4. If the flag is "on" and we encounter "mul(a,b)", then we extract integers a and b, multiply them, and add them to the sum.
    5. If we encounter "don't()", we switch the flag to "off".
    6. If we encounter "do()", we switch the flag back to "on".
    7. If the flag is "off" and we encounter "mul(a,b)", or if for some reason we can't extract a and b, we do nothing.

    It is theoretically possible that we can encounter consecutive "don't()"s or "do()"s in the input string, but that doesn't change anything - for example, if the flag is "off", and we encounter "don't()", the flag stays "off".

    We can implement this logic with foldLeft. In the first parameter list, our foldLeft will have the initial values of the flag (true) and the sum (0). In the second parameter list, we will provide a function that takes the accumulator - a tuple of the flag and the sum - and each subsequent occurrence of a substring that matches our pattern regular expression. The function will parse the substring and return the updated flag and sum.

    occurences.foldLeft((true, 0)) { ((isHighlighted, sum), substring) => ... }

    But wait, we can use pattern matching again, as in Part 1. We can split that function from the second parameter list into cases, each case implementing one line of logic from our description above:

    occurences.foldLeft((true, 0)) {
    case ((true, sum), mulPattern(a, b)) => (true, sum + (a.toInt * b.toInt)) // line 4
    case ((_, sum), "don't()") => (false, sum) // line 5
    case ((_, sum), "do()") => (true, sum) // line 6
    case ((flag, sum), _) => (flag, sum) // line 7
    }

    That last part in line 7, "if for some reason we can't extract a and b", should never occur because allPattern already guards us against it. However, the Scala compiler does not know that, and without that part, it will warn us that the match may not be exhaustive. The compiler only sees that we use mulPattern in the first case but does not have proof that after checking for it, for "don't()", and for "do()", there are no other cases left.

    Now, the only thing left is to get back the sum after foldLeft iterates over all occurrences of found substrings. foldLeft will return a tuple with the final value of the flag and the sum. We don't need the flag anymore, so we can ignore it:

    val (_, result2) = occurences.foldLeft((true, 0)) { ... }

    Final Code

    import java.nio.file.{Files, Path}
    import scala.util.matching.Regex

    object DayThree:
    private def readInput: String = Files.readString(Path.of("resources/input3"))
    private val mulPattern: Regex = """mul\((\d+),(\d+)\)""".r
    private val allPattern: Regex = """(mul\((\d+),(\d+)\)|do\(\)|don't\(\))""".r

    @main def main(): Unit =
    val input = readInput
    // Part 1
    val res1 = mulPattern.findAllIn(input).collect { case mulPattern(a, b) => a.toInt * b.toInt }.sum
    println(res1)
    // Part 2
    val (_, res2) = allPattern.findAllIn(input).foldLeft((true, 0)) {
    case ((true, sum), mulPattern(a, b)) => (true, sum + (a.toInt * b.toInt))
    case ((_, sum), "don't()") => (false, sum)
    case ((_, sum), "do()") => (true, sum)
    case ((flag, sum), _) => (flag, sum)
    }
    println(res2)

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day04/index.html b/2024/puzzles/day04/index.html index 45aac4af6..cdb4e4276 100644 --- a/2024/puzzles/day04/index.html +++ b/2024/puzzles/day04/index.html @@ -5,12 +5,12 @@ Day 4: Ceres Search | Scala Center Advent of Code - - + +
    -

    Day 4: Ceres Search

    by @bishabosha

    Puzzle description

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

    Solution Summary

    Treat the input as a 2D grid, (whose elements are only X, M, A, or S).

    Part 1: iterate through each point in the grid. +

    Day 4: Ceres Search

    by @bishabosha

    Puzzle description

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

    Solution Summary

    Treat the input as a 2D grid, (whose elements are only X, M, A, or S).

    Part 1: iterate through each point in the grid. For each point and for each of the eight directions, try to construct the word "XMAS" starting from the current point, while handling out of bounds. Because each origin point is visited once there is no need to handle duplicates. Count the total matched words.

    Part 2: iterate through each point in the grid. @@ -26,7 +26,7 @@ also the center of a valid X-MAS formation. To do this, we will inspect for "MAS" starting in each of the four surrounding corners.

    e.g. if we picture the four corners in the grid like this,

    1-2
    -A-
    4-3

    Then we will look for "MAS" starting from 1 towards 3, from 2 towards 4, from 3 towards 1, and from 4 towards 2.

    To do that we will need two Dirs for each case, one to translate the current point to the corner, and then the opposite direction to scan in. Here is how you can represent that:

    val UpRight = dirs(4)
    val DownRight = dirs(5)
    val DownLeft = dirs(6)
    val UpLeft = dirs(7)

    val dirsMAS = IArray(
    UpLeft -> DownRight, // 1 -> 3
    UpRight -> DownLeft, // 2 -> 4
    DownRight -> UpLeft, // 3 -> 1
    DownLeft -> UpRight // 4 -> 2
    )

    Then to check if a single point is X-MAS, use the following code:

    def isMAS(x: Int, y: Int, grid: Grid): Boolean =
    grid(y)(x) match
    case 'A' =>
    val seen = dirsMAS.count: (transform, dir) =>
    scanMAS(x + transform.dx, y + transform.dy, dir, grid)
    seen > 1
    case _ => false

    i.e. when the point is 'A', then for each of the four corners, translate the point to the corner which will be the origin point to scan for "MAS". Then pass along the scanning direction and the grid. For a valid X-MAS, two of the corners will be origin points for the word.

    Putting it all together, we then again iterate through the grid, now only counting the points where isMas is true.

    def part2(input: String): Int =
    totalMAS(parse(input))

    def totalMAS(grid: Grid): Int =
    Iterator
    .tabulate(grid.size, grid.size): (y, x) =>
    if isMAS(x, y, grid) then 1 else 0
    .flatten
    .sum

    Final Code

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

    type Grid = IArray[IArray[Char]]

    def parse(input: String): Grid =
    IArray.from(
    input.linesIterator.map(IArray.from)
    )

    case class Dir(dy: Int, dx: Int)

    val dirs = IArray(
    Dir(dy = -1, dx = 0), // up
    Dir(dy = 0, dx = 1), // right
    Dir(dy = 1, dx = 0), // down
    Dir(dy = 0, dx = -1), // left
    Dir(dy = -1, dx = 1), // up-right
    Dir(dy = 1, dx = 1), // down-right
    Dir(dy = 1, dx = -1), // down-left
    Dir(dy = -1, dx = -1) // up-left
    )

    def boundCheck(x: Int, y: Int, grid: Grid): Boolean =
    x >= 0 && x < grid.length && y >= 0 && y < grid(0).length

    def scanner(x: Int, y: Int, dir: Dir, grid: Grid): Iterator[Char] =
    Iterator.unfold((y, x)): (y, x) =>
    Option.when(boundCheck(x, y, grid))(grid(y)(x) -> (y + dir.dy, x + dir.dx))

    def scanString(target: String)(x: Int, y: Int, dir: Dir, grid: Grid): Boolean =
    scanner(x, y, dir, grid).take(target.length).corresponds(target)(_ == _)

    val scanXMAS = scanString("XMAS")

    def totalXMAS(grid: Grid): Int =
    Iterator
    .tabulate(grid.size, grid.size): (y, x) =>
    dirs.count(dir => scanXMAS(x, y, dir, grid))
    .flatten
    .sum

    def part2(input: String): Int =
    totalMAS(parse(input))

    val scanMAS = scanString("MAS")

    val UpRight = dirs(4)
    val DownRight = dirs(5)
    val DownLeft = dirs(6)
    val UpLeft = dirs(7)

    val dirsMAS = IArray(
    UpLeft -> DownRight,
    UpRight -> DownLeft,
    DownRight -> UpLeft,
    DownLeft -> UpRight
    )

    def isMAS(x: Int, y: Int, grid: Grid): Boolean =
    grid(y)(x) match
    case 'A' =>
    val seen = dirsMAS.count: (transform, dir) =>
    scanMAS(x + transform.dx, y + transform.dy, dir, grid)
    seen > 1
    case _ => false

    def totalMAS(grid: Grid): Int =
    Iterator
    .tabulate(grid.size, grid.size): (y, x) =>
    if isMAS(x, y, grid) then 1 else 0
    .flatten
    .sum

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day05/index.html b/2024/puzzles/day05/index.html index ea7f6d244..f7d36b1a5 100644 --- a/2024/puzzles/day05/index.html +++ b/2024/puzzles/day05/index.html @@ -5,14 +5,14 @@ Day 5: Print Queue | Scala Center Advent of Code - - + +
    -

    Day 5: Print Queue

    by @KacperFKorban

    Puzzle description

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

    Solution Summary

    We can treat the data as a graph, where:

    • the ordering rules represent directed edges in the graph
    • each update represents a subset of the nodes

    As a common part of the solution, we will:

    • parse the input into a list of ordering rules (Int, Int) and a list of updates List[List[Int]]
    • represent the rules as an adjacency list Map[Int, List[Int]]
    1. Part 1:
      • For every update, we iterate over its elements (while keeping track of the visited nodes) and for each node check that none of its successors were visited before it.
      • We only keep the updates that don't violate the ordering rules.
      • We compute the middle number of the valid updates.
      • The solution is the sum of the middle numbers of the valid updates.
    2. Part 2:
      • Similarily to part 1, we iterate over the updates and check if the ordering rules are violated, but this time we only keep the updates that do violate the ordering rules.
      • To fix the ordering for each update:
        • We find the nodes that have no incoming edges.
        • Then, we run a BFS starting from these nodes, making sure that we only enqueue nodes when all of their incoming edges have been visited or enqueued.

    Common part

    For both parts of the solution, we will parse the input into an adjacency list and a list of updates.

    def parseRulesAndupdates(input: String): (Map[Int, List[Int]], List[List[Int]]) =
    val ruleRegex: Regex = """(\d+)\|(\d+)""".r
    val Array(rulesStr, updatesStr) = input.split("\n\n")
    val rules: Map[Int, List[Int]] =
    ruleRegex.findAllMatchIn(rulesStr).map { m =>
    m.group(1).toInt -> m.group(2).toInt
    }.toList.groupMap(_._1)(_._2)
    val updates: List[List[Int]] =
    updatesStr.linesIterator.map(_.split(",").map(_.toInt).toList).toList
    (rules, updates)

    We first split the input into two parts. Then, for rules we convert them into a list of pairs (Int, Int) using a regex and group them by the first element. For updates, we simply split them by commas and convert the elements to Ints.

    Part 1

    To check if an update is valid, we iterate over the elements of the update and check if none of the neighbors of the current node were visited before it.

    This can be done in several ways, for example by using a recursive function:

    def isValid(rules: Map[Int, List[Int]])(update: List[Int]): Boolean =
    def rec(update: List[Int], visited: Set[Int] = Set.empty): Boolean = update match
    case Nil => true
    case updateNo :: rest =>
    !rules.getOrElse(updateNo, List.empty).exists(visited.contains)
    && rec(rest, visited + updateNo)
    rec(update)

    another alternative is using boundary-break with a for:

    def isValid(rules: Map[Int, List[Int]])(update: List[Int]): Boolean =
    boundary:
    var visited = Set.empty[Int]
    for updateNo <- update do
    visited += updateNo
    if rules.getOrElse(updateNo, List.empty).exists(visited.contains) then
    break(false)
    true

    or a forall with a local mutable state:

    def isValid(rules: Map[Int, List[Int]])(update: List[Int]): Boolean =
    var visited = Set.empty[Int]
    update.forall { updateNo =>
    visited += updateNo
    !rules.getOrElse(updateNo, List.empty).exists(visited.contains)
    }

    Using the isValid function, we can filter the updates and compute the middle number of the valid updates:

    def part1(input: String) =
    val (rules, updates) = parseRulesAndupdates(input)
    updates.filter(isValid(rules)).map(us => us(us.size / 2)).sum

    Part 2

    We start Part 2 by parsing and filtering the updates that violate the ordering rules, very similarly to Part 1:

    val (rules, updates) = parseRulesAndupdates(input)
    val invalidupdates = updates.filter(!isValid(rules)(_))

    Next, to fix a single update, we first construct local adjacency lists for the relevant nodes and an inverse adjacency list to keep track of the incoming edges:

    def fixUpdate(update: List[Int]): List[Int] =
    val relevantRules = rules
    .filter((k, vs) => update.contains(k) && vs.exists(update.contains))
    .mapValues(_.filter(update.contains)).toMap
    val prevsMap = relevantRules
    .map { case (k, vs) => vs.map(_ -> k) }
    .flatten.groupMap(_._1)(_._2)

    The relevantRules are only those that only use the nodes from update, and the prevsMap is a map from a node to its direct predecessors.

    Then, we start with nodes that have no incoming edges and run a BFS to fix the ordering:

    val startNodes = update.filter(k => !relevantRules.values.flatten.toList.contains(k))

    The BFS function takes a set of visited nodes, a queue of nodes to visit, and a list of nodes in the correct order:

      def bfs(queue: Queue[Int], visited: Set[Int] = Set.empty, res: List[Int] = List.empty): List[Int] = queue.dequeueOption match
    case None => res
    case Some((node, queue1)) =>
    val newVisited = visited + node
    val newRes = res :+ node
    val newQueue = relevantRules.getOrElse(node, List.empty)
    .filter { n =>
    val notVisited = !newVisited.contains(n)
    val notInQueue = !queue1.contains(n)
    val allPrevVisited = prevsMap.getOrElse(n, List.empty).forall(p => newVisited.contains(p) || queue1.contains(p))
    notVisited && notInQueue && allPrevVisited
    }
    .foldLeft(queue1)(_.appended(_))
    bfs(newVisited, newQueue, newRes)

    The BFS works as follows:

    • If the queue is empty, we return the result

    • Otherwise, we dequeue a node and add it to the visited set and the result list. We enqueue all neighbors of the node that:

      • have not been visited yet
      • and are not in the queue
      • and have all of their incoming edges visited or enqueued.

      We then call the BFS function recursively with the updated queue, visited set, and result list.

    The result of the fixUpdate function is call to the bfs function with the startNodes in the queue.

    The solution for part2 is then a sum of the middle numbers of the fixed updates:

    invalidUpdates.map(fixUpdate).map(us => us(us.size / 2)).sum

    The full solution for Part 2 looks like this:


    def part2(input: String) =
    val (rules, updates) = parseRulesAndupdates(input)
    val invalidUpdates = updates.filter(!isValid(rules)(_))

    def fixUpdate(update: List[Int]): List[Int] =
    val relevantRules = rules
    .filter((k, vs) => update.contains(k) && vs.exists(update.contains))
    .mapValues(_.filter(update.contains)).toMap
    val prevsMap = relevantRules
    .map { case (k, vs) => vs.map(_ -> k) }
    .flatten.groupMap(_._1)(_._2)
    val startNodes = update.filter(k => !relevantRules.values.flatten.toList.contains(k))
    def bfs(queue: Queue[Int], visited: Set[Int] = Set.empty, res: List[Int] = List.empty): List[Int] = queue.dequeueOption match
    case None => res
    case Some((node, queue1)) =>
    val newVisited = visited + node
    val newRes = res :+ node
    val newQueue = relevantRules.getOrElse(node, List.empty)
    .filter { n =>
    val notVisited = !newVisited.contains(n)
    val notInQueue = !queue1.contains(n)
    val allPrevVisited = prevsMap.getOrElse(n, List.empty).forall(p => newVisited.contains(p) || queue1.contains(p))
    notVisited && notInQueue && allPrevVisited
    }
    .foldLeft(queue1)(_.appended(_))
    bfs(newQueue, newVisited, newRes)
    bfs(Queue.from(startNodes))

    invalidUpdates.map(fixUpdate).map(us => us(us.size / 2)).sum

    Solutions from the community

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

    Day 5: Print Queue

    by @KacperFKorban

    Puzzle description

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

    Solution Summary

    We can treat the data as a graph, where:

    • the ordering rules represent directed edges in the graph
    • each update represents a subset of the nodes

    As a common part of the solution, we will:

    • parse the input into a list of ordering rules (Int, Int) and a list of updates List[List[Int]]
    • represent the rules as an adjacency list Map[Int, List[Int]]
    1. Part 1:
      • For every update, we iterate over its elements (while keeping track of the visited nodes) and for each node check that none of its successors were visited before it.
      • We only keep the updates that don't violate the ordering rules.
      • We compute the middle number of the valid updates.
      • The solution is the sum of the middle numbers of the valid updates.
    2. Part 2:
      • Similarily to part 1, we iterate over the updates and check if the ordering rules are violated, but this time we only keep the updates that do violate the ordering rules.
      • To fix the ordering for each update:
        • We find the nodes that have no incoming edges.
        • Then, we run a BFS starting from these nodes, making sure that we only enqueue nodes when all of their incoming edges have been visited or enqueued.

    Common part

    For both parts of the solution, we will parse the input into an adjacency list and a list of updates.

    def parseRulesAndupdates(input: String): (Map[Int, List[Int]], List[List[Int]]) =
    val ruleRegex: Regex = """(\d+)\|(\d+)""".r
    val Array(rulesStr, updatesStr) = input.split("\n\n")
    val rules: Map[Int, List[Int]] =
    ruleRegex.findAllMatchIn(rulesStr).map { m =>
    m.group(1).toInt -> m.group(2).toInt
    }.toList.groupMap(_._1)(_._2)
    val updates: List[List[Int]] =
    updatesStr.linesIterator.map(_.split(",").map(_.toInt).toList).toList
    (rules, updates)

    We first split the input into two parts. Then, for rules we convert them into a list of pairs (Int, Int) using a regex and group them by the first element. For updates, we simply split them by commas and convert the elements to Ints.

    Part 1

    To check if an update is valid, we iterate over the elements of the update and check if none of the neighbors of the current node were visited before it.

    This can be done in several ways, for example by using a recursive function:

    def isValid(rules: Map[Int, List[Int]])(update: List[Int]): Boolean =
    def rec(update: List[Int], visited: Set[Int] = Set.empty): Boolean = update match
    case Nil => true
    case updateNo :: rest =>
    !rules.getOrElse(updateNo, List.empty).exists(visited.contains)
    && rec(rest, visited + updateNo)
    rec(update)

    another alternative is using boundary-break with a for:

    def isValid(rules: Map[Int, List[Int]])(update: List[Int]): Boolean =
    boundary:
    var visited = Set.empty[Int]
    for updateNo <- update do
    visited += updateNo
    if rules.getOrElse(updateNo, List.empty).exists(visited.contains) then
    break(false)
    true

    or a forall with a local mutable state:

    def isValid(rules: Map[Int, List[Int]])(update: List[Int]): Boolean =
    var visited = Set.empty[Int]
    update.forall { updateNo =>
    visited += updateNo
    !rules.getOrElse(updateNo, List.empty).exists(visited.contains)
    }

    Using the isValid function, we can filter the updates and compute the middle number of the valid updates:

    def part1(input: String) =
    val (rules, updates) = parseRulesAndupdates(input)
    updates.filter(isValid(rules)).map(us => us(us.size / 2)).sum

    Part 2

    We start Part 2 by parsing and filtering the updates that violate the ordering rules, very similarly to Part 1:

    val (rules, updates) = parseRulesAndupdates(input)
    val invalidupdates = updates.filter(!isValid(rules)(_))

    Next, to fix a single update, we first construct local adjacency lists for the relevant nodes and an inverse adjacency list to keep track of the incoming edges:

    def fixUpdate(update: List[Int]): List[Int] =
    val relevantRules = rules
    .filter((k, vs) => update.contains(k) && vs.exists(update.contains))
    .mapValues(_.filter(update.contains)).toMap
    val prevsMap = relevantRules
    .map { case (k, vs) => vs.map(_ -> k) }
    .flatten.groupMap(_._1)(_._2)

    The relevantRules are only those that only use the nodes from update, and the prevsMap is a map from a node to its direct predecessors.

    Then, we start with nodes that have no incoming edges and run a BFS to fix the ordering:

    val startNodes = update.filter(k => !relevantRules.values.flatten.toList.contains(k))

    The BFS function takes a set of visited nodes, a queue of nodes to visit, and a list of nodes in the correct order:

      def bfs(queue: Queue[Int], visited: Set[Int] = Set.empty, res: List[Int] = List.empty): List[Int] = queue.dequeueOption match
    case None => res
    case Some((node, queue1)) =>
    val newVisited = visited + node
    val newRes = res :+ node
    val newQueue = relevantRules.getOrElse(node, List.empty)
    .filter { n =>
    val notVisited = !newVisited.contains(n)
    val notInQueue = !queue1.contains(n)
    val allPrevVisited = prevsMap.getOrElse(n, List.empty).forall(p => newVisited.contains(p) || queue1.contains(p))
    notVisited && notInQueue && allPrevVisited
    }
    .foldLeft(queue1)(_.appended(_))
    bfs(newVisited, newQueue, newRes)

    The BFS works as follows:

    • If the queue is empty, we return the result

    • Otherwise, we dequeue a node and add it to the visited set and the result list. We enqueue all neighbors of the node that:

      • have not been visited yet
      • and are not in the queue
      • and have all of their incoming edges visited or enqueued.

      We then call the BFS function recursively with the updated queue, visited set, and result list.

    The result of the fixUpdate function is call to the bfs function with the startNodes in the queue.

    The solution for part2 is then a sum of the middle numbers of the fixed updates:

    invalidUpdates.map(fixUpdate).map(us => us(us.size / 2)).sum

    The full solution for Part 2 looks like this:


    def part2(input: String) =
    val (rules, updates) = parseRulesAndupdates(input)
    val invalidUpdates = updates.filter(!isValid(rules)(_))

    def fixUpdate(update: List[Int]): List[Int] =
    val relevantRules = rules
    .filter((k, vs) => update.contains(k) && vs.exists(update.contains))
    .mapValues(_.filter(update.contains)).toMap
    val prevsMap = relevantRules
    .map { case (k, vs) => vs.map(_ -> k) }
    .flatten.groupMap(_._1)(_._2)
    val startNodes = update.filter(k => !relevantRules.values.flatten.toList.contains(k))
    def bfs(queue: Queue[Int], visited: Set[Int] = Set.empty, res: List[Int] = List.empty): List[Int] = queue.dequeueOption match
    case None => res
    case Some((node, queue1)) =>
    val newVisited = visited + node
    val newRes = res :+ node
    val newQueue = relevantRules.getOrElse(node, List.empty)
    .filter { n =>
    val notVisited = !newVisited.contains(n)
    val notInQueue = !queue1.contains(n)
    val allPrevVisited = prevsMap.getOrElse(n, List.empty).forall(p => newVisited.contains(p) || queue1.contains(p))
    notVisited && notInQueue && allPrevVisited
    }
    .foldLeft(queue1)(_.appended(_))
    bfs(newQueue, newVisited, newRes)
    bfs(Queue.from(startNodes))

    invalidUpdates.map(fixUpdate).map(us => us(us.size / 2)).sum

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day06/index.html b/2024/puzzles/day06/index.html index ff1857a21..0cb70522f 100644 --- a/2024/puzzles/day06/index.html +++ b/2024/puzzles/day06/index.html @@ -5,17 +5,17 @@ Day 6: Guard Gallivant | Scala Center Advent of Code - - + +
    -

    Day 6: Guard Gallivant

    by @samuelchassot

    Day 06 - solution

    Part 1

    Let's start by defining some structures to represent the input and some abstractions.

    First of all, let's define a Coordinate type alias to represent a pair of integers. We also define an extension method to add two coordinates:

    type Coordinate = (Int, Int)

    extension (coord: Coordinate) infix def +(other: Coordinate): Coordinate = (coord._1 + other._1, coord._2 + other._2)

    We also define a Direction enumeration to represent the four cardinal directions:

    enum Direction(vectorr: Coordinate):
    def vector: Coordinate = vectorr
    case North extends Direction(vectorr = (0, -1))
    case East extends Direction(vectorr = (1, 0))
    case South extends Direction(vectorr = (0, 1))
    case West extends Direction(vectorr = (-1, 0))
    end Direction

    object Direction:
    def fromChar(c: Char): Direction = c match
    case '^' => North
    case 'v' => South
    case '>' => East
    case '<' => West
    end Direction

    As we will need to represent the guard moving step by step in some directions, we defined the direction to have a vector attribute, so that we can easily move on step in a direction using coordinate addition.

    Now let's define a "point of view" of the guard, which the combination of a coordinate and a direction:

    type PointOfView = (Coordinate, Direction)

    We then define a Lab case class to represent the laboratory. The class has a list of strings to represent the laboratory, and two integers to represent the number of rows and columns. We also define some helper methods to check if a coordinate is within the lab, to get the character at a given coordinate, to check if a coordinate is an obstacle, and to replace a character at a given coordinate, creating a new lab instance:

    case class Lab(l: List[String], northSouthLength: Int, eastWestLength: Int):
    require(l.size == northSouthLength && l.forall(_.size == eastWestLength))

    def isWithinLab(x: Int, y: Int): Boolean = x >= 0 && x < eastWestLength && y >= 0 && y < northSouthLength

    def get(x: Int, y: Int): Char = {
    require(isWithinLab(x, y))
    l(y)(x)
    }
    def isObstacle(x: Int, y: Int): Boolean = isWithinLab(x, y) && get(x, y) == '#'
    def isObstacle(coord: Coordinate): Boolean = isObstacle(coord._1, coord._2)

    def replaceWith(x: Int, y: Int, c: Char): Lab =
    require(isWithinLab(x, y))
    Lab(l.updated(y, l(y).updated(x, c)), northSouthLength, eastWestLength)

    end Lab

    Note that we added some assertions and preconditions, to ensure that we do not work with a broken lab, as this would make the entire program produce incorrect results.

    Now that we have the structure to represent the input, let's implement a structure to implement the rules the guard is following. We therefore define the Guard class as follows:

    case class Guard(lab: Lab):
    def step(pov: PointOfView): PointOfView =
    val isLookingAtObstacle = lab.isObstacle(pov._1 + pov._2.vector)
    if isLookingAtObstacle then
    val newDirection = Guard.rotate(pov._2)
    (pov._1, newDirection)
    else
    (pov._1 + pov._2.vector, pov._2)

    def pathFrom(pov: PointOfView): LazyList[PointOfView] =
    val nextPov = step(pov)
    pov #:: pathFrom(nextPov)

    def simulateWithinLab(pov: PointOfView): LazyList[PointOfView] =
    pathFrom(pov).takeWhile((coord, _) => lab.isWithinLab(coord._1, coord._2))
    end Guard

    The guard offers a step method to move the guard one step in the direction it is looking at. If the area in front of her is free, she moves forward; otherwise, she rotates to the right. We also define a rotate method to rotate the guard to the right:

    object Guard:
    def rotate(dir: Direction): Direction = dir match
    case Direction.North => Direction.East
    case Direction.East => Direction.South
    case Direction.South => Direction.West
    case Direction.West => Direction.North
    end Guard

    Now we define a crucial function for a guard, that computes an infinite sequence of points of view, starting from a given point of view:

    def pathFrom(pov: PointOfView): LazyList[PointOfView] = 
    val nextPov = step(pov)
    pov #:: pathFrom(nextPov)

    To represent this infinite sequence, we rely on a powerful structure of the Scala standard library, the LazyList. A LazyList is a list that is lazily evaluated, meaning that its elements are computed only when they are accessed. This allows us to represent infinite sequences, as we do here.

    Finally, we define a method to simulate the guard's movement within the lab, stopping when the guard hits a wall:

    def simulateWithinLab(pov: PointOfView): LazyList[PointOfView] = 
    pathFrom(pov).takeWhile((coord, _) => lab.isWithinLab(coord._1, coord._2))

    Before we can dive into solving the main question, we need to write a parser to construct a Lab instance from a list of strings and finding the guard starting point of view:

      def parse(l: List[String]): (Guard, PointOfView) = 
    require(l.size > 0 && l.head.size > 0)
    val startingY = l.indexWhere(s => s.contains("^") || s.contains("<") || s.contains(">") || s.contains("v"))
    assert(startingY >= 0 && startingY < l.size)
    val startingX = l(startingY).indexWhere(c => c == '^' || c == '<' || c == '>' || c == 'v')
    assert(startingX >= 0 && startingX < l.head.size)
    val guardChar = l(startingY)(startingX)
    println(guardChar)
    val direction = Direction.fromChar(guardChar)
    val lab = Lab(l.map(s => s.replace(guardChar, '.')), northSouthLength = l.size, eastWestLength = l.head.size)
    val guard = Guard(lab)

    (guard, ((startingX, startingY), direction))

    This function finds the starting point of view of the guard and constructs a Lab instance from the input list of strings. We also replace the character representing the guard with a dot, as the guard will move around the lab.

    Now we can solve the first part of the problem by counting the number of unique points the guard visits:

    def countVisitedDistinctLocations(g: Guard, startingPov: PointOfView): Int = 
    g.simulateWithinLab(startingPov).map(_._1).toSet.size

    To do so, we use the simulateWithinLab method to get the sequence of points of view the guard visits before exiting the lab. We then map the sequence to keep only the coordinates, as we are interested only in the coordinates she visited, not the direction she was facing when doing so. Finally we convert the sequence to a set to remove duplicates and return the size of the set.

    This concludes part 1 of the problem.

    For fun, we can write a function to visualize the lab with the guard's path:

    def visitedMap(g: Guard, startingPov: PointOfView): Lab = 
    g.simulateWithinLab(startingPov).map(_._1)
    .foldLeft(g.lab)((lab, coord) => lab.replaceWith(coord._1, coord._2, 'X'))

    This function takes all the coordinates the guard visited and replaces the corresponding characters in the lab with an 'X'. This way, we can visualize the lab with the guard's path, just as proposed in the problem statement.

    Part 2

    The part 2 asks in how many places we can place a obstacle so that the guard loops forever. To solve this, we will use brute force as it solves it within reasonable time.

    A key observation, is that the guard is looping if the path returned by the simulate function is at least longer than the total area of the lab + 1. Indeed, if the guard has visited all locations within the lab more than once without exiting, she is looping. +

    Day 6: Guard Gallivant

    by @samuelchassot

    Day 06 - solution

    Part 1

    Let's start by defining some structures to represent the input and some abstractions.

    First of all, let's define a Coordinate type alias to represent a pair of integers. We also define an extension method to add two coordinates:

    type Coordinate = (Int, Int)

    extension (coord: Coordinate) infix def +(other: Coordinate): Coordinate = (coord._1 + other._1, coord._2 + other._2)

    We also define a Direction enumeration to represent the four cardinal directions:

    enum Direction(vectorr: Coordinate):
    def vector: Coordinate = vectorr
    case North extends Direction(vectorr = (0, -1))
    case East extends Direction(vectorr = (1, 0))
    case South extends Direction(vectorr = (0, 1))
    case West extends Direction(vectorr = (-1, 0))
    end Direction

    object Direction:
    def fromChar(c: Char): Direction = c match
    case '^' => North
    case 'v' => South
    case '>' => East
    case '<' => West
    end Direction

    As we will need to represent the guard moving step by step in some directions, we defined the direction to have a vector attribute, so that we can easily move on step in a direction using coordinate addition.

    Now let's define a "point of view" of the guard, which the combination of a coordinate and a direction:

    type PointOfView = (Coordinate, Direction)

    We then define a Lab case class to represent the laboratory. The class has a list of strings to represent the laboratory, and two integers to represent the number of rows and columns. We also define some helper methods to check if a coordinate is within the lab, to get the character at a given coordinate, to check if a coordinate is an obstacle, and to replace a character at a given coordinate, creating a new lab instance:

    case class Lab(l: List[String], northSouthLength: Int, eastWestLength: Int):
    require(l.size == northSouthLength && l.forall(_.size == eastWestLength))

    def isWithinLab(x: Int, y: Int): Boolean = x >= 0 && x < eastWestLength && y >= 0 && y < northSouthLength

    def get(x: Int, y: Int): Char = {
    require(isWithinLab(x, y))
    l(y)(x)
    }
    def isObstacle(x: Int, y: Int): Boolean = isWithinLab(x, y) && get(x, y) == '#'
    def isObstacle(coord: Coordinate): Boolean = isObstacle(coord._1, coord._2)

    def replaceWith(x: Int, y: Int, c: Char): Lab =
    require(isWithinLab(x, y))
    Lab(l.updated(y, l(y).updated(x, c)), northSouthLength, eastWestLength)

    end Lab

    Note that we added some assertions and preconditions, to ensure that we do not work with a broken lab, as this would make the entire program produce incorrect results.

    Now that we have the structure to represent the input, let's implement a structure to implement the rules the guard is following. We therefore define the Guard class as follows:

    case class Guard(lab: Lab):
    def step(pov: PointOfView): PointOfView =
    val isLookingAtObstacle = lab.isObstacle(pov._1 + pov._2.vector)
    if isLookingAtObstacle then
    val newDirection = Guard.rotate(pov._2)
    (pov._1, newDirection)
    else
    (pov._1 + pov._2.vector, pov._2)

    def pathFrom(pov: PointOfView): LazyList[PointOfView] =
    val nextPov = step(pov)
    pov #:: pathFrom(nextPov)

    def simulateWithinLab(pov: PointOfView): LazyList[PointOfView] =
    pathFrom(pov).takeWhile((coord, _) => lab.isWithinLab(coord._1, coord._2))
    end Guard

    The guard offers a step method to move the guard one step in the direction it is looking at. If the area in front of her is free, she moves forward; otherwise, she rotates to the right. We also define a rotate method to rotate the guard to the right:

    object Guard:
    def rotate(dir: Direction): Direction = dir match
    case Direction.North => Direction.East
    case Direction.East => Direction.South
    case Direction.South => Direction.West
    case Direction.West => Direction.North
    end Guard

    Now we define a crucial function for a guard, that computes an infinite sequence of points of view, starting from a given point of view:

    def pathFrom(pov: PointOfView): LazyList[PointOfView] = 
    val nextPov = step(pov)
    pov #:: pathFrom(nextPov)

    To represent this infinite sequence, we rely on a powerful structure of the Scala standard library, the LazyList. A LazyList is a list that is lazily evaluated, meaning that its elements are computed only when they are accessed. This allows us to represent infinite sequences, as we do here.

    Finally, we define a method to simulate the guard's movement within the lab, stopping when the guard hits a wall:

    def simulateWithinLab(pov: PointOfView): LazyList[PointOfView] = 
    pathFrom(pov).takeWhile((coord, _) => lab.isWithinLab(coord._1, coord._2))

    Before we can dive into solving the main question, we need to write a parser to construct a Lab instance from a list of strings and finding the guard starting point of view:

      def parse(l: List[String]): (Guard, PointOfView) = 
    require(l.size > 0 && l.head.size > 0)
    val startingY = l.indexWhere(s => s.contains("^") || s.contains("<") || s.contains(">") || s.contains("v"))
    assert(startingY >= 0 && startingY < l.size)
    val startingX = l(startingY).indexWhere(c => c == '^' || c == '<' || c == '>' || c == 'v')
    assert(startingX >= 0 && startingX < l.head.size)
    val guardChar = l(startingY)(startingX)
    println(guardChar)
    val direction = Direction.fromChar(guardChar)
    val lab = Lab(l.map(s => s.replace(guardChar, '.')), northSouthLength = l.size, eastWestLength = l.head.size)
    val guard = Guard(lab)

    (guard, ((startingX, startingY), direction))

    This function finds the starting point of view of the guard and constructs a Lab instance from the input list of strings. We also replace the character representing the guard with a dot, as the guard will move around the lab.

    Now we can solve the first part of the problem by counting the number of unique points the guard visits:

    def countVisitedDistinctLocations(g: Guard, startingPov: PointOfView): Int = 
    g.simulateWithinLab(startingPov).map(_._1).toSet.size

    To do so, we use the simulateWithinLab method to get the sequence of points of view the guard visits before exiting the lab. We then map the sequence to keep only the coordinates, as we are interested only in the coordinates she visited, not the direction she was facing when doing so. Finally we convert the sequence to a set to remove duplicates and return the size of the set.

    This concludes part 1 of the problem.

    For fun, we can write a function to visualize the lab with the guard's path:

    def visitedMap(g: Guard, startingPov: PointOfView): Lab = 
    g.simulateWithinLab(startingPov).map(_._1)
    .foldLeft(g.lab)((lab, coord) => lab.replaceWith(coord._1, coord._2, 'X'))

    This function takes all the coordinates the guard visited and replaces the corresponding characters in the lab with an 'X'. This way, we can visualize the lab with the guard's path, just as proposed in the problem statement.

    Part 2

    The part 2 asks in how many places we can place a obstacle so that the guard loops forever. To solve this, we will use brute force as it solves it within reasonable time.

    A key observation, is that the guard is looping if the path returned by the simulate function is at least longer than the total area of the lab + 1. Indeed, if the guard has visited all locations within the lab more than once without exiting, she is looping. She could (and most likely will) visit a subset of the locations, but if the path is at least as long as the total area of the lab + 1, she is looping. Please note that we could detect the loop with a smaller path, for example, by checking when we detect a point of view that was already visited. However, this is a simple and elegant solution that works well for the input size.

    So we define a looping function as follows:

    def looping(guard: Guard, startingPov: PointOfView): Boolean = 
    val followedPath = guard.simulateWithinLab(startingPov).take(guard.lab.eastWestLength * guard.lab.northSouthLength + 1)
    followedPath.size >= guard.lab.eastWestLength * guard.lab.northSouthLength

    To make sure the function terminates, we can take() with the minimum number of steps we require to detect a loop, which is the total area of the lab + 1. So the returned path will have at most this length. Remember that the LazyList is infinite in the case of looping, so if we just check the length of the path, the function will not terminate.

    Once we have a function to detect whether the guard loops or not given a lab disposition and starting position, we can write a function that computes how many different obstacle positions lead to a looping guard.

    To do so, we will create a list of all possible obstacle positions, which are all positions in the lab except for the starting position, and create copies of the lab with an obstacle in each of these positions. We then count how many of those lead to a looping guard:

    def possibleObstaclesPositionsNumber(g: Guard, startingPov: PointOfView): Int = 
    val possibleObstaclesPositions =
    (
    for
    x <- 0 to g.lab.eastWestLength
    y <- 0 to g.lab.northSouthLength
    if g.lab.isWithinLab(x, y) && (x, y) != startingPov._1
    yield (x, y)
    )

    val newPossibleGuards=
    possibleObstaclesPositions.map(obstaclePos =>
    val newLab = g.lab.replaceWith(obstaclePos._1, obstaclePos._2, '#')
    Guard(newLab)
    )

    newPossibleGuards.par.count(g => looping(g, startingPov))

    As we are brute forcing, let's use a parallel collection, to use all the potential of our nice hardware, to check multiple lab dispositions at the same time. See how simple it is to use parallel collections in Scala, using the .par method.

    This concludes the solution for part 2 of the problem.

    https://adventofcode.com/2024/day/6

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day07/index.html b/2024/puzzles/day07/index.html index 316652695..7f9862b4a 100644 --- a/2024/puzzles/day07/index.html +++ b/2024/puzzles/day07/index.html @@ -5,12 +5,12 @@ Day 7: Bridge Repair | Scala Center Advent of Code - - + +
    -

    Day 7: Bridge Repair

    by @philippus

    Puzzle description

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

    Solution Summary

    1. Parse the input split the lines into the test value and the numbers on which we will operate
    2. Find the equations that can be made valid with the different operators (*, + and/or ||).
    3. Sum the test values of the equations that are possibly true

    Parsing

    Each line of the input contains a test value, followed by a colon and a space (: ) and a list of numbers seperated by +

    Day 7: Bridge Repair

    by @philippus

    Puzzle description

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

    Solution Summary

    1. Parse the input split the lines into the test value and the numbers on which we will operate
    2. Find the equations that can be made valid with the different operators (*, + and/or ||).
    3. Sum the test values of the equations that are possibly true

    Parsing

    Each line of the input contains a test value, followed by a colon and a space (: ) and a list of numbers seperated by a space. Our parser iterates over the lines, extracts the test values and a list of numbers from each line. Since we will be summing potentially a lot of numbers, we use a Long and not an Int.

    def parse(input: String): Seq[(Long, List[Long])] =
    input
    .linesIterator
    .map:
    case s"$testValue: $numbers" => (testValue.toLong, numbers.split(" ").map(_.toLong).toList)
    .toSeq

    Part 1

    To find the equations that could possibly be true we iterate over the equations and check each of them using a recursive checkEquation function. Its input is the test value and the list of numbers and its return value is a boolean. @@ -23,7 +23,7 @@ the checkEquation function.

    We add a withConcat parameter to the checkEquation function, so that we can use the same function for both parts 1 and 2.

    def checkEquation(testValue: Long, numbers: List[Long], withConcat: Boolean = false): Boolean =
    numbers match {
    case fst :: Nil =>
    fst == testValue
    case fst :: snd :: rest =>
    checkEquation(testValue, (fst * snd) :: rest, withConcat) || checkEquation(testValue, (fst + snd) :: rest, withConcat) ||
    (withConcat && checkEquation(testValue, (fst.toString ++ snd.toString).toLong :: rest, withConcat))
    }

    def part2(input: String): Long =
    val equations = parse(input)
    equations.map:
    case (testValue, numbers) =>
    if checkEquation(testValue, numbers, withConcat = true) then testValue else 0L
    .sum
    end part2

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day08/index.html b/2024/puzzles/day08/index.html index 697305dc3..706d1ecc5 100644 --- a/2024/puzzles/day08/index.html +++ b/2024/puzzles/day08/index.html @@ -5,12 +5,12 @@ Day 8: Resonant Collinearity | Scala Center Advent of Code - - + +
    -

    Day 8: Resonant Collinearity

    by @merlinorg

    Puzzle description

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

    Solution summary

    1. Parse the map to identify the locations of all the antennae, +

      Day 8: Resonant Collinearity

      by @merlinorg

      Puzzle description

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

      Solution summary

      1. Parse the map to identify the locations of all the antennae, grouped by their frequency.
      2. Find all the antinode locations within the map boundary.
      3. Count the number of distinct antinodes.

      Data model

      First, we'll define some case classes to represent the antenna map. We could use tuples, but the named classes help readability.

      Antenna map

      An antenna map contains the width and height of the map, along with a sequence of sequences of antenna locations. Each sequence of antenna @@ -34,7 +34,7 @@ Iterator.iterate to generate the infinite series of these locations and then just take while the location is within the map.

      def part2(input: String): String =
      val map = parse(input)

      val antinodes: Iterable[Location] = for
      antennaGroup <- map.antennaGroups
      case a +: b +: _ <- antennaGroup.combinations(2)
      antinode <- Iterator.iterate(a)(_ + (a - b)).takeWhile(_.within(map)) ++
      Iterator.iterate(b)(_ + (b - a)).takeWhile(_.within(map))
      yield antinode

      antinodes.toSet.size.toString
      end part2

      Solutions from the community

      Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day09/index.html b/2024/puzzles/day09/index.html index 6723b70a3..7717a8f93 100644 --- a/2024/puzzles/day09/index.html +++ b/2024/puzzles/day09/index.html @@ -5,13 +5,13 @@ Day 9: Disk Fragmenter | Scala Center Advent of Code - - + +
    -

    Day 9: Disk Fragmenter

    by @dyvrl

    Puzzle description

    https://adventofcode.com/2024/day/9

    Solution Summary

    1. Convert the input to a disk representation:
    • part1: A sequence of optional file indices
    • part2: A sequence of indivisible file/free-space blocks
    1. Create a compact representation of this disk: Starting from the end of the disk,
    • part1: Move individual file indices to the leftmost free space
    • part2: Move file blocks to the to the leftmost free block, if any
    1. Compute the checksum of the resulting disk

    Part 1

    Each part will define its own Disk type. For part1, this will simply be a Seq[Option[Int]], where each charater has an assigned value:

    • Some(index) for file blocks, with their corresponding index
    • None for free blocks

    Our main driver converts the input to a Disk, create a new compact Disk from it and finally computes its checksum

    def part1(input: String): Long =

    type Disk = Seq[Option[Int]]
    extension(disk: Disk)
    def checksum: Long = ???

    def createDisk(input: String): Disk = ???

    def compact(disk: Disk): Disk = ???

    val disk = createDisk(input)
    compact(disk).checksum

    Let's first implement checksum: It is the sum of each file ID times its position in the disk

    extension(disk: Disk)
    def checksum: Long = disk
    .zipWithIndex
    .map(_.getOrElse(0).toLong * _) // Free blocks are mapped to 0
    .sum

    To create our disk from the input, we need to:

    • Convert our input to a List[Int] ranging from 0 to 9
    • Group elements 2 by 2 to create pairs of (file, free) blocks count
    • Zip these groups with their indices
    • Unroll these pairs into an actual sequence with the correct number of elements
    • Concatenate these newly created sequences
    def createDisk(input: String): Disk =
    val intInput = input.toList.map(_ - '0') // Convert characters to int [0, 9]
    val fileFreeGroups = intInput.grouped(2).toVector // Last group will contain a single element
    val zippedGroups = fileFreeGroups.zipWithIndex
    zippedGroups.flatMap:
    case (List(fileN, freeN), i) =>
    // File block followed by free block
    List.fill(fileN)(Some(i)) ::: List.fill(freeN)(None)
    case (List(fileN), i) =>
    // Final file block
    List.fill(fileN)(Some(i))
    case _ => Nil

    Finally, we need to compact the disk we obtain: Iterate over the disk elements, from the beginning (left)

    • If we encounter a free block: replace it with the last element in the disk and repeat the recursion
    • If we encounter a file block: Append it to the result and continue with the next element in the disk

    All of this can be implemented using a tail-recursive function:

    def compact(disk: Disk): Disk =
    @tailrec
    def compactRec(disk: Disk, acc: Disk): Disk =
    if disk.isEmpty then
    acc
    else
    disk.head match
    case None => compactRec(disk.last +: disk.tail.init, acc) // Take the last element, put it first and eliminate free block
    case file@Some(_) => compactRec(disk.tail, acc :+ file) // Append the file block
    compactRec(disk, Vector.empty)

    Part 2

    The code remains very similar to part1. However this time, the Disk structure can't consider characters individually anymore. Consecutive file blocks are indivisible, they form a single Block. Thus, we define a new Block enumeration. All Blocks have a size, but Free blocks do not have any index attached whereas File blocks do:

    enum Block(val size: Int):
    case Free(s: Int) extends Block(s)
    case File(s: Int, i: Int) extends Block(s)

    def index = this match
    case Free(size) => None
    case File(size, id) => Some(id)

    The main driver for part2 has the same components as the one from part1:

    • checksum remains unchanged (except to convert our new Disk to the previous Disk)
    • createDisk produces a sequence of blocks instead of flattening every character into a single sequence
    def part2(input: String): Long =

    enum Block(val size: Int):
    case Free(s: Int) extends Block(s)
    case File(s: Int, i: Int) extends Block(s)

    def index = this match
    case Free(size) => None
    case File(size, id) => Some(id)
    // [...]

    type Disk = Seq[Block]
    extension(disk: Disk)
    def checksum: Long = disk
    .flatMap(b => Vector.fill(b.size)(b.index.getOrElse(0))) // Convert to previous `Disk`
    .zipWithIndex
    .map(_.toLong * _)
    .sum

    def createDisk(input: String): Disk =
    val intInput = input.toList.map(_ - '0') // Convert characters to int [0, 9]
    val fileFreeGroups = intInput.grouped(2).toVector // Last group will contain a single element
    val zippedGroups = fileFreeGroups.zipWithIndex
    zippedGroups.flatMap:
    case (List(fileN, freeN), id) =>
    Vector(Block.File(fileN, id), Block.Free(freeN))
    case (List(fileN), id) =>
    Vector(Block.File(fileN, id))
    case _ => Nil

    def compact(disk: Disk): Disk = ???

    val disk = createDisk(input)
    compact(disk).checksum

    This time, the compact method needs to keep contiguous file blocks in one piece. Iterate over the blocks of the disk, starting from the right:

    • If we encounter a free block, we prepend it to the result
    • If we encounter a file block, we find the leftmost free block large enough to insert file block:
      • If we couldn't find any such free block, prepend the file block to the result
      • Otherwise, insert the file block inside the found free block. This creates a new view of our disk that we will use for subsequent iterations. Prepend a free block of the same size as the file block to the result.

    Again, this can be implemented using a tail-recursive function:

    def compact(disk: Disk): Disk =
    @tailrec
    def compactRec(disk: Disk, acc: Disk): Disk =
    disk.lastOption match
    case None =>
    acc
    case Some(last@Block.Free(_)) =>
    // Free blocks are not moved
    compactRec(disk.init, last +: acc)
    case Some(last@Block.File(size, _)) =>
    // Find first block which can fit the file block
    val fitter = disk
    .zipWithIndex
    .find((block, _) => block.canInsert(last))

    fitter match
    case None =>
    // If it doesn't fit anywhere, don't move it
    compactRec(disk.init, last +: acc)
    case Some(free@Block.Free(_), id) =>
    // If it fits somewhere, insert inside this free block
    val newDisk = disk.take(id) ++ free.insert(last) ++ disk.drop(id+1).init
    compactRec(newDisk, Block.Free(last.size) +: acc)
    case _ => throw new MatchError("Unexpected block type")
    compactRec(disk, Vector.empty)

    Where we defined some auxiliary methods on Blocks to simplify the code:

    enum Block(val size: Int):
    // [...]
    def canInsert(block: Block) = this match
    case Free(size) => size >= block.size
    case _ => false

    extension (free: Block.Free)
    def insert(b: Block): Seq[Block] =
    if b.size < free.size then
    Seq(b, Block.Free(free.size-b.size))
    else
    Seq(b)

    Final code

    def part1(input: String): Long =

    type Disk = Seq[Option[Int]]
    extension(disk: Disk)
    def checksum: Long = disk
    .zipWithIndex
    .map(_.getOrElse(0).toLong * _) // Free blocks are mapped to 0
    .sum

    def createDisk(input: String): Disk =
    val intInput = input.toList.map(_ - '0') // Convert characters to int [0, 9]
    val fileFreeGroups = intInput.grouped(2).toVector // Last group will contain a single element
    val zippedGroups = fileFreeGroups.zipWithIndex
    val disk = zippedGroups.flatMap:
    case (List(fileN, freeN), i) =>
    // File block followed by free block
    List.fill(fileN)(Some(i)) ::: List.fill(freeN)(None)
    case (List(fileN), i) =>
    // Final file block
    List.fill(fileN)(Some(i))
    case _ => Nil
    return disk

    def compact(disk: Disk): Disk =
    @tailrec
    def compactRec(disk: Disk, acc: Disk): Disk =
    if disk.isEmpty then
    acc
    else
    disk.head match
    case None => compactRec(disk.last +: disk.tail.init, acc) // Take the last element, put it first and eliminate free block
    case file@Some(_) => compactRec(disk.tail, acc :+ file) // Append the file block
    compactRec(disk, Vector.empty)

    val disk = createDisk(input)
    compact(disk).checksum

    def part2(input: String): Long =

    enum Block(val size: Int):
    case Free(s: Int) extends Block(s)
    case File(s: Int, i: Int) extends Block(s)

    def index = this match
    case Free(size) => None
    case File(size, id) => Some(id)

    def canInsert(block: Block) = this match
    case Free(size) => size >= block.size
    case _ => false

    extension (free: Block.Free)
    def insert(b: Block): Seq[Block] =
    if b.size < free.size then
    Seq(b, Block.Free(free.size-b.size))
    else
    Seq(b)

    type Disk = Seq[Block]
    extension(disk: Disk)
    def checksum: Long = disk
    .flatMap(b => Vector.fill(b.size)(b.index.getOrElse(0))) // Convert to previous `Disk`
    .zipWithIndex
    .map(_.toLong * _)
    .sum

    def createDisk(input: String): Disk =
    val intInput = input.toList.map(_ - '0') // Convert characters to int [0, 9]
    val fileFreeGroups = intInput.grouped(2).toVector // Last group will contain a single element
    val zippedGroups = fileFreeGroups.zipWithIndex
    val disk = zippedGroups.flatMap:
    case (List(fileN, freeN), id) =>
    Vector(Block.File(fileN, id), Block.Free(freeN))
    case (List(fileN), id) =>
    Vector(Block.File(fileN, id))
    case _ => Nil
    return disk

    def compact(disk: Disk): Disk =
    @tailrec
    def compactRec(disk: Disk, acc: Disk): Disk = disk.lastOption match
    case None =>
    acc
    case Some(last@Block.Free(_)) =>
    // Free blocks are not moved
    compactRec(disk.init, last +: acc)
    case Some(last@Block.File(size, _)) =>
    // Find first block in which we can insert the file block
    val fitter = disk
    .zipWithIndex
    .find((block, _) => block.canInsert(last))

    fitter match
    case None =>
    // If it doesn't fit anywhere, don't move it
    compactRec(disk.init, last +: acc)
    case Some(free@Block.Free(_), id) =>
    // If it fits somewhere, insert inside this free block
    val newDisk = disk.take(id) ++ free.insert(last) ++ disk.drop(id+1).init
    compactRec(newDisk, Block.Free(last.size) +: acc)
    case _ => throw new MatchError("Unexpected block type")
    compactRec(disk, Vector.empty)

    val disk = createDisk(input)
    compact(disk).checksum

    Run it in the browser

    Part 1

    Part 2

    Solutions from the community

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

    - - +

    Day 9: Disk Fragmenter

    by @dyvrl

    Puzzle description

    https://adventofcode.com/2024/day/9

    Solution Summary

    1. Convert the input to a disk representation:
    • part1: A sequence of optional file indices
    • part2: A sequence of indivisible file/free-space blocks
    1. Create a compact representation of this disk: Starting from the end of the disk,
    • part1: Move individual file indices to the leftmost free space
    • part2: Move file blocks to the to the leftmost free block, if any
    1. Compute the checksum of the resulting disk

    Part 1

    Each part will define its own Disk type. For part1, this will simply be a Seq[Option[Int]], where each charater has an assigned value:

    • Some(index) for file blocks, with their corresponding index
    • None for free blocks

    Our main driver converts the input to a Disk, create a new compact Disk from it and finally computes its checksum

    def part1(input: String): Long =

    type Disk = Seq[Option[Int]]
    extension(disk: Disk)
    def checksum: Long = ???

    def createDisk(input: String): Disk = ???

    def compact(disk: Disk): Disk = ???

    val disk = createDisk(input)
    compact(disk).checksum

    Let's first implement checksum: It is the sum of each file ID times its position in the disk

    extension(disk: Disk)
    def checksum: Long = disk
    .zipWithIndex
    .map(_.getOrElse(0).toLong * _) // Free blocks are mapped to 0
    .sum

    To create our disk from the input, we need to:

    • Convert our input to a List[Int] ranging from 0 to 9
    • Group elements 2 by 2 to create pairs of (file, free) blocks count
    • Zip these groups with their indices
    • Unroll these pairs into an actual sequence with the correct number of elements
    • Concatenate these newly created sequences
    def createDisk(input: String): Disk =
    val intInput = input.toList.map(_ - '0') // Convert characters to int [0, 9]
    val fileFreeGroups = intInput.grouped(2).toVector // Last group will contain a single element
    val zippedGroups = fileFreeGroups.zipWithIndex
    zippedGroups.flatMap:
    case (List(fileN, freeN), i) =>
    // File block followed by free block
    List.fill(fileN)(Some(i)) ::: List.fill(freeN)(None)
    case (List(fileN), i) =>
    // Final file block
    List.fill(fileN)(Some(i))
    case _ => Nil

    Finally, we need to compact the disk we obtain: Iterate over the disk elements, from the beginning (left)

    • If we encounter a free block: replace it with the last element in the disk and repeat the recursion
    • If we encounter a file block: Append it to the result and continue with the next element in the disk

    All of this can be implemented using a tail-recursive function:

    def compact(disk: Disk): Disk =
    @tailrec
    def compactRec(disk: Disk, acc: Disk): Disk =
    if disk.isEmpty then
    acc
    else
    disk.head match
    case None => compactRec(disk.last +: disk.tail.init, acc) // Take the last element, put it first and eliminate free block
    case file@Some(_) => compactRec(disk.tail, acc :+ file) // Append the file block
    compactRec(disk, Vector.empty)

    Part 2

    The code remains very similar to part1. However this time, the Disk structure can't consider characters individually anymore. Consecutive file blocks are indivisible, they form a single Block. Thus, we define a new Block enumeration. All Blocks have a size, but Free blocks do not have any index attached whereas File blocks do:

    enum Block(val size: Int):
    case Free(s: Int) extends Block(s)
    case File(s: Int, i: Int) extends Block(s)

    def index = this match
    case Free(size) => None
    case File(size, id) => Some(id)

    The main driver for part2 has the same components as the one from part1:

    • checksum remains unchanged (except to convert our new Disk to the previous Disk)
    • createDisk produces a sequence of blocks instead of flattening every character into a single sequence
    def part2(input: String): Long =

    enum Block(val size: Int):
    case Free(s: Int) extends Block(s)
    case File(s: Int, i: Int) extends Block(s)

    def index = this match
    case Free(size) => None
    case File(size, id) => Some(id)
    // [...]

    type Disk = Seq[Block]
    extension(disk: Disk)
    def checksum: Long = disk
    .flatMap(b => Vector.fill(b.size)(b.index.getOrElse(0))) // Convert to previous `Disk`
    .zipWithIndex
    .map(_.toLong * _)
    .sum

    def createDisk(input: String): Disk =
    val intInput = input.toList.map(_ - '0') // Convert characters to int [0, 9]
    val fileFreeGroups = intInput.grouped(2).toVector // Last group will contain a single element
    val zippedGroups = fileFreeGroups.zipWithIndex
    zippedGroups.flatMap:
    case (List(fileN, freeN), id) =>
    Vector(Block.File(fileN, id), Block.Free(freeN))
    case (List(fileN), id) =>
    Vector(Block.File(fileN, id))
    case _ => Nil

    def compact(disk: Disk): Disk = ???

    val disk = createDisk(input)
    compact(disk).checksum

    This time, the compact method needs to keep contiguous file blocks in one piece. Iterate over the blocks of the disk, starting from the right:

    • If we encounter a free block, we prepend it to the result
    • If we encounter a file block, we find the leftmost free block large enough to insert file block:
      • If we couldn't find any such free block, prepend the file block to the result
      • Otherwise, insert the file block inside the found free block. This creates a new view of our disk that we will use for subsequent iterations. Prepend a free block of the same size as the file block to the result.

    Again, this can be implemented using a tail-recursive function:

    def compact(disk: Disk): Disk =
    @tailrec
    def compactRec(disk: Disk, acc: Disk): Disk =
    disk.lastOption match
    case None =>
    acc
    case Some(last@Block.Free(_)) =>
    // Free blocks are not moved
    compactRec(disk.init, last +: acc)
    case Some(last@Block.File(size, _)) =>
    // Find first block which can fit the file block
    val fitter = disk
    .zipWithIndex
    .find((block, _) => block.canInsert(last))

    fitter match
    case None =>
    // If it doesn't fit anywhere, don't move it
    compactRec(disk.init, last +: acc)
    case Some(free@Block.Free(_), id) =>
    // If it fits somewhere, insert inside this free block
    val newDisk = disk.take(id) ++ free.insert(last) ++ disk.drop(id+1).init
    compactRec(newDisk, Block.Free(last.size) +: acc)
    case _ => throw new MatchError("Unexpected block type")
    compactRec(disk, Vector.empty)

    Where we defined some auxiliary methods on Blocks to simplify the code:

    enum Block(val size: Int):
    // [...]
    def canInsert(block: Block) = this match
    case Free(size) => size >= block.size
    case _ => false

    extension (free: Block.Free)
    def insert(b: Block): Seq[Block] =
    if b.size < free.size then
    Seq(b, Block.Free(free.size-b.size))
    else
    Seq(b)

    Final code

    def part1(input: String): Long =

    type Disk = Seq[Option[Int]]
    extension(disk: Disk)
    def checksum: Long = disk
    .zipWithIndex
    .map(_.getOrElse(0).toLong * _) // Free blocks are mapped to 0
    .sum

    def createDisk(input: String): Disk =
    val intInput = input.toList.map(_ - '0') // Convert characters to int [0, 9]
    val fileFreeGroups = intInput.grouped(2).toVector // Last group will contain a single element
    val zippedGroups = fileFreeGroups.zipWithIndex
    val disk = zippedGroups.flatMap:
    case (List(fileN, freeN), i) =>
    // File block followed by free block
    List.fill(fileN)(Some(i)) ::: List.fill(freeN)(None)
    case (List(fileN), i) =>
    // Final file block
    List.fill(fileN)(Some(i))
    case _ => Nil
    return disk

    def compact(disk: Disk): Disk =
    @tailrec
    def compactRec(disk: Disk, acc: Disk): Disk =
    if disk.isEmpty then
    acc
    else
    disk.head match
    case None => compactRec(disk.last +: disk.tail.init, acc) // Take the last element, put it first and eliminate free block
    case file@Some(_) => compactRec(disk.tail, acc :+ file) // Append the file block
    compactRec(disk, Vector.empty)

    val disk = createDisk(input)
    compact(disk).checksum

    def part2(input: String): Long =

    enum Block(val size: Int):
    case Free(s: Int) extends Block(s)
    case File(s: Int, i: Int) extends Block(s)

    def index = this match
    case Free(size) => None
    case File(size, id) => Some(id)

    def canInsert(block: Block) = this match
    case Free(size) => size >= block.size
    case _ => false

    extension (free: Block.Free)
    def insert(b: Block): Seq[Block] =
    if b.size < free.size then
    Seq(b, Block.Free(free.size-b.size))
    else
    Seq(b)

    type Disk = Seq[Block]
    extension(disk: Disk)
    def checksum: Long = disk
    .flatMap(b => Vector.fill(b.size)(b.index.getOrElse(0))) // Convert to previous `Disk`
    .zipWithIndex
    .map(_.toLong * _)
    .sum

    def createDisk(input: String): Disk =
    val intInput = input.toList.map(_ - '0') // Convert characters to int [0, 9]
    val fileFreeGroups = intInput.grouped(2).toVector // Last group will contain a single element
    val zippedGroups = fileFreeGroups.zipWithIndex
    val disk = zippedGroups.flatMap:
    case (List(fileN, freeN), id) =>
    Vector(Block.File(fileN, id), Block.Free(freeN))
    case (List(fileN), id) =>
    Vector(Block.File(fileN, id))
    case _ => Nil
    return disk

    def compact(disk: Disk): Disk =
    @tailrec
    def compactRec(disk: Disk, acc: Disk): Disk = disk.lastOption match
    case None =>
    acc
    case Some(last@Block.Free(_)) =>
    // Free blocks are not moved
    compactRec(disk.init, last +: acc)
    case Some(last@Block.File(size, _)) =>
    // Find first block in which we can insert the file block
    val fitter = disk
    .zipWithIndex
    .find((block, _) => block.canInsert(last))

    fitter match
    case None =>
    // If it doesn't fit anywhere, don't move it
    compactRec(disk.init, last +: acc)
    case Some(free@Block.Free(_), id) =>
    // If it fits somewhere, insert inside this free block
    val newDisk = disk.take(id) ++ free.insert(last) ++ disk.drop(id+1).init
    compactRec(newDisk, Block.Free(last.size) +: acc)
    case _ => throw new MatchError("Unexpected block type")
    compactRec(disk, Vector.empty)

    val disk = createDisk(input)
    compact(disk).checksum

    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/2024/puzzles/day10/index.html b/2024/puzzles/day10/index.html index 2355e2eed..68b3d8c6a 100644 --- a/2024/puzzles/day10/index.html +++ b/2024/puzzles/day10/index.html @@ -5,12 +5,12 @@ Day 10: Hoof It | Scala Center Advent of Code - - + +
    -

    Day 10: Hoof It

    by @SethTisue

    Puzzle description

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

    Summary

    Like many Advent of Code puzzles, this is a graph search problem. +

    Day 10: Hoof It

    by @SethTisue

    Puzzle description

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

    Summary

    Like many Advent of Code puzzles, this is a graph search problem. Such problems are highly amenable to recursive solutions.

    In large graphs, it may be necessary to memoize intermediate results in order to get good performance. But here, it turns out that the graphs are small enough that recursion alone does the job just fine.

    Shared code

    Let's start by representing coordinate pairs:

      type Pos = (Int, Int)
    extension (pos: Pos)
    def +(other: Pos): Pos =
    (pos(0) + other(0), pos(1) + other(1))

    and the input grid:

      type Topo = Vector[Vector[Int]]
    extension (topo: Topo)
    def apply(pos: Pos): Int =
    topo(pos(0))(pos(1))
    def inBounds(pos: Pos): Boolean =
    pos(0) >= 0 && pos(0) < topo.size &&
    pos(1) >= 0 && pos(1) < topo.head.size
    def positions =
    for row <- topo.indices
    column <- topo.head.indices
    yield (row, column)

    So far this is all quite typical code that is usable in many @@ -36,7 +36,7 @@ on a Set, the result will also be a Set. But we don't want to throw away duplicate counts.

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day11/index.html b/2024/puzzles/day11/index.html index 6a20dc495..c593e819d 100644 --- a/2024/puzzles/day11/index.html +++ b/2024/puzzles/day11/index.html @@ -5,18 +5,18 @@ Day 11: Plutonian Pebbles | Scala Center Advent of Code - - + +
    -

    Day 11: Plutonian Pebbles

    by @bishabosha

    Puzzle description

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

    Solution Summary

    This problem is a great fit for Scala's immutable collections library and pattern matching feature.

    In short, both parts of the problem are identical, asking you to transform a sequence of integers in steps, where in each step each integer is transformed to 1 or 2 new values. You are then tasked to find the total size (i.e. number of stones in the sequence) after a certain number of steps.

    • For part 1, you are told that order matters, so to find the solution, iterate the row of stones as the rules describe - by in each step replacing the stone in the same position by the new value/values.
    • In part 2, it becomes clear that exponential growth when doubling odd-digit-length stones is not sustainable to simulate, so optimise the data representation before iteration.
      • As the problem only asks for the number of stones in total, and transformations only consider the current stone (i.e. no look ahead or behind) - the order does not matter in reality.
      • With some data analysis, by simulating some iterations, it becomes clear that there are many duplicated stones.
      • Therefore for a major space optimisation you only need to consider the frequency of each distinct value.

    Part 1

    The input data is a single line of integer numbers, separated by spaces ' '. Here is a way to parse that using .split and .toLong on strings:

    def parse(input: String): Seq[Long] =
    input.split(" ").map(_.toLong).toSeq

    The next part of the problem is to iterate the sequence of stones after a blink:

    def blink(stones: Seq[Long]): Seq[Long] =
    stones.flatMap:
    case 0 => 1 :: Nil // x)
    case EvenDigits(a, b) => a :: b :: Nil // y)
    case other => other * 2024 :: Nil // z)

    Here you can use .flatMap on a sequence to transform each element into 0 or more elements, which are then concatenated at the end to make a single "flattened" sequence.

    The argument to flatMap is function where you pattern match on each element as follows:

    • x) for 0, map to [1]
    • y) for a number with even digits, split its digits into two numbers a and b, mapping to [a,b]
    • z) for any other number, map to [other * 2024]
    info

    To better illustrate how flatMap works, here is how to duplicate each element:

    Seq(1,2,3).flatMap(i => i :: i :: Nil) // for `i`, map to `[i, i]`

    results in

    Seq(1,1,2,2,3,3)

    The code above uses the pattern EvenDigits(a, b), which makes use of a Scala feature called extractor objects, which lets you define your own custom patterns!

    Let's see how it works:

    object EvenDigits:
    def unapply(n: Long): Option[(Long, Long)] =
    splitDigits(n)

    so I defined an object EvenDigits with an unapply method, its parameter (n: Long) means it can match on Long typed values. The result Option[(Long, Long)] means the pattern will either not match, or will match and extract two Long values.

    Its implementation forwards to the splitDigits method which you can define as follows:

    def splitDigits(n: Long): Option[(Long, Long)] =
    val digits = Iterator // x)
    .unfold(n):
    case 0 => None
    case i => Some((i % 10, i / 10))
    .toArray
    if digits.size % 2 == 0 then // y)
    val (a, b) = digits.reverse.splitAt(digits.size / 2)
    Some((mergeDigits(a), mergeDigits(b)))
    else None

    def mergeDigits(digits: Array[Long]): Long =
    digits.foldLeft(0L): (acc, digit) => // z)
    acc * 10 + digit

    The function works in three parts:

    • x) Convert the input n into an array of digits. You can use Iterator.unfold for this purpose, where you start with some state, then iterate until either the sequence should stop (return None), or return a pair of the next value of the iterator on the left, and the next state on the right. This will produce the digits in reverse order (starting with unit column on the left)
    • y) If the digits are of even length, then reverse the digits, split in the middle, and merge each part back to a single number
    • z) merge the digits back to a single Long, with the inverse of the function used to split them.

    After defining the blink function, you can now run the simulation for the required steps, in this case 25:

    def part1(input: String): Int =
    val stones0 = parse(input)
    val stones1 =
    Iterator
    .iterate(stones0)(blink)
    .drop(25)
    .next
    stones1.size

    In the code above, use the Iterator.iterate(s)(f) method to repeatadly apply a function f to some state s, returning the next state. In this case use the parsed stones for state, and the blink function.

    Then to get the state after 25 iterations, drop 25 elements (which includes the initial state, i.e. before any blinks), and take the next value. This returns the stone sequence after 25 blinks. Then call size to get the number of stones (i.e. the length of the sequence).

    Part 2

    All part 2 asked to do is to run the same simulation, but instead for 75 steps. This does not work, likely intentionally, as the required space to represent the sequence grows exponentially, surpassing much more than any conventional home computer.

    So, somehow you need to compress the space required to represent the sequence. There are a couple of options:

    • a) bin-packing: represent each element by 2 numbers [a1,n1,b1,n2,..., i.e. the value, and then the number of times it appears consecutively. This preserves order, but does not work well if duplicates are not close together. (such as with this problem)
    • b) frequency map: only store the distinct numbers in the sequence, associated to their total count. This is not a sorted representation. HOWEVER you actually do not need to consider order, despite what the problem hints at. The answer only requires the total stones in the sequence (i.e. forgets order), and also each stone is transformed independently when there is a blink, so there is no interaction between earlier or later stones.

    In this case, I used a frequency map, represented with the type Map[Long, Long].

    so how does blink change?

    This time, instead of iterating through each stone in the sequence, you should consider all the stones of a certain value at once. This means you can no longer consider each transformation independently.

    What you can do instead is for each kind of stone, update the totals that will appear after the blink, and the blink won't be considered done until you process each kind of stone that appeared in the last step.

    So to accomplish this, use foldLeft, which within a single blink step will let us iterate the state of the stones. You can even use the same stones value as the initial state because it is immutable - i.e. each update will create a new copy with the necessary changes. Here is the result:

    def blinkUnordered(stones: Map[Long, Long]): Map[Long, Long] =
    stones.foldLeft(stones): (nextStones, stone) =>
    stone match
    case (0, n) => nextStones.diff(0, -n).diff(1, n)
    case (old @ EvenDigits(a, b), n) => nextStones.diff(old, -n).diff(a, n).diff(b, n)
    case (other, n) => nextStones.diff(other, -n).diff(other * 2024, n)

    You still have to define the diff method, but to explain what changed since part 1: now consider the key-value association of each stone to the count in the map. +

    Day 11: Plutonian Pebbles

    by @bishabosha

    Puzzle description

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

    Solution Summary

    This problem is a great fit for Scala's immutable collections library and pattern matching feature.

    In short, both parts of the problem are identical, asking you to transform a sequence of integers in steps, where in each step each integer is transformed to 1 or 2 new values. You are then tasked to find the total size (i.e. number of stones in the sequence) after a certain number of steps.

    • For part 1, you are told that order matters, so to find the solution, iterate the row of stones as the rules describe - by in each step replacing the stone in the same position by the new value/values.
    • In part 2, it becomes clear that exponential growth when doubling odd-digit-length stones is not sustainable to simulate, so optimise the data representation before iteration.
      • As the problem only asks for the number of stones in total, and transformations only consider the current stone (i.e. no look ahead or behind) - the order does not matter in reality.
      • With some data analysis, by simulating some iterations, it becomes clear that there are many duplicated stones.
      • Therefore for a major space optimisation you only need to consider the frequency of each distinct value.

    Part 1

    The input data is a single line of integer numbers, separated by spaces ' '. Here is a way to parse that using .split and .toLong on strings:

    def parse(input: String): Seq[Long] =
    input.split(" ").map(_.toLong).toSeq

    The next part of the problem is to iterate the sequence of stones after a blink:

    def blink(stones: Seq[Long]): Seq[Long] =
    stones.flatMap:
    case 0 => 1 :: Nil // x)
    case EvenDigits(a, b) => a :: b :: Nil // y)
    case other => other * 2024 :: Nil // z)

    Here you can use .flatMap on a sequence to transform each element into 0 or more elements, which are then concatenated at the end to make a single "flattened" sequence.

    The argument to flatMap is function where you pattern match on each element as follows:

    • x) for 0, map to [1]
    • y) for a number with even digits, split its digits into two numbers a and b, mapping to [a,b]
    • z) for any other number, map to [other * 2024]
    info

    To better illustrate how flatMap works, here is how to duplicate each element:

    Seq(1,2,3).flatMap(i => i :: i :: Nil) // for `i`, map to `[i, i]`

    results in

    Seq(1,1,2,2,3,3)

    The code above uses the pattern EvenDigits(a, b), which makes use of a Scala feature called extractor objects, which lets you define your own custom patterns!

    Let's see how it works:

    object EvenDigits:
    def unapply(n: Long): Option[(Long, Long)] =
    splitDigits(n)

    so I defined an object EvenDigits with an unapply method, its parameter (n: Long) means it can match on Long typed values. The result Option[(Long, Long)] means the pattern will either not match, or will match and extract two Long values.

    Its implementation forwards to the splitDigits method which you can define as follows:

    def splitDigits(n: Long): Option[(Long, Long)] =
    val digits = Iterator // x)
    .unfold(n):
    case 0 => None
    case i => Some((i % 10, i / 10))
    .toArray
    if digits.size % 2 == 0 then // y)
    val (a, b) = digits.reverse.splitAt(digits.size / 2)
    Some((mergeDigits(a), mergeDigits(b)))
    else None

    def mergeDigits(digits: Array[Long]): Long =
    digits.foldLeft(0L): (acc, digit) => // z)
    acc * 10 + digit

    The function works in three parts:

    • x) Convert the input n into an array of digits. You can use Iterator.unfold for this purpose, where you start with some state, then iterate until either the sequence should stop (return None), or return a pair of the next value of the iterator on the left, and the next state on the right. This will produce the digits in reverse order (starting with unit column on the left)
    • y) If the digits are of even length, then reverse the digits, split in the middle, and merge each part back to a single number
    • z) merge the digits back to a single Long, with the inverse of the function used to split them.

    After defining the blink function, you can now run the simulation for the required steps, in this case 25:

    def part1(input: String): Int =
    val stones0 = parse(input)
    val stones1 =
    Iterator
    .iterate(stones0)(blink)
    .drop(25)
    .next
    stones1.size

    In the code above, use the Iterator.iterate(s)(f) method to repeatadly apply a function f to some state s, returning the next state. In this case use the parsed stones for state, and the blink function.

    Then to get the state after 25 iterations, drop 25 elements (which includes the initial state, i.e. before any blinks), and take the next value. This returns the stone sequence after 25 blinks. Then call size to get the number of stones (i.e. the length of the sequence).

    Part 2

    All part 2 asked to do is to run the same simulation, but instead for 75 steps. This does not work, likely intentionally, as the required space to represent the sequence grows exponentially, surpassing much more than any conventional home computer.

    So, somehow you need to compress the space required to represent the sequence. There are a couple of options:

    • a) bin-packing: represent each element by 2 numbers [a1,n1,b1,n2,..., i.e. the value, and then the number of times it appears consecutively. This preserves order, but does not work well if duplicates are not close together. (such as with this problem)
    • b) frequency map: only store the distinct numbers in the sequence, associated to their total count. This is not a sorted representation. HOWEVER you actually do not need to consider order, despite what the problem hints at. The answer only requires the total stones in the sequence (i.e. forgets order), and also each stone is transformed independently when there is a blink, so there is no interaction between earlier or later stones.

    In this case, I used a frequency map, represented with the type Map[Long, Long].

    so how does blink change?

    This time, instead of iterating through each stone in the sequence, you should consider all the stones of a certain value at once. This means you can no longer consider each transformation independently.

    What you can do instead is for each kind of stone, update the totals that will appear after the blink, and the blink won't be considered done until you process each kind of stone that appeared in the last step.

    So to accomplish this, use foldLeft, which within a single blink step will let us iterate the state of the stones. You can even use the same stones value as the initial state because it is immutable - i.e. each update will create a new copy with the necessary changes. Here is the result:

    def blinkUnordered(stones: Map[Long, Long]): Map[Long, Long] =
    stones.foldLeft(stones): (nextStones, stone) =>
    stone match
    case (0, n) => nextStones.diff(0, -n).diff(1, n)
    case (old @ EvenDigits(a, b), n) => nextStones.diff(old, -n).diff(a, n).diff(b, n)
    case (other, n) => nextStones.diff(other, -n).diff(other * 2024, n)

    You still have to define the diff method, but to explain what changed since part 1: now consider the key-value association of each stone to the count in the map. In each update, you remove n of the old stone, and add n of each new stone.

    You can define diff as an extension method on Map like this:

    extension (stones: Map[Long, Long])
    def diff(stone: Long, change: Long): Map[Long, Long] =
    stones.updatedWith(stone):
    case None => Some(change)
    case Some(n) =>
    val n0 = n + change
    if n0 == 0 then None else Some(n0)

    To put it all together, now you have to convert the parsed stone sequence into a frequency map. To do that, you can use the groupBy function, which buckets values by the result of a function. If you use identity as the function, then the values themselves will be the key. The resulting map's values will now be sequences of the same number, so you can transform the map to convert the values to the size (i.e. the frequency).

    Then you can again use Iterator.iterate to evolve the frequency map, now using the blinkUnordered function 75 times. The resulting value is still a frequency map, so you can get the total stone count by taking the sum of just the values of the map (i.e. forgetting the keys).

    def part2(input: String): Long =
    val stones0 =
    parse(input)
    .groupBy(identity)
    .map((k, v) => (k, v.size.toLong))
    val stones1 =
    Iterator
    .iterate(stones0)(blinkUnordered)
    .drop(75)
    .next
    stones1.values.sum

    Final Code

    def part1(input: String): Int =
    val stones0 = parse(input)
    val stones1 =
    Iterator
    .iterate(stones0)(blink)
    .drop(25)
    .next
    stones1.size

    def parse(input: String): Seq[Long] =
    input.split(" ").map(_.toLong).toSeq

    def blink(stones: Seq[Long]): Seq[Long] =
    stones.flatMap:
    case 0 => 1 :: Nil
    case EvenDigits(a, b) => a :: b :: Nil
    case other => other * 2024 :: Nil

    object EvenDigits:
    def unapply(n: Long): Option[(Long, Long)] =
    splitDigits(n)

    def splitDigits(n: Long): Option[(Long, Long)] =
    val digits = Iterator
    .unfold(n):
    case 0 => None
    case i => Some((i % 10, i / 10))
    .toArray
    if digits.size % 2 == 0 then
    val (a, b) = digits.reverse.splitAt(digits.size / 2)
    Some((mergeDigits(a), mergeDigits(b)))
    else None

    def mergeDigits(digits: Array[Long]): Long =
    digits.foldLeft(0L): (acc, digit) =>
    acc * 10 + digit

    def part2(input: String): Long =
    val stones0 =
    parse(input)
    .groupBy(identity)
    .map((k, v) => (k, v.size.toLong))
    val stones1 =
    Iterator
    .iterate(stones0)(blinkUnordered)
    .drop(75)
    .next
    stones1.values.sum

    def blinkUnordered(stones: Map[Long, Long]): Map[Long, Long] =
    stones.foldLeft(stones): (nextStones, stone) =>
    stone match
    case (0, n) => nextStones.diff(0, -n).diff(1, n)
    case (old @ EvenDigits(a, b), n) => nextStones.diff(old, -n).diff(a, n).diff(b, n)
    case (other, n) => nextStones.diff(other, -n).diff(other * 2024, n)

    extension (stones: Map[Long, Long])
    def diff(stone: Long, change: Long): Map[Long, Long] =
    stones.updatedWith(stone):
    case None => Some(change)
    case Some(n) =>
    val n0 = n + change
    if n0 == 0 then None else Some(n0)

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day12/index.html b/2024/puzzles/day12/index.html index 84678ef3e..ab6b8d32e 100644 --- a/2024/puzzles/day12/index.html +++ b/2024/puzzles/day12/index.html @@ -5,17 +5,17 @@ Day 12: Garden Groups | Scala Center Advent of Code - - + +
    -

    Day 12: Garden Groups

    by @TheDrawingCoder-Gamer

    Puzzle description

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

    Solution Summary

    1. Convert the input into a vector of strings
    2. Get the regions of the input
    3. Calculate the price of each region
    • part1: Calculate area and perimeter and multiply them together
    • part2: Calculate area and number of sides and multiply them together
    1. Sum prices

    Part 1

    First, let's make a wrapper class for the Vector[String]

    case class PlantMap(plants: Vector[String]) {
    val height: Int = plants.size
    val width: Int = plants.head.length

    def apply(x: Int, y: Int): Char = {
    plants(y)(x)
    }

    def isDefinedAt(x: Int, y: Int): Boolean = {
    x >= 0 && x < width && y >= 0 && y < height
    }

    def get(x: Int, y: Int): Option[Char] = {
    Option.when(isDefinedAt(x, y))(apply(x, y))
    }
    }

    Then, let's parse the input:

    def parse(str: String): PlantMap = PlantMap(str.linesIterator.toVector)

    Next, let's get the regions for the input. The puzzle text explictly states that regions that are seperated are different regions, so we have to use flood fill.

    Here's a simple flood fill implementation for PlantMap:

    import scala.collection.mutable as mut

    type Region = Vector[(Int, Int)]
    def cardinalPositions(x: Int, y: Int): List[(Int, Int)] = {
    List((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1))
    }

    case class PlantMap(plants: Vector[String]) {
    // ...
    def floodFill(x: Int, y: Int): Region = {
    val q = mut.Queue[(Int, Int)]()
    val char = apply(x, y)
    val res = mut.ListBuffer[(Int, Int)]()
    q.addOne((x, y))
    while (q.nonEmpty) {
    val n = q.removeHead()
    if (get(n._1, n._2).contains(char) && !res.contains(n)) {
    res.prepend(n)
    q.addAll(cardinalPositions(n._1, n._2))
    }
    }
    res.toVector
    }
    }

    This can then be used to get all the regions:

    case class PlantMap(plants: Vector[String]) {
    def indices: Vector[(Int, Int)] = {
    (for {
    y <- 0 until height
    x <- 0 until width
    } yield (x, y)).toVector
    }
    // ...
    def regions: List[Region] = {
    List.unfold[Vector[(Int, Int)], Vector[(Int, Int)]](this.indices) { acc =>
    acc.headOption.map { head =>
    val points = floodFill(head._1, head._2)
    (points, acc.diff(points))
    }
    }
    }

    }

    It's also useful now to define a converter from regions to their own map. This lets us avoid having to know the character.

    extension (region: Region) {
    def asPlantMap: Vector[String] = {
    val maxX = region.maxBy(_._1)._1
    val maxY = region.maxBy(_._2)._2
    val res = mut.ArrayBuffer.fill(maxY + 1, maxX + 1)('.')
    region.foreach { (x, y) =>
    res(y)(x) = '#'
    }
    PlantMap(res.map(_.mkString("", "", "")).toVector)
    }
    }

    Then calculate perimeter of the regions, and solve part 1:

    case class PlantMap(plants: Vector[String]) {
    // ...
    def optionalCardinalNeighbors(x: Int, y: Int): List[Option[Char]] = {
    cardinalPositions(x, y).map(get)
    }
    }

    extension (region: Region) {
    // ...
    def area: Int = region.size
    def perimeter: Int = {
    val regionMap = region.asPlantMap
    region.map((x, y) => regionMap.optionalCardinalNeighbors(x, y).count(_.forall(_ != '#'))).sum
    }
    }

    def part1(input: String): Int = {
    val plants = parse(input)

    plants.regions.map(r => r.area * r.perimeter).sum
    }

    Part 2

    The hard part of this one is finding out how to efficiently count the number of sides in a region. +

    Day 12: Garden Groups

    by @TheDrawingCoder-Gamer

    Puzzle description

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

    Solution Summary

    1. Convert the input into a vector of strings
    2. Get the regions of the input
    3. Calculate the price of each region
    • part1: Calculate area and perimeter and multiply them together
    • part2: Calculate area and number of sides and multiply them together
    1. Sum prices

    Part 1

    First, let's make a wrapper class for the Vector[String]

    case class PlantMap(plants: Vector[String]) {
    val height: Int = plants.size
    val width: Int = plants.head.length

    def apply(x: Int, y: Int): Char = {
    plants(y)(x)
    }

    def isDefinedAt(x: Int, y: Int): Boolean = {
    x >= 0 && x < width && y >= 0 && y < height
    }

    def get(x: Int, y: Int): Option[Char] = {
    Option.when(isDefinedAt(x, y))(apply(x, y))
    }
    }

    Then, let's parse the input:

    def parse(str: String): PlantMap = PlantMap(str.linesIterator.toVector)

    Next, let's get the regions for the input. The puzzle text explictly states that regions that are seperated are different regions, so we have to use flood fill.

    Here's a simple flood fill implementation for PlantMap:

    import scala.collection.mutable as mut

    type Region = Vector[(Int, Int)]
    def cardinalPositions(x: Int, y: Int): List[(Int, Int)] = {
    List((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1))
    }

    case class PlantMap(plants: Vector[String]) {
    // ...
    def floodFill(x: Int, y: Int): Region = {
    val q = mut.Queue[(Int, Int)]()
    val char = apply(x, y)
    val res = mut.ListBuffer[(Int, Int)]()
    q.addOne((x, y))
    while (q.nonEmpty) {
    val n = q.removeHead()
    if (get(n._1, n._2).contains(char) && !res.contains(n)) {
    res.prepend(n)
    q.addAll(cardinalPositions(n._1, n._2))
    }
    }
    res.toVector
    }
    }

    This can then be used to get all the regions:

    case class PlantMap(plants: Vector[String]) {
    def indices: Vector[(Int, Int)] = {
    (for {
    y <- 0 until height
    x <- 0 until width
    } yield (x, y)).toVector
    }
    // ...
    def regions: List[Region] = {
    List.unfold[Vector[(Int, Int)], Vector[(Int, Int)]](this.indices) { acc =>
    acc.headOption.map { head =>
    val points = floodFill(head._1, head._2)
    (points, acc.diff(points))
    }
    }
    }

    }

    It's also useful now to define a converter from regions to their own map. This lets us avoid having to know the character.

    extension (region: Region) {
    def asPlantMap: Vector[String] = {
    val maxX = region.maxBy(_._1)._1
    val maxY = region.maxBy(_._2)._2
    val res = mut.ArrayBuffer.fill(maxY + 1, maxX + 1)('.')
    region.foreach { (x, y) =>
    res(y)(x) = '#'
    }
    PlantMap(res.map(_.mkString("", "", "")).toVector)
    }
    }

    Then calculate perimeter of the regions, and solve part 1:

    case class PlantMap(plants: Vector[String]) {
    // ...
    def optionalCardinalNeighbors(x: Int, y: Int): List[Option[Char]] = {
    cardinalPositions(x, y).map(get)
    }
    }

    extension (region: Region) {
    // ...
    def area: Int = region.size
    def perimeter: Int = {
    val regionMap = region.asPlantMap
    region.map((x, y) => regionMap.optionalCardinalNeighbors(x, y).count(_.forall(_ != '#'))).sum
    }
    }

    def part1(input: String): Int = {
    val plants = parse(input)

    plants.regions.map(r => r.area * r.perimeter).sum
    }

    Part 2

    The hard part of this one is finding out how to efficiently count the number of sides in a region. Thankfully, there is a fun math fact that can help here: The number of sides in a polygon is equal to the number of corners. So all we have to do is count the number of corners in a region and we will get the number of sides.

    Finding corners in a 1x1 integer grid is hard, but doubling the size of the grid reduces the amount of cases we have to check.

    Doubling the grid lets you inspect each corner of each block individually. Through experimentation in a pixel editor, you can find that when using 2x2 squares aligned to a 2x2 grid, there are only a few number of neighbors each pixel can have.

    Here are those cases outlined:

    • Not a corner, internal (8)
    • Not a corner, Edge (5)
    • Not a corner, adjacent to "concave-like" corner (6)
    • A corner, "convex-like" (3)
    • A corner, "concave-like" (7)
    • A corner, "convex like" with diagonal neighbor (4)
    ..........
    ...3553...
    ...5886...
    ...588763.
    ...455553.
    .34.......
    .33.......

    and the same region with the corners marked:

    ..........
    ...X##X...
    ...####...
    ...###X#X.
    ...X####X.
    .XX.......
    .XX.......

    Let's add an extension to double the region:

    extension (region: Region) {
    // ...
    def inflate: Region = {
    region.flatMap((x, y) => List((x * 2, y * 2), (x * 2 + 1, y * 2), (x * 2, y * 2 + 1), (x * 2 + 1, y * 2 + 1)))
    }
    }

    Next, let's actually count the sides in the region:

    def neighborPositions(ix: Int, iy: Int): List[(Int, Int)] = {
    (ix - 1 to ix + 1).flatMap { x =>
    (iy - 1 to iy + 1).flatMap { y =>
    Option.when(x != ix || y != iy)((x, y))
    }
    }.toList
    }

    case class PlantMap(plants: Vector[String]) {
    def optionalNeighbors(x: Int, y: Int): List[Option[Char]] = {
    neighborPositions(x, y).map(get)
    }
    }

    extension (region: Region) {
    // ...
    def sides: Int = {
    val bigRegion = region.inflate
    val regionMap = PlantMap.fromRegion(bigRegion)
    bigRegion.count { (x, y) =>
    val neighborCount = regionMap.optionalNeighbors(x, y).count(_.contains('#'))
    neighborCount match {
    case 3 | 4 | 7 => true
    case _ => false
    }
    }
    }
    }

    Then we can price the regions and solve part 2:

    def part2(input: String): Int = {
    val plants = parse(input)

    plants.regions.map(r => r.area * r.sides).sum
    }

    Final code:

    import scala.collection.mutable as mut

    type Region = Vector[(Int, Int)]
    def cardinalPositions(x: Int, y: Int): List[(Int, Int)] = {
    List((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1))
    }

    def neighborPositions(ix: Int, iy: Int): List[(Int, Int)] = {
    (ix - 1 to ix + 1).flatMap { x =>
    (iy - 1 to iy + 1).flatMap { y =>
    Option.when(x != ix || y != iy)((x, y))
    }
    }.toList
    }

    extension (region: Region) {
    def asPlantMap: PlantMap = {
    val maxX = region.maxBy(_._1)._1
    val maxY = region.maxBy(_._2)._2
    val res = mut.ArrayBuffer.fill(maxY + 1, maxX + 1)('.')
    region.foreach { (x, y) =>
    res(y)(x) = '#'
    }
    PlantMap(res.map(_.mkString("", "", "")).toVector)
    }

    def inflate: Region = {
    region.flatMap((x, y) => List((x * 2, y * 2), (x * 2 + 1, y * 2), (x * 2, y * 2 + 1), (x * 2 + 1, y * 2 + 1)))
    }

    def sides: Int = {
    val bigRegion = region.inflate
    val regionMap = bigRegion.asPlantMap
    bigRegion.count { (x, y) =>
    val neighborCount = regionMap.optionalNeighbors(x, y).count(_.contains('#'))
    neighborCount match {
    case 3 | 4 | 7 => true
    case _ => false
    }
    }
    }

    def area: Int = region.size
    def perimeter: Int = {
    val regionMap = region.asPlantMap
    region.map((x, y) => regionMap.optionalCardinalNeighbors(x, y).count(_.forall(_ != '#'))).sum
    }
    }

    case class PlantMap(plants: Vector[String]) {
    val height: Int = plants.size
    val width: Int = plants.head.length
    // Length should be equal
    assert(plants.forall(_.length == width))

    def apply(x: Int, y: Int): Char = {
    plants(y)(x)
    }

    def get(x: Int, y: Int): Option[Char] = {
    Option.when(isDefinedAt(x, y))(apply(x, y))
    }

    def isDefinedAt(x: Int, y: Int): Boolean = {
    x >= 0 && x < width && y >= 0 && y < height
    }

    def indices: Vector[(Int, Int)] = {
    (for {
    y <- 0 until height
    x <- 0 until width
    } yield (x, y)).toVector
    }

    def optionalCardinalNeighbors(x: Int, y: Int): List[Option[Char]] = {
    cardinalPositions(x, y).map(get)
    }

    def optionalNeighbors(x: Int, y: Int): List[Option[Char]] = {
    neighborPositions(x, y).map(get)
    }

    def floodFill(x: Int, y: Int): Region = {
    val q = mut.Queue[(Int, Int)]()
    val char = apply(x, y)
    val res = mut.ListBuffer[(Int, Int)]()
    q.addOne((x, y))
    while (q.nonEmpty) {
    val n = q.removeHead()
    if (get(n._1, n._2).contains(char) && !res.contains(n)) {
    res.prepend(n)
    q.addAll(cardinalPositions(n._1, n._2))
    }
    }
    res.toVector
    }

    def regions: List[Region] = {
    List.unfold[Region, Vector[(Int, Int)]](this.indices) { acc =>
    acc.headOption.map { head =>
    val points = floodFill(head._1, head._2)
    (points, acc.diff(points))
    }
    }
    }
    }


    def parse(str: String): PlantMap = {
    PlantMap(str.linesIterator.toVector)
    }

    def part1(input: String): Int = {
    val plants = parse(input)

    plants.regions.map(r => r.area * r.perimeter).sum
    }

    def part2(input: String): Int = {
    val plants = parse(input)

    plants.regions.map(r => r.area * r.sides).sum
    }

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day13/index.html b/2024/puzzles/day13/index.html index e3b47cc03..12a6d00c5 100644 --- a/2024/puzzles/day13/index.html +++ b/2024/puzzles/day13/index.html @@ -5,14 +5,14 @@ Day 13: Claw Contraption | Scala Center Advent of Code - - + +
    -

    Day 13: Claw Contraption

    by @scarf005

    Puzzle description

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

    Solution Summary

    This problem requires you to work with equations and numbers.

    Both parts of the problem ask you to calculate the smallest number of tokens needed to win as many prizes as possible, which means to calculate the optimal number of A and B button presses.

    • For part 1, the question assures you that buttons are pressed no more than 100 times, so it can simply be brute forced for all possible combinations.
    • For part 2, the position of prize is higher by 10000000000000, which means a brute-force approach is not feasible. We need to use the power of math to calculate the optimal solution.
      • First, describe the relation between button presses and prize position as a multivariable equation.
      • Then, use algebraic properties to simplify the equation.
      • This way, we reduce the complexity of the problem to O(1).

    Parsing

    The input data is quite complex to parse. First, let's create a case class to store the amount of claw movements and the prize position:

    case class Claw(ax: Long, ay: Long, bx: Long, by: Long, x: Long, y: Long)

    Since a lot of numbers need to be parsed, Extractor Objects come in handy. this L object with an unapply method will parse a string to Long:

    object L:
    def unapply(s: String): Option[Long] = s.toLongOption

    Then the inputs can be pattern-matched, like this:

    object Claw:
    def parse(xs: Seq[String]): Option[Claw] = xs match
    case Seq(
    s"Button A: X+${L(ax)}, Y+${L(ay)}",
    s"Button B: X+${L(bx)}, Y+${L(by)}",
    s"Prize: X=${L(x)}, Y=${L(y)}",
    ) =>
    Some(Claw(ax, ay, bx, by, x, y))
    case _ => None

    To use Claw.parse, we need to pass 3 lines at a time. We can use .split and .grouped:

    def parse(input: String): Seq[Claw] =
    input.split("\n+").toSeq.grouped(3).flatMap(Claw.parse).toSeq

    Part 1

    Brute forcing part 1 is trivial; as the upper bound of button press is 100, we can just try all 10,000 combinations:

    def part1(input: String): Long =
    def solve(c: Claw) =
    for
    a <- 0 to 100
    b <- 0 to 100
    if a * c.ax + b * c.bx == c.x
    if a * c.ay + b * c.by == c.y
    yield (a * 3L + b)

    parse(input).flatMap(solve).sum

    Part 2

    However, we need to completely ditch our day 1 solution, because now our targets are suddenly farther by 10 trillion. We won't be able to run it till the heat death of the universe! (probably)

    We need another approach. Let's look at the condition carefully...

    Turns out we can express it using system of equations! For number of button presses A and B, our target x and y can be described as following equation:

    {Aax+Bbx=xAay+Bby=y\begin{cases} A \cdot ax + B \cdot bx = x \\ A \cdot ay + B \cdot by = y \end{cases}

    Which can be solved for A in terms of B:

    A=xBbxax,A=yBbyayA = \frac{x - B \cdot bx}{ax}, \quad A = \frac{y - B \cdot by}{ay}

    Then A can be equated in both expressions:

    xBbxax=yBbyay\frac{x - B \cdot bx}{ax} = \frac{y - B \cdot by}{ay}

    Now ax and ay can be cross-multiplied to eliminate denominators:

    (xBbx)ay=(yBby)ax(x - B \cdot bx) \cdot ay = (y - B \cdot by) \cdot ax

    ...Expand and rearrange:

    xayBbxay=yaxBbyaxx \cdot ay - B \cdot bx \cdot ay = y \cdot ax - B \cdot by \cdot ax

    Group terms involving B:

    xayyax=B(bxaybyax)x \cdot ay - y \cdot ax = B \cdot (bx \cdot ay - by \cdot ax)

    Then solve for B:

    B=xayyaxbxaybyaxB = \frac{x \cdot ay - y \cdot ax}{bx \cdot ay - by \cdot ax}

    Using B, A can also be solved:

    A=xBbxaxA = \frac{x - B \cdot bx}{ax}

    There's two more important requirement for A and B:

    1. A and B should both be an natural number.
    2. denominator musn't be 0 (divide by zero!)

    Let's write a safeDiv function to address them:

    extension (a: Long)
    infix def safeDiv(b: Long): Option[Long] =
    Option.when(b != 0 && a % b == 0)(a / b)

    we check that denominator is not zero and that numerator is divisible by denominator.

    With the help of safeDiv, the solution can be cleanly expressed as:

    case class Claw(ax: Long, ay: Long, bx: Long, by: Long, x: Long, y: Long):
    def solve: Option[Long] = for
    b <- (x * ay - y * ax) safeDiv (bx * ay - by * ax)
    a <- (x - b * bx) safeDiv ax
    yield a * 3 + b

    also don't forget to add 10000000000000 to the prize position:

    def part2(input: String): Long =
    val diff = 10_000_000_000_000L
    parse(input)
    .map(c => c.copy(x = c.x + diff, y = c.y + diff))
    .flatMap(_.solve)
    .sum

    Final Code

    case class Claw(ax: Long, ay: Long, bx: Long, by: Long, x: Long, y: Long):
    def solve: Option[Long] = for
    b <- (x * ay - y * ax) safeDiv (bx * ay - by * ax)
    a <- (x - b * bx) safeDiv ax
    yield a * 3 + b

    object Claw:
    def parse(xs: Seq[String]): Option[Claw] = xs match
    case Seq(
    s"Button A: X+${L(ax)}, Y+${L(ay)}",
    s"Button B: X+${L(bx)}, Y+${L(by)}",
    s"Prize: X=${L(x)}, Y=${L(y)}",
    ) =>
    Some(Claw(ax, ay, bx, by, x, y))
    case _ => None

    def parse(input: String): Seq[Claw] =
    input.split("\n+").toSeq.grouped(3).flatMap(Claw.parse).toSeq

    extension (a: Long)
    infix def safeDiv(b: Long): Option[Long] =
    Option.when(b != 0 && a % b == 0)(a / b)

    object L:
    def unapply(s: String): Option[Long] = s.toLongOption

    def part1(input: String): Long =
    parse(input).flatMap(_.solve).sum

    def part2(input: String): Long =
    val diff = 10_000_000_000_000L
    parse(input)
    .map(c => c.copy(x = c.x + diff, y = c.y + diff))
    .flatMap(_.solve)
    .sum

    Solutions from the community

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

    Day 13: Claw Contraption

    by @scarf005

    Puzzle description

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

    Solution Summary

    This problem requires you to work with equations and numbers.

    Both parts of the problem ask you to calculate the smallest number of tokens needed to win as many prizes as possible, which means to calculate the optimal number of A and B button presses.

    • For part 1, the question assures you that buttons are pressed no more than 100 times, so it can simply be brute forced for all possible combinations.
    • For part 2, the position of prize is higher by 10000000000000, which means a brute-force approach is not feasible. We need to use the power of math to calculate the optimal solution.
      • First, describe the relation between button presses and prize position as a multivariable equation.
      • Then, use algebraic properties to simplify the equation.
      • This way, we reduce the complexity of the problem to O(1).

    Parsing

    The input data is quite complex to parse. First, let's create a case class to store the amount of claw movements and the prize position:

    case class Claw(ax: Long, ay: Long, bx: Long, by: Long, x: Long, y: Long)

    Since a lot of numbers need to be parsed, Extractor Objects come in handy. this L object with an unapply method will parse a string to Long:

    object L:
    def unapply(s: String): Option[Long] = s.toLongOption

    Then the inputs can be pattern-matched, like this:

    object Claw:
    def parse(xs: Seq[String]): Option[Claw] = xs match
    case Seq(
    s"Button A: X+${L(ax)}, Y+${L(ay)}",
    s"Button B: X+${L(bx)}, Y+${L(by)}",
    s"Prize: X=${L(x)}, Y=${L(y)}",
    ) =>
    Some(Claw(ax, ay, bx, by, x, y))
    case _ => None

    To use Claw.parse, we need to pass 3 lines at a time. We can use .split and .grouped:

    def parse(input: String): Seq[Claw] =
    input.split("\n+").toSeq.grouped(3).flatMap(Claw.parse).toSeq

    Part 1

    Brute forcing part 1 is trivial; as the upper bound of button press is 100, we can just try all 10,000 combinations:

    def part1(input: String): Long =
    def solve(c: Claw) =
    for
    a <- 0 to 100
    b <- 0 to 100
    if a * c.ax + b * c.bx == c.x
    if a * c.ay + b * c.by == c.y
    yield (a * 3L + b)

    parse(input).flatMap(solve).sum

    Part 2

    However, we need to completely ditch our day 1 solution, because now our targets are suddenly farther by 10 trillion. We won't be able to run it till the heat death of the universe! (probably)

    We need another approach. Let's look at the condition carefully...

    Turns out we can express it using system of equations! For number of button presses A and B, our target x and y can be described as following equation:

    {Aax+Bbx=xAay+Bby=y\begin{cases} A \cdot ax + B \cdot bx = x \\ A \cdot ay + B \cdot by = y \end{cases}

    Which can be solved for A in terms of B:

    A=xBbxax,A=yBbyayA = \frac{x - B \cdot bx}{ax}, \quad A = \frac{y - B \cdot by}{ay}

    Then A can be equated in both expressions:

    xBbxax=yBbyay\frac{x - B \cdot bx}{ax} = \frac{y - B \cdot by}{ay}

    Now ax and ay can be cross-multiplied to eliminate denominators:

    (xBbx)ay=(yBby)ax(x - B \cdot bx) \cdot ay = (y - B \cdot by) \cdot ax

    ...Expand and rearrange:

    xayBbxay=yaxBbyaxx \cdot ay - B \cdot bx \cdot ay = y \cdot ax - B \cdot by \cdot ax

    Group terms involving B:

    xayyax=B(bxaybyax)x \cdot ay - y \cdot ax = B \cdot (bx \cdot ay - by \cdot ax)

    Then solve for B:

    B=xayyaxbxaybyaxB = \frac{x \cdot ay - y \cdot ax}{bx \cdot ay - by \cdot ax}

    Using B, A can also be solved:

    A=xBbxaxA = \frac{x - B \cdot bx}{ax}

    There's two more important requirement for A and B:

    1. A and B should both be an natural number.
    2. denominator musn't be 0 (divide by zero!)

    Let's write a safeDiv function to address them:

    extension (a: Long)
    infix def safeDiv(b: Long): Option[Long] =
    Option.when(b != 0 && a % b == 0)(a / b)

    we check that denominator is not zero and that numerator is divisible by denominator.

    With the help of safeDiv, the solution can be cleanly expressed as:

    case class Claw(ax: Long, ay: Long, bx: Long, by: Long, x: Long, y: Long):
    def solve: Option[Long] = for
    b <- (x * ay - y * ax) safeDiv (bx * ay - by * ax)
    a <- (x - b * bx) safeDiv ax
    yield a * 3 + b

    also don't forget to add 10000000000000 to the prize position:

    def part2(input: String): Long =
    val diff = 10_000_000_000_000L
    parse(input)
    .map(c => c.copy(x = c.x + diff, y = c.y + diff))
    .flatMap(_.solve)
    .sum

    Final Code

    case class Claw(ax: Long, ay: Long, bx: Long, by: Long, x: Long, y: Long):
    def solve: Option[Long] = for
    b <- (x * ay - y * ax) safeDiv (bx * ay - by * ax)
    a <- (x - b * bx) safeDiv ax
    yield a * 3 + b

    object Claw:
    def parse(xs: Seq[String]): Option[Claw] = xs match
    case Seq(
    s"Button A: X+${L(ax)}, Y+${L(ay)}",
    s"Button B: X+${L(bx)}, Y+${L(by)}",
    s"Prize: X=${L(x)}, Y=${L(y)}",
    ) =>
    Some(Claw(ax, ay, bx, by, x, y))
    case _ => None

    def parse(input: String): Seq[Claw] =
    input.split("\n+").toSeq.grouped(3).flatMap(Claw.parse).toSeq

    extension (a: Long)
    infix def safeDiv(b: Long): Option[Long] =
    Option.when(b != 0 && a % b == 0)(a / b)

    object L:
    def unapply(s: String): Option[Long] = s.toLongOption

    def part1(input: String): Long =
    parse(input).flatMap(_.solve).sum

    def part2(input: String): Long =
    val diff = 10_000_000_000_000L
    parse(input)
    .map(c => c.copy(x = c.x + diff, y = c.y + diff))
    .flatMap(_.solve)
    .sum

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day14/index.html b/2024/puzzles/day14/index.html index 0d1ca0534..27bbe39f6 100644 --- a/2024/puzzles/day14/index.html +++ b/2024/puzzles/day14/index.html @@ -5,14 +5,14 @@ Day 14: Restroom Redoubt | Scala Center Advent of Code - - + +
    -

    Day 14: Restroom Redoubt

    Puzzle description

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

    Solutions from the community

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

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day15/index.html b/2024/puzzles/day15/index.html index 9670d7bfc..2b4184db0 100644 --- a/2024/puzzles/day15/index.html +++ b/2024/puzzles/day15/index.html @@ -5,14 +5,14 @@ Day 15: Warehouse Woes | Scala Center Advent of Code - - + +
    -

    Day 15: Warehouse Woes

    Puzzle description

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

    Solutions from the community

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

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day16/index.html b/2024/puzzles/day16/index.html index fe4d10bf1..d0d4c18f6 100644 --- a/2024/puzzles/day16/index.html +++ b/2024/puzzles/day16/index.html @@ -5,12 +5,12 @@ Day 16: Reindeer Maze | Scala Center Advent of Code - - + +
    -

    Day 16: Reindeer Maze

    by @merlinorg

    Puzzle description

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

    Solution summary

    Any time you see a puzzle like this, where you are searching for the +

    Day 16: Reindeer Maze

    by @merlinorg

    Puzzle description

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

    Solution summary

    Any time you see a puzzle like this, where you are searching for the lowest-cost route to a goal, you will pretty much immediately reach for Dijkstra's algorithm or one of its variants. This algorithm maintains a priority queue of @@ -84,7 +84,7 @@ with the mutable version with just a small update to iterate in chunks, so performance is not really a deciding factor between the solutions.

    import scala.collection.mutable

    def part1(input: String): Int =
    val (reindeer, _) = solve(input)
    reindeer.score

    def part2(input: String): Int =
    val (reindeer, queue) = solve(input)
    val paths = mutable.Set.from(reindeer.path)
    while queue.head.score == reindeer.score do
    val next = queue.dequeue()
    if next.pos == reindeer.pos then paths.addAll(next.path)
    paths.size

    def solve(input: String): (Reindeer, mutable.PriorityQueue[Reindeer]) =
    val maze = input.split("\n")
    val start = maze.findPosition('S').get
    val end = maze.findPosition('E').get
    val reindeer = Reindeer(0, start, East, Vector(start))
    val visited = mutable.Set.empty[(Position, Direction)]
    val queue = mutable.PriorityQueue.from(Seq(reindeer))

    while queue.head.pos != end do
    val reindeer = queue.dequeue()

    val neighbours = reindeer.neighbours.filter: next =>
    maze(next.pos) != '#' && !visited(next.pos -> next.dir)

    visited.addOne(reindeer.pos -> reindeer.dir)
    queue.addAll(neighbours)

    (queue.dequeue(), queue)

    case class Reindeer(score: Int, pos: Position, dir: Direction, path: Vector[Position]):
    def neighbours: Vector[Reindeer] = Vector(
    Reindeer(score + 1, pos + dir, dir, path :+ (pos + dir)),
    Reindeer(score + 1000, pos, dir.cw, path),
    Reindeer(score + 1000, pos, dir.ccw, path)
    )

    given Ordering[Reindeer] = Ordering.by(-_.score)

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day17/index.html b/2024/puzzles/day17/index.html index 4befad5a4..bcf745dcf 100644 --- a/2024/puzzles/day17/index.html +++ b/2024/puzzles/day17/index.html @@ -5,14 +5,14 @@ Day 17: Chronospatial Computer | Scala Center Advent of Code - - + +
    -

    Day 17: Chronospatial Computer

    Puzzle description

    https://adventofcode.com/2024/day/17

    Solutions from the community

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

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day18/index.html b/2024/puzzles/day18/index.html index d612c2123..b3ae1d746 100644 --- a/2024/puzzles/day18/index.html +++ b/2024/puzzles/day18/index.html @@ -5,12 +5,12 @@ Day 18: RAM Run | Scala Center Advent of Code - - + +
    -

    Day 18: RAM Run

    by @TheDrawingCoder-Gamer

    Puzzle description

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

    Solution Summary

    1. Parse input into a List of points
    2. For part 1, take the first 1024 points and path-find through them
    3. For part 2, find the tipping point where pathfinding can't be done anymore

    Part 1

    First, let's make the Vec2i class for our points:

    case class Vec2i(x: Int, y: Int):
    def cardinalNeighbors: List[Vec2i] =
    List(Vec2i(x - 1, y), Vec2i(x + 1, y), Vec2i(x, y - 1), Vec2i(x, y + 1))

    def isContainedIn(w: Int, h: Int): Boolean =
    x >= 0 && x < w && y >= 0 && y < h

    Then let's parse all our points. (because the example is so different from the actual input, there is special casing for it.)

    def parse(str: String): (Int, Int, List[Vec2i]) =
    val ps = str.linesIterator.map:
    case s"$x,$y" => Vec2i(x.toInt, y.toInt)
    .toList
    (if ps.length == 25 then 7 else 71, if ps.length == 25 then 12 else 1024, ps)

    For both parts, we'll need a path finding function. While I would usually use a grid, because of part 2 it's easier to pathfind using a Set. +

    Day 18: RAM Run

    by @TheDrawingCoder-Gamer

    Puzzle description

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

    Solution Summary

    1. Parse input into a List of points
    2. For part 1, take the first 1024 points and path-find through them
    3. For part 2, find the tipping point where pathfinding can't be done anymore

    Part 1

    First, let's make the Vec2i class for our points:

    case class Vec2i(x: Int, y: Int):
    def cardinalNeighbors: List[Vec2i] =
    List(Vec2i(x - 1, y), Vec2i(x + 1, y), Vec2i(x, y - 1), Vec2i(x, y + 1))

    def isContainedIn(w: Int, h: Int): Boolean =
    x >= 0 && x < w && y >= 0 && y < h

    Then let's parse all our points. (because the example is so different from the actual input, there is special casing for it.)

    def parse(str: String): (Int, Int, List[Vec2i]) =
    val ps = str.linesIterator.map:
    case s"$x,$y" => Vec2i(x.toInt, y.toInt)
    .toList
    (if ps.length == 25 then 7 else 71, if ps.length == 25 then 12 else 1024, ps)

    For both parts, we'll need a path finding function. While I would usually use a grid, because of part 2 it's easier to pathfind using a Set. Here's an implementation of Dijkstra's algorithim with sets:

    extension (walls: Set[Vec2i])
    def search(gridSize: Int): Option[List[Vec2i]] =
    def reconstructPath(cameFrom: Map[Vec2i, Vec2i], p: Vec2i): List[Vec2i] = {
    val totalPath = mut.ListBuffer[Vec2i](p)
    var current = p
    while (cameFrom.contains(current)) {
    current = cameFrom(current)
    totalPath.prepend(current)
    }
    totalPath.toList
    }

    val start = Vec2i(0, 0)
    val goal = Vec2i(gridSize - 1, gridSize - 1)
    val cameFrom = mut.HashMap[Vec2i, Vec2i]()

    val dist = mut.HashMap(start -> 0d)

    val q = mut.PriorityQueue(start -> 0d)(using Ordering.by[(Vec2i, Double), Double](_._2).reverse)

    while q.nonEmpty && q.head._1 != goal do
    val (current, score) = q.dequeue()

    for neighbor <- current.cardinalNeighbors.filter(it => it.isContainedIn(gridSize, gridSize) && !walls.contains(it)) do
    val alt = score + 1d
    if alt < dist.getOrElse(neighbor, Double.PositiveInfinity) then
    cameFrom(neighbor) = current
    dist(neighbor) = alt
    q.addOne(neighbor -> alt)

    q.headOption.map(it => reconstructPath(cameFrom.toMap, it._1))

    Now part 1 is just running this with the correct inputs:

    def part1(str: String): Int =
    val (size, take, walls) = parse(str)
    walls.take(take).toSet.search(size).get.size - 1

    (We subtract 1 here to get the number of moves made, instead of the number of tiles.)

    Part 2

    Part 2 is finding the point where the path is impossible to reach. A naive approach would be a linear search, and with the input size it is perfectly acceptable (only taking 2 seconds), but a faster approach is a binary search.

    Part 2 is just a short code block:

    def part2(str: String): Vec2i =
    val (size, take, walls) = parse(str)
    Iterator.iterate(take -> walls.size): (i0, i1) =>
    if walls.take((i0 + i1) / 2).toSet.search(size).isEmpty
    then i0 -> (i0 + i1) / 2 else (i0 + i1) / 2 + 1 -> i1
    .flatMap: (i0, i1) =>
    Option.when(i0 == i1)(walls(i0 - 1))
    .next()

    This code will need some explaining. The iterate function is iterating through a binary search: it picks the midpoint of its two endpoints and searches the path with that amount of bytes fallen. If the result is None, meaning there is no valid path, then it pins its higher end point @@ -19,7 +19,7 @@ around 50 milliseconds, so that's a 40x speedup just from using binary search.

    The flatMap(...).next() just finds the first point where i0 == i1, or where the binary search has completed. Because we've been taking, and not indexing, we have to subtract one to get the proper index.

    Final code:

    case class Vec2i(x: Int, y: Int):
    def cardinalNeighbors: List[Vec2i] =
    List(Vec2i(x - 1, y), Vec2i(x + 1, y), Vec2i(x, y - 1), Vec2i(x, y + 1))

    def isContainedIn(w: Int, h: Int): Boolean =
    x >= 0 && x < w && y >= 0 && y < h

    def parse(str: String): (Int, Int, List[Vec2i]) =
    val ps = str.linesIterator.map:
    case s"$x,$y" => Vec2i(x.toInt, y.toInt)
    .toList
    (if ps.length == 25 then 7 else 71, if ps.length == 25 then 12 else 1024, ps)

    extension (walls: Set[Vec2i])
    def search(gridSize: Int): Option[List[Vec2i]] =
    def reconstructPath(cameFrom: Map[Vec2i, Vec2i], p: Vec2i): List[Vec2i] = {
    val totalPath = mut.ListBuffer[Vec2i](p)
    var current = p
    while (cameFrom.contains(current)) {
    current = cameFrom(current)
    totalPath.prepend(current)
    }
    totalPath.toList
    }

    val start = Vec2i(0, 0)
    val goal = Vec2i(gridSize - 1, gridSize - 1)
    val cameFrom = mut.HashMap[Vec2i, Vec2i]()

    val dist = mut.HashMap(start -> 0d)

    val q = mut.PriorityQueue(start -> 0d)(using Ordering.by[(Vec2i, Double), Double](_._2).reverse)

    while q.nonEmpty && q.head._1 != goal do
    val (current, score) = q.dequeue()

    for neighbor <- current.cardinalNeighbors.filter(it => it.isContainedIn(gridSize, gridSize) && !walls.contains(it)) do
    val alt = score + 1d
    if alt < dist.getOrElse(neighbor, Double.PositiveInfinity) then
    cameFrom(neighbor) = current
    dist(neighbor) = alt
    q.addOne(neighbor -> alt)

    q.headOption.map(it => reconstructPath(cameFrom.toMap, it._1))

    def part1(str: String): Int =
    val (size, take, walls) = parse(str)
    walls.take(take).toSet.search(size).get.size - 1

    def part2(str: String): Vec2i =
    val (size, take, walls) = parse(str)
    Iterator.iterate(take -> walls.size): (i0, i1) =>
    if walls.take((i0 + i1) / 2).toSet.search(size).isEmpty
    then i0 -> (i0 + i1) / 2 else (i0 + i1) / 2 + 1 -> i1
    .flatMap: (i0, i1) =>
    Option.when(i0 == i1)(walls(i0 - 1))
    .next()

    Solutions from the community

    Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day19/index.html b/2024/puzzles/day19/index.html index ffb673f56..182a0f92b 100644 --- a/2024/puzzles/day19/index.html +++ b/2024/puzzles/day19/index.html @@ -5,14 +5,14 @@ Day 19: Linen Layout | Scala Center Advent of Code - - + +
    -

    Day 19: Linen Layout

    Puzzle description

    https://adventofcode.com/2024/day/19

    Solutions from the community

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

    - - + + \ No newline at end of file diff --git a/2024/puzzles/day20/index.html b/2024/puzzles/day20/index.html index baff88081..951c20f5b 100644 --- a/2024/puzzles/day20/index.html +++ b/2024/puzzles/day20/index.html @@ -5,14 +5,14 @@ Day 20: Race Condition | Scala Center Advent of Code - - + +
    -
    - - +
    + + \ No newline at end of file diff --git a/2024/puzzles/day21/index.html b/2024/puzzles/day21/index.html new file mode 100644 index 000000000..c4024bf0a --- /dev/null +++ b/2024/puzzles/day21/index.html @@ -0,0 +1,18 @@ + + + + + +Day 21: Keypad Conundrum | Scala Center Advent of Code + + + + + +
    +
    + + + + \ No newline at end of file diff --git a/404.html b/404.html index 7f924d181..0930e3843 100644 --- a/404.html +++ b/404.html @@ -5,13 +5,13 @@ Page Not Found | Scala Center Advent of Code - - + + - - +

    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/73b9919f.33a91d3e.js b/assets/js/73b9919f.33a91d3e.js new file mode 100644 index 000000000..31f0ed52d --- /dev/null +++ b/assets/js/73b9919f.33a91d3e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[4052],{4163:(t,e,a)=>{a.r(e),a.d(e,{assets:()=>p,contentTitle:()=>o,default:()=>d,frontMatter:()=>r,metadata:()=>s,toc:()=>h});var n=a(7462),i=(a(7294),a(3905)),l=a(6340);const r={},o="Day 1: Trebuchet?!",s={unversionedId:"2023/puzzles/day01",id:"2023/puzzles/day01",title:"Day 1: Trebuchet?!",description:"by @sjrd",source:"@site/target/mdoc/2023/puzzles/day01.md",sourceDirName:"2023/puzzles",slug:"/2023/puzzles/day01",permalink:"/scala-advent-of-code/2023/puzzles/day01",draft:!1,editUrl:"https://github.com/scalacenter/scala-advent-of-code/edit/website/docs/2023/puzzles/day01.md",tags:[],version:"current",frontMatter:{},sidebar:"adventOfCodeSidebar",previous:{title:"Day 21: Keypad Conundrum",permalink:"/scala-advent-of-code/2024/puzzles/day21"},next:{title:"Day 2: Cube Conundrum",permalink:"/scala-advent-of-code/2023/puzzles/day02"}},p={},h=[{value:"Puzzle description",id:"puzzle-description",level:2},{value:"Solution Summary",id:"solution-summary",level:2},{value:"Part 1",id:"part-1",level:3},{value:"Part 2",id:"part-2",level:3},{value:"Final Code",id:"final-code",level:2},{value:"Run it in the browser",id:"run-it-in-the-browser",level:3},{value:"Part 1",id:"part-1-1",level:4},{value:"Part 2",id:"part-2-1",level:4},{value:"Solutions from the community",id:"solutions-from-the-community",level:2}],m={toc:h};function d(t){let{components:e,...a}=t;return(0,i.kt)("wrapper",(0,n.Z)({},m,a,{components:e,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"day-1-trebuchet"},"Day 1: Trebuchet?!"),(0,i.kt)("p",null,"by ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/sjrd"},"@sjrd")),(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/1"},"https://adventofcode.com/2023/day/1")),(0,i.kt)("h2",{id:"solution-summary"},"Solution Summary"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},"Iterate over each line of the input."),(0,i.kt)("li",{parentName:"ol"},"Convert each line into coordinates, using the appropriate mechanism for ",(0,i.kt)("inlineCode",{parentName:"li"},"part1")," and ",(0,i.kt)("inlineCode",{parentName:"li"},"part2"),"."),(0,i.kt)("li",{parentName:"ol"},"Sum the coordinates.")),(0,i.kt)("h3",{id:"part-1"},"Part 1"),(0,i.kt)("p",null,"Our main driver iterates over the lines, converts each line into coordinates, then sums them.\nIt therefore looks like:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"def part1(input: String): String =\n // Convert one line into the appropriate coordinates\n def lineToCoordinates(line: String): Int =\n ???\n\n // Convert each line to its coordinates and sum all the coordinates\n val result = input\n .linesIterator\n .map(lineToCoordinates(_))\n .sum\n result.toString()\nend part1\n")),(0,i.kt)("p",null,"In order to convert a line into coordinates, we find the first and last digits in the line.\nWe then put them next to each other in a string to interpret them as coordinates, as asked."),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'// Convert one line into the appropriate coordinates\ndef lineToCoordinates(line: String): Int =\n val firstDigit = line.find(_.isDigit).get\n val lastDigit = line.findLast(_.isDigit).get\n s"$firstDigit$lastDigit".toInt\n')),(0,i.kt)("h3",{id:"part-2"},"Part 2"),(0,i.kt)("p",null,"The main driver is the same as for part 1.\nWhat changes is how we convert each line into coordinates."),(0,i.kt)("p",null,"We first build a hard-coded map of string representations to numeric values:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'/** The textual representation of digits. */\nval stringDigitReprs: Map[String, Int] = Map(\n "one" -> 1,\n "two" -> 2,\n "three" -> 3,\n "four" -> 4,\n "five" -> 5,\n "six" -> 6,\n "seven" -> 7,\n "eight" -> 8,\n "nine" -> 9,\n)\n\n/** All the string representation of digits, including the digits themselves. */\nval digitReprs: Map[String, Int] =\n stringDigitReprs ++ (1 to 9).map(i => i.toString() -> i)\n')),(0,i.kt)("p",null,"We will now have to find the first and last string representation in the line.\nAlthough not the most efficient solution, we do this by building a regular expression that matches any key of the above map."),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'// A regex that matches any of the keys of `digitReprs`\nval digitReprRegex = digitReprs.keysIterator.mkString("|").r\n')),(0,i.kt)("p",null,"Now, we find all matches of the regex in the line, convert them to their numeric values, and concatenate them as before:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'def lineToCoordinates(line: String): Int =\n // Find all the digit representations in the line.\n val matches = digitReprRegex.findAllIn(line).toList\n\n // Convert the string representations into actual digits and form the result\n val firstDigit = digitReprs(matches.head)\n val lastDigit = digitReprs(matches.last)\n s"$firstDigit$lastDigit".toInt\nend lineToCoordinates\n')),(0,i.kt)("p",null,"However, this does not seem to be correct.\nWhen we submit our answer with the above, the checker tells us that our answer is too low.\nWhat went wrong?"),(0,i.kt)("p",null,"It turns out that the dataset contains lines where two textual representations overlap.\nFor example, our data contained:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre"},"29oneightt\n")),(0,i.kt)("p",null,(0,i.kt)("inlineCode",{parentName:"p"},"Regex.findAllIn")," only finds ",(0,i.kt)("em",{parentName:"p"},"non-overlapping")," matches in a string.\nIt therefore returns ",(0,i.kt)("inlineCode",{parentName:"p"},"2"),", ",(0,i.kt)("inlineCode",{parentName:"p"},"9"),", and ",(0,i.kt)("inlineCode",{parentName:"p"},"one"),", but misses the ",(0,i.kt)("inlineCode",{parentName:"p"},"eight")," that overlaps with ",(0,i.kt)("inlineCode",{parentName:"p"},"one"),"."),(0,i.kt)("p",null,"There is no built-in function to handle overlapping matches, nor to find the ",(0,i.kt)("em",{parentName:"p"},"last")," match of a regex in a string.\nInstead, we manually iterate over all the indices to see if a match starts there.\nThis is equivalent to looking for ",(0,i.kt)("em",{parentName:"p"},"prefix")," matches in all the ",(0,i.kt)("em",{parentName:"p"},"suffixes")," of line.\nConveniently, ",(0,i.kt)("inlineCode",{parentName:"p"},"line.tails")," iterates over all such suffixes, and ",(0,i.kt)("inlineCode",{parentName:"p"},"Regex.findPrefixOf")," will look only for prefixes."),(0,i.kt)("p",null,"Our fixed computation for ",(0,i.kt)("inlineCode",{parentName:"p"},"matches")," is now:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"val matchesIter =\n for\n lineTail <- line.tails\n oneMatch <- digitReprRegex.findPrefixOf(lineTail)\n yield\n oneMatch\nval matches = matchesIter.toList\n")),(0,i.kt)("h2",{id:"final-code"},"Final Code"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'def part1(input: String): String =\n // Convert one line into the appropriate coordinates\n def lineToCoordinates(line: String): Int =\n val firstDigit = line.find(_.isDigit).get\n val lastDigit = line.findLast(_.isDigit).get\n s"$firstDigit$lastDigit".toInt\n\n // Convert each line to its coordinates and sum all the coordinates\n val result = input\n .linesIterator\n .map(lineToCoordinates(_))\n .sum\n result.toString()\nend part1\n\n/** The textual representation of digits. */\nval stringDigitReprs = Map(\n "one" -> 1,\n "two" -> 2,\n "three" -> 3,\n "four" -> 4,\n "five" -> 5,\n "six" -> 6,\n "seven" -> 7,\n "eight" -> 8,\n "nine" -> 9,\n)\n\n/** All the string representation of digits, including the digits themselves. */\nval digitReprs = stringDigitReprs ++ (1 to 9).map(i => i.toString() -> i)\n\ndef part2(input: String): String =\n // A regex that matches any of the keys of `digitReprs`\n val digitReprRegex = digitReprs.keysIterator.mkString("|").r\n\n def lineToCoordinates(line: String): Int =\n // Find all the digit representations in the line\n val matchesIter =\n for\n lineTail <- line.tails\n oneMatch <- digitReprRegex.findPrefixOf(lineTail)\n yield\n oneMatch\n val matches = matchesIter.toList\n\n // Convert the string representations into actual digits and form the result\n val firstDigit = digitReprs(matches.head)\n val lastDigit = digitReprs(matches.last)\n s"$firstDigit$lastDigit".toInt\n end lineToCoordinates\n\n // Process lines as in part1\n val result = input\n .linesIterator\n .map(lineToCoordinates(_))\n .sum\n result.toString()\nend part2\n')),(0,i.kt)("h3",{id:"run-it-in-the-browser"},"Run it in the browser"),(0,i.kt)("h4",{id:"part-1-1"},"Part 1"),(0,i.kt)(l.Z,{puzzle:"day01-part1",year:"2023",mdxType:"Solver"}),(0,i.kt)("h4",{id:"part-2-1"},"Part 2"),(0,i.kt)(l.Z,{puzzle:"day01-part2",year:"2023",mdxType:"Solver"}),(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/pkarthick/AdventOfCode/blob/master/2023/scala/src/main/scala/day01.scala"},"Solution")," of ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/pkarthick"},"Karthick Pachiappan")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/Jannyboy11/AdventOfCode2023/blob/master/src/main/scala/day01/Day01.scala"},"Solution")," of ",(0,i.kt)("a",{parentName:"li",href:"https://twitter.com/JanBoerman95"},"Jan Boerman"),"."),(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/day1.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/spamegg1/advent-of-code-2023-scala/blob/solutions/01.worksheet.sc#L65"},"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/jnclt/adventofcode2023/tree/main/day01"},"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://github.com/KristianAN/AoC2023/blob/main/scala/src/Day1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/KristianAN"},"KristianAN")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://gist.github.com/CJSmith-0141/a84b3d213bdd8ed7c256561132d19b8d"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/CJSmith-0141"},"CJ Smith")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/ChidiRnweke/AOC23/blob/main/src/main/scala/day1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/ChidiRnweke"},"Chidi Nweke")),(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/Day1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/YannMoisan"},"YannMoisan")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/bxiang/advent-of-code-2023/blob/main/src/main/scala/com/aoc/day1/Solution.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/bxiang"},"Brian Xiang")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/bishabosha/advent-of-code-2023/blob/main/2023-day01.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/SethTisue/adventofcode/blob/main/2023/src/test/scala/Day01.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/SethTisue"},"Seth Tisue")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/Philippus/adventofcode/tree/main/src/main/scala/adventofcode2023/day1/Day1.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/kbielefe/advent-of-code/blob/edf8e706229a5f3785291824f26778de8a583c35/2023/src/main/scala/1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/kbielefe"},"Karl Bielefeldt")),(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/day01/DayOne.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/joeledwards/advent-of-code/blob/master/2023/src/main/scala/com/buzuli/advent/days/day1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/joeledwards"},"Joel Edwards")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/wbillingsley/advent-of-code-2023-scala/blob/star2/solver.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/wbillingsley"},"Will Billingsley")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/mpilquist/aoc/blob/main/2023/day1.sc"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/mpilquist"},"Michael Pilquist")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/GrigoriiBerezin/advent_code_2023/tree/master/task01/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/marconilanna/advent-of-code/blob/master/2023/Day01.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/marconilanna"},"Marconi Lanna")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/xRuiAlves/advent-of-code-2023/blob/main/Day1.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/AvaPL/Advent-of-Code-2023/tree/main/src/main/scala/day1"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/AvaPL"},"Pawe\u0142 Cembaluk"))),(0,i.kt)("p",null,"Share your solution to the Scala community by editing this page."))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/73b9919f.d577c60d.js b/assets/js/73b9919f.d577c60d.js deleted file mode 100644 index 5373a63f9..000000000 --- a/assets/js/73b9919f.d577c60d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[4052],{4163:(t,e,a)=>{a.r(e),a.d(e,{assets:()=>p,contentTitle:()=>o,default:()=>d,frontMatter:()=>r,metadata:()=>s,toc:()=>h});var n=a(7462),i=(a(7294),a(3905)),l=a(6340);const r={},o="Day 1: Trebuchet?!",s={unversionedId:"2023/puzzles/day01",id:"2023/puzzles/day01",title:"Day 1: Trebuchet?!",description:"by @sjrd",source:"@site/target/mdoc/2023/puzzles/day01.md",sourceDirName:"2023/puzzles",slug:"/2023/puzzles/day01",permalink:"/scala-advent-of-code/2023/puzzles/day01",draft:!1,editUrl:"https://github.com/scalacenter/scala-advent-of-code/edit/website/docs/2023/puzzles/day01.md",tags:[],version:"current",frontMatter:{},sidebar:"adventOfCodeSidebar",previous:{title:"Day 20: Race Condition",permalink:"/scala-advent-of-code/2024/puzzles/day20"},next:{title:"Day 2: Cube Conundrum",permalink:"/scala-advent-of-code/2023/puzzles/day02"}},p={},h=[{value:"Puzzle description",id:"puzzle-description",level:2},{value:"Solution Summary",id:"solution-summary",level:2},{value:"Part 1",id:"part-1",level:3},{value:"Part 2",id:"part-2",level:3},{value:"Final Code",id:"final-code",level:2},{value:"Run it in the browser",id:"run-it-in-the-browser",level:3},{value:"Part 1",id:"part-1-1",level:4},{value:"Part 2",id:"part-2-1",level:4},{value:"Solutions from the community",id:"solutions-from-the-community",level:2}],m={toc:h};function d(t){let{components:e,...a}=t;return(0,i.kt)("wrapper",(0,n.Z)({},m,a,{components:e,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"day-1-trebuchet"},"Day 1: Trebuchet?!"),(0,i.kt)("p",null,"by ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/sjrd"},"@sjrd")),(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/1"},"https://adventofcode.com/2023/day/1")),(0,i.kt)("h2",{id:"solution-summary"},"Solution Summary"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},"Iterate over each line of the input."),(0,i.kt)("li",{parentName:"ol"},"Convert each line into coordinates, using the appropriate mechanism for ",(0,i.kt)("inlineCode",{parentName:"li"},"part1")," and ",(0,i.kt)("inlineCode",{parentName:"li"},"part2"),"."),(0,i.kt)("li",{parentName:"ol"},"Sum the coordinates.")),(0,i.kt)("h3",{id:"part-1"},"Part 1"),(0,i.kt)("p",null,"Our main driver iterates over the lines, converts each line into coordinates, then sums them.\nIt therefore looks like:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"def part1(input: String): String =\n // Convert one line into the appropriate coordinates\n def lineToCoordinates(line: String): Int =\n ???\n\n // Convert each line to its coordinates and sum all the coordinates\n val result = input\n .linesIterator\n .map(lineToCoordinates(_))\n .sum\n result.toString()\nend part1\n")),(0,i.kt)("p",null,"In order to convert a line into coordinates, we find the first and last digits in the line.\nWe then put them next to each other in a string to interpret them as coordinates, as asked."),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'// Convert one line into the appropriate coordinates\ndef lineToCoordinates(line: String): Int =\n val firstDigit = line.find(_.isDigit).get\n val lastDigit = line.findLast(_.isDigit).get\n s"$firstDigit$lastDigit".toInt\n')),(0,i.kt)("h3",{id:"part-2"},"Part 2"),(0,i.kt)("p",null,"The main driver is the same as for part 1.\nWhat changes is how we convert each line into coordinates."),(0,i.kt)("p",null,"We first build a hard-coded map of string representations to numeric values:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'/** The textual representation of digits. */\nval stringDigitReprs: Map[String, Int] = Map(\n "one" -> 1,\n "two" -> 2,\n "three" -> 3,\n "four" -> 4,\n "five" -> 5,\n "six" -> 6,\n "seven" -> 7,\n "eight" -> 8,\n "nine" -> 9,\n)\n\n/** All the string representation of digits, including the digits themselves. */\nval digitReprs: Map[String, Int] =\n stringDigitReprs ++ (1 to 9).map(i => i.toString() -> i)\n')),(0,i.kt)("p",null,"We will now have to find the first and last string representation in the line.\nAlthough not the most efficient solution, we do this by building a regular expression that matches any key of the above map."),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'// A regex that matches any of the keys of `digitReprs`\nval digitReprRegex = digitReprs.keysIterator.mkString("|").r\n')),(0,i.kt)("p",null,"Now, we find all matches of the regex in the line, convert them to their numeric values, and concatenate them as before:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'def lineToCoordinates(line: String): Int =\n // Find all the digit representations in the line.\n val matches = digitReprRegex.findAllIn(line).toList\n\n // Convert the string representations into actual digits and form the result\n val firstDigit = digitReprs(matches.head)\n val lastDigit = digitReprs(matches.last)\n s"$firstDigit$lastDigit".toInt\nend lineToCoordinates\n')),(0,i.kt)("p",null,"However, this does not seem to be correct.\nWhen we submit our answer with the above, the checker tells us that our answer is too low.\nWhat went wrong?"),(0,i.kt)("p",null,"It turns out that the dataset contains lines where two textual representations overlap.\nFor example, our data contained:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre"},"29oneightt\n")),(0,i.kt)("p",null,(0,i.kt)("inlineCode",{parentName:"p"},"Regex.findAllIn")," only finds ",(0,i.kt)("em",{parentName:"p"},"non-overlapping")," matches in a string.\nIt therefore returns ",(0,i.kt)("inlineCode",{parentName:"p"},"2"),", ",(0,i.kt)("inlineCode",{parentName:"p"},"9"),", and ",(0,i.kt)("inlineCode",{parentName:"p"},"one"),", but misses the ",(0,i.kt)("inlineCode",{parentName:"p"},"eight")," that overlaps with ",(0,i.kt)("inlineCode",{parentName:"p"},"one"),"."),(0,i.kt)("p",null,"There is no built-in function to handle overlapping matches, nor to find the ",(0,i.kt)("em",{parentName:"p"},"last")," match of a regex in a string.\nInstead, we manually iterate over all the indices to see if a match starts there.\nThis is equivalent to looking for ",(0,i.kt)("em",{parentName:"p"},"prefix")," matches in all the ",(0,i.kt)("em",{parentName:"p"},"suffixes")," of line.\nConveniently, ",(0,i.kt)("inlineCode",{parentName:"p"},"line.tails")," iterates over all such suffixes, and ",(0,i.kt)("inlineCode",{parentName:"p"},"Regex.findPrefixOf")," will look only for prefixes."),(0,i.kt)("p",null,"Our fixed computation for ",(0,i.kt)("inlineCode",{parentName:"p"},"matches")," is now:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},"val matchesIter =\n for\n lineTail <- line.tails\n oneMatch <- digitReprRegex.findPrefixOf(lineTail)\n yield\n oneMatch\nval matches = matchesIter.toList\n")),(0,i.kt)("h2",{id:"final-code"},"Final Code"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-scala"},'def part1(input: String): String =\n // Convert one line into the appropriate coordinates\n def lineToCoordinates(line: String): Int =\n val firstDigit = line.find(_.isDigit).get\n val lastDigit = line.findLast(_.isDigit).get\n s"$firstDigit$lastDigit".toInt\n\n // Convert each line to its coordinates and sum all the coordinates\n val result = input\n .linesIterator\n .map(lineToCoordinates(_))\n .sum\n result.toString()\nend part1\n\n/** The textual representation of digits. */\nval stringDigitReprs = Map(\n "one" -> 1,\n "two" -> 2,\n "three" -> 3,\n "four" -> 4,\n "five" -> 5,\n "six" -> 6,\n "seven" -> 7,\n "eight" -> 8,\n "nine" -> 9,\n)\n\n/** All the string representation of digits, including the digits themselves. */\nval digitReprs = stringDigitReprs ++ (1 to 9).map(i => i.toString() -> i)\n\ndef part2(input: String): String =\n // A regex that matches any of the keys of `digitReprs`\n val digitReprRegex = digitReprs.keysIterator.mkString("|").r\n\n def lineToCoordinates(line: String): Int =\n // Find all the digit representations in the line\n val matchesIter =\n for\n lineTail <- line.tails\n oneMatch <- digitReprRegex.findPrefixOf(lineTail)\n yield\n oneMatch\n val matches = matchesIter.toList\n\n // Convert the string representations into actual digits and form the result\n val firstDigit = digitReprs(matches.head)\n val lastDigit = digitReprs(matches.last)\n s"$firstDigit$lastDigit".toInt\n end lineToCoordinates\n\n // Process lines as in part1\n val result = input\n .linesIterator\n .map(lineToCoordinates(_))\n .sum\n result.toString()\nend part2\n')),(0,i.kt)("h3",{id:"run-it-in-the-browser"},"Run it in the browser"),(0,i.kt)("h4",{id:"part-1-1"},"Part 1"),(0,i.kt)(l.Z,{puzzle:"day01-part1",year:"2023",mdxType:"Solver"}),(0,i.kt)("h4",{id:"part-2-1"},"Part 2"),(0,i.kt)(l.Z,{puzzle:"day01-part2",year:"2023",mdxType:"Solver"}),(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/pkarthick/AdventOfCode/blob/master/2023/scala/src/main/scala/day01.scala"},"Solution")," of ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/pkarthick"},"Karthick Pachiappan")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/Jannyboy11/AdventOfCode2023/blob/master/src/main/scala/day01/Day01.scala"},"Solution")," of ",(0,i.kt)("a",{parentName:"li",href:"https://twitter.com/JanBoerman95"},"Jan Boerman"),"."),(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/day1.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/spamegg1/advent-of-code-2023-scala/blob/solutions/01.worksheet.sc#L65"},"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/jnclt/adventofcode2023/tree/main/day01"},"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://github.com/KristianAN/AoC2023/blob/main/scala/src/Day1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/KristianAN"},"KristianAN")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://gist.github.com/CJSmith-0141/a84b3d213bdd8ed7c256561132d19b8d"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/CJSmith-0141"},"CJ Smith")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/ChidiRnweke/AOC23/blob/main/src/main/scala/day1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/ChidiRnweke"},"Chidi Nweke")),(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/Day1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/YannMoisan"},"YannMoisan")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/bxiang/advent-of-code-2023/blob/main/src/main/scala/com/aoc/day1/Solution.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/bxiang"},"Brian Xiang")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/bishabosha/advent-of-code-2023/blob/main/2023-day01.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/SethTisue/adventofcode/blob/main/2023/src/test/scala/Day01.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/SethTisue"},"Seth Tisue")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/Philippus/adventofcode/tree/main/src/main/scala/adventofcode2023/day1/Day1.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/kbielefe/advent-of-code/blob/edf8e706229a5f3785291824f26778de8a583c35/2023/src/main/scala/1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/kbielefe"},"Karl Bielefeldt")),(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/day01/DayOne.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/joeledwards/advent-of-code/blob/master/2023/src/main/scala/com/buzuli/advent/days/day1.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/joeledwards"},"Joel Edwards")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/wbillingsley/advent-of-code-2023-scala/blob/star2/solver.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/wbillingsley"},"Will Billingsley")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/mpilquist/aoc/blob/main/2023/day1.sc"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/mpilquist"},"Michael Pilquist")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/GrigoriiBerezin/advent_code_2023/tree/master/task01/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/marconilanna/advent-of-code/blob/master/2023/Day01.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/marconilanna"},"Marconi Lanna")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/xRuiAlves/advent-of-code-2023/blob/main/Day1.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/AvaPL/Advent-of-Code-2023/tree/main/src/main/scala/day1"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/AvaPL"},"Pawe\u0142 Cembaluk"))),(0,i.kt)("p",null,"Share your solution to the Scala community by editing this page."))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/935f2afb.4524d548.js b/assets/js/935f2afb.4524d548.js deleted file mode 100644 index 2ed2dbda8..000000000 --- a/assets/js/935f2afb.4524d548.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[53],{1109:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"adventOfCodeSidebar":[{"type":"link","label":"Introduction","href":"/scala-advent-of-code/introduction","docId":"introduction"},{"type":"link","label":"Setup","href":"/scala-advent-of-code/setup","docId":"setup"},{"type":"category","label":"2024 Puzzles","items":[{"type":"link","label":"Day 1: Historian Hysteria","href":"/scala-advent-of-code/2024/puzzles/day01","docId":"2024/puzzles/day01"},{"type":"link","label":"Day 2: Red-Nosed Reports","href":"/scala-advent-of-code/2024/puzzles/day02","docId":"2024/puzzles/day02"},{"type":"link","label":"Day 3: Mull It Over","href":"/scala-advent-of-code/2024/puzzles/day03","docId":"2024/puzzles/day03"},{"type":"link","label":"Day 4: Ceres Search","href":"/scala-advent-of-code/2024/puzzles/day04","docId":"2024/puzzles/day04"},{"type":"link","label":"Day 5: Print Queue","href":"/scala-advent-of-code/2024/puzzles/day05","docId":"2024/puzzles/day05"},{"type":"link","label":"Day 6: Guard Gallivant","href":"/scala-advent-of-code/2024/puzzles/day06","docId":"2024/puzzles/day06"},{"type":"link","label":"Day 7: Bridge Repair","href":"/scala-advent-of-code/2024/puzzles/day07","docId":"2024/puzzles/day07"},{"type":"link","label":"Day 8: Resonant Collinearity","href":"/scala-advent-of-code/2024/puzzles/day08","docId":"2024/puzzles/day08"},{"type":"link","label":"Day 9: Disk Fragmenter","href":"/scala-advent-of-code/2024/puzzles/day09","docId":"2024/puzzles/day09"},{"type":"link","label":"Day 10: Hoof It","href":"/scala-advent-of-code/2024/puzzles/day10","docId":"2024/puzzles/day10"},{"type":"link","label":"Day 11: Plutonian Pebbles","href":"/scala-advent-of-code/2024/puzzles/day11","docId":"2024/puzzles/day11"},{"type":"link","label":"Day 12: Garden Groups","href":"/scala-advent-of-code/2024/puzzles/day12","docId":"2024/puzzles/day12"},{"type":"link","label":"Day 13: Claw Contraption","href":"/scala-advent-of-code/2024/puzzles/day13","docId":"2024/puzzles/day13"},{"type":"link","label":"Day 14: Restroom Redoubt","href":"/scala-advent-of-code/2024/puzzles/day14","docId":"2024/puzzles/day14"},{"type":"link","label":"Day 15: Warehouse Woes","href":"/scala-advent-of-code/2024/puzzles/day15","docId":"2024/puzzles/day15"},{"type":"link","label":"Day 16: Reindeer Maze","href":"/scala-advent-of-code/2024/puzzles/day16","docId":"2024/puzzles/day16"},{"type":"link","label":"Day 17: Chronospatial Computer","href":"/scala-advent-of-code/2024/puzzles/day17","docId":"2024/puzzles/day17"},{"type":"link","label":"Day 18: RAM Run","href":"/scala-advent-of-code/2024/puzzles/day18","docId":"2024/puzzles/day18"},{"type":"link","label":"Day 19: Linen Layout","href":"/scala-advent-of-code/2024/puzzles/day19","docId":"2024/puzzles/day19"},{"type":"link","label":"Day 20: Race Condition","href":"/scala-advent-of-code/2024/puzzles/day20","docId":"2024/puzzles/day20"}],"collapsed":true,"collapsible":true},{"type":"category","label":"2023 Puzzles","items":[{"type":"link","label":"Day 1: Trebuchet?!","href":"/scala-advent-of-code/2023/puzzles/day01","docId":"2023/puzzles/day01"},{"type":"link","label":"Day 2: Cube Conundrum","href":"/scala-advent-of-code/2023/puzzles/day02","docId":"2023/puzzles/day02"},{"type":"link","label":"Day 3: Gear Ratios","href":"/scala-advent-of-code/2023/puzzles/day03","docId":"2023/puzzles/day03"},{"type":"link","label":"Day 4: Scratchcards","href":"/scala-advent-of-code/2023/puzzles/day04","docId":"2023/puzzles/day04"},{"type":"link","label":"Day 5: If You Give A Seed A Fertilizer","href":"/scala-advent-of-code/2023/puzzles/day05","docId":"2023/puzzles/day05"},{"type":"link","label":"Day 6: Wait For It","href":"/scala-advent-of-code/2023/puzzles/day06","docId":"2023/puzzles/day06"},{"type":"link","label":"Day 7: Camel Cards","href":"/scala-advent-of-code/2023/puzzles/day07","docId":"2023/puzzles/day07"},{"type":"link","label":"Day 8: Haunted Wasteland","href":"/scala-advent-of-code/2023/puzzles/day08","docId":"2023/puzzles/day08"},{"type":"link","label":"Day 9: Mirage Maintenance","href":"/scala-advent-of-code/2023/puzzles/day09","docId":"2023/puzzles/day09"},{"type":"link","label":"Day 10: Pipe Maze","href":"/scala-advent-of-code/2023/puzzles/day10","docId":"2023/puzzles/day10"},{"type":"link","label":"Day 11: Cosmic Expansion","href":"/scala-advent-of-code/2023/puzzles/day11","docId":"2023/puzzles/day11"},{"type":"link","label":"Day 12: Hot Springs","href":"/scala-advent-of-code/2023/puzzles/day12","docId":"2023/puzzles/day12"},{"type":"link","label":"Day 13: Point of Incidence","href":"/scala-advent-of-code/2023/puzzles/day13","docId":"2023/puzzles/day13"},{"type":"link","label":"Day 14: Parabolic Reflector Dish","href":"/scala-advent-of-code/2023/puzzles/day14","docId":"2023/puzzles/day14"},{"type":"link","label":"Day 15: Lens Library","href":"/scala-advent-of-code/2023/puzzles/day15","docId":"2023/puzzles/day15"},{"type":"link","label":"Day 16: The Floor Will Be Lava","href":"/scala-advent-of-code/2023/puzzles/day16","docId":"2023/puzzles/day16"},{"type":"link","label":"Day 17: Clumsy Crucible","href":"/scala-advent-of-code/2023/puzzles/day17","docId":"2023/puzzles/day17"},{"type":"link","label":"Day 18: Lavaduct Lagoon","href":"/scala-advent-of-code/2023/puzzles/day18","docId":"2023/puzzles/day18"},{"type":"link","label":"Day 19: Aplenty","href":"/scala-advent-of-code/2023/puzzles/day19","docId":"2023/puzzles/day19"},{"type":"link","label":"Day 20: Pulse Propagation","href":"/scala-advent-of-code/2023/puzzles/day20","docId":"2023/puzzles/day20"},{"type":"link","label":"Day 21: Step Counter","href":"/scala-advent-of-code/2023/puzzles/day21","docId":"2023/puzzles/day21"},{"type":"link","label":"Day 22: Sand Slabs","href":"/scala-advent-of-code/2023/puzzles/day22","docId":"2023/puzzles/day22"},{"type":"link","label":"Day 23: A Long Walk","href":"/scala-advent-of-code/2023/puzzles/day23","docId":"2023/puzzles/day23"},{"type":"link","label":"Day 24: Never Tell Me The Odds","href":"/scala-advent-of-code/2023/puzzles/day24","docId":"2023/puzzles/day24"},{"type":"link","label":"Day 25: Snowverload","href":"/scala-advent-of-code/2023/puzzles/day25","docId":"2023/puzzles/day25"}],"collapsed":true,"collapsible":true},{"type":"category","label":"2022 Puzzles","items":[{"type":"link","label":"Day 1: Calorie Counting","href":"/scala-advent-of-code/2022/puzzles/day01","docId":"2022/puzzles/day01"},{"type":"link","label":"Day 2: Rock Paper Scissors","href":"/scala-advent-of-code/2022/puzzles/day02","docId":"2022/puzzles/day02"},{"type":"link","label":"Day 3: Rucksack Reorganization","href":"/scala-advent-of-code/2022/puzzles/day03","docId":"2022/puzzles/day03"},{"type":"link","label":"Day 4: Camp Cleanup","href":"/scala-advent-of-code/2022/puzzles/day04","docId":"2022/puzzles/day04"},{"type":"link","label":"Day 5: Supply Stacks","href":"/scala-advent-of-code/2022/puzzles/day05","docId":"2022/puzzles/day05"},{"type":"link","label":"Day 6: Tuning Trouble","href":"/scala-advent-of-code/2022/puzzles/day06","docId":"2022/puzzles/day06"},{"type":"link","label":"Day 7: No Space Left On Device","href":"/scala-advent-of-code/2022/puzzles/day07","docId":"2022/puzzles/day07"},{"type":"link","label":"Day 8: Treetop Tree House","href":"/scala-advent-of-code/2022/puzzles/day08","docId":"2022/puzzles/day08"},{"type":"link","label":"Day 9: Rope Bridge","href":"/scala-advent-of-code/2022/puzzles/day09","docId":"2022/puzzles/day09"},{"type":"link","label":"Day 10: Cathode-Ray Tube","href":"/scala-advent-of-code/2022/puzzles/day10","docId":"2022/puzzles/day10"},{"type":"link","label":"Day 11: Monkey in the Middle","href":"/scala-advent-of-code/2022/puzzles/day11","docId":"2022/puzzles/day11"},{"type":"link","label":"Day 12: Hill Climbing Algorithm","href":"/scala-advent-of-code/2022/puzzles/day12","docId":"2022/puzzles/day12"},{"type":"link","label":"Day 13: Distress Signal","href":"/scala-advent-of-code/2022/puzzles/day13","docId":"2022/puzzles/day13"},{"type":"link","label":"Day 14: Regolith Reservoir","href":"/scala-advent-of-code/2022/puzzles/day14","docId":"2022/puzzles/day14"},{"type":"link","label":"Day 15: Beacon Exclusion Zone","href":"/scala-advent-of-code/2022/puzzles/day15","docId":"2022/puzzles/day15"},{"type":"link","label":"Day 16: Proboscidea Volcanium","href":"/scala-advent-of-code/2022/puzzles/day16","docId":"2022/puzzles/day16"},{"type":"link","label":"Day 17: Pyroclastic Flow","href":"/scala-advent-of-code/2022/puzzles/day17","docId":"2022/puzzles/day17"},{"type":"link","label":"Day 18: Boiling Boulders","href":"/scala-advent-of-code/2022/puzzles/day18","docId":"2022/puzzles/day18"},{"type":"link","label":"Day 19: Not Enough Minerals","href":"/scala-advent-of-code/2022/puzzles/day19","docId":"2022/puzzles/day19"},{"type":"link","label":"Day 20: Grove Positioning System","href":"/scala-advent-of-code/2022/puzzles/day20","docId":"2022/puzzles/day20"},{"type":"link","label":"Day 21: Monkey Math","href":"/scala-advent-of-code/2022/puzzles/day21","docId":"2022/puzzles/day21"},{"type":"link","label":"Day 22: Monkey Map","href":"/scala-advent-of-code/2022/puzzles/day22","docId":"2022/puzzles/day22"},{"type":"link","label":"Day 23: Unstable Diffusion","href":"/scala-advent-of-code/2022/puzzles/day23","docId":"2022/puzzles/day23"},{"type":"link","label":"Day 24: Blizzard Basin","href":"/scala-advent-of-code/2022/puzzles/day24","docId":"2022/puzzles/day24"},{"type":"link","label":"Day 25: Full of Hot Air","href":"/scala-advent-of-code/2022/puzzles/day25","docId":"2022/puzzles/day25"}],"collapsed":true,"collapsible":true},{"type":"category","label":"2021 Puzzles","items":[{"type":"link","label":"Day 1: Sonar Sweep","href":"/scala-advent-of-code/puzzles/day1","docId":"puzzles/day1"},{"type":"link","label":"Day 2: Dive!","href":"/scala-advent-of-code/puzzles/day2","docId":"puzzles/day2"},{"type":"link","label":"Day 3: Binary Diagnostic","href":"/scala-advent-of-code/puzzles/day3","docId":"puzzles/day3"},{"type":"link","label":"Day 4: Giant Squid","href":"/scala-advent-of-code/puzzles/day4","docId":"puzzles/day4"},{"type":"link","label":"Day 5: Hydrothermal Venture","href":"/scala-advent-of-code/puzzles/day5","docId":"puzzles/day5"},{"type":"link","label":"Day 6: Lanternfish","href":"/scala-advent-of-code/puzzles/day6","docId":"puzzles/day6"},{"type":"link","label":"Day 7: The Treachery of Whales","href":"/scala-advent-of-code/puzzles/day7","docId":"puzzles/day7"},{"type":"link","label":"Day 8: Seven Segment Search","href":"/scala-advent-of-code/puzzles/day8","docId":"puzzles/day8"},{"type":"link","label":"Day 9: Smoke Basin","href":"/scala-advent-of-code/puzzles/day9","docId":"puzzles/day9"},{"type":"link","label":"Day 10: Syntax Scoring","href":"/scala-advent-of-code/puzzles/day10","docId":"puzzles/day10"},{"type":"link","label":"Day 11: Dumbo Octopus","href":"/scala-advent-of-code/puzzles/day11","docId":"puzzles/day11"},{"type":"link","label":"Day 12: Passage Pathing","href":"/scala-advent-of-code/puzzles/day12","docId":"puzzles/day12"},{"type":"link","label":"Day 13: Transparent Origami","href":"/scala-advent-of-code/puzzles/day13","docId":"puzzles/day13"},{"type":"link","label":"Day 14: Extended Polymerization","href":"/scala-advent-of-code/puzzles/day14","docId":"puzzles/day14"},{"type":"link","label":"Day 15: Chiton","href":"/scala-advent-of-code/puzzles/day15","docId":"puzzles/day15"},{"type":"link","label":"Day 16: Packet Decoder","href":"/scala-advent-of-code/puzzles/day16","docId":"puzzles/day16"},{"type":"link","label":"Day 17: Trick Shot","href":"/scala-advent-of-code/puzzles/day17","docId":"puzzles/day17"},{"type":"link","label":"Day 18: Snailfish","href":"/scala-advent-of-code/puzzles/day18","docId":"puzzles/day18"},{"type":"link","label":"Day 19: Beacon Scanner","href":"/scala-advent-of-code/puzzles/day19","docId":"puzzles/day19"},{"type":"link","label":"Day 20: Trench Map","href":"/scala-advent-of-code/puzzles/day20","docId":"puzzles/day20"},{"type":"link","label":"Day 21: Dirac Dice","href":"/scala-advent-of-code/puzzles/day21","docId":"puzzles/day21"},{"type":"link","label":"Day 22: Reactor Reboot","href":"/scala-advent-of-code/puzzles/day22","docId":"puzzles/day22"},{"type":"link","label":"Day 23: Amphipod","href":"/scala-advent-of-code/puzzles/day23","docId":"puzzles/day23"},{"type":"link","label":"Day 24: Arithmetic Logic Unit","href":"/scala-advent-of-code/puzzles/day24","docId":"puzzles/day24"},{"type":"link","label":"Day 25: Sea Cucumber","href":"/scala-advent-of-code/puzzles/day25","docId":"puzzles/day25"}],"collapsed":true,"collapsible":true}]},"docs":{"2022/puzzles/day01":{"id":"2022/puzzles/day01","title":"Day 1: Calorie Counting","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day02":{"id":"2022/puzzles/day02","title":"Day 2: Rock Paper Scissors","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day03":{"id":"2022/puzzles/day03","title":"Day 3: Rucksack Reorganization","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day04":{"id":"2022/puzzles/day04","title":"Day 4: Camp Cleanup","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day05":{"id":"2022/puzzles/day05","title":"Day 5: Supply Stacks","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day06":{"id":"2022/puzzles/day06","title":"Day 6: Tuning Trouble","description":"Code by Jan Boerman, and Jamie Thompson.","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day07":{"id":"2022/puzzles/day07","title":"Day 7: No Space Left On Device","description":"code by Jan Boerman","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day08":{"id":"2022/puzzles/day08","title":"Day 8: Treetop Tree House","description":"code and article by Quentin Bernet","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day09":{"id":"2022/puzzles/day09","title":"Day 9: Rope Bridge","description":"code by Jamie Thompson","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day10":{"id":"2022/puzzles/day10","title":"Day 10: Cathode-Ray Tube","description":"code and article by Mewen Crespo (reviewed by Jamie Thompson)","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day11":{"id":"2022/puzzles/day11","title":"Day 11: Monkey in the Middle","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day12":{"id":"2022/puzzles/day12","title":"Day 12: Hill Climbing Algorithm","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day13":{"id":"2022/puzzles/day13","title":"Day 13: Distress Signal","description":"by Jamie Thompson","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day14":{"id":"2022/puzzles/day14","title":"Day 14: Regolith Reservoir","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day15":{"id":"2022/puzzles/day15","title":"Day 15: Beacon Exclusion Zone","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day16":{"id":"2022/puzzles/day16","title":"Day 16: Proboscidea Volcanium","description":"code by Tyler Coles (javadocmd.com), Quentin Bernet, @sjrd, and @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day17":{"id":"2022/puzzles/day17","title":"Day 17: Pyroclastic Flow","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day18":{"id":"2022/puzzles/day18","title":"Day 18: Boiling Boulders","description":"by LaurenceWarne","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day19":{"id":"2022/puzzles/day19","title":"Day 19: Not Enough Minerals","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day20":{"id":"2022/puzzles/day20","title":"Day 20: Grove Positioning System","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day21":{"id":"2022/puzzles/day21","title":"Day 21: Monkey Math","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day22":{"id":"2022/puzzles/day22","title":"Day 22: Monkey Map","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day23":{"id":"2022/puzzles/day23","title":"Day 23: Unstable Diffusion","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day24":{"id":"2022/puzzles/day24","title":"Day 24: Blizzard Basin","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day25":{"id":"2022/puzzles/day25","title":"Day 25: Full of Hot Air","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day01":{"id":"2023/puzzles/day01","title":"Day 1: Trebuchet?!","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day02":{"id":"2023/puzzles/day02","title":"Day 2: Cube Conundrum","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day03":{"id":"2023/puzzles/day03","title":"Day 3: Gear Ratios","description":"by @bishabosha and @iusildra","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day04":{"id":"2023/puzzles/day04","title":"Day 4: Scratchcards","description":"by @shardulc","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day05":{"id":"2023/puzzles/day05","title":"Day 5: If You Give A Seed A Fertilizer","description":"by @g.berezin and @bishabosha","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day06":{"id":"2023/puzzles/day06","title":"Day 6: Wait For It","description":"by Spamegg","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day07":{"id":"2023/puzzles/day07","title":"Day 7: Camel Cards","description":"by @anatoliykmetyuk","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day08":{"id":"2023/puzzles/day08","title":"Day 8: Haunted Wasteland","description":"by @prinsniels","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day09":{"id":"2023/puzzles/day09","title":"Day 9: Mirage Maintenance","description":"by @SethTisue","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day10":{"id":"2023/puzzles/day10","title":"Day 10: Pipe Maze","description":"by @EugeneFlesselle","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day11":{"id":"2023/puzzles/day11","title":"Day 11: Cosmic Expansion","description":"by @natsukagami","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day12":{"id":"2023/puzzles/day12","title":"Day 12: Hot Springs","description":"by @mbovel","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day13":{"id":"2023/puzzles/day13","title":"Day 13: Point of Incidence","description":"by @adpi2","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day14":{"id":"2023/puzzles/day14","title":"Day 14: Parabolic Reflector Dish","description":"by @anatoliykmetyuk","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day15":{"id":"2023/puzzles/day15","title":"Day 15: Lens Library","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day16":{"id":"2023/puzzles/day16","title":"Day 16: The Floor Will Be Lava","description":"By @iusildra","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day17":{"id":"2023/puzzles/day17","title":"Day 17: Clumsy Crucible","description":"by @stewSquared","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day18":{"id":"2023/puzzles/day18","title":"Day 18: Lavaduct Lagoon","description":"by @EugeneFlesselle","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day19":{"id":"2023/puzzles/day19","title":"Day 19: Aplenty","description":"by @mbovel","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day20":{"id":"2023/puzzles/day20","title":"Day 20: Pulse Propagation","description":"by @merlinorg","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day21":{"id":"2023/puzzles/day21","title":"Day 21: Step Counter","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day22":{"id":"2023/puzzles/day22","title":"Day 22: Sand Slabs","description":"by Pawe\u0142 Cembaluk","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day23":{"id":"2023/puzzles/day23","title":"Day 23: A Long Walk","description":"by @stewSquared","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day24":{"id":"2023/puzzles/day24","title":"Day 24: Never Tell Me The Odds","description":"by @merlinorg","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day25":{"id":"2023/puzzles/day25","title":"Day 25: Snowverload","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day0":{"id":"2024/puzzles/day0","title":"day0","description":"Redirecting..."},"2024/puzzles/day01":{"id":"2024/puzzles/day01","title":"Day 1: Historian Hysteria","description":"by @spamegg1","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day02":{"id":"2024/puzzles/day02","title":"Day 2: Red-Nosed Reports","description":"by @spamegg1","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day03":{"id":"2024/puzzles/day03","title":"Day 3: Mull It Over","description":"by @makingthematrix","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day04":{"id":"2024/puzzles/day04","title":"Day 4: Ceres Search","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day05":{"id":"2024/puzzles/day05","title":"Day 5: Print Queue","description":"by @KacperFKorban","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day06":{"id":"2024/puzzles/day06","title":"Day 6: Guard Gallivant","description":"by @samuelchassot","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day07":{"id":"2024/puzzles/day07","title":"Day 7: Bridge Repair","description":"by @philippus","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day08":{"id":"2024/puzzles/day08","title":"Day 8: Resonant Collinearity","description":"by @merlinorg","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day09":{"id":"2024/puzzles/day09","title":"Day 9: Disk Fragmenter","description":"by @dyvrl","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day10":{"id":"2024/puzzles/day10","title":"Day 10: Hoof It","description":"by @SethTisue","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day11":{"id":"2024/puzzles/day11","title":"Day 11: Plutonian Pebbles","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day12":{"id":"2024/puzzles/day12","title":"Day 12: Garden Groups","description":"by @TheDrawingCoder-Gamer","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day13":{"id":"2024/puzzles/day13","title":"Day 13: Claw Contraption","description":"by @scarf005","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day14":{"id":"2024/puzzles/day14","title":"Day 14: Restroom Redoubt","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day15":{"id":"2024/puzzles/day15","title":"Day 15: Warehouse Woes","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day16":{"id":"2024/puzzles/day16","title":"Day 16: Reindeer Maze","description":"by @merlinorg","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day17":{"id":"2024/puzzles/day17","title":"Day 17: Chronospatial Computer","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day18":{"id":"2024/puzzles/day18","title":"Day 18: RAM Run","description":"by @TheDrawingCoder-Gamer","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day19":{"id":"2024/puzzles/day19","title":"Day 19: Linen Layout","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day20":{"id":"2024/puzzles/day20","title":"Day 20: Race Condition","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"introduction":{"id":"introduction","title":"Introduction","description":"Welcome to the Scala Center\'s take on Advent of Code.","sidebar":"adventOfCodeSidebar"},"puzzles/day1":{"id":"puzzles/day1","title":"Day 1: Sonar Sweep","description":"by @adpi2","sidebar":"adventOfCodeSidebar"},"puzzles/day10":{"id":"puzzles/day10","title":"Day 10: Syntax Scoring","description":"by @VincenzoBaz","sidebar":"adventOfCodeSidebar"},"puzzles/day11":{"id":"puzzles/day11","title":"Day 11: Dumbo Octopus","description":"by @tgodzik","sidebar":"adventOfCodeSidebar"},"puzzles/day12":{"id":"puzzles/day12","title":"Day 12: Passage Pathing","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day13":{"id":"puzzles/day13","title":"Day 13: Transparent Origami","description":"by @adpi2","sidebar":"adventOfCodeSidebar"},"puzzles/day14":{"id":"puzzles/day14","title":"Day 14: Extended Polymerization","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"puzzles/day15":{"id":"puzzles/day15","title":"Day 15: Chiton","description":"By @anatoliykmetyuk","sidebar":"adventOfCodeSidebar"},"puzzles/day16":{"id":"puzzles/day16","title":"Day 16: Packet Decoder","description":"by @tgodzik","sidebar":"adventOfCodeSidebar"},"puzzles/day17":{"id":"puzzles/day17","title":"Day 17: Trick Shot","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"puzzles/day18":{"id":"puzzles/day18","title":"Day 18: Snailfish","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day19":{"id":"puzzles/day19","title":"Day 19: Beacon Scanner","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day2":{"id":"puzzles/day2","title":"Day 2: Dive!","description":"by @mlachkar","sidebar":"adventOfCodeSidebar"},"puzzles/day20":{"id":"puzzles/day20","title":"Day 20: Trench Map","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day21":{"id":"puzzles/day21","title":"Day 21: Dirac Dice","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"puzzles/day22":{"id":"puzzles/day22","title":"Day 22: Reactor Reboot","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"puzzles/day23":{"id":"puzzles/day23","title":"Day 23: Amphipod","description":"by @adpi2","sidebar":"adventOfCodeSidebar"},"puzzles/day24":{"id":"puzzles/day24","title":"Day 24: Arithmetic Logic Unit","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day25":{"id":"puzzles/day25","title":"Day 25: Sea Cucumber","description":"by @Sporarum, student at EPFL, and @adpi2","sidebar":"adventOfCodeSidebar"},"puzzles/day3":{"id":"puzzles/day3","title":"Day 3: Binary Diagnostic","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"puzzles/day4":{"id":"puzzles/day4","title":"Day 4: Giant Squid","description":"by @Sporarum, student at EPFL.","sidebar":"adventOfCodeSidebar"},"puzzles/day5":{"id":"puzzles/day5","title":"Day 5: Hydrothermal Venture","description":"by @tgodzik","sidebar":"adventOfCodeSidebar"},"puzzles/day6":{"id":"puzzles/day6","title":"Day 6: Lanternfish","description":"by @julienrf","sidebar":"adventOfCodeSidebar"},"puzzles/day7":{"id":"puzzles/day7","title":"Day 7: The Treachery of Whales","description":"by @tgodzik","sidebar":"adventOfCodeSidebar"},"puzzles/day8":{"id":"puzzles/day8","title":"Day 8: Seven Segment Search","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"puzzles/day9":{"id":"puzzles/day9","title":"Day 9: Smoke Basin","description":"by @VincenzoBaz","sidebar":"adventOfCodeSidebar"},"setup":{"id":"setup","title":"Setup","description":"There are many ways to get started with Scala and we will suggest that you try Scala CLI, developed by VirtusLab.","sidebar":"adventOfCodeSidebar"}}}')}}]); \ No newline at end of file diff --git a/assets/js/935f2afb.ac7730e4.js b/assets/js/935f2afb.ac7730e4.js new file mode 100644 index 000000000..b9308568d --- /dev/null +++ b/assets/js/935f2afb.ac7730e4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[53],{1109:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"adventOfCodeSidebar":[{"type":"link","label":"Introduction","href":"/scala-advent-of-code/introduction","docId":"introduction"},{"type":"link","label":"Setup","href":"/scala-advent-of-code/setup","docId":"setup"},{"type":"category","label":"2024 Puzzles","items":[{"type":"link","label":"Day 1: Historian Hysteria","href":"/scala-advent-of-code/2024/puzzles/day01","docId":"2024/puzzles/day01"},{"type":"link","label":"Day 2: Red-Nosed Reports","href":"/scala-advent-of-code/2024/puzzles/day02","docId":"2024/puzzles/day02"},{"type":"link","label":"Day 3: Mull It Over","href":"/scala-advent-of-code/2024/puzzles/day03","docId":"2024/puzzles/day03"},{"type":"link","label":"Day 4: Ceres Search","href":"/scala-advent-of-code/2024/puzzles/day04","docId":"2024/puzzles/day04"},{"type":"link","label":"Day 5: Print Queue","href":"/scala-advent-of-code/2024/puzzles/day05","docId":"2024/puzzles/day05"},{"type":"link","label":"Day 6: Guard Gallivant","href":"/scala-advent-of-code/2024/puzzles/day06","docId":"2024/puzzles/day06"},{"type":"link","label":"Day 7: Bridge Repair","href":"/scala-advent-of-code/2024/puzzles/day07","docId":"2024/puzzles/day07"},{"type":"link","label":"Day 8: Resonant Collinearity","href":"/scala-advent-of-code/2024/puzzles/day08","docId":"2024/puzzles/day08"},{"type":"link","label":"Day 9: Disk Fragmenter","href":"/scala-advent-of-code/2024/puzzles/day09","docId":"2024/puzzles/day09"},{"type":"link","label":"Day 10: Hoof It","href":"/scala-advent-of-code/2024/puzzles/day10","docId":"2024/puzzles/day10"},{"type":"link","label":"Day 11: Plutonian Pebbles","href":"/scala-advent-of-code/2024/puzzles/day11","docId":"2024/puzzles/day11"},{"type":"link","label":"Day 12: Garden Groups","href":"/scala-advent-of-code/2024/puzzles/day12","docId":"2024/puzzles/day12"},{"type":"link","label":"Day 13: Claw Contraption","href":"/scala-advent-of-code/2024/puzzles/day13","docId":"2024/puzzles/day13"},{"type":"link","label":"Day 14: Restroom Redoubt","href":"/scala-advent-of-code/2024/puzzles/day14","docId":"2024/puzzles/day14"},{"type":"link","label":"Day 15: Warehouse Woes","href":"/scala-advent-of-code/2024/puzzles/day15","docId":"2024/puzzles/day15"},{"type":"link","label":"Day 16: Reindeer Maze","href":"/scala-advent-of-code/2024/puzzles/day16","docId":"2024/puzzles/day16"},{"type":"link","label":"Day 17: Chronospatial Computer","href":"/scala-advent-of-code/2024/puzzles/day17","docId":"2024/puzzles/day17"},{"type":"link","label":"Day 18: RAM Run","href":"/scala-advent-of-code/2024/puzzles/day18","docId":"2024/puzzles/day18"},{"type":"link","label":"Day 19: Linen Layout","href":"/scala-advent-of-code/2024/puzzles/day19","docId":"2024/puzzles/day19"},{"type":"link","label":"Day 20: Race Condition","href":"/scala-advent-of-code/2024/puzzles/day20","docId":"2024/puzzles/day20"},{"type":"link","label":"Day 21: Keypad Conundrum","href":"/scala-advent-of-code/2024/puzzles/day21","docId":"2024/puzzles/day21"}],"collapsed":true,"collapsible":true},{"type":"category","label":"2023 Puzzles","items":[{"type":"link","label":"Day 1: Trebuchet?!","href":"/scala-advent-of-code/2023/puzzles/day01","docId":"2023/puzzles/day01"},{"type":"link","label":"Day 2: Cube Conundrum","href":"/scala-advent-of-code/2023/puzzles/day02","docId":"2023/puzzles/day02"},{"type":"link","label":"Day 3: Gear Ratios","href":"/scala-advent-of-code/2023/puzzles/day03","docId":"2023/puzzles/day03"},{"type":"link","label":"Day 4: Scratchcards","href":"/scala-advent-of-code/2023/puzzles/day04","docId":"2023/puzzles/day04"},{"type":"link","label":"Day 5: If You Give A Seed A Fertilizer","href":"/scala-advent-of-code/2023/puzzles/day05","docId":"2023/puzzles/day05"},{"type":"link","label":"Day 6: Wait For It","href":"/scala-advent-of-code/2023/puzzles/day06","docId":"2023/puzzles/day06"},{"type":"link","label":"Day 7: Camel Cards","href":"/scala-advent-of-code/2023/puzzles/day07","docId":"2023/puzzles/day07"},{"type":"link","label":"Day 8: Haunted Wasteland","href":"/scala-advent-of-code/2023/puzzles/day08","docId":"2023/puzzles/day08"},{"type":"link","label":"Day 9: Mirage Maintenance","href":"/scala-advent-of-code/2023/puzzles/day09","docId":"2023/puzzles/day09"},{"type":"link","label":"Day 10: Pipe Maze","href":"/scala-advent-of-code/2023/puzzles/day10","docId":"2023/puzzles/day10"},{"type":"link","label":"Day 11: Cosmic Expansion","href":"/scala-advent-of-code/2023/puzzles/day11","docId":"2023/puzzles/day11"},{"type":"link","label":"Day 12: Hot Springs","href":"/scala-advent-of-code/2023/puzzles/day12","docId":"2023/puzzles/day12"},{"type":"link","label":"Day 13: Point of Incidence","href":"/scala-advent-of-code/2023/puzzles/day13","docId":"2023/puzzles/day13"},{"type":"link","label":"Day 14: Parabolic Reflector Dish","href":"/scala-advent-of-code/2023/puzzles/day14","docId":"2023/puzzles/day14"},{"type":"link","label":"Day 15: Lens Library","href":"/scala-advent-of-code/2023/puzzles/day15","docId":"2023/puzzles/day15"},{"type":"link","label":"Day 16: The Floor Will Be Lava","href":"/scala-advent-of-code/2023/puzzles/day16","docId":"2023/puzzles/day16"},{"type":"link","label":"Day 17: Clumsy Crucible","href":"/scala-advent-of-code/2023/puzzles/day17","docId":"2023/puzzles/day17"},{"type":"link","label":"Day 18: Lavaduct Lagoon","href":"/scala-advent-of-code/2023/puzzles/day18","docId":"2023/puzzles/day18"},{"type":"link","label":"Day 19: Aplenty","href":"/scala-advent-of-code/2023/puzzles/day19","docId":"2023/puzzles/day19"},{"type":"link","label":"Day 20: Pulse Propagation","href":"/scala-advent-of-code/2023/puzzles/day20","docId":"2023/puzzles/day20"},{"type":"link","label":"Day 21: Step Counter","href":"/scala-advent-of-code/2023/puzzles/day21","docId":"2023/puzzles/day21"},{"type":"link","label":"Day 22: Sand Slabs","href":"/scala-advent-of-code/2023/puzzles/day22","docId":"2023/puzzles/day22"},{"type":"link","label":"Day 23: A Long Walk","href":"/scala-advent-of-code/2023/puzzles/day23","docId":"2023/puzzles/day23"},{"type":"link","label":"Day 24: Never Tell Me The Odds","href":"/scala-advent-of-code/2023/puzzles/day24","docId":"2023/puzzles/day24"},{"type":"link","label":"Day 25: Snowverload","href":"/scala-advent-of-code/2023/puzzles/day25","docId":"2023/puzzles/day25"}],"collapsed":true,"collapsible":true},{"type":"category","label":"2022 Puzzles","items":[{"type":"link","label":"Day 1: Calorie Counting","href":"/scala-advent-of-code/2022/puzzles/day01","docId":"2022/puzzles/day01"},{"type":"link","label":"Day 2: Rock Paper Scissors","href":"/scala-advent-of-code/2022/puzzles/day02","docId":"2022/puzzles/day02"},{"type":"link","label":"Day 3: Rucksack Reorganization","href":"/scala-advent-of-code/2022/puzzles/day03","docId":"2022/puzzles/day03"},{"type":"link","label":"Day 4: Camp Cleanup","href":"/scala-advent-of-code/2022/puzzles/day04","docId":"2022/puzzles/day04"},{"type":"link","label":"Day 5: Supply Stacks","href":"/scala-advent-of-code/2022/puzzles/day05","docId":"2022/puzzles/day05"},{"type":"link","label":"Day 6: Tuning Trouble","href":"/scala-advent-of-code/2022/puzzles/day06","docId":"2022/puzzles/day06"},{"type":"link","label":"Day 7: No Space Left On Device","href":"/scala-advent-of-code/2022/puzzles/day07","docId":"2022/puzzles/day07"},{"type":"link","label":"Day 8: Treetop Tree House","href":"/scala-advent-of-code/2022/puzzles/day08","docId":"2022/puzzles/day08"},{"type":"link","label":"Day 9: Rope Bridge","href":"/scala-advent-of-code/2022/puzzles/day09","docId":"2022/puzzles/day09"},{"type":"link","label":"Day 10: Cathode-Ray Tube","href":"/scala-advent-of-code/2022/puzzles/day10","docId":"2022/puzzles/day10"},{"type":"link","label":"Day 11: Monkey in the Middle","href":"/scala-advent-of-code/2022/puzzles/day11","docId":"2022/puzzles/day11"},{"type":"link","label":"Day 12: Hill Climbing Algorithm","href":"/scala-advent-of-code/2022/puzzles/day12","docId":"2022/puzzles/day12"},{"type":"link","label":"Day 13: Distress Signal","href":"/scala-advent-of-code/2022/puzzles/day13","docId":"2022/puzzles/day13"},{"type":"link","label":"Day 14: Regolith Reservoir","href":"/scala-advent-of-code/2022/puzzles/day14","docId":"2022/puzzles/day14"},{"type":"link","label":"Day 15: Beacon Exclusion Zone","href":"/scala-advent-of-code/2022/puzzles/day15","docId":"2022/puzzles/day15"},{"type":"link","label":"Day 16: Proboscidea Volcanium","href":"/scala-advent-of-code/2022/puzzles/day16","docId":"2022/puzzles/day16"},{"type":"link","label":"Day 17: Pyroclastic Flow","href":"/scala-advent-of-code/2022/puzzles/day17","docId":"2022/puzzles/day17"},{"type":"link","label":"Day 18: Boiling Boulders","href":"/scala-advent-of-code/2022/puzzles/day18","docId":"2022/puzzles/day18"},{"type":"link","label":"Day 19: Not Enough Minerals","href":"/scala-advent-of-code/2022/puzzles/day19","docId":"2022/puzzles/day19"},{"type":"link","label":"Day 20: Grove Positioning System","href":"/scala-advent-of-code/2022/puzzles/day20","docId":"2022/puzzles/day20"},{"type":"link","label":"Day 21: Monkey Math","href":"/scala-advent-of-code/2022/puzzles/day21","docId":"2022/puzzles/day21"},{"type":"link","label":"Day 22: Monkey Map","href":"/scala-advent-of-code/2022/puzzles/day22","docId":"2022/puzzles/day22"},{"type":"link","label":"Day 23: Unstable Diffusion","href":"/scala-advent-of-code/2022/puzzles/day23","docId":"2022/puzzles/day23"},{"type":"link","label":"Day 24: Blizzard Basin","href":"/scala-advent-of-code/2022/puzzles/day24","docId":"2022/puzzles/day24"},{"type":"link","label":"Day 25: Full of Hot Air","href":"/scala-advent-of-code/2022/puzzles/day25","docId":"2022/puzzles/day25"}],"collapsed":true,"collapsible":true},{"type":"category","label":"2021 Puzzles","items":[{"type":"link","label":"Day 1: Sonar Sweep","href":"/scala-advent-of-code/puzzles/day1","docId":"puzzles/day1"},{"type":"link","label":"Day 2: Dive!","href":"/scala-advent-of-code/puzzles/day2","docId":"puzzles/day2"},{"type":"link","label":"Day 3: Binary Diagnostic","href":"/scala-advent-of-code/puzzles/day3","docId":"puzzles/day3"},{"type":"link","label":"Day 4: Giant Squid","href":"/scala-advent-of-code/puzzles/day4","docId":"puzzles/day4"},{"type":"link","label":"Day 5: Hydrothermal Venture","href":"/scala-advent-of-code/puzzles/day5","docId":"puzzles/day5"},{"type":"link","label":"Day 6: Lanternfish","href":"/scala-advent-of-code/puzzles/day6","docId":"puzzles/day6"},{"type":"link","label":"Day 7: The Treachery of Whales","href":"/scala-advent-of-code/puzzles/day7","docId":"puzzles/day7"},{"type":"link","label":"Day 8: Seven Segment Search","href":"/scala-advent-of-code/puzzles/day8","docId":"puzzles/day8"},{"type":"link","label":"Day 9: Smoke Basin","href":"/scala-advent-of-code/puzzles/day9","docId":"puzzles/day9"},{"type":"link","label":"Day 10: Syntax Scoring","href":"/scala-advent-of-code/puzzles/day10","docId":"puzzles/day10"},{"type":"link","label":"Day 11: Dumbo Octopus","href":"/scala-advent-of-code/puzzles/day11","docId":"puzzles/day11"},{"type":"link","label":"Day 12: Passage Pathing","href":"/scala-advent-of-code/puzzles/day12","docId":"puzzles/day12"},{"type":"link","label":"Day 13: Transparent Origami","href":"/scala-advent-of-code/puzzles/day13","docId":"puzzles/day13"},{"type":"link","label":"Day 14: Extended Polymerization","href":"/scala-advent-of-code/puzzles/day14","docId":"puzzles/day14"},{"type":"link","label":"Day 15: Chiton","href":"/scala-advent-of-code/puzzles/day15","docId":"puzzles/day15"},{"type":"link","label":"Day 16: Packet Decoder","href":"/scala-advent-of-code/puzzles/day16","docId":"puzzles/day16"},{"type":"link","label":"Day 17: Trick Shot","href":"/scala-advent-of-code/puzzles/day17","docId":"puzzles/day17"},{"type":"link","label":"Day 18: Snailfish","href":"/scala-advent-of-code/puzzles/day18","docId":"puzzles/day18"},{"type":"link","label":"Day 19: Beacon Scanner","href":"/scala-advent-of-code/puzzles/day19","docId":"puzzles/day19"},{"type":"link","label":"Day 20: Trench Map","href":"/scala-advent-of-code/puzzles/day20","docId":"puzzles/day20"},{"type":"link","label":"Day 21: Dirac Dice","href":"/scala-advent-of-code/puzzles/day21","docId":"puzzles/day21"},{"type":"link","label":"Day 22: Reactor Reboot","href":"/scala-advent-of-code/puzzles/day22","docId":"puzzles/day22"},{"type":"link","label":"Day 23: Amphipod","href":"/scala-advent-of-code/puzzles/day23","docId":"puzzles/day23"},{"type":"link","label":"Day 24: Arithmetic Logic Unit","href":"/scala-advent-of-code/puzzles/day24","docId":"puzzles/day24"},{"type":"link","label":"Day 25: Sea Cucumber","href":"/scala-advent-of-code/puzzles/day25","docId":"puzzles/day25"}],"collapsed":true,"collapsible":true}]},"docs":{"2022/puzzles/day01":{"id":"2022/puzzles/day01","title":"Day 1: Calorie Counting","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day02":{"id":"2022/puzzles/day02","title":"Day 2: Rock Paper Scissors","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day03":{"id":"2022/puzzles/day03","title":"Day 3: Rucksack Reorganization","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day04":{"id":"2022/puzzles/day04","title":"Day 4: Camp Cleanup","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day05":{"id":"2022/puzzles/day05","title":"Day 5: Supply Stacks","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day06":{"id":"2022/puzzles/day06","title":"Day 6: Tuning Trouble","description":"Code by Jan Boerman, and Jamie Thompson.","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day07":{"id":"2022/puzzles/day07","title":"Day 7: No Space Left On Device","description":"code by Jan Boerman","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day08":{"id":"2022/puzzles/day08","title":"Day 8: Treetop Tree House","description":"code and article by Quentin Bernet","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day09":{"id":"2022/puzzles/day09","title":"Day 9: Rope Bridge","description":"code by Jamie Thompson","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day10":{"id":"2022/puzzles/day10","title":"Day 10: Cathode-Ray Tube","description":"code and article by Mewen Crespo (reviewed by Jamie Thompson)","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day11":{"id":"2022/puzzles/day11","title":"Day 11: Monkey in the Middle","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day12":{"id":"2022/puzzles/day12","title":"Day 12: Hill Climbing Algorithm","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day13":{"id":"2022/puzzles/day13","title":"Day 13: Distress Signal","description":"by Jamie Thompson","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day14":{"id":"2022/puzzles/day14","title":"Day 14: Regolith Reservoir","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day15":{"id":"2022/puzzles/day15","title":"Day 15: Beacon Exclusion Zone","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day16":{"id":"2022/puzzles/day16","title":"Day 16: Proboscidea Volcanium","description":"code by Tyler Coles (javadocmd.com), Quentin Bernet, @sjrd, and @bishabosha","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day17":{"id":"2022/puzzles/day17","title":"Day 17: Pyroclastic Flow","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day18":{"id":"2022/puzzles/day18","title":"Day 18: Boiling Boulders","description":"by LaurenceWarne","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day19":{"id":"2022/puzzles/day19","title":"Day 19: Not Enough Minerals","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day20":{"id":"2022/puzzles/day20","title":"Day 20: Grove Positioning System","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day21":{"id":"2022/puzzles/day21","title":"Day 21: Monkey Math","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day22":{"id":"2022/puzzles/day22","title":"Day 22: Monkey Map","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day23":{"id":"2022/puzzles/day23","title":"Day 23: Unstable Diffusion","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day24":{"id":"2022/puzzles/day24","title":"Day 24: Blizzard Basin","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2022/puzzles/day25":{"id":"2022/puzzles/day25","title":"Day 25: Full of Hot Air","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day01":{"id":"2023/puzzles/day01","title":"Day 1: Trebuchet?!","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day02":{"id":"2023/puzzles/day02","title":"Day 2: Cube Conundrum","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day03":{"id":"2023/puzzles/day03","title":"Day 3: Gear Ratios","description":"by @bishabosha and @iusildra","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day04":{"id":"2023/puzzles/day04","title":"Day 4: Scratchcards","description":"by @shardulc","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day05":{"id":"2023/puzzles/day05","title":"Day 5: If You Give A Seed A Fertilizer","description":"by @g.berezin and @bishabosha","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day06":{"id":"2023/puzzles/day06","title":"Day 6: Wait For It","description":"by Spamegg","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day07":{"id":"2023/puzzles/day07","title":"Day 7: Camel Cards","description":"by @anatoliykmetyuk","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day08":{"id":"2023/puzzles/day08","title":"Day 8: Haunted Wasteland","description":"by @prinsniels","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day09":{"id":"2023/puzzles/day09","title":"Day 9: Mirage Maintenance","description":"by @SethTisue","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day10":{"id":"2023/puzzles/day10","title":"Day 10: Pipe Maze","description":"by @EugeneFlesselle","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day11":{"id":"2023/puzzles/day11","title":"Day 11: Cosmic Expansion","description":"by @natsukagami","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day12":{"id":"2023/puzzles/day12","title":"Day 12: Hot Springs","description":"by @mbovel","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day13":{"id":"2023/puzzles/day13","title":"Day 13: Point of Incidence","description":"by @adpi2","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day14":{"id":"2023/puzzles/day14","title":"Day 14: Parabolic Reflector Dish","description":"by @anatoliykmetyuk","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day15":{"id":"2023/puzzles/day15","title":"Day 15: Lens Library","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day16":{"id":"2023/puzzles/day16","title":"Day 16: The Floor Will Be Lava","description":"By @iusildra","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day17":{"id":"2023/puzzles/day17","title":"Day 17: Clumsy Crucible","description":"by @stewSquared","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day18":{"id":"2023/puzzles/day18","title":"Day 18: Lavaduct Lagoon","description":"by @EugeneFlesselle","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day19":{"id":"2023/puzzles/day19","title":"Day 19: Aplenty","description":"by @mbovel","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day20":{"id":"2023/puzzles/day20","title":"Day 20: Pulse Propagation","description":"by @merlinorg","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day21":{"id":"2023/puzzles/day21","title":"Day 21: Step Counter","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day22":{"id":"2023/puzzles/day22","title":"Day 22: Sand Slabs","description":"by Pawe\u0142 Cembaluk","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day23":{"id":"2023/puzzles/day23","title":"Day 23: A Long Walk","description":"by @stewSquared","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day24":{"id":"2023/puzzles/day24","title":"Day 24: Never Tell Me The Odds","description":"by @merlinorg","sidebar":"adventOfCodeSidebar"},"2023/puzzles/day25":{"id":"2023/puzzles/day25","title":"Day 25: Snowverload","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day0":{"id":"2024/puzzles/day0","title":"day0","description":"Redirecting..."},"2024/puzzles/day01":{"id":"2024/puzzles/day01","title":"Day 1: Historian Hysteria","description":"by @spamegg1","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day02":{"id":"2024/puzzles/day02","title":"Day 2: Red-Nosed Reports","description":"by @spamegg1","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day03":{"id":"2024/puzzles/day03","title":"Day 3: Mull It Over","description":"by @makingthematrix","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day04":{"id":"2024/puzzles/day04","title":"Day 4: Ceres Search","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day05":{"id":"2024/puzzles/day05","title":"Day 5: Print Queue","description":"by @KacperFKorban","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day06":{"id":"2024/puzzles/day06","title":"Day 6: Guard Gallivant","description":"by @samuelchassot","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day07":{"id":"2024/puzzles/day07","title":"Day 7: Bridge Repair","description":"by @philippus","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day08":{"id":"2024/puzzles/day08","title":"Day 8: Resonant Collinearity","description":"by @merlinorg","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day09":{"id":"2024/puzzles/day09","title":"Day 9: Disk Fragmenter","description":"by @dyvrl","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day10":{"id":"2024/puzzles/day10","title":"Day 10: Hoof It","description":"by @SethTisue","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day11":{"id":"2024/puzzles/day11","title":"Day 11: Plutonian Pebbles","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day12":{"id":"2024/puzzles/day12","title":"Day 12: Garden Groups","description":"by @TheDrawingCoder-Gamer","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day13":{"id":"2024/puzzles/day13","title":"Day 13: Claw Contraption","description":"by @scarf005","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day14":{"id":"2024/puzzles/day14","title":"Day 14: Restroom Redoubt","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day15":{"id":"2024/puzzles/day15","title":"Day 15: Warehouse Woes","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day16":{"id":"2024/puzzles/day16","title":"Day 16: Reindeer Maze","description":"by @merlinorg","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day17":{"id":"2024/puzzles/day17","title":"Day 17: Chronospatial Computer","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day18":{"id":"2024/puzzles/day18","title":"Day 18: RAM Run","description":"by @TheDrawingCoder-Gamer","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day19":{"id":"2024/puzzles/day19","title":"Day 19: Linen Layout","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day20":{"id":"2024/puzzles/day20","title":"Day 20: Race Condition","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"2024/puzzles/day21":{"id":"2024/puzzles/day21","title":"Day 21: Keypad Conundrum","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"introduction":{"id":"introduction","title":"Introduction","description":"Welcome to the Scala Center\'s take on Advent of Code.","sidebar":"adventOfCodeSidebar"},"puzzles/day1":{"id":"puzzles/day1","title":"Day 1: Sonar Sweep","description":"by @adpi2","sidebar":"adventOfCodeSidebar"},"puzzles/day10":{"id":"puzzles/day10","title":"Day 10: Syntax Scoring","description":"by @VincenzoBaz","sidebar":"adventOfCodeSidebar"},"puzzles/day11":{"id":"puzzles/day11","title":"Day 11: Dumbo Octopus","description":"by @tgodzik","sidebar":"adventOfCodeSidebar"},"puzzles/day12":{"id":"puzzles/day12","title":"Day 12: Passage Pathing","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day13":{"id":"puzzles/day13","title":"Day 13: Transparent Origami","description":"by @adpi2","sidebar":"adventOfCodeSidebar"},"puzzles/day14":{"id":"puzzles/day14","title":"Day 14: Extended Polymerization","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"puzzles/day15":{"id":"puzzles/day15","title":"Day 15: Chiton","description":"By @anatoliykmetyuk","sidebar":"adventOfCodeSidebar"},"puzzles/day16":{"id":"puzzles/day16","title":"Day 16: Packet Decoder","description":"by @tgodzik","sidebar":"adventOfCodeSidebar"},"puzzles/day17":{"id":"puzzles/day17","title":"Day 17: Trick Shot","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"puzzles/day18":{"id":"puzzles/day18","title":"Day 18: Snailfish","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day19":{"id":"puzzles/day19","title":"Day 19: Beacon Scanner","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day2":{"id":"puzzles/day2","title":"Day 2: Dive!","description":"by @mlachkar","sidebar":"adventOfCodeSidebar"},"puzzles/day20":{"id":"puzzles/day20","title":"Day 20: Trench Map","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day21":{"id":"puzzles/day21","title":"Day 21: Dirac Dice","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"puzzles/day22":{"id":"puzzles/day22","title":"Day 22: Reactor Reboot","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"puzzles/day23":{"id":"puzzles/day23","title":"Day 23: Amphipod","description":"by @adpi2","sidebar":"adventOfCodeSidebar"},"puzzles/day24":{"id":"puzzles/day24","title":"Day 24: Arithmetic Logic Unit","description":"Puzzle description","sidebar":"adventOfCodeSidebar"},"puzzles/day25":{"id":"puzzles/day25","title":"Day 25: Sea Cucumber","description":"by @Sporarum, student at EPFL, and @adpi2","sidebar":"adventOfCodeSidebar"},"puzzles/day3":{"id":"puzzles/day3","title":"Day 3: Binary Diagnostic","description":"by @sjrd","sidebar":"adventOfCodeSidebar"},"puzzles/day4":{"id":"puzzles/day4","title":"Day 4: Giant Squid","description":"by @Sporarum, student at EPFL.","sidebar":"adventOfCodeSidebar"},"puzzles/day5":{"id":"puzzles/day5","title":"Day 5: Hydrothermal Venture","description":"by @tgodzik","sidebar":"adventOfCodeSidebar"},"puzzles/day6":{"id":"puzzles/day6","title":"Day 6: Lanternfish","description":"by @julienrf","sidebar":"adventOfCodeSidebar"},"puzzles/day7":{"id":"puzzles/day7","title":"Day 7: The Treachery of Whales","description":"by @tgodzik","sidebar":"adventOfCodeSidebar"},"puzzles/day8":{"id":"puzzles/day8","title":"Day 8: Seven Segment Search","description":"by @bishabosha","sidebar":"adventOfCodeSidebar"},"puzzles/day9":{"id":"puzzles/day9","title":"Day 9: Smoke Basin","description":"by @VincenzoBaz","sidebar":"adventOfCodeSidebar"},"setup":{"id":"setup","title":"Setup","description":"There are many ways to get started with Scala and we will suggest that you try Scala CLI, developed by VirtusLab.","sidebar":"adventOfCodeSidebar"}}}')}}]); \ No newline at end of file diff --git a/assets/js/980a4c0d.4c069f5d.js b/assets/js/980a4c0d.4c069f5d.js new file mode 100644 index 000000000..b206339c9 --- /dev/null +++ b/assets/js/980a4c0d.4c069f5d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[3791],{3189:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>l,default:()=>m,frontMatter:()=>n,metadata:()=>r,toc:()=>s});var o=a(7462),i=(a(7294),a(3905));a(6340);const n={},l="Day 20: Race Condition",r={unversionedId:"2024/puzzles/day20",id:"2024/puzzles/day20",title:"Day 20: Race Condition",description:"Puzzle description",source:"@site/target/mdoc/2024/puzzles/day20.md",sourceDirName:"2024/puzzles",slug:"/2024/puzzles/day20",permalink:"/scala-advent-of-code/2024/puzzles/day20",draft:!1,editUrl:"https://github.com/scalacenter/scala-advent-of-code/edit/website/docs/2024/puzzles/day20.md",tags:[],version:"current",frontMatter:{},sidebar:"adventOfCodeSidebar",previous:{title:"Day 19: Linen Layout",permalink:"/scala-advent-of-code/2024/puzzles/day19"},next:{title:"Day 21: Keypad Conundrum",permalink:"/scala-advent-of-code/2024/puzzles/day21"}},c={},s=[{value:"Puzzle description",id:"puzzle-description",level:2},{value:"Solutions from the community",id:"solutions-from-the-community",level:2}],u={toc:s};function m(e){let{components:t,...a}=e;return(0,i.kt)("wrapper",(0,o.Z)({},u,a,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"day-20-race-condition"},"Day 20: Race Condition"),(0,i.kt)("h2",{id:"puzzle-description"},"Puzzle description"),(0,i.kt)("p",null,(0,i.kt)("a",{parentName:"p",href:"https://adventofcode.com/2024/day/20"},"https://adventofcode.com/2024/day/20")),(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/rmarbeck/advent2024/blob/main/day20/src/main/scala/Solution.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/rmarbeck"},"Rapha\xebl Marbeck")," "),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/nikiforo/aoc24/blob/main/src/main/scala/io/github/nikiforo/aoc24/D20T2.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/nikiforo"},"Artem Nikiforov")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/aamiguet/advent-2024/blob/main/src/main/scala/ch/aamiguet/advent2024/Day20.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/aamiguet"},"Antoine Amiguet")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"https://github.com/merlinorg/aoc2024/blob/main/src/main/scala/Day20.scala"},"Solution")," by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/merlinorg"},"merlinorg"))),(0,i.kt)("p",null,"Share your solution to the Scala community by editing this page.\nYou can even write the whole article! ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/scalacenter/scala-advent-of-code/discussions/424"},"See here for the expected format")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/980a4c0d.b5e51a7d.js b/assets/js/980a4c0d.b5e51a7d.js deleted file mode 100644 index 401f63e9a..000000000 --- a/assets/js/980a4c0d.b5e51a7d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[3791],{3189:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>l,default:()=>m,frontMatter:()=>n,metadata:()=>r,toc:()=>s});var i=a(7462),o=(a(7294),a(3905));a(6340);const n={},l="Day 20: Race Condition",r={unversionedId:"2024/puzzles/day20",id:"2024/puzzles/day20",title:"Day 20: Race Condition",description:"Puzzle description",source:"@site/target/mdoc/2024/puzzles/day20.md",sourceDirName:"2024/puzzles",slug:"/2024/puzzles/day20",permalink:"/scala-advent-of-code/2024/puzzles/day20",draft:!1,editUrl:"https://github.com/scalacenter/scala-advent-of-code/edit/website/docs/2024/puzzles/day20.md",tags:[],version:"current",frontMatter:{},sidebar:"adventOfCodeSidebar",previous:{title:"Day 19: Linen Layout",permalink:"/scala-advent-of-code/2024/puzzles/day19"},next:{title:"Day 1: Trebuchet?!",permalink:"/scala-advent-of-code/2023/puzzles/day01"}},c={},s=[{value:"Puzzle description",id:"puzzle-description",level:2},{value:"Solutions from the community",id:"solutions-from-the-community",level:2}],u={toc:s};function m(e){let{components:t,...a}=e;return(0,o.kt)("wrapper",(0,i.Z)({},u,a,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"day-20-race-condition"},"Day 20: Race Condition"),(0,o.kt)("h2",{id:"puzzle-description"},"Puzzle description"),(0,o.kt)("p",null,(0,o.kt)("a",{parentName:"p",href:"https://adventofcode.com/2024/day/20"},"https://adventofcode.com/2024/day/20")),(0,o.kt)("h2",{id:"solutions-from-the-community"},"Solutions from the community"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"https://github.com/rmarbeck/advent2024/blob/main/day20/src/main/scala/Solution.scala"},"Solution")," by ",(0,o.kt)("a",{parentName:"li",href:"https://github.com/rmarbeck"},"Rapha\xebl Marbeck")," "),(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"https://github.com/nikiforo/aoc24/blob/main/src/main/scala/io/github/nikiforo/aoc24/D20T2.scala"},"Solution")," by ",(0,o.kt)("a",{parentName:"li",href:"https://github.com/nikiforo"},"Artem Nikiforov")),(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"https://github.com/aamiguet/advent-2024/blob/main/src/main/scala/ch/aamiguet/advent2024/Day20.scala"},"Solution")," by ",(0,o.kt)("a",{parentName:"li",href:"https://github.com/aamiguet"},"Antoine Amiguet")),(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"https://github.com/merlinorg/aoc2024/blob/main/src/main/scala/Day20.scala"},"Solution")," by ",(0,o.kt)("a",{parentName:"li",href:"https://github.com/merlinorg"},"merlinorg"))),(0,o.kt)("p",null,"Share your solution to the Scala community by editing this page.\nYou can even write the whole article! ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/scalacenter/scala-advent-of-code/discussions/424"},"See here for the expected format")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/f5509088.998a04b7.js b/assets/js/f5509088.998a04b7.js new file mode 100644 index 000000000..924a3ad65 --- /dev/null +++ b/assets/js/f5509088.998a04b7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[7570],{3194:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>u,contentTitle:()=>s,default:()=>l,frontMatter:()=>d,metadata:()=>i,toc:()=>r});var o=a(7462),n=(a(7294),a(3905));a(6340);const d={},s="Day 21: Keypad Conundrum",i={unversionedId:"2024/puzzles/day21",id:"2024/puzzles/day21",title:"Day 21: Keypad Conundrum",description:"Puzzle description",source:"@site/target/mdoc/2024/puzzles/day21.md",sourceDirName:"2024/puzzles",slug:"/2024/puzzles/day21",permalink:"/scala-advent-of-code/2024/puzzles/day21",draft:!1,editUrl:"https://github.com/scalacenter/scala-advent-of-code/edit/website/docs/2024/puzzles/day21.md",tags:[],version:"current",frontMatter:{},sidebar:"adventOfCodeSidebar",previous:{title:"Day 20: Race Condition",permalink:"/scala-advent-of-code/2024/puzzles/day20"},next:{title:"Day 1: Trebuchet?!",permalink:"/scala-advent-of-code/2023/puzzles/day01"}},u={},r=[{value:"Puzzle description",id:"puzzle-description",level:2},{value:"Solutions from the community",id:"solutions-from-the-community",level:2}],c={toc:r};function l(e){let{components:t,...a}=e;return(0,n.kt)("wrapper",(0,o.Z)({},c,a,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("h1",{id:"day-21-keypad-conundrum"},"Day 21: Keypad Conundrum"),(0,n.kt)("h2",{id:"puzzle-description"},"Puzzle description"),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://adventofcode.com/2024/day/21"},"https://adventofcode.com/2024/day/21")),(0,n.kt)("h2",{id:"solutions-from-the-community"},"Solutions from the community"),(0,n.kt)("p",null,"Share your solution to the Scala community by editing this page.\nYou can even write the whole article! ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/scalacenter/scala-advent-of-code/discussions/424"},"See here for the expected format")))}l.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/main.c82ce0e7.js b/assets/js/main.c82ce0e7.js deleted file mode 100644 index 343d6c091..000000000 --- a/assets/js/main.c82ce0e7.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! For license information please see main.c82ce0e7.js.LICENSE.txt */ -(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[179],{8726:(e,t,n)=>{"use strict";function a(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}function r(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function l(){return l=Object.assign||function(e){for(var t=1;t{"use strict";n.d(t,{Z:()=>p});var a=n(7294),r=n(7462),o=n(8726),l=n.n(o),i=n(6887);const s={"003324e5":[()=>Promise.all([n.e(8592),n.e(4871)]).then(n.bind(n,7916)),"@site/target/mdoc/2022/puzzles/day02.md",7916],"06fa13a8":[()=>Promise.all([n.e(8592),n.e(1)]).then(n.bind(n,6978)),"@site/target/mdoc/2022/puzzles/day01.md",6978],"08bf2fb8":[()=>Promise.all([n.e(8592),n.e(3431)]).then(n.bind(n,1730)),"@site/target/mdoc/2024/puzzles/day16.md",1730],"0a673ff4":[()=>Promise.all([n.e(8592),n.e(3159)]).then(n.bind(n,7483)),"@site/target/mdoc/puzzles/day18.md",7483],"0f5219f4":[()=>Promise.all([n.e(8592),n.e(4760)]).then(n.bind(n,7788)),"@site/target/mdoc/2022/puzzles/day21.md",7788],"1077c9b3":[()=>Promise.all([n.e(8592),n.e(5002)]).then(n.bind(n,6261)),"@site/target/mdoc/puzzles/day9.md",6261],"1279ac73":[()=>Promise.all([n.e(8592),n.e(7111)]).then(n.bind(n,4562)),"@site/target/mdoc/2022/puzzles/day25.md",4562],17896441:[()=>Promise.all([n.e(532),n.e(8592),n.e(7918)]).then(n.bind(n,7565)),"@theme/DocItem",7565],"18dca702":[()=>Promise.all([n.e(8592),n.e(9010)]).then(n.bind(n,4058)),"@site/target/mdoc/2024/puzzles/day02.md",4058],"18e96609":[()=>Promise.all([n.e(8592),n.e(7546)]).then(n.bind(n,5249)),"@site/target/mdoc/puzzles/day13.md",5249],"18ed2f59":[()=>Promise.all([n.e(8592),n.e(4204)]).then(n.bind(n,4962)),"@site/target/mdoc/puzzles/day8.md",4962],"1bbb7d86":[()=>Promise.all([n.e(8592),n.e(3856)]).then(n.bind(n,2679)),"@site/target/mdoc/2022/puzzles/day10.md",2679],"1be78505":[()=>Promise.all([n.e(532),n.e(9514)]).then(n.bind(n,9963)),"@theme/DocPage",9963],"1d157876":[()=>Promise.all([n.e(532),n.e(2953)]).then(n.bind(n,9899)),"@site/src/pages/2024/index.js",9899],"1d3291d8":[()=>Promise.all([n.e(8592),n.e(3530)]).then(n.bind(n,1568)),"@site/target/mdoc/2024/puzzles/day17.md",1568],"1d39ce0c":[()=>Promise.all([n.e(8592),n.e(4480)]).then(n.bind(n,7672)),"@site/target/mdoc/puzzles/day23.md",7672],"1d58c4bc":[()=>Promise.all([n.e(532),n.e(6030)]).then(n.bind(n,4563)),"@site/src/pages/2023/index.js",4563],"1d76bc70":[()=>Promise.all([n.e(8592),n.e(6754)]).then(n.bind(n,2153)),"@site/target/mdoc/puzzles/day12.md",2153],"1e184e91":[()=>Promise.all([n.e(8592),n.e(6801)]).then(n.bind(n,4393)),"@site/target/mdoc/2024/puzzles/day05.md",4393],"212d8bd4":[()=>Promise.all([n.e(8592),n.e(5481)]).then(n.bind(n,74)),"@site/target/mdoc/introduction.md",74],"2301f76b":[()=>Promise.all([n.e(532),n.e(8592),n.e(9898)]).then(n.bind(n,555)),"@site/target/mdoc/setup.md",555],"23b96a74":[()=>Promise.all([n.e(8592),n.e(8752)]).then(n.bind(n,1307)),"@site/target/mdoc/2023/puzzles/day20.md",1307],"23e93687":[()=>Promise.all([n.e(8592),n.e(6535)]).then(n.bind(n,2077)),"@site/target/mdoc/2023/puzzles/day14.md",2077],"2e8f18da":[()=>Promise.all([n.e(8592),n.e(2863)]).then(n.bind(n,1959)),"@site/target/mdoc/2022/puzzles/day23.md",1959],"338c8fb2":[()=>Promise.all([n.e(8592),n.e(6324)]).then(n.bind(n,9130)),"@site/target/mdoc/puzzles/day20.md",9130],"33dcef45":[()=>Promise.all([n.e(8592),n.e(8815)]).then(n.bind(n,7908)),"@site/target/mdoc/2024/puzzles/day06.md",7908],"38acdf51":[()=>Promise.all([n.e(8592),n.e(6249)]).then(n.bind(n,4484)),"@site/target/mdoc/2024/puzzles/day19.md",4484],"3c0fac00":[()=>Promise.all([n.e(8592),n.e(5692)]).then(n.bind(n,7693)),"@site/target/mdoc/2023/puzzles/day04.md",7693],"3cbbe704":[()=>Promise.all([n.e(8592),n.e(8801)]).then(n.bind(n,1073)),"@site/target/mdoc/2024/puzzles/day08.md",1073],"3f15e8e2":[()=>Promise.all([n.e(8592),n.e(1655)]).then(n.bind(n,4603)),"@site/target/mdoc/puzzles/day19.md",4603],"4337dc21":[()=>Promise.all([n.e(8592),n.e(5557)]).then(n.bind(n,4847)),"@site/target/mdoc/puzzles/day24.md",4847],"4496c5bc":[()=>Promise.all([n.e(8592),n.e(2888)]).then(n.bind(n,878)),"@site/target/mdoc/2023/puzzles/day06.md",878],"4542ed61":[()=>Promise.all([n.e(8592),n.e(8212)]).then(n.bind(n,2003)),"@site/target/mdoc/2024/puzzles/day18.md",2003],"4cf3b8de":[()=>Promise.all([n.e(8592),n.e(578)]).then(n.bind(n,5374)),"@site/target/mdoc/2022/puzzles/day24.md",5374],"4e95716f":[()=>Promise.all([n.e(8592),n.e(3298)]).then(n.bind(n,4115)),"@site/target/mdoc/2022/puzzles/day19.md",4115],"4f7f4db9":[()=>Promise.all([n.e(8592),n.e(1020)]).then(n.bind(n,2513)),"@site/target/mdoc/2023/puzzles/day21.md",2513],"4fe84b5f":[()=>Promise.all([n.e(8592),n.e(3414)]).then(n.bind(n,731)),"@site/target/mdoc/2022/puzzles/day11.md",731],"507065a6":[()=>Promise.all([n.e(8592),n.e(7399)]).then(n.bind(n,6721)),"@site/target/mdoc/puzzles/day15.md",6721],"53a8669b":[()=>Promise.all([n.e(8592),n.e(4404)]).then(n.bind(n,2959)),"@site/target/mdoc/2023/puzzles/day03.md",2959],"559dca7a":[()=>Promise.all([n.e(8592),n.e(2967)]).then(n.bind(n,2657)),"@site/target/mdoc/2022/puzzles/day17.md",2657],"5a9b63f1":[()=>Promise.all([n.e(8592),n.e(2402)]).then(n.bind(n,8452)),"@site/target/mdoc/2023/puzzles/day08.md",8452],"5d7c1fea":[()=>Promise.all([n.e(8592),n.e(7010)]).then(n.bind(n,2772)),"@site/target/mdoc/2024/puzzles/day07.md",2772],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,6809)),"@generated/docusaurus.config",6809],"62432f74":[()=>Promise.all([n.e(8592),n.e(341)]).then(n.bind(n,4950)),"@site/target/mdoc/2022/puzzles/day22.md",4950],"63121b24":[()=>Promise.all([n.e(8592),n.e(4608)]).then(n.bind(n,2828)),"@site/target/mdoc/2023/puzzles/day09.md",2828],"63b7595f":[()=>Promise.all([n.e(8592),n.e(6279)]).then(n.bind(n,5531)),"@site/target/mdoc/puzzles/day22.md",5531],"63e9be74":[()=>Promise.all([n.e(8592),n.e(6543)]).then(n.bind(n,614)),"@site/target/mdoc/2023/puzzles/day15.md",614],"66145d18":[()=>Promise.all([n.e(8592),n.e(5099)]).then(n.bind(n,1237)),"@site/target/mdoc/puzzles/day4.md",1237],"6c07b263":[()=>Promise.all([n.e(8592),n.e(7522)]).then(n.bind(n,2914)),"@site/target/mdoc/puzzles/day7.md",2914],"6cdb47d2":[()=>Promise.all([n.e(8592),n.e(7684)]).then(n.bind(n,4945)),"@site/target/mdoc/2022/puzzles/day20.md",4945],"738d623f":[()=>Promise.all([n.e(8592),n.e(4159)]).then(n.bind(n,4045)),"@site/target/mdoc/puzzles/day11.md",4045],"73b9919f":[()=>Promise.all([n.e(8592),n.e(4052)]).then(n.bind(n,4163)),"@site/target/mdoc/2023/puzzles/day01.md",4163],"7c5e7584":[()=>Promise.all([n.e(8592),n.e(7508)]).then(n.bind(n,61)),"@site/target/mdoc/2022/puzzles/day04.md",61],"7db7ff8c":[()=>Promise.all([n.e(8592),n.e(8998)]).then(n.bind(n,8838)),"@site/target/mdoc/2024/puzzles/day01.md",8838],"808dec60":[()=>Promise.all([n.e(8592),n.e(9465)]).then(n.bind(n,940)),"@site/target/mdoc/2023/puzzles/day11.md",940],"8235678d":[()=>Promise.all([n.e(8592),n.e(7749)]).then(n.bind(n,3783)),"@site/target/mdoc/2024/puzzles/day12.md",3783],"826e4024":[()=>Promise.all([n.e(8592),n.e(1377)]).then(n.bind(n,581)),"@site/target/mdoc/2023/puzzles/day17.md",581],"8790bf4c":[()=>Promise.all([n.e(532),n.e(3760)]).then(n.bind(n,4701)),"@site/src/pages/2022/index.js",4701],"89cb3c3f":[()=>n.e(1935).then(n.t.bind(n,5745,19)),"/home/runner/work/scala-advent-of-code/scala-advent-of-code/website/.docusaurus/docusaurus-plugin-content-pages/default/plugin-route-context-module-100.json",5745],"8ebe7983":[()=>Promise.all([n.e(8592),n.e(2678)]).then(n.bind(n,5357)),"@site/target/mdoc/puzzles/day2.md",5357],"91df6ac7":[()=>Promise.all([n.e(8592),n.e(5934)]).then(n.bind(n,128)),"@site/target/mdoc/2023/puzzles/day19.md",128],"935f2afb":[()=>n.e(53).then(n.t.bind(n,1109,19)),"~docs/default/version-current-metadata-prop-751.json",1109],"94550ecd":[()=>Promise.all([n.e(8592),n.e(7238)]).then(n.bind(n,3542)),"@site/target/mdoc/2022/puzzles/day16.md",3542],"950c6b93":[()=>n.e(3240).then(n.t.bind(n,3769,19)),"/home/runner/work/scala-advent-of-code/scala-advent-of-code/website/.docusaurus/docusaurus-plugin-content-docs/default/plugin-route-context-module-100.json",3769],"95444edb":[()=>Promise.all([n.e(8592),n.e(6712)]).then(n.bind(n,1119)),"@site/target/mdoc/puzzles/day14.md",1119],"980a4c0d":[()=>Promise.all([n.e(8592),n.e(3791)]).then(n.bind(n,3189)),"@site/target/mdoc/2024/puzzles/day20.md",3189],"9d8df37e":[()=>Promise.all([n.e(8592),n.e(2784)]).then(n.bind(n,4991)),"@site/target/mdoc/2024/puzzles/day0.md",4991],"9f228057":[()=>Promise.all([n.e(8592),n.e(9468)]).then(n.bind(n,4852)),"@site/target/mdoc/2022/puzzles/day12.md",4852],a1a15f20:[()=>Promise.all([n.e(8592),n.e(4104)]).then(n.bind(n,105)),"@site/target/mdoc/2024/puzzles/day11.md",105],a2d65943:[()=>Promise.all([n.e(8592),n.e(8071)]).then(n.bind(n,8368)),"@site/target/mdoc/2024/puzzles/day14.md",8368],a66862ae:[()=>Promise.all([n.e(8592),n.e(7589)]).then(n.bind(n,4909)),"@site/target/mdoc/2023/puzzles/day10.md",4909],a6e610da:[()=>Promise.all([n.e(8592),n.e(2255)]).then(n.bind(n,8133)),"@site/target/mdoc/2022/puzzles/day03.md",8133],a746aacc:[()=>Promise.all([n.e(8592),n.e(3047)]).then(n.bind(n,1573)),"@site/target/mdoc/2023/puzzles/day24.md",1573],a7c819ed:[()=>Promise.all([n.e(8592),n.e(1295)]).then(n.bind(n,1262)),"@site/target/mdoc/2022/puzzles/day13.md",1262],a9de53fa:[()=>Promise.all([n.e(8592),n.e(8158)]).then(n.bind(n,4374)),"@site/target/mdoc/2023/puzzles/day25.md",4374],adcb9b5c:[()=>Promise.all([n.e(8592),n.e(8119)]).then(n.bind(n,3941)),"@site/target/mdoc/puzzles/day10.md",3941],ae238375:[()=>Promise.all([n.e(8592),n.e(8243)]).then(n.bind(n,4180)),"@site/target/mdoc/2022/puzzles/day09.md",4180],b0cdce56:[()=>Promise.all([n.e(8592),n.e(5511)]).then(n.bind(n,8773)),"@site/target/mdoc/puzzles/day6.md",8773],b2616183:[()=>Promise.all([n.e(8592),n.e(8685)]).then(n.bind(n,3382)),"@site/target/mdoc/2024/puzzles/day15.md",3382],b468346a:[()=>Promise.all([n.e(8592),n.e(9185)]).then(n.bind(n,5123)),"@site/target/mdoc/2022/puzzles/day15.md",5123],b84e93db:[()=>Promise.all([n.e(8592),n.e(2999)]).then(n.bind(n,4410)),"@site/target/mdoc/2024/puzzles/day10.md",4410],ba4125a6:[()=>Promise.all([n.e(8592),n.e(9138)]).then(n.bind(n,9706)),"@site/target/mdoc/2024/puzzles/day09.md",9706],ba72e685:[()=>Promise.all([n.e(8592),n.e(771)]).then(n.bind(n,9619)),"@site/target/mdoc/puzzles/day1.md",9619],bd59612b:[()=>Promise.all([n.e(8592),n.e(3866)]).then(n.bind(n,2438)),"@site/target/mdoc/2022/puzzles/day06.md",2438],bd8a6e4f:[()=>Promise.all([n.e(8592),n.e(1022)]).then(n.bind(n,1032)),"@site/target/mdoc/2023/puzzles/day13.md",1032],c4f5d8e4:[()=>Promise.all([n.e(532),n.e(4195)]).then(n.bind(n,2841)),"@site/src/pages/index.js",2841],c702c251:[()=>Promise.all([n.e(8592),n.e(8960)]).then(n.bind(n,6970)),"@site/target/mdoc/2023/puzzles/day05.md",6970],cf8e1c86:[()=>Promise.all([n.e(8592),n.e(7333)]).then(n.bind(n,3023)),"@site/target/mdoc/puzzles/day3.md",3023],d053cd56:[()=>Promise.all([n.e(8592),n.e(8819)]).then(n.bind(n,7668)),"@site/target/mdoc/2024/puzzles/day13.md",7668],d22639e6:[()=>Promise.all([n.e(8592),n.e(912)]).then(n.bind(n,5012)),"@site/target/mdoc/puzzles/day25.md",5012],d5cd8a03:[()=>Promise.all([n.e(8592),n.e(2014)]).then(n.bind(n,564)),"@site/target/mdoc/puzzles/day17.md",564],d83a7229:[()=>Promise.all([n.e(8592),n.e(6523)]).then(n.bind(n,5779)),"@site/target/mdoc/2023/puzzles/day18.md",5779],dcec897c:[()=>Promise.all([n.e(8592),n.e(4949)]).then(n.bind(n,6053)),"@site/target/mdoc/2023/puzzles/day16.md",6053],dd8a65aa:[()=>Promise.all([n.e(8592),n.e(7784)]).then(n.bind(n,4874)),"@site/target/mdoc/2023/puzzles/day02.md",4874],de026f3e:[()=>Promise.all([n.e(8592),n.e(1220)]).then(n.bind(n,5961)),"@site/target/mdoc/2023/puzzles/day07.md",5961],e0cc4b4f:[()=>Promise.all([n.e(8592),n.e(2288)]).then(n.bind(n,8352)),"@site/target/mdoc/2022/puzzles/day14.md",8352],e2b8a71b:[()=>Promise.all([n.e(8592),n.e(738)]).then(n.bind(n,5726)),"@site/target/mdoc/2022/puzzles/day08.md",5726],e7ad7ee9:[()=>Promise.all([n.e(8592),n.e(3360)]).then(n.bind(n,6640)),"@site/target/mdoc/2022/puzzles/day18.md",6640],eb8b4f18:[()=>Promise.all([n.e(8592),n.e(1602)]).then(n.bind(n,8755)),"@site/target/mdoc/2023/puzzles/day23.md",8755],ef006d58:[()=>Promise.all([n.e(8592),n.e(1866)]).then(n.bind(n,3197)),"@site/target/mdoc/2022/puzzles/day07.md",3197],f03f5f80:[()=>Promise.all([n.e(8592),n.e(6538)]).then(n.bind(n,5477)),"@site/target/mdoc/2024/puzzles/day03.md",5477],f38a41fc:[()=>Promise.all([n.e(8592),n.e(3116)]).then(n.bind(n,7169)),"@site/target/mdoc/2023/puzzles/day22.md",7169],f4f7cb3a:[()=>Promise.all([n.e(8592),n.e(4120)]).then(n.bind(n,6279)),"@site/target/mdoc/puzzles/day16.md",6279],f5cc1f00:[()=>Promise.all([n.e(8592),n.e(197)]).then(n.bind(n,4217)),"@site/target/mdoc/2024/puzzles/day04.md",4217],f89f89b6:[()=>Promise.all([n.e(8592),n.e(9965)]).then(n.bind(n,4873)),"@site/target/mdoc/2022/puzzles/day05.md",4873],fbe461f6:[()=>Promise.all([n.e(8592),n.e(968)]).then(n.bind(n,9584)),"@site/target/mdoc/2023/puzzles/day12.md",9584],fe0d56d5:[()=>Promise.all([n.e(8592),n.e(4459)]).then(n.bind(n,8698)),"@site/target/mdoc/puzzles/day5.md",8698],fe537a1c:[()=>Promise.all([n.e(8592),n.e(6091)]).then(n.bind(n,4414)),"@site/target/mdoc/puzzles/day21.md",4414],ff5f3c5c:[()=>Promise.all([n.e(532),n.e(1078)]).then(n.bind(n,8123)),"@site/src/pages/2021/index.js",8123]};function c(e){let{error:t,retry:n,pastDelay:r}=e;return t?a.createElement("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"}},a.createElement("p",null,String(t)),a.createElement("div",null,a.createElement("button",{type:"button",onClick:n},"Retry"))):r?a.createElement("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"}},a.createElement("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb"},a.createElement("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2"},a.createElement("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0"},a.createElement("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),a.createElement("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),a.createElement("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})),a.createElement("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0"},a.createElement("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),a.createElement("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),a.createElement("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})),a.createElement("circle",{cx:"22",cy:"22",r:"8"},a.createElement("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"}))))):null}var u=n(9670),d=n(226);function f(e,t){if("*"===e)return l()({loading:c,loader:()=>n.e(4972).then(n.bind(n,4972)),modules:["@theme/NotFound"],webpack:()=>[4972],render(e,t){const n=e.default;return a.createElement(d.z,{value:{plugin:{name:"native",id:"default"}}},a.createElement(n,t))}});const o=i[`${e}-${t}`],f={},p=[],m=[],h=(0,u.Z)(o);return Object.entries(h).forEach((e=>{let[t,n]=e;const a=s[n];a&&(f[t]=a[0],p.push(a[1]),m.push(a[2]))})),l().Map({loading:c,loader:f,modules:p,webpack:()=>m,render(t,n){const l=JSON.parse(JSON.stringify(o));Object.entries(t).forEach((t=>{let[n,a]=t;const r=a.default;if(!r)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof r&&"function"!=typeof r||Object.keys(a).filter((e=>"default"!==e)).forEach((e=>{r[e]=a[e]}));let o=l;const i=n.split(".");i.slice(0,-1).forEach((e=>{o=o[e]})),o[i[i.length-1]]=r}));const i=l.__comp;delete l.__comp;const s=l.__context;return delete l.__context,a.createElement(d.z,{value:s},a.createElement(i,(0,r.Z)({},l,n)))}})}const p=[{path:"/scala-advent-of-code/2021/",component:f("/scala-advent-of-code/2021/","38e"),exact:!0},{path:"/scala-advent-of-code/2022/",component:f("/scala-advent-of-code/2022/","eb2"),exact:!0},{path:"/scala-advent-of-code/2023/",component:f("/scala-advent-of-code/2023/","bb5"),exact:!0},{path:"/scala-advent-of-code/2024/",component:f("/scala-advent-of-code/2024/","8a6"),exact:!0},{path:"/scala-advent-of-code/",component:f("/scala-advent-of-code/","27d"),exact:!0},{path:"/scala-advent-of-code/",component:f("/scala-advent-of-code/","1ce"),routes:[{path:"/scala-advent-of-code/2022/puzzles/day01",component:f("/scala-advent-of-code/2022/puzzles/day01","5e1"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day02",component:f("/scala-advent-of-code/2022/puzzles/day02","e5a"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day03",component:f("/scala-advent-of-code/2022/puzzles/day03","f3a"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day04",component:f("/scala-advent-of-code/2022/puzzles/day04","d1c"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day05",component:f("/scala-advent-of-code/2022/puzzles/day05","247"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day06",component:f("/scala-advent-of-code/2022/puzzles/day06","e81"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day07",component:f("/scala-advent-of-code/2022/puzzles/day07","275"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day08",component:f("/scala-advent-of-code/2022/puzzles/day08","b7e"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day09",component:f("/scala-advent-of-code/2022/puzzles/day09","d53"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day10",component:f("/scala-advent-of-code/2022/puzzles/day10","904"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day11",component:f("/scala-advent-of-code/2022/puzzles/day11","1f3"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day12",component:f("/scala-advent-of-code/2022/puzzles/day12","93f"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day13",component:f("/scala-advent-of-code/2022/puzzles/day13","3b7"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day14",component:f("/scala-advent-of-code/2022/puzzles/day14","6df"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day15",component:f("/scala-advent-of-code/2022/puzzles/day15","c3b"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day16",component:f("/scala-advent-of-code/2022/puzzles/day16","2da"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day17",component:f("/scala-advent-of-code/2022/puzzles/day17","3c8"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day18",component:f("/scala-advent-of-code/2022/puzzles/day18","1b1"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day19",component:f("/scala-advent-of-code/2022/puzzles/day19","244"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day20",component:f("/scala-advent-of-code/2022/puzzles/day20","a60"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day21",component:f("/scala-advent-of-code/2022/puzzles/day21","953"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day22",component:f("/scala-advent-of-code/2022/puzzles/day22","a05"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day23",component:f("/scala-advent-of-code/2022/puzzles/day23","db8"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day24",component:f("/scala-advent-of-code/2022/puzzles/day24","da8"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2022/puzzles/day25",component:f("/scala-advent-of-code/2022/puzzles/day25","baf"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day01",component:f("/scala-advent-of-code/2023/puzzles/day01","76a"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day02",component:f("/scala-advent-of-code/2023/puzzles/day02","c41"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day03",component:f("/scala-advent-of-code/2023/puzzles/day03","a88"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day04",component:f("/scala-advent-of-code/2023/puzzles/day04","e11"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day05",component:f("/scala-advent-of-code/2023/puzzles/day05","e4f"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day06",component:f("/scala-advent-of-code/2023/puzzles/day06","6ed"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day07",component:f("/scala-advent-of-code/2023/puzzles/day07","cbe"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day08",component:f("/scala-advent-of-code/2023/puzzles/day08","607"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day09",component:f("/scala-advent-of-code/2023/puzzles/day09","b4d"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day10",component:f("/scala-advent-of-code/2023/puzzles/day10","8bf"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day11",component:f("/scala-advent-of-code/2023/puzzles/day11","654"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day12",component:f("/scala-advent-of-code/2023/puzzles/day12","5a0"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day13",component:f("/scala-advent-of-code/2023/puzzles/day13","b39"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day14",component:f("/scala-advent-of-code/2023/puzzles/day14","396"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day15",component:f("/scala-advent-of-code/2023/puzzles/day15","aad"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day16",component:f("/scala-advent-of-code/2023/puzzles/day16","529"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day17",component:f("/scala-advent-of-code/2023/puzzles/day17","9e1"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day18",component:f("/scala-advent-of-code/2023/puzzles/day18","6f9"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day19",component:f("/scala-advent-of-code/2023/puzzles/day19","363"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day20",component:f("/scala-advent-of-code/2023/puzzles/day20","186"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day21",component:f("/scala-advent-of-code/2023/puzzles/day21","b91"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day22",component:f("/scala-advent-of-code/2023/puzzles/day22","1e1"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day23",component:f("/scala-advent-of-code/2023/puzzles/day23","692"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day24",component:f("/scala-advent-of-code/2023/puzzles/day24","2b8"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2023/puzzles/day25",component:f("/scala-advent-of-code/2023/puzzles/day25","c5a"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day0",component:f("/scala-advent-of-code/2024/puzzles/day0","4ed"),exact:!0},{path:"/scala-advent-of-code/2024/puzzles/day01",component:f("/scala-advent-of-code/2024/puzzles/day01","9ea"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day02",component:f("/scala-advent-of-code/2024/puzzles/day02","15a"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day03",component:f("/scala-advent-of-code/2024/puzzles/day03","ec9"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day04",component:f("/scala-advent-of-code/2024/puzzles/day04","f2e"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day05",component:f("/scala-advent-of-code/2024/puzzles/day05","712"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day06",component:f("/scala-advent-of-code/2024/puzzles/day06","c65"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day07",component:f("/scala-advent-of-code/2024/puzzles/day07","be4"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day08",component:f("/scala-advent-of-code/2024/puzzles/day08","c57"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day09",component:f("/scala-advent-of-code/2024/puzzles/day09","8fb"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day10",component:f("/scala-advent-of-code/2024/puzzles/day10","97c"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day11",component:f("/scala-advent-of-code/2024/puzzles/day11","75b"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day12",component:f("/scala-advent-of-code/2024/puzzles/day12","1a1"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day13",component:f("/scala-advent-of-code/2024/puzzles/day13","c0c"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day14",component:f("/scala-advent-of-code/2024/puzzles/day14","f6d"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day15",component:f("/scala-advent-of-code/2024/puzzles/day15","9cf"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day16",component:f("/scala-advent-of-code/2024/puzzles/day16","201"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day17",component:f("/scala-advent-of-code/2024/puzzles/day17","974"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day18",component:f("/scala-advent-of-code/2024/puzzles/day18","ed7"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day19",component:f("/scala-advent-of-code/2024/puzzles/day19","350"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/2024/puzzles/day20",component:f("/scala-advent-of-code/2024/puzzles/day20","b6a"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/introduction",component:f("/scala-advent-of-code/introduction","d7e"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day1",component:f("/scala-advent-of-code/puzzles/day1","58f"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day10",component:f("/scala-advent-of-code/puzzles/day10","651"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day11",component:f("/scala-advent-of-code/puzzles/day11","95b"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day12",component:f("/scala-advent-of-code/puzzles/day12","8aa"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day13",component:f("/scala-advent-of-code/puzzles/day13","462"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day14",component:f("/scala-advent-of-code/puzzles/day14","c7e"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day15",component:f("/scala-advent-of-code/puzzles/day15","cab"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day16",component:f("/scala-advent-of-code/puzzles/day16","067"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day17",component:f("/scala-advent-of-code/puzzles/day17","286"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day18",component:f("/scala-advent-of-code/puzzles/day18","587"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day19",component:f("/scala-advent-of-code/puzzles/day19","f77"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day2",component:f("/scala-advent-of-code/puzzles/day2","7ac"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day20",component:f("/scala-advent-of-code/puzzles/day20","602"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day21",component:f("/scala-advent-of-code/puzzles/day21","db4"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day22",component:f("/scala-advent-of-code/puzzles/day22","b73"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day23",component:f("/scala-advent-of-code/puzzles/day23","bdb"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day24",component:f("/scala-advent-of-code/puzzles/day24","c5b"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day25",component:f("/scala-advent-of-code/puzzles/day25","7d9"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day3",component:f("/scala-advent-of-code/puzzles/day3","210"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day4",component:f("/scala-advent-of-code/puzzles/day4","63d"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day5",component:f("/scala-advent-of-code/puzzles/day5","d92"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day6",component:f("/scala-advent-of-code/puzzles/day6","122"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day7",component:f("/scala-advent-of-code/puzzles/day7","ada"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day8",component:f("/scala-advent-of-code/puzzles/day8","7cc"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/puzzles/day9",component:f("/scala-advent-of-code/puzzles/day9","193"),exact:!0,sidebar:"adventOfCodeSidebar"},{path:"/scala-advent-of-code/setup",component:f("/scala-advent-of-code/setup","2be"),exact:!0,sidebar:"adventOfCodeSidebar"}]},{path:"*",component:f("*")}]},8934:(e,t,n)=>{"use strict";n.d(t,{_:()=>r,t:()=>o});var a=n(7294);const r=a.createContext(!1);function o(e){let{children:t}=e;const[n,o]=(0,a.useState)(!1);return(0,a.useEffect)((()=>{o(!0)}),[]),a.createElement(r.Provider,{value:n},t)}},9383:(e,t,n)=>{"use strict";var a=n(7294),r=n(3935),o=n(3727),l=n(405),i=n(412);const s=[n(2497),n(3310),n(8320),n(2295)];var c=n(723),u=n(6550),d=n(8790);function f(e){let{children:t}=e;return a.createElement(a.Fragment,null,t)}var p=n(7462),m=n(5742),h=n(2263),g=n(4996),b=n(6668),y=n(1944),v=n(4711),z=n(9727),w=n(3320),E=n(197);function k(){const{i18n:{defaultLocale:e,localeConfigs:t}}=(0,h.Z)(),n=(0,v.l)();return a.createElement(m.Z,null,Object.entries(t).map((e=>{let[t,{htmlLang:r}]=e;return a.createElement("link",{key:t,rel:"alternate",href:n.createUrl({locale:t,fullyQualified:!0}),hrefLang:r})})),a.createElement("link",{rel:"alternate",href:n.createUrl({locale:e,fullyQualified:!0}),hrefLang:"x-default"}))}function S(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,h.Z)(),r=function(){const{siteConfig:{url:e}}=(0,h.Z)(),{pathname:t}=(0,u.TH)();return e+(0,g.Z)(t)}(),o=t?`${n}${t}`:r;return a.createElement(m.Z,null,a.createElement("meta",{property:"og:url",content:o}),a.createElement("link",{rel:"canonical",href:o}))}function _(){const{i18n:{currentLocale:e}}=(0,h.Z)(),{metadata:t,image:n}=(0,b.L)();return a.createElement(a.Fragment,null,a.createElement(m.Z,null,a.createElement("meta",{name:"twitter:card",content:"summary_large_image"}),a.createElement("body",{className:z.h})),n&&a.createElement(y.d,{image:n}),a.createElement(S,null),a.createElement(k,null),a.createElement(E.Z,{tag:w.HX,locale:e}),a.createElement(m.Z,null,t.map(((e,t)=>a.createElement("meta",(0,p.Z)({key:t},e))))))}const C=new Map;function x(e){if(C.has(e.pathname))return{...e,pathname:C.get(e.pathname)};if((0,d.f)(c.Z,e.pathname).some((e=>{let{route:t}=e;return!0===t.exact})))return C.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return C.set(e.pathname,t),{...e,pathname:t}}var O=n(8934),T=n(8940);function P(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),a=1;a(t.default?.[e]??t[e])?.(...n)));return()=>r.forEach((e=>e?.()))}const I=function(e){let{children:t,location:n,previousLocation:r}=e;return(0,a.useLayoutEffect)((()=>{r!==n&&(r&&function(e){const{hash:t}=e;if(t){const e=decodeURIComponent(t.substring(1));document.getElementById(e)?.scrollIntoView()}else window.scrollTo(0,0)}(n),P("onRouteDidUpdate",{previousLocation:r,location:n}))}),[r,n]),t};function A(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,d.f)(c.Z,e))).flat();return Promise.all(t.map((e=>e.route.component.preload?.())))}class L extends a.Component{constructor(e){super(e),this.previousLocation=void 0,this.routeUpdateCleanupCb=void 0,this.previousLocation=null,this.routeUpdateCleanupCb=i.Z.canUseDOM?P("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=P("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),A(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return a.createElement(I,{previousLocation:this.previousLocation,location:t},a.createElement(u.AW,{location:t,render:()=>e}))}}const N=L,R="docusaurus-base-url-issue-banner-container",D="docusaurus-base-url-issue-banner-suggestion-container",M="__DOCUSAURUS_INSERT_BASEURL_BANNER";function F(e){return`\nwindow['${M}'] = true;\n\ndocument.addEventListener('DOMContentLoaded', maybeInsertBanner);\n\nfunction maybeInsertBanner() {\n var shouldInsert = window['${M}'];\n shouldInsert && insertBanner();\n}\n\nfunction insertBanner() {\n var bannerContainer = document.getElementById('${R}');\n if (!bannerContainer) {\n return;\n }\n var bannerHtml = ${JSON.stringify(function(e){return`\n
    \n

    Your Docusaurus site did not load properly.

    \n

    A very common reason is a wrong site baseUrl configuration.

    \n

    Current configured baseUrl = ${e} ${"/"===e?" (default value)":""}

    \n

    We suggest trying baseUrl =

    \n
    \n`}(e)).replace(/{window[M]=!1}),[]),a.createElement(a.Fragment,null,!i.Z.canUseDOM&&a.createElement(m.Z,null,a.createElement("script",null,F(e))),a.createElement("div",{id:R}))}function $(){const{siteConfig:{baseUrl:e,baseUrlIssueBanner:t}}=(0,h.Z)(),{pathname:n}=(0,u.TH)();return t&&n===e?a.createElement(B,null):null}function U(){const{siteConfig:{favicon:e,title:t,noIndex:n},i18n:{currentLocale:r,localeConfigs:o}}=(0,h.Z)(),l=(0,g.Z)(e),{htmlLang:i,direction:s}=o[r];return a.createElement(m.Z,null,a.createElement("html",{lang:i,dir:s}),a.createElement("title",null,t),a.createElement("meta",{property:"og:title",content:t}),a.createElement("meta",{name:"viewport",content:"width=device-width, initial-scale=1.0"}),n&&a.createElement("meta",{name:"robots",content:"noindex, nofollow"}),e&&a.createElement("link",{rel:"icon",href:l}))}var j=n(4763);function Z(){const e=(0,d.H)(c.Z),t=(0,u.TH)();return a.createElement(j.Z,null,a.createElement(T.M,null,a.createElement(O.t,null,a.createElement(f,null,a.createElement(U,null),a.createElement(_,null),a.createElement($,null),a.createElement(N,{location:x(t)},e)))))}var H=n(6887);const V=function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise(((t,n)=>{if("undefined"==typeof document)return void n();const a=document.createElement("link");a.setAttribute("rel","prefetch"),a.setAttribute("href",e),a.onload=()=>t(),a.onerror=()=>n();(document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode)?.appendChild(a)}))}:function(e){return new Promise(((t,n)=>{const a=new XMLHttpRequest;a.open("GET",e,!0),a.withCredentials=!0,a.onload=()=>{200===a.status?t():n()},a.send(null)}))};var W=n(9670);const G=new Set,q=new Set,Y=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,K={prefetch(e){if(!(e=>!Y()&&!q.has(e)&&!G.has(e))(e))return!1;G.add(e);const t=(0,d.f)(c.Z,e).flatMap((e=>{return t=e.route.path,Object.entries(H).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,W.Z)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?V(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!Y()&&!q.has(e))(e)&&(q.add(e),A(e))},Q=Object.freeze(K);if(i.Z.canUseDOM){window.docusaurus=Q;const e=r.hydrate;A(window.location.pathname).then((()=>{e(a.createElement(l.B6,null,a.createElement(o.VK,null,a.createElement(Z,null))),document.getElementById("__docusaurus"))}))}},8940:(e,t,n)=>{"use strict";n.d(t,{_:()=>u,M:()=>d});var a=n(7294),r=n(6809);const o=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/scala-advent-of-code/","versions":[{"name":"current","label":"Next","isLast":true,"path":"/scala-advent-of-code/","mainDocId":"introduction","docs":[{"id":"2022/puzzles/day01","path":"/scala-advent-of-code/2022/puzzles/day01","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day02","path":"/scala-advent-of-code/2022/puzzles/day02","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day03","path":"/scala-advent-of-code/2022/puzzles/day03","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day04","path":"/scala-advent-of-code/2022/puzzles/day04","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day05","path":"/scala-advent-of-code/2022/puzzles/day05","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day06","path":"/scala-advent-of-code/2022/puzzles/day06","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day07","path":"/scala-advent-of-code/2022/puzzles/day07","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day08","path":"/scala-advent-of-code/2022/puzzles/day08","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day09","path":"/scala-advent-of-code/2022/puzzles/day09","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day10","path":"/scala-advent-of-code/2022/puzzles/day10","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day11","path":"/scala-advent-of-code/2022/puzzles/day11","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day12","path":"/scala-advent-of-code/2022/puzzles/day12","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day13","path":"/scala-advent-of-code/2022/puzzles/day13","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day14","path":"/scala-advent-of-code/2022/puzzles/day14","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day15","path":"/scala-advent-of-code/2022/puzzles/day15","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day16","path":"/scala-advent-of-code/2022/puzzles/day16","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day17","path":"/scala-advent-of-code/2022/puzzles/day17","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day18","path":"/scala-advent-of-code/2022/puzzles/day18","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day19","path":"/scala-advent-of-code/2022/puzzles/day19","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day20","path":"/scala-advent-of-code/2022/puzzles/day20","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day21","path":"/scala-advent-of-code/2022/puzzles/day21","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day22","path":"/scala-advent-of-code/2022/puzzles/day22","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day23","path":"/scala-advent-of-code/2022/puzzles/day23","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day24","path":"/scala-advent-of-code/2022/puzzles/day24","sidebar":"adventOfCodeSidebar"},{"id":"2022/puzzles/day25","path":"/scala-advent-of-code/2022/puzzles/day25","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day01","path":"/scala-advent-of-code/2023/puzzles/day01","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day02","path":"/scala-advent-of-code/2023/puzzles/day02","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day03","path":"/scala-advent-of-code/2023/puzzles/day03","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day04","path":"/scala-advent-of-code/2023/puzzles/day04","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day05","path":"/scala-advent-of-code/2023/puzzles/day05","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day06","path":"/scala-advent-of-code/2023/puzzles/day06","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day07","path":"/scala-advent-of-code/2023/puzzles/day07","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day08","path":"/scala-advent-of-code/2023/puzzles/day08","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day09","path":"/scala-advent-of-code/2023/puzzles/day09","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day10","path":"/scala-advent-of-code/2023/puzzles/day10","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day11","path":"/scala-advent-of-code/2023/puzzles/day11","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day12","path":"/scala-advent-of-code/2023/puzzles/day12","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day13","path":"/scala-advent-of-code/2023/puzzles/day13","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day14","path":"/scala-advent-of-code/2023/puzzles/day14","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day15","path":"/scala-advent-of-code/2023/puzzles/day15","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day16","path":"/scala-advent-of-code/2023/puzzles/day16","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day17","path":"/scala-advent-of-code/2023/puzzles/day17","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day18","path":"/scala-advent-of-code/2023/puzzles/day18","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day19","path":"/scala-advent-of-code/2023/puzzles/day19","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day20","path":"/scala-advent-of-code/2023/puzzles/day20","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day21","path":"/scala-advent-of-code/2023/puzzles/day21","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day22","path":"/scala-advent-of-code/2023/puzzles/day22","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day23","path":"/scala-advent-of-code/2023/puzzles/day23","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day24","path":"/scala-advent-of-code/2023/puzzles/day24","sidebar":"adventOfCodeSidebar"},{"id":"2023/puzzles/day25","path":"/scala-advent-of-code/2023/puzzles/day25","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day0","path":"/scala-advent-of-code/2024/puzzles/day0"},{"id":"2024/puzzles/day01","path":"/scala-advent-of-code/2024/puzzles/day01","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day02","path":"/scala-advent-of-code/2024/puzzles/day02","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day03","path":"/scala-advent-of-code/2024/puzzles/day03","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day04","path":"/scala-advent-of-code/2024/puzzles/day04","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day05","path":"/scala-advent-of-code/2024/puzzles/day05","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day06","path":"/scala-advent-of-code/2024/puzzles/day06","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day07","path":"/scala-advent-of-code/2024/puzzles/day07","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day08","path":"/scala-advent-of-code/2024/puzzles/day08","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day09","path":"/scala-advent-of-code/2024/puzzles/day09","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day10","path":"/scala-advent-of-code/2024/puzzles/day10","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day11","path":"/scala-advent-of-code/2024/puzzles/day11","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day12","path":"/scala-advent-of-code/2024/puzzles/day12","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day13","path":"/scala-advent-of-code/2024/puzzles/day13","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day14","path":"/scala-advent-of-code/2024/puzzles/day14","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day15","path":"/scala-advent-of-code/2024/puzzles/day15","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day16","path":"/scala-advent-of-code/2024/puzzles/day16","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day17","path":"/scala-advent-of-code/2024/puzzles/day17","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day18","path":"/scala-advent-of-code/2024/puzzles/day18","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day19","path":"/scala-advent-of-code/2024/puzzles/day19","sidebar":"adventOfCodeSidebar"},{"id":"2024/puzzles/day20","path":"/scala-advent-of-code/2024/puzzles/day20","sidebar":"adventOfCodeSidebar"},{"id":"introduction","path":"/scala-advent-of-code/introduction","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day1","path":"/scala-advent-of-code/puzzles/day1","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day10","path":"/scala-advent-of-code/puzzles/day10","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day11","path":"/scala-advent-of-code/puzzles/day11","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day12","path":"/scala-advent-of-code/puzzles/day12","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day13","path":"/scala-advent-of-code/puzzles/day13","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day14","path":"/scala-advent-of-code/puzzles/day14","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day15","path":"/scala-advent-of-code/puzzles/day15","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day16","path":"/scala-advent-of-code/puzzles/day16","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day17","path":"/scala-advent-of-code/puzzles/day17","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day18","path":"/scala-advent-of-code/puzzles/day18","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day19","path":"/scala-advent-of-code/puzzles/day19","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day2","path":"/scala-advent-of-code/puzzles/day2","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day20","path":"/scala-advent-of-code/puzzles/day20","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day21","path":"/scala-advent-of-code/puzzles/day21","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day22","path":"/scala-advent-of-code/puzzles/day22","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day23","path":"/scala-advent-of-code/puzzles/day23","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day24","path":"/scala-advent-of-code/puzzles/day24","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day25","path":"/scala-advent-of-code/puzzles/day25","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day3","path":"/scala-advent-of-code/puzzles/day3","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day4","path":"/scala-advent-of-code/puzzles/day4","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day5","path":"/scala-advent-of-code/puzzles/day5","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day6","path":"/scala-advent-of-code/puzzles/day6","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day7","path":"/scala-advent-of-code/puzzles/day7","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day8","path":"/scala-advent-of-code/puzzles/day8","sidebar":"adventOfCodeSidebar"},{"id":"puzzles/day9","path":"/scala-advent-of-code/puzzles/day9","sidebar":"adventOfCodeSidebar"},{"id":"setup","path":"/scala-advent-of-code/setup","sidebar":"adventOfCodeSidebar"}],"draftIds":[],"sidebars":{"adventOfCodeSidebar":{"link":{"path":"/scala-advent-of-code/introduction","label":"introduction"}}}}],"breadcrumbs":true}}}'),l=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var i=n(7529);const s=JSON.parse('{"docusaurusVersion":"2.2.0","siteVersion":"0.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"2.2.0"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"2.2.0"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"2.2.0"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"2.2.0"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"2.2.0"}}}'),c={siteConfig:r.default,siteMetadata:s,globalData:o,i18n:l,codeTranslations:i},u=a.createContext(c);function d(e){let{children:t}=e;return a.createElement(u.Provider,{value:c},t)}},4763:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});var a=n(7294),r=n(412),o=n(5742),l=n(7676);function i(e){let{error:t,tryAgain:n}=e;return a.createElement("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"center",height:"50vh",width:"100%",fontSize:"20px"}},a.createElement("h1",null,"This page crashed."),a.createElement("p",null,t.message),a.createElement("button",{type:"button",onClick:n},"Try again"))}function s(e){let{error:t,tryAgain:n}=e;return a.createElement(u,{fallback:()=>a.createElement(i,{error:t,tryAgain:n})},a.createElement(o.Z,null,a.createElement("title",null,"Page Error")),a.createElement(l.Z,null,a.createElement(i,{error:t,tryAgain:n})))}const c=e=>a.createElement(s,e);class u extends a.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){r.Z.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??c)(e)}return e??null}}},412:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r});const a="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,r={canUseDOM:a,canUseEventListeners:a&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:a&&"IntersectionObserver"in window,canUseViewport:a&&"screen"in window}},5742:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var a=n(7294),r=n(405);function o(e){return a.createElement(r.ql,e)}},9960:(e,t,n)=>{"use strict";n.d(t,{Z:()=>p});var a=n(7462),r=n(7294),o=n(3727),l=n(8780),i=n(2263),s=n(3919),c=n(412);const u=r.createContext({collectLink:()=>{}});var d=n(4996);function f(e,t){let{isNavLink:n,to:f,href:p,activeClassName:m,isActive:h,"data-noBrokenLinkCheck":g,autoAddBaseUrl:b=!0,...y}=e;const{siteConfig:{trailingSlash:v,baseUrl:z}}=(0,i.Z)(),{withBaseUrl:w}=(0,d.C)(),E=(0,r.useContext)(u),k=(0,r.useRef)(null);(0,r.useImperativeHandle)(t,(()=>k.current));const S=f||p;const _=(0,s.Z)(S),C=S?.replace("pathname://","");let x=void 0!==C?(O=C,b&&(e=>e.startsWith("/"))(O)?w(O):O):void 0;var O;x&&_&&(x=(0,l.applyTrailingSlash)(x,{trailingSlash:v,baseUrl:z}));const T=(0,r.useRef)(!1),P=n?o.OL:o.rU,I=c.Z.canUseIntersectionObserver,A=(0,r.useRef)(),L=()=>{T.current||null==x||(window.docusaurus.preload(x),T.current=!0)};(0,r.useEffect)((()=>(!I&&_&&null!=x&&window.docusaurus.prefetch(x),()=>{I&&A.current&&A.current.disconnect()})),[A,x,I,_]);const N=x?.startsWith("#")??!1,R=!x||!_||N;return R||g||E.collectLink(x),R?r.createElement("a",(0,a.Z)({ref:k,href:x},S&&!_&&{target:"_blank",rel:"noopener noreferrer"},y)):r.createElement(P,(0,a.Z)({},y,{onMouseEnter:L,onTouchStart:L,innerRef:e=>{k.current=e,I&&e&&_&&(A.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(A.current.unobserve(e),A.current.disconnect(),null!=x&&window.docusaurus.prefetch(x))}))})),A.current.observe(e))},to:x},n&&{isActive:h,activeClassName:m}))}const p=r.forwardRef(f)},1875:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});const a=()=>null},5999:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s,I:()=>i});var a=n(7294);function r(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,a.isValidElement)(e)))?n.map(((e,t)=>(0,a.isValidElement)(e)?a.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var o=n(7529);function l(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return o[t??n]??n??t}function i(e,t){let{message:n,id:a}=e;return r(l({message:n,id:a}),t)}function s(e){let{children:t,id:n,values:o}=e;if(t&&"string"!=typeof t)throw console.warn("Illegal children",t),new Error("The Docusaurus component only accept simple string values");const i=l({message:t,id:n});return a.createElement(a.Fragment,null,r(i,o))}},9935:(e,t,n)=>{"use strict";n.d(t,{m:()=>a});const a="default"},3919:(e,t,n)=>{"use strict";function a(e){return/^(?:\w*:|\/\/)/.test(e)}function r(e){return void 0!==e&&!a(e)}n.d(t,{Z:()=>r,b:()=>a})},4996:(e,t,n)=>{"use strict";n.d(t,{C:()=>o,Z:()=>l});var a=n(2263),r=n(3919);function o(){const{siteConfig:{baseUrl:e,url:t}}=(0,a.Z)();return{withBaseUrl:(n,a)=>function(e,t,n,a){let{forcePrependBaseUrl:o=!1,absolute:l=!1}=void 0===a?{}:a;if(!n||n.startsWith("#")||(0,r.b)(n))return n;if(o)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;const i=n.startsWith(t)?n:t+n.replace(/^\//,"");return l?e+i:i}(t,e,n,a)}}function l(e,t){void 0===t&&(t={});const{withBaseUrl:n}=o();return n(e,t)}},2263:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var a=n(7294),r=n(8940);function o(){return(0,a.useContext)(r._)}},8084:(e,t,n)=>{"use strict";n.d(t,{OD:()=>l,ZP:()=>o,eZ:()=>i});var a=n(2263),r=n(9935);function o(){const{globalData:e}=(0,a.Z)();return e}function l(e,t){void 0===t&&(t={});const n=o()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}function i(e,t,n){void 0===t&&(t=r.m),void 0===n&&(n={});const a=l(e)?.[t];if(!a&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return a}},2389:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var a=n(7294),r=n(8934);function o(){return(0,a.useContext)(r._)}},9670:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});function a(e){const t={};return function e(n,a){Object.entries(n).forEach((n=>{let[r,o]=n;const l=a?`${a}.${r}`:r;var i;"object"==typeof(i=o)&&i&&Object.keys(i).length>0?e(o,l):t[l]=o}))}(e),t}},226:(e,t,n)=>{"use strict";n.d(t,{_:()=>r,z:()=>o});var a=n(7294);const r=a.createContext(null);function o(e){let{children:t,value:n}=e;const o=a.useContext(r),l=(0,a.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const a={...t.data,...n?.data};return{plugin:t.plugin,data:a}}({parent:o,value:n})),[o,n]);return a.createElement(r.Provider,{value:l},t)}},4104:(e,t,n)=>{"use strict";n.d(t,{Iw:()=>p,gA:()=>u,_r:()=>s,Jo:()=>m,zh:()=>c,yW:()=>f,gB:()=>d});var a=n(6550),r=n(8084);const o=e=>e.versions.find((e=>e.isLast));function l(e,t){const n=function(e,t){const n=o(e);return[...e.versions.filter((e=>e!==n)),n].find((e=>!!(0,a.LX)(t,{path:e.path,exact:!1,strict:!1})))}(e,t),r=n?.docs.find((e=>!!(0,a.LX)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:r,alternateDocVersions:r?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((a=>{a.id===t&&(n[e.name]=a)}))})),n}(r.id):{}}}const i={},s=()=>(0,r.OD)("docusaurus-plugin-content-docs")??i,c=e=>(0,r.eZ)("docusaurus-plugin-content-docs",e,{failfast:!0});function u(e){void 0===e&&(e={});const t=s(),{pathname:n}=(0,a.TH)();return function(e,t,n){void 0===n&&(n={});const r=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,a.LX)(t,{path:n.path,exact:!1,strict:!1})})),o=r?{pluginId:r[0],pluginData:r[1]}:void 0;if(!o&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return o}(t,n,e)}function d(e){return c(e).versions}function f(e){const t=c(e);return o(t)}function p(e){const t=c(e),{pathname:n}=(0,a.TH)();return l(t,n)}function m(e){const t=c(e),{pathname:n}=(0,a.TH)();return function(e,t){const n=o(e);return{latestDocSuggestion:l(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},8320:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var a=n(4865),r=n.n(a);r().configure({showSpinner:!1});const o={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{r().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){r().done()}}},3310:(e,t,n)=>{"use strict";n.r(t);var a=n(7410),r=n(6809);!function(e){const{themeConfig:{prism:t}}=r.default,{additionalLanguages:a}=t;globalThis.Prism=e,a.forEach((e=>{n(737)(`./prism-${e}`)})),delete globalThis.Prism}(a.Z)},9471:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var a=n(7294);const r="iconExternalLink_nPIU";function o(e){let{width:t=13.5,height:n=13.5}=e;return a.createElement("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:r},a.createElement("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"}))}},7676:(e,t,n)=>{"use strict";n.d(t,{Z:()=>ct});var a=n(7294),r=n(6010),o=n(4763),l=n(1944),i=n(7462),s=n(6550),c=n(5999),u=n(5936);const d="docusaurus_skipToContent_fallback";function f(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function p(){const e=(0,a.useRef)(null),{action:t}=(0,s.k6)(),n=(0,a.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(d);t&&f(t)}),[]);return(0,u.S)((n=>{let{location:a}=n;e.current&&!a.hash&&"PUSH"===t&&f(e.current)})),{containerRef:e,onClick:n}}const m=(0,c.I)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function h(e){const t=e.children??m,{containerRef:n,onClick:r}=p();return a.createElement("div",{ref:n,role:"region","aria-label":m},a.createElement("a",(0,i.Z)({},e,{href:`#${d}`,onClick:r}),t))}var g=n(5281),b=n(9727);const y="skipToContent_fXgn";function v(){return a.createElement(h,{className:y})}var z=n(6668),w=n(9689);function E(e){let{width:t=21,height:n=21,color:r="currentColor",strokeWidth:o=1.2,className:l,...s}=e;return a.createElement("svg",(0,i.Z)({viewBox:"0 0 15 15",width:t,height:n},s),a.createElement("g",{stroke:r,strokeWidth:o},a.createElement("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})))}const k="closeButton_CVFx";function S(e){return a.createElement("button",(0,i.Z)({type:"button","aria-label":(0,c.I)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"})},e,{className:(0,r.Z)("clean-btn close",k,e.className)}),a.createElement(E,{width:14,height:14,strokeWidth:3.1}))}const _="content_knG7";function C(e){const{announcementBar:t}=(0,z.L)(),{content:n}=t;return a.createElement("div",(0,i.Z)({},e,{className:(0,r.Z)(_,e.className),dangerouslySetInnerHTML:{__html:n}}))}const x="announcementBar_mb4j",O="announcementBarPlaceholder_vyr4",T="announcementBarClose_gvF7",P="announcementBarContent_xLdY";function I(){const{announcementBar:e}=(0,z.L)(),{isActive:t,close:n}=(0,w.nT)();if(!t)return null;const{backgroundColor:r,textColor:o,isCloseable:l}=e;return a.createElement("div",{className:x,style:{backgroundColor:r,color:o},role:"banner"},l&&a.createElement("div",{className:O}),a.createElement(C,{className:P}),l&&a.createElement(S,{onClick:n,className:T}))}var A=n(2961),L=n(2466);var N=n(902),R=n(3102);const D=a.createContext(null);function M(e){let{children:t}=e;const n=function(){const e=(0,A.e)(),t=(0,R.HY)(),[n,r]=(0,a.useState)(!1),o=null!==t.component,l=(0,N.D9)(o);return(0,a.useEffect)((()=>{o&&!l&&r(!0)}),[o,l]),(0,a.useEffect)((()=>{o?e.shown||r(!0):r(!1)}),[e.shown,o]),(0,a.useMemo)((()=>[n,r]),[n])}();return a.createElement(D.Provider,{value:n},t)}function F(e){if(e.component){const t=e.component;return a.createElement(t,e.props)}}function B(){const e=(0,a.useContext)(D);if(!e)throw new N.i6("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,r=(0,a.useCallback)((()=>n(!1)),[n]),o=(0,R.HY)();return(0,a.useMemo)((()=>({shown:t,hide:r,content:F(o)})),[r,o,t])}function $(e){let{header:t,primaryMenu:n,secondaryMenu:o}=e;const{shown:l}=B();return a.createElement("div",{className:"navbar-sidebar"},t,a.createElement("div",{className:(0,r.Z)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":l})},a.createElement("div",{className:"navbar-sidebar__item menu"},n),a.createElement("div",{className:"navbar-sidebar__item menu"},o)))}var U=n(2949),j=n(2389);function Z(e){return a.createElement("svg",(0,i.Z)({viewBox:"0 0 24 24",width:24,height:24},e),a.createElement("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"}))}function H(e){return a.createElement("svg",(0,i.Z)({viewBox:"0 0 24 24",width:24,height:24},e),a.createElement("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"}))}const V={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function W(e){let{className:t,value:n,onChange:o}=e;const l=(0,j.Z)(),i=(0,c.I)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===n?(0,c.I)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,c.I)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return a.createElement("div",{className:(0,r.Z)(V.toggle,t)},a.createElement("button",{className:(0,r.Z)("clean-btn",V.toggleButton,!l&&V.toggleButtonDisabled),type:"button",onClick:()=>o("dark"===n?"light":"dark"),disabled:!l,title:i,"aria-label":i,"aria-live":"polite"},a.createElement(Z,{className:(0,r.Z)(V.toggleIcon,V.lightToggleIcon)}),a.createElement(H,{className:(0,r.Z)(V.toggleIcon,V.darkToggleIcon)})))}const G=a.memo(W);function q(e){let{className:t}=e;const n=(0,z.L)().colorMode.disableSwitch,{colorMode:r,setColorMode:o}=(0,U.I)();return n?null:a.createElement(G,{className:t,value:r,onChange:o})}var Y=n(1327);function K(){return a.createElement(Y.Z,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function Q(){const e=(0,A.e)();return a.createElement("button",{type:"button","aria-label":(0,c.I)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle()},a.createElement(E,{color:"var(--ifm-color-emphasis-600)"}))}function X(){return a.createElement("div",{className:"navbar-sidebar__brand"},a.createElement(K,null),a.createElement(q,{className:"margin-right--md"}),a.createElement(Q,null))}var J=n(9960),ee=n(4996),te=n(3919);function ne(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}var ae=n(9471);function re(e){let{activeBasePath:t,activeBaseRegex:n,to:r,href:o,label:l,html:s,isDropdownLink:c,prependBaseUrlToHref:u,...d}=e;const f=(0,ee.Z)(r),p=(0,ee.Z)(t),m=(0,ee.Z)(o,{forcePrependBaseUrl:!0}),h=l&&o&&!(0,te.Z)(o),g=s?{dangerouslySetInnerHTML:{__html:s}}:{children:a.createElement(a.Fragment,null,l,h&&a.createElement(ae.Z,c&&{width:12,height:12}))};return o?a.createElement(J.Z,(0,i.Z)({href:u?m:o},d,g)):a.createElement(J.Z,(0,i.Z)({to:f,isNavLink:!0},(t||n)&&{isActive:(e,t)=>n?ne(n,t.pathname):t.pathname.startsWith(p)},d,g))}function oe(e){let{className:t,isDropdownItem:n=!1,...o}=e;const l=a.createElement(re,(0,i.Z)({className:(0,r.Z)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n},o));return n?a.createElement("li",null,l):l}function le(e){let{className:t,isDropdownItem:n,...o}=e;return a.createElement("li",{className:"menu__list-item"},a.createElement(re,(0,i.Z)({className:(0,r.Z)("menu__link",t)},o)))}function ie(e){let{mobile:t=!1,position:n,...r}=e;const o=t?le:oe;return a.createElement(o,(0,i.Z)({},r,{activeClassName:r.activeClassName??(t?"menu__link--active":"navbar__link--active")}))}var se=n(6043),ce=n(8596),ue=n(2263);function de(e,t){return e.some((e=>function(e,t){return!!(0,ce.Mg)(e.to,t)||!!ne(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function fe(e){let{items:t,position:n,className:o,onClick:l,...s}=e;const c=(0,a.useRef)(null),[u,d]=(0,a.useState)(!1);return(0,a.useEffect)((()=>{const e=e=>{c.current&&!c.current.contains(e.target)&&d(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e)}}),[c]),a.createElement("div",{ref:c,className:(0,r.Z)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":u})},a.createElement(re,(0,i.Z)({"aria-haspopup":"true","aria-expanded":u,role:"button",href:s.to?void 0:"#",className:(0,r.Z)("navbar__link",o)},s,{onClick:s.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),d(!u))}}),s.children??s.label),a.createElement("ul",{className:"dropdown__menu"},t.map(((e,n)=>a.createElement(Ce,(0,i.Z)({isDropdownItem:!0,onKeyDown:e=>{if(n===t.length-1&&"Tab"===e.key){e.preventDefault(),d(!1);const t=c.current.nextElementSibling;if(t){(t instanceof HTMLAnchorElement?t:t.querySelector("a")).focus()}}},activeClassName:"dropdown__link--active"},e,{key:n}))))))}function pe(e){let{items:t,className:n,position:o,onClick:l,...c}=e;const u=function(){const{siteConfig:{baseUrl:e}}=(0,ue.Z)(),{pathname:t}=(0,s.TH)();return t.replace(e,"/")}(),d=de(t,u),{collapsed:f,toggleCollapsed:p,setCollapsed:m}=(0,se.u)({initialState:()=>!d});return(0,a.useEffect)((()=>{d&&m(!d)}),[u,d,m]),a.createElement("li",{className:(0,r.Z)("menu__list-item",{"menu__list-item--collapsed":f})},a.createElement(re,(0,i.Z)({role:"button",className:(0,r.Z)("menu__link menu__link--sublist menu__link--sublist-caret",n)},c,{onClick:e=>{e.preventDefault(),p()}}),c.children??c.label),a.createElement(se.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:f},t.map(((e,t)=>a.createElement(Ce,(0,i.Z)({mobile:!0,isDropdownItem:!0,onClick:l,activeClassName:"menu__link--active"},e,{key:t}))))))}function me(e){let{mobile:t=!1,...n}=e;const r=t?pe:fe;return a.createElement(r,n)}var he=n(4711);function ge(e){let{width:t=20,height:n=20,...r}=e;return a.createElement("svg",(0,i.Z)({viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0},r),a.createElement("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"}))}const be="iconLanguage_nlXk";var ye=n(1875);const ve="searchBox_ZlJk";function ze(e){let{children:t,className:n}=e;return a.createElement("div",{className:(0,r.Z)(n,ve)},t)}var we=n(4104),Ee=n(3438);var ke=n(373);const Se=e=>e.docs.find((t=>t.id===e.mainDocId));const _e={default:ie,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:r,...o}=e;const{i18n:{currentLocale:l,locales:u,localeConfigs:d}}=(0,ue.Z)(),f=(0,he.l)(),{search:p,hash:m}=(0,s.TH)(),h=[...n,...u.map((e=>{const n=`${`pathname://${f.createUrl({locale:e,fullyQualified:!1})}`}${p}${m}`;return{label:d[e].label,lang:d[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===l?t?"menu__link--active":"dropdown__link--active":""}})),...r],g=t?(0,c.I)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):d[l].label;return a.createElement(me,(0,i.Z)({},o,{mobile:t,label:a.createElement(a.Fragment,null,a.createElement(ge,{className:be}),g),items:h}))},search:function(e){let{mobile:t,className:n}=e;return t?null:a.createElement(ze,{className:n},a.createElement(ye.Z,null))},dropdown:me,html:function(e){let{value:t,className:n,mobile:o=!1,isDropdownItem:l=!1}=e;const i=l?"li":"div";return a.createElement(i,{className:(0,r.Z)({navbar__item:!o&&!l,"menu__list-item":o},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:l}=(0,we.Iw)(r),s=(0,Ee.vY)(t,r);return null===s?null:a.createElement(ie,(0,i.Z)({exact:!0},o,{isActive:()=>l?.path===s.path||!!l?.sidebar&&l.sidebar===s.sidebar,label:n??s.id,to:s.path}))},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:l}=(0,we.Iw)(r),s=(0,Ee.oz)(t,r).link;if(!s)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return a.createElement(ie,(0,i.Z)({exact:!0},o,{isActive:()=>l?.sidebar===t,label:n??s.label,to:s.path}))},docsVersion:function(e){let{label:t,to:n,docsPluginId:r,...o}=e;const l=(0,Ee.lO)(r)[0],s=t??l.label,c=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(l).path;return a.createElement(ie,(0,i.Z)({},o,{label:s,to:c}))},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:r,dropdownItemsBefore:o,dropdownItemsAfter:l,...u}=e;const{search:d,hash:f}=(0,s.TH)(),p=(0,we.Iw)(n),m=(0,we.gB)(n),{savePreferredVersionName:h}=(0,ke.J)(n),g=[...o,...m.map((e=>{const t=p.alternateDocVersions[e.name]??Se(e);return{label:e.label,to:`${t.path}${d}${f}`,isActive:()=>e===p.activeVersion,onClick:()=>h(e.name)}})),...l],b=(0,Ee.lO)(n)[0],y=t&&g.length>1?(0,c.I)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):b.label,v=t&&g.length>1?void 0:Se(b).path;return g.length<=1?a.createElement(ie,(0,i.Z)({},u,{mobile:t,label:y,to:v,isActive:r?()=>!1:void 0})):a.createElement(me,(0,i.Z)({},u,{mobile:t,label:y,to:v,items:g,isActive:r?()=>!1:void 0}))}};function Ce(e){let{type:t,...n}=e;const r=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),o=_e[r];if(!o)throw new Error(`No NavbarItem component found for type "${t}".`);return a.createElement(o,n)}function xe(){const e=(0,A.e)(),t=(0,z.L)().navbar.items;return a.createElement("ul",{className:"menu__list"},t.map(((t,n)=>a.createElement(Ce,(0,i.Z)({mobile:!0},t,{onClick:()=>e.toggle(),key:n})))))}function Oe(e){return a.createElement("button",(0,i.Z)({},e,{type:"button",className:"clean-btn navbar-sidebar__back"}),a.createElement(c.Z,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"},"\u2190 Back to main menu"))}function Te(){const e=0===(0,z.L)().navbar.items.length,t=B();return a.createElement(a.Fragment,null,!e&&a.createElement(Oe,{onClick:()=>t.hide()}),t.content)}function Pe(){const e=(0,A.e)();var t;return void 0===(t=e.shown)&&(t=!0),(0,a.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?a.createElement($,{header:a.createElement(X,null),primaryMenu:a.createElement(xe,null),secondaryMenu:a.createElement(Te,null)}):null}const Ie="navbarHideable_m1mJ",Ae="navbarHidden_jGov";function Le(e){return a.createElement("div",(0,i.Z)({role:"presentation"},e,{className:(0,r.Z)("navbar-sidebar__backdrop",e.className)}))}function Ne(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:o}}=(0,z.L)(),l=(0,A.e)(),{navbarRef:i,isNavbarVisible:s}=function(e){const[t,n]=(0,a.useState)(e),r=(0,a.useRef)(!1),o=(0,a.useRef)(0),l=(0,a.useCallback)((e=>{null!==e&&(o.current=e.getBoundingClientRect().height)}),[]);return(0,L.RF)(((t,a)=>{let{scrollY:l}=t;if(!e)return;if(l=i?n(!1):l+c{if(!e)return;const a=t.location.hash;if(a?document.getElementById(a.substring(1)):void 0)return r.current=!0,void n(!1);n(!0)})),{navbarRef:l,isNavbarVisible:t}}(n);return a.createElement("nav",{ref:i,className:(0,r.Z)("navbar","navbar--fixed-top",n&&[Ie,!s&&Ae],{"navbar--dark":"dark"===o,"navbar--primary":"primary"===o,"navbar-sidebar--show":l.shown})},t,a.createElement(Le,{onClick:l.toggle}),a.createElement(Pe,null))}function Re(e){let{width:t=30,height:n=30,className:r,...o}=e;return a.createElement("svg",(0,i.Z)({className:r,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true"},o),a.createElement("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"}))}function De(){const{toggle:e,shown:t}=(0,A.e)();return a.createElement("button",{onClick:e,"aria-label":(0,c.I)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button"},a.createElement(Re,null))}const Me="colorModeToggle_DEke";function Fe(e){let{items:t}=e;return a.createElement(a.Fragment,null,t.map(((e,t)=>a.createElement(Ce,(0,i.Z)({},e,{key:t})))))}function Be(e){let{left:t,right:n}=e;return a.createElement("div",{className:"navbar__inner"},a.createElement("div",{className:"navbar__items"},t),a.createElement("div",{className:"navbar__items navbar__items--right"},n))}function $e(){const e=(0,A.e)(),t=(0,z.L)().navbar.items,[n,r]=function(e){function t(e){return"left"===(e.position??"right")}return[e.filter(t),e.filter((e=>!t(e)))]}(t),o=t.find((e=>"search"===e.type));return a.createElement(Be,{left:a.createElement(a.Fragment,null,!e.disabled&&a.createElement(De,null),a.createElement(K,null),a.createElement(Fe,{items:n})),right:a.createElement(a.Fragment,null,a.createElement(Fe,{items:r}),a.createElement(q,{className:Me}),!o&&a.createElement(ze,null,a.createElement(ye.Z,null)))})}function Ue(){return a.createElement(Ne,null,a.createElement($e,null))}function je(e){let{item:t}=e;const{to:n,href:r,label:o,prependBaseUrlToHref:l,...s}=t,c=(0,ee.Z)(n),u=(0,ee.Z)(r,{forcePrependBaseUrl:!0});return a.createElement(J.Z,(0,i.Z)({className:"footer__link-item"},r?{href:l?u:r}:{to:c},s),o,r&&!(0,te.Z)(r)&&a.createElement(ae.Z,null))}function Ze(e){let{item:t}=e;return t.html?a.createElement("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):a.createElement("li",{key:t.href??t.to,className:"footer__item"},a.createElement(je,{item:t}))}function He(e){let{column:t}=e;return a.createElement("div",{className:"col footer__col"},a.createElement("div",{className:"footer__title"},t.title),a.createElement("ul",{className:"footer__items clean-list"},t.items.map(((e,t)=>a.createElement(Ze,{key:t,item:e})))))}function Ve(e){let{columns:t}=e;return a.createElement("div",{className:"row footer__links"},t.map(((e,t)=>a.createElement(He,{key:t,column:e}))))}function We(){return a.createElement("span",{className:"footer__link-separator"},"\xb7")}function Ge(e){let{item:t}=e;return t.html?a.createElement("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):a.createElement(je,{item:t})}function qe(e){let{links:t}=e;return a.createElement("div",{className:"footer__links text--center"},a.createElement("div",{className:"footer__links"},t.map(((e,n)=>a.createElement(a.Fragment,{key:n},a.createElement(Ge,{item:e}),t.length!==n+1&&a.createElement(We,null))))))}function Ye(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?a.createElement(Ve,{columns:t}):a.createElement(qe,{links:t})}var Ke=n(941);const Qe="footerLogoLink_BH7S";function Xe(e){let{logo:t}=e;const{withBaseUrl:n}=(0,ee.C)(),o={light:n(t.src),dark:n(t.srcDark??t.src)};return a.createElement(Ke.Z,{className:(0,r.Z)("footer__logo",t.className),alt:t.alt,sources:o,width:t.width,height:t.height,style:t.style})}function Je(e){let{logo:t}=e;return t.href?a.createElement(J.Z,{href:t.href,className:Qe,target:t.target},a.createElement(Xe,{logo:t})):a.createElement(Xe,{logo:t})}function et(e){let{copyright:t}=e;return a.createElement("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function tt(e){let{style:t,links:n,logo:o,copyright:l}=e;return a.createElement("footer",{className:(0,r.Z)("footer",{"footer--dark":"dark"===t})},a.createElement("div",{className:"container container-fluid"},n,(o||l)&&a.createElement("div",{className:"footer__bottom text--center"},o&&a.createElement("div",{className:"margin-bottom--sm"},o),l)))}function nt(){const{footer:e}=(0,z.L)();if(!e)return null;const{copyright:t,links:n,logo:r,style:o}=e;return a.createElement(tt,{style:o,links:n&&n.length>0&&a.createElement(Ye,{links:n}),logo:r&&a.createElement(Je,{logo:r}),copyright:t&&a.createElement(et,{copyright:t})})}const at=a.memo(nt);var rt=n(7094);const ot=(0,N.Qc)([U.S,w.pl,rt.z,L.OC,ke.L5,l.VC,function(e){let{children:t}=e;return a.createElement(R.n2,null,a.createElement(A.M,null,a.createElement(M,null,t)))}]);function lt(e){let{children:t}=e;return a.createElement(ot,null,t)}function it(e){let{error:t,tryAgain:n}=e;return a.createElement("main",{className:"container margin-vert--xl"},a.createElement("div",{className:"row"},a.createElement("div",{className:"col col--6 col--offset-3"},a.createElement("h1",{className:"hero__title"},a.createElement(c.Z,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed"},"This page crashed.")),a.createElement("p",null,t.message),a.createElement("div",null,a.createElement("button",{type:"button",onClick:n},a.createElement(c.Z,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again when the page crashed"},"Try again"))))))}const st="mainWrapper_z2l0";function ct(e){const{children:t,noFooter:n,wrapperClassName:i,title:s,description:c}=e;return(0,b.t)(),a.createElement(lt,null,a.createElement(l.d,{title:s,description:c}),a.createElement(v,null),a.createElement(I,null),a.createElement(Ue,null),a.createElement("div",{id:d,className:(0,r.Z)(g.k.wrapper.main,st,i)},a.createElement(o.Z,{fallback:e=>a.createElement(it,e)},t)),!n&&a.createElement(at,null))}},1327:(e,t,n)=>{"use strict";n.d(t,{Z:()=>d});var a=n(7462),r=n(7294),o=n(9960),l=n(4996),i=n(2263),s=n(6668),c=n(941);function u(e){let{logo:t,alt:n,imageClassName:a}=e;const o={light:(0,l.Z)(t.src),dark:(0,l.Z)(t.srcDark||t.src)},i=r.createElement(c.Z,{className:t.className,sources:o,height:t.height,width:t.width,alt:n,style:t.style});return a?r.createElement("div",{className:a},i):i}function d(e){const{siteConfig:{title:t}}=(0,i.Z)(),{navbar:{title:n,logo:c}}=(0,s.L)(),{imageClassName:d,titleClassName:f,...p}=e,m=(0,l.Z)(c?.href||"/"),h=n?"":t,g=c?.alt??h;return r.createElement(o.Z,(0,a.Z)({to:m},p,c?.target&&{target:c.target}),c&&r.createElement(u,{logo:c,alt:g,imageClassName:d}),null!=n&&r.createElement("b",{className:f},n))}},197:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var a=n(7294),r=n(5742);function o(e){let{locale:t,version:n,tag:o}=e;const l=t;return a.createElement(r.Z,null,t&&a.createElement("meta",{name:"docusaurus_locale",content:t}),n&&a.createElement("meta",{name:"docusaurus_version",content:n}),o&&a.createElement("meta",{name:"docusaurus_tag",content:o}),l&&a.createElement("meta",{name:"docsearch:language",content:l}),n&&a.createElement("meta",{name:"docsearch:version",content:n}),o&&a.createElement("meta",{name:"docsearch:docusaurus_tag",content:o}))}},941:(e,t,n)=>{"use strict";n.d(t,{Z:()=>c});var a=n(7462),r=n(7294),o=n(6010),l=n(2389),i=n(2949);const s={themedImage:"themedImage_ToTc","themedImage--light":"themedImage--light_HNdA","themedImage--dark":"themedImage--dark_i4oU"};function c(e){const t=(0,l.Z)(),{colorMode:n}=(0,i.I)(),{sources:c,className:u,alt:d,...f}=e,p=t?"dark"===n?["dark"]:["light"]:["light","dark"];return r.createElement(r.Fragment,null,p.map((e=>r.createElement("img",(0,a.Z)({key:e,src:c[e],alt:d,className:(0,o.Z)(s.themedImage,s[`themedImage--${e}`],u)},f)))))}},6043:(e,t,n)=>{"use strict";n.d(t,{u:()=>l,z:()=>m});var a=n(7462),r=n(7294),o=n(412);function l(e){let{initialState:t}=e;const[n,a]=(0,r.useState)(t??!1),o=(0,r.useCallback)((()=>{a((e=>!e))}),[]);return{collapsed:n,setCollapsed:a,toggleCollapsed:o}}const i={display:"none",overflow:"hidden",height:"0px"},s={display:"block",overflow:"visible",height:"auto"};function c(e,t){const n=t?i:s;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function u(e){let{collapsibleRef:t,collapsed:n,animation:a}=e;const o=(0,r.useRef)(!1);(0,r.useEffect)((()=>{const e=t.current;function r(){const t=e.scrollHeight,n=a?.duration??function(e){const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${a?.easing??"ease-in-out"}`,height:`${t}px`}}function l(){const t=r();e.style.transition=t.transition,e.style.height=t.height}if(!o.current)return c(e,n),void(o.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(l(),requestAnimationFrame((()=>{e.style.height=i.height,e.style.overflow=i.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{l()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,a])}function d(e){if(!o.Z.canUseDOM)return e?i:s}function f(e){let{as:t="div",collapsed:n,children:a,animation:o,onCollapseTransitionEnd:l,className:i,disableSSRStyle:s}=e;const f=(0,r.useRef)(null);return u({collapsibleRef:f,collapsed:n,animation:o}),r.createElement(t,{ref:f,style:s?void 0:d(n),onTransitionEnd:e=>{"height"===e.propertyName&&(c(f.current,n),l?.(n))},className:i},a)}function p(e){let{collapsed:t,...n}=e;const[o,l]=(0,r.useState)(!t),[i,s]=(0,r.useState)(t);return(0,r.useLayoutEffect)((()=>{t||l(!0)}),[t]),(0,r.useLayoutEffect)((()=>{o&&s(t)}),[o,t]),o?r.createElement(f,(0,a.Z)({},n,{collapsed:i})):null}function m(e){let{lazy:t,...n}=e;const a=t?p:f;return r.createElement(a,n)}},9689:(e,t,n)=>{"use strict";n.d(t,{nT:()=>m,pl:()=>p});var a=n(7294),r=n(2389),o=n(12),l=n(902),i=n(6668);const s=(0,o.W)("docusaurus.announcement.dismiss"),c=(0,o.W)("docusaurus.announcement.id"),u=()=>"true"===s.get(),d=e=>s.set(String(e)),f=a.createContext(null);function p(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,i.L)(),t=(0,r.Z)(),[n,o]=(0,a.useState)((()=>!!t&&u()));(0,a.useEffect)((()=>{o(u())}),[]);const l=(0,a.useCallback)((()=>{d(!0),o(!0)}),[]);return(0,a.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=c.get();"annoucement-bar"===n&&(n="announcement-bar");const a=t!==n;c.set(t),a&&d(!1),!a&&u()||o(!1)}),[e]),(0,a.useMemo)((()=>({isActive:!!e&&!n,close:l})),[e,n,l])}();return a.createElement(f.Provider,{value:n},t)}function m(){const e=(0,a.useContext)(f);if(!e)throw new l.i6("AnnouncementBarProvider");return e}},2949:(e,t,n)=>{"use strict";n.d(t,{I:()=>g,S:()=>h});var a=n(7294),r=n(412),o=n(902),l=n(12),i=n(6668);const s=a.createContext(void 0),c="theme",u=(0,l.W)(c),d="light",f="dark",p=e=>e===f?f:d;function m(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,i.L)(),[o,l]=(0,a.useState)((e=>r.Z.canUseDOM?p(document.documentElement.getAttribute("data-theme")):p(e))(e));(0,a.useEffect)((()=>{t&&u.del()}),[t]);const s=(0,a.useCallback)((function(t,a){void 0===a&&(a={});const{persist:r=!0}=a;t?(l(t),r&&(e=>{u.set(p(e))})(t)):(l(n?window.matchMedia("(prefers-color-scheme: dark)").matches?f:d:e),u.del())}),[n,e]);(0,a.useEffect)((()=>{document.documentElement.setAttribute("data-theme",p(o))}),[o]),(0,a.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==c)return;const t=u.get();null!==t&&s(p(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,s]);const m=(0,a.useRef)(!1);return(0,a.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),a=()=>{window.matchMedia("print").matches||m.current?m.current=window.matchMedia("print").matches:s(null)};return e.addListener(a),()=>e.removeListener(a)}),[s,t,n]),(0,a.useMemo)((()=>({colorMode:o,setColorMode:s,get isDarkTheme(){return o===f},setLightTheme(){s(d)},setDarkTheme(){s(f)}})),[o,s])}function h(e){let{children:t}=e;const n=m();return a.createElement(s.Provider,{value:n},t)}function g(){const e=(0,a.useContext)(s);if(null==e)throw new o.i6("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},373:(e,t,n)=>{"use strict";n.d(t,{J:()=>v,L5:()=>b});var a=n(7294),r=n(4104),o=n(9935),l=n(6668),i=n(3438),s=n(902),c=n(12);const u=e=>`docs-preferred-version-${e}`,d=(e,t,n)=>{(0,c.W)(u(e),{persistence:t}).set(n)},f=(e,t)=>(0,c.W)(u(e),{persistence:t}).get(),p=(e,t)=>{(0,c.W)(u(e),{persistence:t}).del()};const m=a.createContext(null);function h(){const e=(0,r._r)(),t=(0,l.L)().docs.versionPersistence,n=(0,a.useMemo)((()=>Object.keys(e)),[e]),[o,i]=(0,a.useState)((()=>(e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}]))))(n)));(0,a.useEffect)((()=>{i(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:a}=e;function r(e){const t=f(e,n);return a[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(p(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,r(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[o,(0,a.useMemo)((()=>({savePreferredVersion:function(e,n){d(e,t,n),i((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function g(e){let{children:t}=e;const n=h();return a.createElement(m.Provider,{value:n},t)}function b(e){let{children:t}=e;return i.cE?a.createElement(g,null,t):a.createElement(a.Fragment,null,t)}function y(){const e=(0,a.useContext)(m);if(!e)throw new s.i6("DocsPreferredVersionContextProvider");return e}function v(e){void 0===e&&(e=o.m);const t=(0,r.zh)(e),[n,l]=y(),{preferredVersionName:i}=n[e];return{preferredVersion:t.versions.find((e=>e.name===i))??null,savePreferredVersionName:(0,a.useCallback)((t=>{l.savePreferredVersion(e,t)}),[l,e])}}},1116:(e,t,n)=>{"use strict";n.d(t,{V:()=>s,b:()=>i});var a=n(7294),r=n(902);const o=Symbol("EmptyContext"),l=a.createContext(o);function i(e){let{children:t,name:n,items:r}=e;const o=(0,a.useMemo)((()=>n&&r?{name:n,items:r}:null),[n,r]);return a.createElement(l.Provider,{value:o},t)}function s(){const e=(0,a.useContext)(l);if(e===o)throw new r.i6("DocsSidebarProvider");return e}},2961:(e,t,n)=>{"use strict";n.d(t,{M:()=>f,e:()=>p});var a=n(7294),r=n(3102),o=n(7524),l=n(6550),i=n(902);function s(e){!function(e){const t=(0,l.k6)(),n=(0,i.zX)(e);(0,a.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}var c=n(6668);const u=a.createContext(void 0);function d(){const e=function(){const e=(0,r.HY)(),{items:t}=(0,c.L)().navbar;return 0===t.length&&!e.component}(),t=(0,o.i)(),n=!e&&"mobile"===t,[l,i]=(0,a.useState)(!1);s((()=>{if(l)return i(!1),!1}));const u=(0,a.useCallback)((()=>{i((e=>!e))}),[]);return(0,a.useEffect)((()=>{"desktop"===t&&i(!1)}),[t]),(0,a.useMemo)((()=>({disabled:e,shouldRender:n,toggle:u,shown:l})),[e,n,u,l])}function f(e){let{children:t}=e;const n=d();return a.createElement(u.Provider,{value:n},t)}function p(){const e=a.useContext(u);if(void 0===e)throw new i.i6("NavbarMobileSidebarProvider");return e}},3102:(e,t,n)=>{"use strict";n.d(t,{HY:()=>i,Zo:()=>s,n2:()=>l});var a=n(7294),r=n(902);const o=a.createContext(null);function l(e){let{children:t}=e;const n=(0,a.useState)({component:null,props:null});return a.createElement(o.Provider,{value:n},t)}function i(){const e=(0,a.useContext)(o);if(!e)throw new r.i6("NavbarSecondaryMenuContentProvider");return e[0]}function s(e){let{component:t,props:n}=e;const l=(0,a.useContext)(o);if(!l)throw new r.i6("NavbarSecondaryMenuContentProvider");const[,i]=l,s=(0,r.Ql)(n);return(0,a.useEffect)((()=>{i({component:t,props:s})}),[i,t,s]),(0,a.useEffect)((()=>()=>i({component:null,props:null})),[i]),null}},7094:(e,t,n)=>{"use strict";n.d(t,{U:()=>c,z:()=>s});var a=n(7294),r=n(12),o=n(902);const l="docusaurus.tab.",i=a.createContext(void 0);function s(e){let{children:t}=e;const n=function(){const[e,t]=(0,a.useState)({}),n=(0,a.useCallback)(((e,t)=>{(0,r.W)(`docusaurus.tab.${e}`).set(t)}),[]);(0,a.useEffect)((()=>{try{const e={};(0,r._)().forEach((t=>{if(t.startsWith(l)){const n=t.substring(l.length);e[n]=(0,r.W)(t).get()}})),t(e)}catch(e){console.error(e)}}),[]);const o=(0,a.useCallback)(((e,a)=>{t((t=>({...t,[e]:a}))),n(e,a)}),[n]);return(0,a.useMemo)((()=>({tabGroupChoices:e,setTabGroupChoices:o})),[e,o])}();return a.createElement(i.Provider,{value:n},t)}function c(){const e=(0,a.useContext)(i);if(null==e)throw new o.i6("TabGroupChoiceProvider");return e}},9727:(e,t,n)=>{"use strict";n.d(t,{h:()=>r,t:()=>o});var a=n(7294);const r="navigation-with-keyboard";function o(){(0,a.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(r),"mousedown"===e.type&&document.body.classList.remove(r)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(r),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},7524:(e,t,n)=>{"use strict";n.d(t,{i:()=>c});var a=n(7294),r=n(412);const o="desktop",l="mobile",i="ssr";function s(){return r.Z.canUseDOM?window.innerWidth>996?o:l:i}function c(){const[e,t]=(0,a.useState)((()=>s()));return(0,a.useEffect)((()=>{function e(){t(s())}return window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e),clearTimeout(undefined)}}),[]),e}},5281:(e,t,n)=>{"use strict";n.d(t,{k:()=>a});const a={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{}}},3438:(e,t,n)=>{"use strict";n.d(t,{Wl:()=>f,_F:()=>m,cE:()=>d,hI:()=>z,lO:()=>b,oz:()=>y,s1:()=>g,vY:()=>v});var a=n(7294),r=n(6550),o=n(8790),l=n(4104),i=n(373),s=n(1116),c=n(7392),u=n(8596);const d=!!l._r;function f(e){if(e.href)return e.href;for(const t of e.items){if("link"===t.type)return t.href;if("category"===t.type){const e=f(t);if(e)return e}}}const p=(e,t)=>void 0!==e&&(0,u.Mg)(e,t);function m(e,t){return"link"===e.type?p(e.href,t):"category"===e.type&&(p(e.href,t)||((e,t)=>e.some((e=>m(e,t))))(e.items,t))}function h(e){let{sidebarItems:t,pathname:n,onlyCategories:a=!1}=e;const r=[];return function e(t){for(const o of t)if("category"===o.type&&((0,u.Mg)(o.href,n)||e(o.items))||"link"===o.type&&(0,u.Mg)(o.href,n)){return a&&"category"!==o.type||r.unshift(o),!0}return!1}(t),r}function g(){const e=(0,s.V)(),{pathname:t}=(0,r.TH)(),n=(0,l.gA)()?.pluginData.breadcrumbs;return!1!==n&&e?h({sidebarItems:e.items,pathname:t}):null}function b(e){const{activeVersion:t}=(0,l.Iw)(e),{preferredVersion:n}=(0,i.J)(e),r=(0,l.yW)(e);return(0,a.useMemo)((()=>(0,c.j)([t,n,r].filter(Boolean))),[t,n,r])}function y(e,t){const n=b(t);return(0,a.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),a=t.find((t=>t[0]===e));if(!a)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\n Available sidebar ids are:\n - ${Object.keys(t).join("\n- ")}`);return a[1]}),[e,n])}function v(e,t){const n=b(t);return(0,a.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),a=t.find((t=>t.id===e));if(!a){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`DocNavbarItem: couldn't find any doc with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${(0,c.j)(t.map((e=>e.id))).join("\n- ")}`)}return a}),[e,n])}function z(e){let{route:t,versionMetadata:n}=e;const a=(0,r.TH)(),l=t.routes,i=l.find((e=>(0,r.LX)(a.pathname,e)));if(!i)return null;const s=i.sidebar,c=s?n.docsSidebars[s]:void 0;return{docElement:(0,o.H)(l),sidebarName:s,sidebarItems:c}}},7392:(e,t,n)=>{"use strict";function a(e,t){return void 0===t&&(t=(e,t)=>e===t),e.filter(((n,a)=>e.findIndex((e=>t(e,n)))!==a))}function r(e){return Array.from(new Set(e))}n.d(t,{j:()=>r,l:()=>a})},1944:(e,t,n)=>{"use strict";n.d(t,{FG:()=>f,d:()=>u,VC:()=>p});var a=n(7294),r=n(6010),o=n(5742),l=n(226);function i(){const e=a.useContext(l._);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}var s=n(4996),c=n(2263);function u(e){let{title:t,description:n,keywords:r,image:l,children:i}=e;const u=function(e){const{siteConfig:t}=(0,c.Z)(),{title:n,titleDelimiter:a}=t;return e?.trim().length?`${e.trim()} ${a} ${n}`:n}(t),{withBaseUrl:d}=(0,s.C)(),f=l?d(l,{absolute:!0}):void 0;return a.createElement(o.Z,null,t&&a.createElement("title",null,u),t&&a.createElement("meta",{property:"og:title",content:u}),n&&a.createElement("meta",{name:"description",content:n}),n&&a.createElement("meta",{property:"og:description",content:n}),r&&a.createElement("meta",{name:"keywords",content:Array.isArray(r)?r.join(","):r}),f&&a.createElement("meta",{property:"og:image",content:f}),f&&a.createElement("meta",{name:"twitter:image",content:f}),i)}const d=a.createContext(void 0);function f(e){let{className:t,children:n}=e;const l=a.useContext(d),i=(0,r.Z)(l,t);return a.createElement(d.Provider,{value:i},a.createElement(o.Z,null,a.createElement("html",{className:i})),n)}function p(e){let{children:t}=e;const n=i(),o=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const l=`plugin-id-${n.plugin.id}`;return a.createElement(f,{className:(0,r.Z)(o,l)},t)}},902:(e,t,n)=>{"use strict";n.d(t,{D9:()=>l,Qc:()=>c,Ql:()=>s,i6:()=>i,zX:()=>o});var a=n(7294);const r=n(412).Z.canUseDOM?a.useLayoutEffect:a.useEffect;function o(e){const t=(0,a.useRef)(e);return r((()=>{t.current=e}),[e]),(0,a.useCallback)((function(){return t.current(...arguments)}),[])}function l(e){const t=(0,a.useRef)();return r((()=>{t.current=e})),t.current}class i extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function s(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,a.useMemo)((()=>e),t.flat())}function c(e){return t=>{let{children:n}=t;return a.createElement(a.Fragment,null,e.reduceRight(((e,t)=>a.createElement(t,null,e)),n))}}},8596:(e,t,n)=>{"use strict";n.d(t,{Mg:()=>l,Ns:()=>i});var a=n(7294),r=n(723),o=n(2263);function l(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function i(){const{baseUrl:e}=(0,o.Z)().siteConfig;return(0,a.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function a(e){return e.path===t&&!0===e.exact}function r(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(a)||e(t.filter(r).flatMap((e=>e.routes??[])))}(n)}({routes:r.Z,baseUrl:e})),[e])}},2466:(e,t,n)=>{"use strict";n.d(t,{Ct:()=>p,OC:()=>s,RF:()=>d,o5:()=>f});var a=n(7294),r=n(412),o=n(2389),l=n(902);const i=a.createContext(void 0);function s(e){let{children:t}=e;const n=function(){const e=(0,a.useRef)(!0);return(0,a.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return a.createElement(i.Provider,{value:n},t)}function c(){const e=(0,a.useContext)(i);if(null==e)throw new l.i6("ScrollControllerProvider");return e}const u=()=>r.Z.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function d(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=c(),r=(0,a.useRef)(u()),o=(0,l.zX)(e);(0,a.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=u();o(e,r.current),r.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[o,n,...t])}function f(){const e=c(),t=function(){const e=(0,a.useRef)({elem:null,top:0}),t=(0,a.useCallback)((t=>{e.current={elem:t,top:t.getBoundingClientRect().top}}),[]),n=(0,a.useCallback)((()=>{const{current:{elem:t,top:n}}=e;if(!t)return{restored:!1};const a=t.getBoundingClientRect().top-n;return a&&window.scrollBy({left:0,top:a}),e.current={elem:null,top:0},{restored:0!==a}}),[]);return(0,a.useMemo)((()=>({save:t,restore:n})),[n,t])}(),n=(0,a.useRef)(void 0),r=(0,a.useCallback)((a=>{t.save(a),e.disableScrollEvents(),n.current=()=>{const{restored:a}=t.restore();if(n.current=void 0,a){const t=()=>{e.enableScrollEvents(),window.removeEventListener("scroll",t)};window.addEventListener("scroll",t)}else e.enableScrollEvents()}}),[e,t]);return(0,a.useLayoutEffect)((()=>{n.current?.()})),{blockElementScrollPositionUntilNextRender:r}}function p(){const e=(0,a.useRef)(null),t=(0,o.Z)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function a(){const r=document.documentElement.scrollTop;(n&&r>e||!n&&rt&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},3320:(e,t,n)=>{"use strict";n.d(t,{HX:()=>a,os:()=>r});n(2263);const a="default";function r(e,t){return`docs-${e}-${t}`}},12:(e,t,n)=>{"use strict";n.d(t,{W:()=>i,_:()=>s});const a="localStorage";function r(e){if(void 0===e&&(e=a),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,o||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),o=!0),null}var t}let o=!1;const l={get:()=>null,set:()=>{},del:()=>{}};function i(e,t){if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t}}(e);const n=r(t?.persistence);return null===n?l:{get:()=>{try{return n.getItem(e)}catch(t){return console.error(`Docusaurus storage error, can't get key=${e}`,t),null}},set:t=>{try{n.setItem(e,t)}catch(a){console.error(`Docusaurus storage error, can't set ${e}=${t}`,a)}},del:()=>{try{n.removeItem(e)}catch(t){console.error(`Docusaurus storage error, can't delete key=${e}`,t)}}}}function s(e){void 0===e&&(e=a);const t=r(e);if(!t)return[];const n=[];for(let a=0;a{"use strict";n.d(t,{l:()=>o});var a=n(2263),r=n(6550);function o(){const{siteConfig:{baseUrl:e,url:t},i18n:{defaultLocale:n,currentLocale:o}}=(0,a.Z)(),{pathname:l}=(0,r.TH)(),i=o===n?e:e.replace(`/${o}/`,"/"),s=l.replace(e,"");return{createUrl:function(e){let{locale:a,fullyQualified:r}=e;return`${r?t:""}${function(e){return e===n?`${i}`:`${i}${e}/`}(a)}${s}`}}}},5936:(e,t,n)=>{"use strict";n.d(t,{S:()=>l});var a=n(7294),r=n(6550),o=n(902);function l(e){const t=(0,r.TH)(),n=(0,o.D9)(t),l=(0,o.zX)(e);(0,a.useEffect)((()=>{n&&t!==n&&l({location:t,previousLocation:n})}),[l,t,n])}},6668:(e,t,n)=>{"use strict";n.d(t,{L:()=>r});var a=n(2263);function r(){return(0,a.Z)().siteConfig.themeConfig}},8802:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){const{trailingSlash:n,baseUrl:a}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[r]=e.split(/[#?]/),o="/"===r||r===a?r:(l=r,n?function(e){return e.endsWith("/")?e:`${e}/`}(l):function(e){return e.endsWith("/")?e.slice(0,-1):e}(l));var l;return e.replace(r,o)}},8780:function(e,t,n){"use strict";var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.applyTrailingSlash=t.blogPostContainerID=void 0,t.blogPostContainerID="post-content";var r=n(8802);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return a(r).default}})},6010:(e,t,n)=>{"use strict";function a(e){var t,n,r="";if("string"==typeof e||"number"==typeof e)r+=e;else if("object"==typeof e)if(Array.isArray(e))for(t=0;tr});const r=function(){for(var e,t,n=0,r="";n{"use strict";n.d(t,{lX:()=>z,q_:()=>C,ob:()=>p,PP:()=>O,Ep:()=>f});var a=n(7462);function r(e){return"/"===e.charAt(0)}function o(e,t){for(var n=t,a=n+1,r=e.length;a=0;f--){var p=l[f];"."===p?o(l,f):".."===p?(o(l,f),d++):d&&(o(l,f),d--)}if(!c)for(;d--;d)l.unshift("..");!c||""===l[0]||l[0]&&r(l[0])||l.unshift("");var m=l.join("/");return n&&"/"!==m.substr(-1)&&(m+="/"),m};var i=n(8776);function s(e){return"/"===e.charAt(0)?e:"/"+e}function c(e){return"/"===e.charAt(0)?e.substr(1):e}function u(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function f(e){var t=e.pathname,n=e.search,a=e.hash,r=t||"/";return n&&"?"!==n&&(r+="?"===n.charAt(0)?n:"?"+n),a&&"#"!==a&&(r+="#"===a.charAt(0)?a:"#"+a),r}function p(e,t,n,r){var o;"string"==typeof e?(o=function(e){var t=e||"/",n="",a="",r=t.indexOf("#");-1!==r&&(a=t.substr(r),t=t.substr(0,r));var o=t.indexOf("?");return-1!==o&&(n=t.substr(o),t=t.substr(0,o)),{pathname:t,search:"?"===n?"":n,hash:"#"===a?"":a}}(e),o.state=t):(void 0===(o=(0,a.Z)({},e)).pathname&&(o.pathname=""),o.search?"?"!==o.search.charAt(0)&&(o.search="?"+o.search):o.search="",o.hash?"#"!==o.hash.charAt(0)&&(o.hash="#"+o.hash):o.hash="",void 0!==t&&void 0===o.state&&(o.state=t));try{o.pathname=decodeURI(o.pathname)}catch(i){throw i instanceof URIError?new URIError('Pathname "'+o.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):i}return n&&(o.key=n),r?o.pathname?"/"!==o.pathname.charAt(0)&&(o.pathname=l(o.pathname,r.pathname)):o.pathname=r.pathname:o.pathname||(o.pathname="/"),o}function m(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,a,r){if(null!=e){var o="function"==typeof e?e(t,n):e;"string"==typeof o?"function"==typeof a?a(o,r):r(!0):r(!1!==o)}else r(!0)},appendListener:function(e){var n=!0;function a(){n&&e.apply(void 0,arguments)}return t.push(a),function(){n=!1,t=t.filter((function(e){return e!==a}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),a=0;at?n.splice(t,n.length-t,r):n.push(r),d({action:a,location:r,index:t,entries:n})}}))},replace:function(e,t){var a="REPLACE",r=p(e,t,h(),z.location);u.confirmTransitionTo(r,a,n,(function(e){e&&(z.entries[z.index]=r,d({action:a,location:r}))}))},go:v,goBack:function(){v(-1)},goForward:function(){v(1)},canGo:function(e){var t=z.index+e;return t>=0&&t{"use strict";var a=n(9864),r={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},o={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},l={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},i={};function s(e){return a.isMemo(e)?l:i[e.$$typeof]||r}i[a.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},i[a.Memo]=l;var c=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,f=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,m=Object.prototype;e.exports=function e(t,n,a){if("string"!=typeof n){if(m){var r=p(n);r&&r!==m&&e(t,r,a)}var l=u(n);d&&(l=l.concat(d(n)));for(var i=s(t),h=s(n),g=0;g{"use strict";e.exports=function(e,t,n,a,r,o,l,i){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,a,r,o,l,i],u=0;(s=new Error(t.replace(/%s/g,(function(){return c[u++]})))).name="Invariant Violation"}throw s.framesToPop=1,s}}},5826:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},2497:(e,t,n)=>{"use strict";n.r(t)},2295:(e,t,n)=>{"use strict";n.r(t)},4865:function(e,t,n){var a,r;a=function(){var e,t,n={version:"0.2.0"},a=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
    '};function r(e,t,n){return en?n:e}function o(e){return 100*(-1+e)}function l(e,t,n){var r;return(r="translate3d"===a.positionUsing?{transform:"translate3d("+o(e)+"%,0,0)"}:"translate"===a.positionUsing?{transform:"translate("+o(e)+"%,0)"}:{"margin-left":o(e)+"%"}).transition="all "+t+"ms "+n,r}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(a[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=r(e,a.minimum,1),n.status=1===e?null:e;var o=n.render(!t),c=o.querySelector(a.barSelector),u=a.speed,d=a.easing;return o.offsetWidth,i((function(t){""===a.positionUsing&&(a.positionUsing=n.getPositioningCSS()),s(c,l(e,u,d)),1===e?(s(o,{transition:"none",opacity:1}),o.offsetWidth,setTimeout((function(){s(o,{transition:"all "+u+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),u)}),u)):setTimeout(t,u)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),a.trickleSpeed)};return a.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*r(Math.random()*t,.1,.95)),t=r(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*a.trickleRate)},e=0,t=0,n.promise=function(a){return a&&"resolved"!==a.state()?(0===t&&n.start(),e++,t++,a.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=a.template;var r,l=t.querySelector(a.barSelector),i=e?"-100":o(n.status||0),c=document.querySelector(a.parent);return s(l,{transition:"all 0 linear",transform:"translate3d("+i+"%,0,0)"}),a.showSpinner||(r=t.querySelector(a.spinnerSelector))&&p(r),c!=document.body&&u(c,"nprogress-custom-parent"),c.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(a.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&p(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var i=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),s=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function a(t){var n=document.body.style;if(t in n)return t;for(var a,r=e.length,o=t.charAt(0).toUpperCase()+t.slice(1);r--;)if((a=e[r]+o)in n)return a;return t}function r(e){return e=n(e),t[e]||(t[e]=a(e))}function o(e,t,n){t=r(t),e.style[t]=n}return function(e,t){var n,a,r=arguments;if(2==r.length)for(n in t)void 0!==(a=t[n])&&t.hasOwnProperty(n)&&o(e,n,a);else o(e,r[1],r[2])}}();function c(e,t){return("string"==typeof e?e:f(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=f(e),a=n+t;c(n,t)||(e.className=a.substring(1))}function d(e,t){var n,a=f(e);c(e,t)&&(n=a.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function f(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function p(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(r="function"==typeof a?a.call(t,n,t,e):a)||(e.exports=r)},7418:e=>{"use strict";var t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;function r(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var a={};return"abcdefghijklmnopqrst".split("").forEach((function(e){a[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},a)).join("")}catch(r){return!1}}()?Object.assign:function(e,o){for(var l,i,s=r(e),c=1;c{"use strict";n.d(t,{Z:()=>o});var a=function(){var e=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,n={},a={util:{encode:function e(t){return t instanceof r?new r(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/=d.reach);k+=E.value.length,E=E.next){var S=E.value;if(t.length>e.length)return;if(!(S instanceof r)){var _,C=1;if(y){if(!(_=o(w,k,e,b))||_.index>=e.length)break;var x=_.index,O=_.index+_[0].length,T=k;for(T+=E.value.length;x>=T;)T+=(E=E.next).value.length;if(k=T-=E.value.length,E.value instanceof r)continue;for(var P=E;P!==t.tail&&(Td.reach&&(d.reach=N);var R=E.prev;if(A&&(R=s(t,R,A),k+=A.length),c(t,R,C),E=s(t,R,new r(f,g?a.tokenize(I,g):I,v,I)),L&&s(t,E,L),C>1){var D={cause:f+","+m,reach:N};l(e,t,n,E.prev,k,D),d&&D.reach>d.reach&&(d.reach=D.reach)}}}}}}function i(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function s(e,t,n){var a=t.next,r={value:n,prev:t,next:a};return t.next=r,a.prev=r,e.length++,r}function c(e,t,n){for(var a=t.next,r=0;r"+o.content+""},a}(),r=a;a.default=a,r.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},r.languages.markup.tag.inside["attr-value"].inside.entity=r.languages.markup.entity,r.languages.markup.doctype.inside["internal-subset"].inside=r.languages.markup,r.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))})),Object.defineProperty(r.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:r.languages[t]},n.cdata=/^$/i;var a={"included-cdata":{pattern://i,inside:n}};a["language-"+t]={pattern:/[\s\S]+/,inside:r.languages[t]};var o={};o[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:a},r.languages.insertBefore("markup","cdata",o)}}),Object.defineProperty(r.languages.markup.tag,"addAttribute",{value:function(e,t){r.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:r.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),r.languages.html=r.languages.markup,r.languages.mathml=r.languages.markup,r.languages.svg=r.languages.markup,r.languages.xml=r.languages.extend("markup",{}),r.languages.ssml=r.languages.xml,r.languages.atom=r.languages.xml,r.languages.rss=r.languages.xml,function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",n={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},a={bash:n,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:a},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:n}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:a},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:a.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:a.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},n.inside=e.languages.bash;for(var r=["comment","function-name","for-or-select","assign-left","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=a.variable[1].inside,l=0;l]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},r.languages.c=r.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),r.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),r.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},r.languages.c.string],char:r.languages.c.char,comment:r.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:r.languages.c}}}}),r.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete r.languages.c.boolean,function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,n=/\b(?!)\w+(?:\s*\.\s*\w+)*\b/.source.replace(//g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp(/(\b(?:class|concept|enum|struct|typename)\s+)(?!)\w+/.source.replace(//g,(function(){return t.source}))),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),e.languages.insertBefore("cpp","string",{module:{pattern:RegExp(/(\b(?:import|module)\s+)/.source+"(?:"+/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|<[^<>\r\n]*>/.source+"|"+/(?:\s*:\s*)?|:\s*/.source.replace(//g,(function(){return n}))+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:e.languages.cpp}}}}),e.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"])}(r),function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-](?:[^;{\s]|\s+(?![\s{]))*(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),n.tag.addAttribute("style","css"))}(r),function(e){var t,n=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css.selector={pattern:e.languages.css.selector.pattern,lookbehind:!0,inside:t={"pseudo-element":/:(?:after|before|first-letter|first-line|selection)|::[-\w]+/,"pseudo-class":/:[-\w]+/,class:/\.[-\w]+/,id:/#[-\w]+/,attribute:{pattern:RegExp("\\[(?:[^[\\]\"']|"+n.source+")*\\]"),greedy:!0,inside:{punctuation:/^\[|\]$/,"case-sensitivity":{pattern:/(\s)[si]$/i,lookbehind:!0,alias:"keyword"},namespace:{pattern:/^(\s*)(?:(?!\s)[-*\w\xA0-\uFFFF])*\|(?!=)/,lookbehind:!0,inside:{punctuation:/\|$/}},"attr-name":{pattern:/^(\s*)(?:(?!\s)[-\w\xA0-\uFFFF])+/,lookbehind:!0},"attr-value":[n,{pattern:/(=\s*)(?:(?!\s)[-\w\xA0-\uFFFF])+(?=\s*$)/,lookbehind:!0}],operator:/[|~*^$]?=/}},"n-th":[{pattern:/(\(\s*)[+-]?\d*[\dn](?:\s*[+-]\s*\d+)?(?=\s*\))/,lookbehind:!0,inside:{number:/[\dn]+/,operator:/[+-]/}},{pattern:/(\(\s*)(?:even|odd)(?=\s*\))/i,lookbehind:!0}],combinator:/>|\+|~|\|\|/,punctuation:/[(),]/}},e.languages.css.atrule.inside["selector-function-argument"].inside=t,e.languages.insertBefore("css","property",{variable:{pattern:/(^|[^-\w\xA0-\uFFFF])--(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*/i,lookbehind:!0}});var a={pattern:/(\b\d+)(?:%|[a-z]+(?![\w-]))/,lookbehind:!0},r={pattern:/(^|[^\w.-])-?(?:\d+(?:\.\d+)?|\.\d+)/,lookbehind:!0};e.languages.insertBefore("css","function",{operator:{pattern:/(\s)[+\-*\/](?=\s)/,lookbehind:!0},hexcode:{pattern:/\B#[\da-f]{3,8}\b/i,alias:"color"},color:[{pattern:/(^|[^\w-])(?:AliceBlue|AntiqueWhite|Aqua|Aquamarine|Azure|Beige|Bisque|Black|BlanchedAlmond|Blue|BlueViolet|Brown|BurlyWood|CadetBlue|Chartreuse|Chocolate|Coral|CornflowerBlue|Cornsilk|Crimson|Cyan|DarkBlue|DarkCyan|DarkGoldenRod|DarkGr[ae]y|DarkGreen|DarkKhaki|DarkMagenta|DarkOliveGreen|DarkOrange|DarkOrchid|DarkRed|DarkSalmon|DarkSeaGreen|DarkSlateBlue|DarkSlateGr[ae]y|DarkTurquoise|DarkViolet|DeepPink|DeepSkyBlue|DimGr[ae]y|DodgerBlue|FireBrick|FloralWhite|ForestGreen|Fuchsia|Gainsboro|GhostWhite|Gold|GoldenRod|Gr[ae]y|Green|GreenYellow|HoneyDew|HotPink|IndianRed|Indigo|Ivory|Khaki|Lavender|LavenderBlush|LawnGreen|LemonChiffon|LightBlue|LightCoral|LightCyan|LightGoldenRodYellow|LightGr[ae]y|LightGreen|LightPink|LightSalmon|LightSeaGreen|LightSkyBlue|LightSlateGr[ae]y|LightSteelBlue|LightYellow|Lime|LimeGreen|Linen|Magenta|Maroon|MediumAquaMarine|MediumBlue|MediumOrchid|MediumPurple|MediumSeaGreen|MediumSlateBlue|MediumSpringGreen|MediumTurquoise|MediumVioletRed|MidnightBlue|MintCream|MistyRose|Moccasin|NavajoWhite|Navy|OldLace|Olive|OliveDrab|Orange|OrangeRed|Orchid|PaleGoldenRod|PaleGreen|PaleTurquoise|PaleVioletRed|PapayaWhip|PeachPuff|Peru|Pink|Plum|PowderBlue|Purple|Red|RosyBrown|RoyalBlue|SaddleBrown|Salmon|SandyBrown|SeaGreen|SeaShell|Sienna|Silver|SkyBlue|SlateBlue|SlateGr[ae]y|Snow|SpringGreen|SteelBlue|Tan|Teal|Thistle|Tomato|Transparent|Turquoise|Violet|Wheat|White|WhiteSmoke|Yellow|YellowGreen)(?![\w-])/i,lookbehind:!0},{pattern:/\b(?:hsl|rgb)\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*\)\B|\b(?:hsl|rgb)a\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*,\s*(?:0|0?\.\d+|1)\s*\)\B/i,inside:{unit:a,number:r,function:/[\w-]+(?=\()/,punctuation:/[(),]/}}],entity:/\\[\da-f]{1,8}/i,unit:a,number:r})}(r),r.languages.javascript=r.languages.extend("clike",{"class-name":[r.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),r.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,r.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:r.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:r.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:r.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:r.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:r.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),r.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:r.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),r.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),r.languages.markup&&(r.languages.markup.tag.addInlined("script","javascript"),r.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),r.languages.js=r.languages.javascript,function(e){var t=/#(?!\{).+/,n={pattern:/#\{[^}]+\}/,alias:"variable"};e.languages.coffeescript=e.languages.extend("javascript",{comment:t,string:[{pattern:/'(?:\\[\s\S]|[^\\'])*'/,greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,greedy:!0,inside:{interpolation:n}}],keyword:/\b(?:and|break|by|catch|class|continue|debugger|delete|do|each|else|extend|extends|false|finally|for|if|in|instanceof|is|isnt|let|loop|namespace|new|no|not|null|of|off|on|or|own|return|super|switch|then|this|throw|true|try|typeof|undefined|unless|until|when|while|window|with|yes|yield)\b/,"class-member":{pattern:/@(?!\d)\w+/,alias:"variable"}}),e.languages.insertBefore("coffeescript","comment",{"multiline-comment":{pattern:/###[\s\S]+?###/,alias:"comment"},"block-regex":{pattern:/\/{3}[\s\S]*?\/{3}/,alias:"regex",inside:{comment:t,interpolation:n}}}),e.languages.insertBefore("coffeescript","string",{"inline-javascript":{pattern:/`(?:\\[\s\S]|[^\\`])*`/,inside:{delimiter:{pattern:/^`|`$/,alias:"punctuation"},script:{pattern:/[\s\S]+/,alias:"language-javascript",inside:e.languages.javascript}}},"multiline-string":[{pattern:/'''[\s\S]*?'''/,greedy:!0,alias:"string"},{pattern:/"""[\s\S]*?"""/,greedy:!0,alias:"string",inside:{interpolation:n}}]}),e.languages.insertBefore("coffeescript","keyword",{property:/(?!\d)\w+(?=\s*:(?!:))/}),delete e.languages.coffeescript["template-string"],e.languages.coffee=e.languages.coffeescript}(r),function(e){var t=/[*&][^\s[\]{},]+/,n=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,a="(?:"+n.source+"(?:[ \t]+"+t.source+")?|"+t.source+"(?:[ \t]+"+n.source+")?)",r=/(?:[^\s\x00-\x08\x0e-\x1f!"#%&'*,\-:>?@[\]`{|}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*/.source.replace(//g,(function(){return/[^\s\x00-\x08\x0e-\x1f,[\]{}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]/.source})),o=/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/.source;function l(e,t){t=(t||"").replace(/m/g,"")+"m";var n=/([:\-,[{]\s*(?:\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\]|\}|(?:[\r\n]\s*)?#))/.source.replace(/<>/g,(function(){return a})).replace(/<>/g,(function(){return e}));return RegExp(n,t)}e.languages.yaml={scalar:{pattern:RegExp(/([\-:]\s*(?:\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\S[^\r\n]*(?:\2[^\r\n]+)*)/.source.replace(/<>/g,(function(){return a}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp(/((?:^|[:\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\s*:\s)/.source.replace(/<>/g,(function(){return a})).replace(/<>/g,(function(){return"(?:"+r+"|"+o+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:l(/\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?(?:[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?))?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?/.source),lookbehind:!0,alias:"number"},boolean:{pattern:l(/false|true/.source,"i"),lookbehind:!0,alias:"important"},null:{pattern:l(/null|~/.source,"i"),lookbehind:!0,alias:"important"},string:{pattern:l(o),lookbehind:!0,greedy:!0},number:{pattern:l(/[+-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\.inf|\.nan)/.source,"i"),lookbehind:!0},tag:n,important:t,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(r),function(e){var t=/(?:\\.|[^\\\n\r]|(?:\n|\r\n?)(?![\r\n]))/.source;function n(e){return e=e.replace(//g,(function(){return t})),RegExp(/((?:^|[^\\])(?:\\{2})*)/.source+"(?:"+e+")")}var a=/(?:\\.|``(?:[^`\r\n]|`(?!`))+``|`[^`\r\n]+`|[^\\|\r\n`])+/.source,r=/\|?__(?:\|__)+\|?(?:(?:\n|\r\n?)|(?![\s\S]))/.source.replace(/__/g,(function(){return a})),o=/\|?[ \t]*:?-{3,}:?[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)+\|?(?:\n|\r\n?)/.source;e.languages.markdown=e.languages.extend("markup",{}),e.languages.insertBefore("markdown","prolog",{"front-matter-block":{pattern:/(^(?:\s*[\r\n])?)---(?!.)[\s\S]*?[\r\n]---(?!.)/,lookbehind:!0,greedy:!0,inside:{punctuation:/^---|---$/,"front-matter":{pattern:/\S+(?:\s+\S+)*/,alias:["yaml","language-yaml"],inside:e.languages.yaml}}},blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},table:{pattern:RegExp("^"+r+o+"(?:"+r+")*","m"),inside:{"table-data-rows":{pattern:RegExp("^("+r+o+")(?:"+r+")*$"),lookbehind:!0,inside:{"table-data":{pattern:RegExp(a),inside:e.languages.markdown},punctuation:/\|/}},"table-line":{pattern:RegExp("^("+r+")"+o+"$"),lookbehind:!0,inside:{punctuation:/\||:?-{3,}:?/}},"table-header-row":{pattern:RegExp("^"+r+"$"),inside:{"table-header":{pattern:RegExp(a),alias:"important",inside:e.languages.markdown},punctuation:/\|/}}}},code:[{pattern:/((?:^|\n)[ \t]*\n|(?:^|\r\n?)[ \t]*\r\n?)(?: {4}|\t).+(?:(?:\n|\r\n?)(?: {4}|\t).+)*/,lookbehind:!0,alias:"keyword"},{pattern:/^```[\s\S]*?^```$/m,greedy:!0,inside:{"code-block":{pattern:/^(```.*(?:\n|\r\n?))[\s\S]+?(?=(?:\n|\r\n?)^```$)/m,lookbehind:!0},"code-language":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\S.*(?:\n|\r\n?)(?:==+|--+)(?=[ \t]*$)/m,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:n(/\b__(?:(?!_)|_(?:(?!_))+_)+__\b|\*\*(?:(?!\*)|\*(?:(?!\*))+\*)+\*\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\s\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\*\*|__/}},italic:{pattern:n(/\b_(?:(?!_)|__(?:(?!_))+__)+_\b|\*(?:(?!\*)|\*\*(?:(?!\*))+\*\*)+\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\s\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:n(/(~~?)(?:(?!~))+\2/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\s\S]+(?=\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},"code-snippet":{pattern:/(^|[^\\`])(?:``[^`\r\n]+(?:`[^`\r\n]+)*``(?!`)|`[^`\r\n]+`(?!`))/,lookbehind:!0,greedy:!0,alias:["code","keyword"]},url:{pattern:n(/!?\[(?:(?!\]))+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)|[ \t]?\[(?:(?!\]))+\])/.source),lookbehind:!0,greedy:!0,inside:{operator:/^!/,content:{pattern:/(^\[)[^\]]+(?=\])/,lookbehind:!0,inside:{}},variable:{pattern:/(^\][ \t]?\[)[^\]]+(?=\]$)/,lookbehind:!0},url:{pattern:/(^\]\()[^\s)]+/,lookbehind:!0},string:{pattern:/(^[ \t]+)"(?:\\.|[^"\\])*"(?=\)$)/,lookbehind:!0}}}}),["url","bold","italic","strike"].forEach((function(t){["url","bold","italic","strike","code-snippet"].forEach((function(n){t!==n&&(e.languages.markdown[t].inside.content.inside[n]=e.languages.markdown[n])}))})),e.hooks.add("after-tokenize",(function(e){"markdown"!==e.language&&"md"!==e.language||function e(t){if(t&&"string"!=typeof t)for(var n=0,a=t.length;n",quot:'"'},s=String.fromCodePoint||String.fromCharCode;e.languages.md=e.languages.markdown}(r),r.languages.graphql={comment:/#.*/,description:{pattern:/(?:"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*")(?=\s*[a-z_])/i,greedy:!0,alias:"string",inside:{"language-markdown":{pattern:/(^"(?:"")?)(?!\1)[\s\S]+(?=\1$)/,lookbehind:!0,inside:r.languages.markdown}}},string:{pattern:/"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*"/,greedy:!0},number:/(?:\B-|\b)\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,boolean:/\b(?:false|true)\b/,variable:/\$[a-z_]\w*/i,directive:{pattern:/@[a-z_]\w*/i,alias:"function"},"attr-name":{pattern:/\b[a-z_]\w*(?=\s*(?:\((?:[^()"]|"(?:\\.|[^\\"\r\n])*")*\))?:)/i,greedy:!0},"atom-input":{pattern:/\b[A-Z]\w*Input\b/,alias:"class-name"},scalar:/\b(?:Boolean|Float|ID|Int|String)\b/,constant:/\b[A-Z][A-Z_\d]*\b/,"class-name":{pattern:/(\b(?:enum|implements|interface|on|scalar|type|union)\s+|&\s*|:\s*|\[)[A-Z_]\w*/,lookbehind:!0},fragment:{pattern:/(\bfragment\s+|\.{3}\s*(?!on\b))[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-mutation":{pattern:/(\bmutation\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-query":{pattern:/(\bquery\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},keyword:/\b(?:directive|enum|extend|fragment|implements|input|interface|mutation|on|query|repeatable|scalar|schema|subscription|type|union)\b/,operator:/[!=|&]|\.{3}/,"property-query":/\w+(?=\s*\()/,object:/\w+(?=\s*\{)/,punctuation:/[!(){}\[\]:=,]/,property:/\w+/},r.hooks.add("after-tokenize",(function(e){if("graphql"===e.language)for(var t=e.tokens.filter((function(e){return"string"!=typeof e&&"comment"!==e.type&&"scalar"!==e.type})),n=0;n0)){var i=f(/^\{$/,/^\}$/);if(-1===i)continue;for(var s=n;s=0&&p(c,"variable-input")}}}}function u(e){return t[n+e]}function d(e,t){t=t||0;for(var n=0;n?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/},function(e){var t=e.languages.javascript["template-string"],n=t.pattern.source,a=t.inside.interpolation,r=a.inside["interpolation-punctuation"],o=a.pattern.source;function l(t,a){if(e.languages[t])return{pattern:RegExp("((?:"+a+")\\s*)"+n),lookbehind:!0,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},"embedded-code":{pattern:/[\s\S]+/,alias:t}}}}function i(e,t){return"___"+t.toUpperCase()+"_"+e+"___"}function s(t,n,a){var r={code:t,grammar:n,language:a};return e.hooks.run("before-tokenize",r),r.tokens=e.tokenize(r.code,r.grammar),e.hooks.run("after-tokenize",r),r.tokens}function c(t){var n={};n["interpolation-punctuation"]=r;var o=e.tokenize(t,n);if(3===o.length){var l=[1,1];l.push.apply(l,s(o[1],e.languages.javascript,"javascript")),o.splice.apply(o,l)}return new e.Token("interpolation",o,a.alias,t)}function u(t,n,a){var r=e.tokenize(t,{interpolation:{pattern:RegExp(o),lookbehind:!0}}),l=0,u={},d=s(r.map((function(e){if("string"==typeof e)return e;for(var n,r=e.content;-1!==t.indexOf(n=i(l++,a)););return u[n]=r,n})).join(""),n,a),f=Object.keys(u);return l=0,function e(t){for(var n=0;n=f.length)return;var a=t[n];if("string"==typeof a||"string"==typeof a.content){var r=f[l],o="string"==typeof a?a:a.content,i=o.indexOf(r);if(-1!==i){++l;var s=o.substring(0,i),d=c(u[r]),p=o.substring(i+r.length),m=[];if(s&&m.push(s),m.push(d),p){var h=[p];e(h),m.push.apply(m,h)}"string"==typeof a?(t.splice.apply(t,[n,1].concat(m)),n+=m.length-1):a.content=m}}else{var g=a.content;Array.isArray(g)?e(g):e([g])}}}(d),new e.Token(a,d,"language-"+a,t)}e.languages.javascript["template-string"]=[l("css",/\b(?:styled(?:\([^)]*\))?(?:\s*\.\s*\w+(?:\([^)]*\))*)*|css(?:\s*\.\s*(?:global|resolve))?|createGlobalStyle|keyframes)/.source),l("html",/\bhtml|\.\s*(?:inner|outer)HTML\s*\+?=/.source),l("svg",/\bsvg/.source),l("markdown",/\b(?:markdown|md)/.source),l("graphql",/\b(?:gql|graphql(?:\s*\.\s*experimental)?)/.source),l("sql",/\bsql/.source),t].filter(Boolean);var d={javascript:!0,js:!0,typescript:!0,ts:!0,jsx:!0,tsx:!0};function f(e){return"string"==typeof e?e:Array.isArray(e)?e.map(f).join(""):f(e.content)}e.hooks.add("after-tokenize",(function(t){t.language in d&&function t(n){for(var a=0,r=n.length;a]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var t=e.languages.extend("typescript",{});delete t["class-name"],e.languages.typescript["class-name"].inside=t,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:t}}}}),e.languages.ts=e.languages.typescript}(r),function(e){function t(e,t){return RegExp(e.replace(//g,(function(){return/(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/.source})),t)}e.languages.insertBefore("javascript","function-variable",{"method-variable":{pattern:RegExp("(\\.\\s*)"+e.languages.javascript["function-variable"].pattern.source),lookbehind:!0,alias:["function-variable","method","function","property-access"]}}),e.languages.insertBefore("javascript","function",{method:{pattern:RegExp("(\\.\\s*)"+e.languages.javascript.function.source),lookbehind:!0,alias:["function","property-access"]}}),e.languages.insertBefore("javascript","constant",{"known-class-name":[{pattern:/\b(?:(?:Float(?:32|64)|(?:Int|Uint)(?:8|16|32)|Uint8Clamped)?Array|ArrayBuffer|BigInt|Boolean|DataView|Date|Error|Function|Intl|JSON|(?:Weak)?(?:Map|Set)|Math|Number|Object|Promise|Proxy|Reflect|RegExp|String|Symbol|WebAssembly)\b/,alias:"class-name"},{pattern:/\b(?:[A-Z]\w*)Error\b/,alias:"class-name"}]}),e.languages.insertBefore("javascript","keyword",{imports:{pattern:t(/(\bimport\b\s*)(?:(?:\s*,\s*(?:\*\s*as\s+|\{[^{}]*\}))?|\*\s*as\s+|\{[^{}]*\})(?=\s*\bfrom\b)/.source),lookbehind:!0,inside:e.languages.javascript},exports:{pattern:t(/(\bexport\b\s*)(?:\*(?:\s*as\s+)?(?=\s*\bfrom\b)|\{[^{}]*\})/.source),lookbehind:!0,inside:e.languages.javascript}}),e.languages.javascript.keyword.unshift({pattern:/\b(?:as|default|export|from|import)\b/,alias:"module"},{pattern:/\b(?:await|break|catch|continue|do|else|finally|for|if|return|switch|throw|try|while|yield)\b/,alias:"control-flow"},{pattern:/\bnull\b/,alias:["null","nil"]},{pattern:/\bundefined\b/,alias:"nil"}),e.languages.insertBefore("javascript","operator",{spread:{pattern:/\.{3}/,alias:"operator"},arrow:{pattern:/=>/,alias:"operator"}}),e.languages.insertBefore("javascript","punctuation",{"property-access":{pattern:t(/(\.\s*)#?/.source),lookbehind:!0},"maybe-class-name":{pattern:/(^|[^$\w\xA0-\uFFFF])[A-Z][$\w\xA0-\uFFFF]+/,lookbehind:!0},dom:{pattern:/\b(?:document|(?:local|session)Storage|location|navigator|performance|window)\b/,alias:"variable"},console:{pattern:/\bconsole(?=\s*\.)/,alias:"class-name"}});for(var n=["function","function-variable","method","method-variable","property-access"],a=0;a*\.{3}(?:[^{}]|)*\})/.source;function o(e,t){return e=e.replace(//g,(function(){return n})).replace(//g,(function(){return a})).replace(//g,(function(){return r})),RegExp(e,t)}r=o(r).source,e.languages.jsx=e.languages.extend("markup",t),e.languages.jsx.tag.pattern=o(/<\/?(?:[\w.:-]+(?:+(?:[\w.:$-]+(?:=(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s{'"/>=]+|))?|))**\/?)?>/.source),e.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/,e.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,e.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,e.languages.jsx.tag.inside.comment=t.comment,e.languages.insertBefore("inside","attr-name",{spread:{pattern:o(//.source),inside:e.languages.jsx}},e.languages.jsx.tag),e.languages.insertBefore("inside","special-attr",{script:{pattern:o(/=/.source),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:e.languages.jsx}}},e.languages.jsx.tag);var l=function(e){return e?"string"==typeof e?e:"string"==typeof e.content?e.content:e.content.map(l).join(""):""},i=function(t){for(var n=[],a=0;a0&&n[n.length-1].tagName===l(r.content[0].content[1])&&n.pop():"/>"===r.content[r.content.length-1].content||n.push({tagName:l(r.content[0].content[1]),openedBraces:0}):n.length>0&&"punctuation"===r.type&&"{"===r.content?n[n.length-1].openedBraces++:n.length>0&&n[n.length-1].openedBraces>0&&"punctuation"===r.type&&"}"===r.content?n[n.length-1].openedBraces--:o=!0),(o||"string"==typeof r)&&n.length>0&&0===n[n.length-1].openedBraces){var s=l(r);a0&&("string"==typeof t[a-1]||"plain-text"===t[a-1].type)&&(s=l(t[a-1])+s,t.splice(a-1,1),a--),t[a]=new e.Token("plain-text",s,null,s)}r.content&&"string"!=typeof r.content&&i(r.content)}};e.hooks.add("after-tokenize",(function(e){"jsx"!==e.language&&"tsx"!==e.language||i(e.tokens)}))}(r),function(e){e.languages.diff={coord:[/^(?:\*{3}|-{3}|\+{3}).*$/m,/^@@.*@@$/m,/^\d.*$/m]};var t={"deleted-sign":"-","deleted-arrow":"<","inserted-sign":"+","inserted-arrow":">",unchanged:" ",diff:"!"};Object.keys(t).forEach((function(n){var a=t[n],r=[];/^\w+$/.test(n)||r.push(/\w+/.exec(n)[0]),"diff"===n&&r.push("bold"),e.languages.diff[n]={pattern:RegExp("^(?:["+a+"].*(?:\r\n?|\n|(?![\\s\\S])))+","m"),alias:r,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(n)[0]}}}})),Object.defineProperty(e.languages.diff,"PREFIXES",{value:t})}(r),r.languages.git={comment:/^#.*/m,deleted:/^[-\u2013].*/m,inserted:/^\+.*/m,string:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,command:{pattern:/^.*\$ git .*$/m,inside:{parameter:/\s--?\w+/}},coord:/^@@.*@@$/m,"commit-sha1":/^commit \w{40}$/m},r.languages.go=r.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),r.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete r.languages.go["class-name"],function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,a,r,o){if(n.language===a){var l=n.tokenStack=[];n.code=n.code.replace(r,(function(e){if("function"==typeof o&&!o(e))return e;for(var r,i=l.length;-1!==n.code.indexOf(r=t(a,i));)++i;return l[i]=e,r})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,a){if(n.language===a&&n.tokenStack){n.grammar=e.languages[a];var r=0,o=Object.keys(n.tokenStack);!function l(i){for(var s=0;s=o.length);s++){var c=i[s];if("string"==typeof c||c.content&&"string"==typeof c.content){var u=o[r],d=n.tokenStack[u],f="string"==typeof c?c:c.content,p=t(a,u),m=f.indexOf(p);if(m>-1){++r;var h=f.substring(0,m),g=new e.Token(a,e.tokenize(d,n.grammar),"language-"+a,d),b=f.substring(m+p.length),y=[];h&&y.push.apply(y,l([h])),y.push(g),b&&y.push.apply(y,l([b])),"string"==typeof c?i.splice.apply(i,[s,1].concat(y)):c.content=y}}else c.content&&l(c.content)}return i}(n.tokens)}}}})}(r),function(e){e.languages.handlebars={comment:/\{\{![\s\S]*?\}\}/,delimiter:{pattern:/^\{\{\{?|\}\}\}?$/,alias:"punctuation"},string:/(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee][+-]?\d+)?/,boolean:/\b(?:false|true)\b/,block:{pattern:/^(\s*(?:~\s*)?)[#\/]\S+?(?=\s*(?:~\s*)?$|\s)/,lookbehind:!0,alias:"keyword"},brackets:{pattern:/\[[^\]]+\]/,inside:{punctuation:/\[|\]/,variable:/[\s\S]+/}},punctuation:/[!"#%&':()*+,.\/;<=>@\[\\\]^`{|}~]/,variable:/[^!"#%&'()*+,\/;<=>@\[\\\]^`{|}~\s]+/},e.hooks.add("before-tokenize",(function(t){e.languages["markup-templating"].buildPlaceholders(t,"handlebars",/\{\{\{[\s\S]+?\}\}\}|\{\{[\s\S]+?\}\}/g)})),e.hooks.add("after-tokenize",(function(t){e.languages["markup-templating"].tokenizePlaceholders(t,"handlebars")})),e.languages.hbs=e.languages.handlebars}(r),r.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},r.languages.webmanifest=r.languages.json,r.languages.less=r.languages.extend("css",{comment:[/\/\*[\s\S]*?\*\//,{pattern:/(^|[^\\])\/\/.*/,lookbehind:!0}],atrule:{pattern:/@[\w-](?:\((?:[^(){}]|\([^(){}]*\))*\)|[^(){};\s]|\s+(?!\s))*?(?=\s*\{)/,inside:{punctuation:/[:()]/}},selector:{pattern:/(?:@\{[\w-]+\}|[^{};\s@])(?:@\{[\w-]+\}|\((?:[^(){}]|\([^(){}]*\))*\)|[^(){};@\s]|\s+(?!\s))*?(?=\s*\{)/,inside:{variable:/@+[\w-]+/}},property:/(?:@\{[\w-]+\}|[\w-])+(?:\+_?)?(?=\s*:)/,operator:/[+\-*\/]/}),r.languages.insertBefore("less","property",{variable:[{pattern:/@[\w-]+\s*:/,inside:{punctuation:/:/}},/@@?[\w-]+/],"mixin-usage":{pattern:/([{;]\s*)[.#](?!\d)[\w-].*?(?=[(;])/,lookbehind:!0,alias:"function"}}),r.languages.makefile={comment:{pattern:/(^|[^\\])#(?:\\(?:\r\n|[\s\S])|[^\\\r\n])*/,lookbehind:!0},string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"builtin-target":{pattern:/\.[A-Z][^:#=\s]+(?=\s*:(?!=))/,alias:"builtin"},target:{pattern:/^(?:[^:=\s]|[ \t]+(?![\s:]))+(?=\s*:(?!=))/m,alias:"symbol",inside:{variable:/\$+(?:(?!\$)[^(){}:#=\s]+|(?=[({]))/}},variable:/\$+(?:(?!\$)[^(){}:#=\s]+|\([@*%<^+?][DF]\)|(?=[({]))/,keyword:/-include\b|\b(?:define|else|endef|endif|export|ifn?def|ifn?eq|include|override|private|sinclude|undefine|unexport|vpath)\b/,function:{pattern:/(\()(?:abspath|addsuffix|and|basename|call|dir|error|eval|file|filter(?:-out)?|findstring|firstword|flavor|foreach|guile|if|info|join|lastword|load|notdir|or|origin|patsubst|realpath|shell|sort|strip|subst|suffix|value|warning|wildcard|word(?:list|s)?)(?=[ \t])/,lookbehind:!0},operator:/(?:::|[?:+!])?=|[|@]/,punctuation:/[:;(){}]/},r.languages.objectivec=r.languages.extend("c",{string:{pattern:/@?"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},keyword:/\b(?:asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|in|inline|int|long|register|return|self|short|signed|sizeof|static|struct|super|switch|typedef|typeof|union|unsigned|void|volatile|while)\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\b/,operator:/-[->]?|\+\+?|!=?|<>?=?|==?|&&?|\|\|?|[~^%?*\/@]/}),delete r.languages.objectivec["class-name"],r.languages.objc=r.languages.objectivec,r.languages.ocaml={comment:{pattern:/\(\*[\s\S]*?\*\)/,greedy:!0},char:{pattern:/'(?:[^\\\r\n']|\\(?:.|[ox]?[0-9a-f]{1,3}))'/i,greedy:!0},string:[{pattern:/"(?:\\(?:[\s\S]|\r\n)|[^\\\r\n"])*"/,greedy:!0},{pattern:/\{([a-z_]*)\|[\s\S]*?\|\1\}/,greedy:!0}],number:[/\b(?:0b[01][01_]*|0o[0-7][0-7_]*)\b/i,/\b0x[a-f0-9][a-f0-9_]*(?:\.[a-f0-9_]*)?(?:p[+-]?\d[\d_]*)?(?!\w)/i,/\b\d[\d_]*(?:\.[\d_]*)?(?:e[+-]?\d[\d_]*)?(?!\w)/i],directive:{pattern:/\B#\w+/,alias:"property"},label:{pattern:/\B~\w+/,alias:"property"},"type-variable":{pattern:/\B'\w+/,alias:"function"},variant:{pattern:/`\w+/,alias:"symbol"},keyword:/\b(?:as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|match|method|module|mutable|new|nonrec|object|of|open|private|rec|sig|struct|then|to|try|type|val|value|virtual|when|where|while|with)\b/,boolean:/\b(?:false|true)\b/,"operator-like-punctuation":{pattern:/\[[<>|]|[>|]\]|\{<|>\}/,alias:"punctuation"},operator:/\.[.~]|:[=>]|[=<>@^|&+\-*\/$%!?~][!$%&*+\-.\/:<=>?@^|~]*|\b(?:and|asr|land|lor|lsl|lsr|lxor|mod|or)\b/,punctuation:/;;|::|[(){}\[\].,:;#]|\b_\b/},r.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},r.languages.python["string-interpolation"].inside.interpolation.inside.rest=r.languages.python,r.languages.py=r.languages.python,r.languages.reason=r.languages.extend("clike",{string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^\\\r\n"])*"/,greedy:!0},"class-name":/\b[A-Z]\w*/,keyword:/\b(?:and|as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|method|module|mutable|new|nonrec|object|of|open|or|private|rec|sig|struct|switch|then|to|try|type|val|virtual|when|while|with)\b/,operator:/\.{3}|:[:=]|\|>|->|=(?:==?|>)?|<=?|>=?|[|^?'#!~`]|[+\-*\/]\.?|\b(?:asr|land|lor|lsl|lsr|lxor|mod)\b/}),r.languages.insertBefore("reason","class-name",{char:{pattern:/'(?:\\x[\da-f]{2}|\\o[0-3][0-7][0-7]|\\\d{3}|\\.|[^'\\\r\n])'/,greedy:!0},constructor:/\b[A-Z]\w*\b(?!\s*\.)/,label:{pattern:/\b[a-z]\w*(?=::)/,alias:"symbol"}}),delete r.languages.reason.function,function(e){e.languages.sass=e.languages.extend("css",{comment:{pattern:/^([ \t]*)\/[\/*].*(?:(?:\r?\n|\r)\1[ \t].+)*/m,lookbehind:!0,greedy:!0}}),e.languages.insertBefore("sass","atrule",{"atrule-line":{pattern:/^(?:[ \t]*)[@+=].+/m,greedy:!0,inside:{atrule:/(?:@[\w-]+|[+=])/}}}),delete e.languages.sass.atrule;var t=/\$[-\w]+|#\{\$[-\w]+\}/,n=[/[+*\/%]|[=!]=|<=?|>=?|\b(?:and|not|or)\b/,{pattern:/(\s)-(?=\s)/,lookbehind:!0}];e.languages.insertBefore("sass","property",{"variable-line":{pattern:/^[ \t]*\$.+/m,greedy:!0,inside:{punctuation:/:/,variable:t,operator:n}},"property-line":{pattern:/^[ \t]*(?:[^:\s]+ *:.*|:[^:\s].*)/m,greedy:!0,inside:{property:[/[^:\s]+(?=\s*:)/,{pattern:/(:)[^:\s]+/,lookbehind:!0}],punctuation:/:/,variable:t,operator:n,important:e.languages.sass.important}}}),delete e.languages.sass.property,delete e.languages.sass.important,e.languages.insertBefore("sass","punctuation",{selector:{pattern:/^([ \t]*)\S(?:,[^,\r\n]+|[^,\r\n]*)(?:,[^,\r\n]+)*(?:,(?:\r?\n|\r)\1[ \t]+\S(?:,[^,\r\n]+|[^,\r\n]*)(?:,[^,\r\n]+)*)*/m,lookbehind:!0,greedy:!0}})}(r),r.languages.scss=r.languages.extend("css",{comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0},atrule:{pattern:/@[\w-](?:\([^()]+\)|[^()\s]|\s+(?!\s))*?(?=\s+[{;])/,inside:{rule:/@[\w-]+/}},url:/(?:[-a-z]+-)?url(?=\()/i,selector:{pattern:/(?=\S)[^@;{}()]?(?:[^@;{}()\s]|\s+(?!\s)|#\{\$[-\w]+\})+(?=\s*\{(?:\}|\s|[^}][^:{}]*[:{][^}]))/,inside:{parent:{pattern:/&/,alias:"important"},placeholder:/%[-\w]+/,variable:/\$[-\w]+|#\{\$[-\w]+\}/}},property:{pattern:/(?:[-\w]|\$[-\w]|#\{\$[-\w]+\})+(?=\s*:)/,inside:{variable:/\$[-\w]+|#\{\$[-\w]+\}/}}}),r.languages.insertBefore("scss","atrule",{keyword:[/@(?:content|debug|each|else(?: if)?|extend|for|forward|function|if|import|include|mixin|return|use|warn|while)\b/i,{pattern:/( )(?:from|through)(?= )/,lookbehind:!0}]}),r.languages.insertBefore("scss","important",{variable:/\$[-\w]+|#\{\$[-\w]+\}/}),r.languages.insertBefore("scss","function",{"module-modifier":{pattern:/\b(?:as|hide|show|with)\b/i,alias:"keyword"},placeholder:{pattern:/%[-\w]+/,alias:"selector"},statement:{pattern:/\B!(?:default|optional)\b/i,alias:"keyword"},boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"},operator:{pattern:/(\s)(?:[-+*\/%]|[=!]=|<=?|>=?|and|not|or)(?=\s)/,lookbehind:!0}}),r.languages.scss.atrule.inside.rest=r.languages.scss,function(e){var t={pattern:/(\b\d+)(?:%|[a-z]+)/,lookbehind:!0},n={pattern:/(^|[^\w.-])-?(?:\d+(?:\.\d+)?|\.\d+)/,lookbehind:!0},a={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0},url:{pattern:/\burl\((["']?).*?\1\)/i,greedy:!0},string:{pattern:/("|')(?:(?!\1)[^\\\r\n]|\\(?:\r\n|[\s\S]))*\1/,greedy:!0},interpolation:null,func:null,important:/\B!(?:important|optional)\b/i,keyword:{pattern:/(^|\s+)(?:(?:else|for|if|return|unless)(?=\s|$)|@[\w-]+)/,lookbehind:!0},hexcode:/#[\da-f]{3,6}/i,color:[/\b(?:AliceBlue|AntiqueWhite|Aqua|Aquamarine|Azure|Beige|Bisque|Black|BlanchedAlmond|Blue|BlueViolet|Brown|BurlyWood|CadetBlue|Chartreuse|Chocolate|Coral|CornflowerBlue|Cornsilk|Crimson|Cyan|DarkBlue|DarkCyan|DarkGoldenRod|DarkGr[ae]y|DarkGreen|DarkKhaki|DarkMagenta|DarkOliveGreen|DarkOrange|DarkOrchid|DarkRed|DarkSalmon|DarkSeaGreen|DarkSlateBlue|DarkSlateGr[ae]y|DarkTurquoise|DarkViolet|DeepPink|DeepSkyBlue|DimGr[ae]y|DodgerBlue|FireBrick|FloralWhite|ForestGreen|Fuchsia|Gainsboro|GhostWhite|Gold|GoldenRod|Gr[ae]y|Green|GreenYellow|HoneyDew|HotPink|IndianRed|Indigo|Ivory|Khaki|Lavender|LavenderBlush|LawnGreen|LemonChiffon|LightBlue|LightCoral|LightCyan|LightGoldenRodYellow|LightGr[ae]y|LightGreen|LightPink|LightSalmon|LightSeaGreen|LightSkyBlue|LightSlateGr[ae]y|LightSteelBlue|LightYellow|Lime|LimeGreen|Linen|Magenta|Maroon|MediumAquaMarine|MediumBlue|MediumOrchid|MediumPurple|MediumSeaGreen|MediumSlateBlue|MediumSpringGreen|MediumTurquoise|MediumVioletRed|MidnightBlue|MintCream|MistyRose|Moccasin|NavajoWhite|Navy|OldLace|Olive|OliveDrab|Orange|OrangeRed|Orchid|PaleGoldenRod|PaleGreen|PaleTurquoise|PaleVioletRed|PapayaWhip|PeachPuff|Peru|Pink|Plum|PowderBlue|Purple|Red|RosyBrown|RoyalBlue|SaddleBrown|Salmon|SandyBrown|SeaGreen|SeaShell|Sienna|Silver|SkyBlue|SlateBlue|SlateGr[ae]y|Snow|SpringGreen|SteelBlue|Tan|Teal|Thistle|Tomato|Transparent|Turquoise|Violet|Wheat|White|WhiteSmoke|Yellow|YellowGreen)\b/i,{pattern:/\b(?:hsl|rgb)\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*\)\B|\b(?:hsl|rgb)a\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*,\s*(?:0|0?\.\d+|1)\s*\)\B/i,inside:{unit:t,number:n,function:/[\w-]+(?=\()/,punctuation:/[(),]/}}],entity:/\\[\da-f]{1,8}/i,unit:t,boolean:/\b(?:false|true)\b/,operator:[/~|[+!\/%<>?=]=?|[-:]=|\*[*=]?|\.{2,3}|&&|\|\||\B-\B|\b(?:and|in|is(?: a| defined| not|nt)?|not|or)\b/],number:n,punctuation:/[{}()\[\];:,]/};a.interpolation={pattern:/\{[^\r\n}:]+\}/,alias:"variable",inside:{delimiter:{pattern:/^\{|\}$/,alias:"punctuation"},rest:a}},a.func={pattern:/[\w-]+\([^)]*\).*/,inside:{function:/^[^(]+/,rest:a}},e.languages.stylus={"atrule-declaration":{pattern:/(^[ \t]*)@.+/m,lookbehind:!0,inside:{atrule:/^@[\w-]+/,rest:a}},"variable-declaration":{pattern:/(^[ \t]*)[\w$-]+\s*.?=[ \t]*(?:\{[^{}]*\}|\S.*|$)/m,lookbehind:!0,inside:{variable:/^\S+/,rest:a}},statement:{pattern:/(^[ \t]*)(?:else|for|if|return|unless)[ \t].+/m,lookbehind:!0,inside:{keyword:/^\S+/,rest:a}},"property-declaration":{pattern:/((?:^|\{)([ \t]*))(?:[\w-]|\{[^}\r\n]+\})+(?:\s*:\s*|[ \t]+)(?!\s)[^{\r\n]*(?:;|[^{\r\n,]$(?!(?:\r?\n|\r)(?:\{|\2[ \t])))/m,lookbehind:!0,inside:{property:{pattern:/^[^\s:]+/,inside:{interpolation:a.interpolation}},rest:a}},selector:{pattern:/(^[ \t]*)(?:(?=\S)(?:[^{}\r\n:()]|::?[\w-]+(?:\([^)\r\n]*\)|(?![\w-]))|\{[^}\r\n]+\})+)(?:(?:\r?\n|\r)(?:\1(?:(?=\S)(?:[^{}\r\n:()]|::?[\w-]+(?:\([^)\r\n]*\)|(?![\w-]))|\{[^}\r\n]+\})+)))*(?:,$|\{|(?=(?:\r?\n|\r)(?:\{|\1[ \t])))/m,lookbehind:!0,inside:{interpolation:a.interpolation,comment:a.comment,punctuation:/[{},]/}},func:a.func,string:a.string,comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0,greedy:!0},interpolation:a.interpolation,punctuation:/[{}()\[\];:.]/}}(r),function(e){var t=e.util.clone(e.languages.typescript);e.languages.tsx=e.languages.extend("jsx",t),delete e.languages.tsx.parameter,delete e.languages.tsx["literal-property"];var n=e.languages.tsx.tag;n.pattern=RegExp(/(^|[^\w$]|(?=<\/))/.source+"(?:"+n.pattern.source+")",n.pattern.flags),n.lookbehind=!0}(r),r.languages.wasm={comment:[/\(;[\s\S]*?;\)/,{pattern:/;;.*/,greedy:!0}],string:{pattern:/"(?:\\[\s\S]|[^"\\])*"/,greedy:!0},keyword:[{pattern:/\b(?:align|offset)=/,inside:{operator:/=/}},{pattern:/\b(?:(?:f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|neg?|nearest|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|sqrt|store(?:8|16|32)?|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))?|memory\.(?:grow|size))\b/,inside:{punctuation:/\./}},/\b(?:anyfunc|block|br(?:_if|_table)?|call(?:_indirect)?|data|drop|elem|else|end|export|func|get_(?:global|local)|global|if|import|local|loop|memory|module|mut|nop|offset|param|result|return|select|set_(?:global|local)|start|table|tee_local|then|type|unreachable)\b/],variable:/\$[\w!#$%&'*+\-./:<=>?@\\^`|~]+/,number:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/,punctuation:/[()]/};const o=r},3292:()=>{!function(e){var t=/%%?[~:\w]+%?|!\S+!/,n={pattern:/\/[a-z?]+(?=[ :]|$):?|-[a-z]\b|--[a-z-]+\b/im,alias:"attr-name",inside:{punctuation:/:/}},a=/"(?:[\\"]"|[^"])*"(?!")/,r=/(?:\b|-)\d+\b/;e.languages.batch={comment:[/^::.*/m,{pattern:/((?:^|[&(])[ \t]*)rem\b(?:[^^&)\r\n]|\^(?:\r\n|[\s\S]))*/im,lookbehind:!0}],label:{pattern:/^:.*/m,alias:"property"},command:[{pattern:/((?:^|[&(])[ \t]*)for(?: \/[a-z?](?:[ :](?:"[^"]*"|[^\s"/]\S*))?)* \S+ in \([^)]+\) do/im,lookbehind:!0,inside:{keyword:/\b(?:do|in)\b|^for\b/i,string:a,parameter:n,variable:t,number:r,punctuation:/[()',]/}},{pattern:/((?:^|[&(])[ \t]*)if(?: \/[a-z?](?:[ :](?:"[^"]*"|[^\s"/]\S*))?)* (?:not )?(?:cmdextversion \d+|defined \w+|errorlevel \d+|exist \S+|(?:"[^"]*"|(?!")(?:(?!==)\S)+)?(?:==| (?:equ|geq|gtr|leq|lss|neq) )(?:"[^"]*"|[^\s"]\S*))/im,lookbehind:!0,inside:{keyword:/\b(?:cmdextversion|defined|errorlevel|exist|not)\b|^if\b/i,string:a,parameter:n,variable:t,number:r,operator:/\^|==|\b(?:equ|geq|gtr|leq|lss|neq)\b/i}},{pattern:/((?:^|[&()])[ \t]*)else\b/im,lookbehind:!0,inside:{keyword:/^else\b/i}},{pattern:/((?:^|[&(])[ \t]*)set(?: \/[a-z](?:[ :](?:"[^"]*"|[^\s"/]\S*))?)* (?:[^^&)\r\n]|\^(?:\r\n|[\s\S]))*/im,lookbehind:!0,inside:{keyword:/^set\b/i,string:a,parameter:n,variable:[t,/\w+(?=(?:[*\/%+\-&^|]|<<|>>)?=)/],number:r,operator:/[*\/%+\-&^|]=?|<<=?|>>=?|[!~_=]/,punctuation:/[()',]/}},{pattern:/((?:^|[&(])[ \t]*@?)\w+\b(?:"(?:[\\"]"|[^"])*"(?!")|[^"^&)\r\n]|\^(?:\r\n|[\s\S]))*/m,lookbehind:!0,inside:{keyword:/^\w+\b/,string:a,parameter:n,label:{pattern:/(^\s*):\S+/m,lookbehind:!0,alias:"property"},variable:t,number:r,operator:/\^/}}],operator:/[&@]/,punctuation:/[()']/}}(Prism)},2503:()=>{!function(e){var t=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,n=/(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,a={pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"/,lookbehind:!0,greedy:!0},"class-name":[a,{pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z]\w*(?=\s+\w+\s*[;,=()]|\s*(?:\[[\s,]*\]\s*)?::\s*new\b)/.source),lookbehind:!0,inside:a.inside},{pattern:RegExp(/(\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\s+)/.source+n+/[A-Z]\w*\b/.source),lookbehind:!0,inside:a.inside}],keyword:t,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\b[A-Z][A-Z_\d]+\b/}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"},char:{pattern:/'(?:\\.|[^'\\\r\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":a,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp(/(\bimport\s+)/.source+n+/(?:[A-Z]\w*|\*)(?=\s*;)/.source),lookbehind:!0,inside:{namespace:a.inside.namespace,punctuation:/\./,operator:/\*/,"class-name":/\w+/}},{pattern:RegExp(/(\bimport\s+static\s+)/.source+n+/(?:\w+|\*)(?=\s*;)/.source),lookbehind:!0,alias:"static",inside:{namespace:a.inside.namespace,static:/\b\w+$/,punctuation:/\./,operator:/\*/,"class-name":/\w+/}}],namespace:{pattern:RegExp(/(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)(?!)[a-z]\w*(?:\.[a-z]\w*)*\.?/.source.replace(//g,(function(){return t.source}))),lookbehind:!0,inside:{punctuation:/\./}}})}(Prism)},2886:()=>{Prism.languages.scala=Prism.languages.extend("java",{"triple-quoted-string":{pattern:/"""[\s\S]*?"""/,greedy:!0,alias:"string"},string:{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},keyword:/<-|=>|\b(?:abstract|case|catch|class|def|derives|do|else|enum|extends|extension|final|finally|for|forSome|given|if|implicit|import|infix|inline|lazy|match|new|null|object|opaque|open|override|package|private|protected|return|sealed|self|super|this|throw|trait|transparent|try|type|using|val|var|while|with|yield)\b/,number:/\b0x(?:[\da-f]*\.)?[\da-f]+|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e\d+)?[dfl]?/i,builtin:/\b(?:Any|AnyRef|AnyVal|Boolean|Byte|Char|Double|Float|Int|Long|Nothing|Short|String|Unit)\b/,symbol:/'[^\d\s\\]\w*/}),Prism.languages.insertBefore("scala","triple-quoted-string",{"string-interpolation":{pattern:/\b[a-z]\w*(?:"""(?:[^$]|\$(?:[^{]|\{(?:[^{}]|\{[^{}]*\})*\}))*?"""|"(?:[^$"\r\n]|\$(?:[^{]|\{(?:[^{}]|\{[^{}]*\})*\}))*")/i,greedy:!0,inside:{id:{pattern:/^\w+/,greedy:!0,alias:"function"},escape:{pattern:/\\\$"|\$[$"]/,greedy:!0,alias:"symbol"},interpolation:{pattern:/\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/,greedy:!0,inside:{punctuation:/^\$\{?|\}$/,expression:{pattern:/[\s\S]+/,inside:Prism.languages.scala}}},string:/[\s\S]+/}}}),delete Prism.languages.scala["class-name"],delete Prism.languages.scala.function,delete Prism.languages.scala.constant},737:(e,t,n)=>{var a={"./prism-batch":3292,"./prism-java":2503,"./prism-scala":2886};function r(e){var t=o(e);return n(t)}function o(e){if(!n.o(a,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return a[e]}r.keys=function(){return Object.keys(a)},r.resolve=o,e.exports=r,r.id=737},2703:(e,t,n)=>{"use strict";var a=n(414);function r(){}function o(){}o.resetWarningCache=r,e.exports=function(){function e(e,t,n,r,o,l){if(l!==a){var i=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw i.name="Invariant Violation",i}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:r};return n.PropTypes=n,n}},5697:(e,t,n)=>{e.exports=n(2703)()},414:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},4448:(e,t,n)=>{"use strict";var a=n(7294),r=n(7418),o=n(3840);function l(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n
    + + \ No newline at end of file diff --git a/introduction/index.html b/introduction/index.html index 5297409bc..fa767d871 100644 --- a/introduction/index.html +++ b/introduction/index.html @@ -5,12 +5,12 @@ Introduction | Scala Center Advent of Code - - + +
    -

    Introduction

    Welcome to the Scala Center's take on Advent of Code.

    In the words of its author:

    Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like. +

    Introduction

    Welcome to the Scala Center's take on Advent of Code.

    In the words of its author:

    Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like. People use them as a speed contest, interview prep, company training, university coursework, practice problems, or to challenge each other. You don't need a computer science background to participate - just a little programming knowledge and some problem solving skills will get you pretty far. Nor do you need a fancy computer; every problem has a solution that completes in at most 15 seconds on ten-year-old hardware.

    At the Scala Center, we would like to use Advent of Code as an opportunity to challenge ourselves and learn from each other how best to use Scala.

    During the week days of Advent of Code, we will solve some of the proposed puzzles, and we encourage you to do the same on your side. @@ -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 27734c909..48167944d 100644 --- a/puzzles/day1/index.html +++ b/puzzles/day1/index.html @@ -5,18 +5,18 @@ Day 1: Sonar Sweep | Scala Center Advent of Code - - + +
    -

    Day 1: Sonar Sweep

    by @adpi2

    Puzzle description

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

    Solution of Part 1

    The first step is to transform the input to a sequence of integers. +

    Day 1: Sonar Sweep

    by @adpi2

    Puzzle description

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

    Solution of Part 1

    The first step is to transform the input to a sequence of integers. We can do so with:

    val depths: Seq[Int] = input.linesIterator.map(_.toInt).toSeq

    The second step and hardest challenge of this puzzle is to compute all pairs of consecutive depth measurments.

    For each index i from 0 until depths.size - 1 we can create a pair of the depths at index i and i + 1.

    val pairs: Seq[(Int, Int)] =
    for i <- 0 until depths.size - 1
    yield (depths(i), depths(i + 1))
    tip
    • 0 until n is an exclusive range, it does not contain the upper bound n.
    • 0 to n is an inclusive range, it contains the upper bound n.

    For the input Seq(10, 20, 30, 40), pairs is Seq((10,20), (20, 30), (30, 40)).

    Then we can count the pairs whose first element is smaller than its second element.

    pairs.count((first, second) => first < second)

    That gives us:

    def part1(input: String): Int =
    val depths: Seq[Int] = input.linesIterator.map(_.toInt).toSeq
    val pairs: Seq[(Int, Int)] =
    for i <- 0 until depths.size - 1
    yield (depths(i), depths(i + 1))
    pairs.count((first, second) => first < second)

    Solution of Part 2

    In the second part we need to compute the sums of all consecutive three elements.

    We can use a similar approach to part 1.

    val sums: Seq[Int] =
    for i <- 0 until depths.size - 2
    yield depths(i) + depths(i + 1) + depths(i + 2)

    Notice that we can sum the three elements in the yield part of the for comprehension.

    The remaining code of this second puzzle is very similar to what we did in part 1.

    Generalization to sliding method

    In part 1 we computed all pairs of consecutive elements. In part 2 we computed the sums of all consecutive three elements.

    In each case there is a notion of sliding window of size n, where n is 2 or 3. 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.

    - - + + \ No newline at end of file diff --git a/puzzles/day10/index.html b/puzzles/day10/index.html index 0cfaf7dfd..6dd95df95 100644 --- a/puzzles/day10/index.html +++ b/puzzles/day10/index.html @@ -5,12 +5,12 @@ Day 10: Syntax Scoring | Scala Center Advent of Code - - + +
    -

    Day 10: Syntax Scoring

    by @VincenzoBaz

    Puzzle description

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

    Solution overview

    Day 10 focuses on detecting unbalanced markers in the navigation system of the +

    Day 10: Syntax Scoring

    by @VincenzoBaz

    Puzzle description

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

    Solution overview

    Day 10 focuses on detecting unbalanced markers in the navigation system of the submarine. The possible markers are ()[]{}<>. The input contains several lines, our task is to check whether each line is balanced, incomplete or invalid.

    I propose a solution centered around the algorithm that verifies each line and @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day11/index.html b/puzzles/day11/index.html index 1c0d21c39..d0ba4ef8b 100644 --- a/puzzles/day11/index.html +++ b/puzzles/day11/index.html @@ -5,14 +5,14 @@ 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. +

    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! See here for the expected format

    - - + + \ No newline at end of file diff --git a/puzzles/day12/index.html b/puzzles/day12/index.html index b7721c9e0..fa7daf594 100644 --- a/puzzles/day12/index.html +++ b/puzzles/day12/index.html @@ -5,14 +5,14 @@ Day 12: Passage Pathing | Scala Center Advent of Code - - + +
    -

    Day 12: Passage Pathing

    Puzzle description

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

    Solutions from the community

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

    - - + + \ No newline at end of file diff --git a/puzzles/day13/index.html b/puzzles/day13/index.html index e02ad9239..b792e653d 100644 --- a/puzzles/day13/index.html +++ b/puzzles/day13/index.html @@ -5,12 +5,12 @@ Day 13: Transparent Origami | Scala Center Advent of Code - - + +
    -

    Day 13: Transparent Origami

    by @adpi2

    Puzzle description

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

    Modeling the objects

    The instructions of the manual are composed of dots and folds. +

    Day 13: Transparent Origami

    by @adpi2

    Puzzle description

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

    Modeling the objects

    The instructions of the manual are composed of dots and folds. We can model those two objects in Scala 3 with:

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

    enum Fold:
    case Vertical(x: Int)
    case Horizontal(y: Int)

    A Dot is made of two integers: x and y. A Fold is either Vertical if it has an x coordinate or Horizontal if it has a y coordinate.

    Parsing

    In order to parse dots and folds, we create two parse functions in the companion objects of Dot and Fold.

    object Dot:
    def parse(line: String): Dot =
    line match
    case s"$x,$y" => Dot(x.toInt, y.toInt)
    case _ => throw new Exception(s"Cannot parse '$line' to Dot")

    object Fold:
    def parse(line: String): Fold =
    line match
    case s"fold along x=$x" => Vertical(x.toInt)
    case s"fold along y=$y" => Horizontal(y.toInt)
    case _ => throw new Exception(s"Cannot parse '$line' to Fold")

    Now, all the instructions can be parsed as follow:

    def parseInstructions(input: String): (Set[Dot], List[Fold]) =
    val sections = input.split("\n\n")
    val dots = sections(0).linesIterator.map(Dot.parse).toSet
    val folds = sections(1).linesIterator.map(Fold.parse).toList
    (dots, folds)

    Notice that we return a set of Dot and a list of Fold.

    A set is different from a list because it cannot contain the same element twice. Indeed, inserting an element in a set that already contains it does not alter the set. @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day14/index.html b/puzzles/day14/index.html index 71b85f31d..6f286b971 100644 --- a/puzzles/day14/index.html +++ b/puzzles/day14/index.html @@ -5,12 +5,12 @@ Day 14: Extended Polymerization | Scala Center Advent of Code - - + +
    -

    Day 14: Extended Polymerization

    by @sjrd

    Puzzle description

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

    Modeling and parsing the input

    Today, the input is an initial polymer and a list of insertion rules. +

    Day 14: Extended Polymerization

    by @sjrd

    Puzzle description

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

    Modeling and parsing the input

    Today, the input is an initial polymer and a list of insertion rules. The insertion rules map a pair of characters to another character to insert. We model them as

    type Polymer = List[Char]
    type CharPair = (Char, Char)
    type InsertionRules = Map[CharPair, Char]

    and we can parse the input with

    def parseInput(input: String): (Polymer, InsertionRules) =
    val sections = input.split("\n\n")
    val initialPolymer = sections(0).toList
    val insertionRules = sections(1).linesIterator.map(parseRule).toMap
    (initialPolymer, insertionRules)

    If you have been following our previous posts, the above is nothing out of the ordinary.

    From frequencies to the end result

    At the end of the day, we will need a count of all the frequencies of all characters. We model it as a map from char to long:

    type Frequencies = Map[Char, Long]

    Assuming we have computed the frequencies in the final polymer, the result will be the difference between the maximum frequency and the minimum frequency. @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day15/index.html b/puzzles/day15/index.html index 1ad8d90f3..a8b41f086 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.

    - - +

    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.

    + + \ No newline at end of file diff --git a/puzzles/day16/index.html b/puzzles/day16/index.html index af4dda6e0..0fbab7a30 100644 --- a/puzzles/day16/index.html +++ b/puzzles/day16/index.html @@ -5,12 +5,12 @@ Day 16: Packet Decoder | Scala Center Advent of Code - - + +
    -

    Day 16: Packet Decoder

    by @tgodzik

    Puzzle description

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

    Part1: You've got mail!

    It seems that we can split our problem into two parts. First, we need to parse +

    Day 16: Packet Decoder

    by @tgodzik

    Puzzle description

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

    Part1: You've got mail!

    It seems that we can split our problem into two parts. First, we need to parse the example into structures that we can later use to calculate our results.

    Let's start with defining the data structures to use:

    enum Packet(version: Int, typeId: Int):
    case Literal(version: Int, value: Long) extends Packet(version, 4)
    case Operator(version: Int, typeId: Int, exprs: List[Packet]) extends Packet(version, typeId)

    Packet.Literal will represent simple literal packets, that contain only a value. We are using Long, just in case of large integer numbers based on the experience with previous Advent of Code puzzles.

    Packet.Operator will represent all the other operators that can contain other @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day17/index.html b/puzzles/day17/index.html index 26c167697..cb0308660 100644 --- a/puzzles/day17/index.html +++ b/puzzles/day17/index.html @@ -5,12 +5,12 @@ Day 17: Trick Shot | Scala Center Advent of Code - - + +
    -

    Day 17: Trick Shot

    by @bishabosha

    Puzzle description

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

    Solution of Part 1

    Modelling The Domain

    A Moving Probe

    The problem asks us to consider the trajectory of a projectile probe +

    Day 17: Trick Shot

    by @bishabosha

    Puzzle description

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

    Solution of Part 1

    Modelling The Domain

    A Moving Probe

    The problem asks us to consider the trajectory of a projectile probe which has both a position and a velocity. Both positions and velocities have two directions, x and y, with integer values. We model them with case classes:

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

    and then the probe is as follows:

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

    We also find out that a probe always has an initial position of 0,0, so @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day18/index.html b/puzzles/day18/index.html index 7c82354a4..07029629e 100644 --- a/puzzles/day18/index.html +++ b/puzzles/day18/index.html @@ -5,14 +5,14 @@ Day 18: Snailfish | Scala Center Advent of Code - - + +
    -

    Day 18: Snailfish

    Puzzle description

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

    Solutions from the community

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

    - - + + \ No newline at end of file diff --git a/puzzles/day19/index.html b/puzzles/day19/index.html index 2ff22eb6f..9ab94072a 100644 --- a/puzzles/day19/index.html +++ b/puzzles/day19/index.html @@ -5,14 +5,14 @@ Day 19: Beacon Scanner | Scala Center Advent of Code - - + +
    -

    Day 19: Beacon Scanner

    Puzzle description

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

    Solutions from the community

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

    - - + + \ No newline at end of file diff --git a/puzzles/day2/index.html b/puzzles/day2/index.html index fb3eb8c57..26b8481aa 100644 --- a/puzzles/day2/index.html +++ b/puzzles/day2/index.html @@ -5,12 +5,12 @@ Day 2: Dive! | Scala Center Advent of Code - - + +
    -

    Day 2: Dive!

    by @mlachkar

    Puzzle description

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

    Solution of Part 1

    Parsing the file

    The first step is to model the problem and to parse the input file.

    The command can be either Forward, Down or Up. I use an Enumeration to model it.

    An enumeration is used to define a type consisting of a set of named values. Read the official documentation +

    Day 2: Dive!

    by @mlachkar

    Puzzle description

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

    Solution of Part 1

    Parsing the file

    The first step is to model the problem and to parse the input file.

    The command can be either Forward, Down or Up. I use an Enumeration to model it.

    An enumeration is used to define a type consisting of a set of named values. Read the official documentation for more details.

    Scala 3 enums are more concise and easier to read that the Scala 2 ADTs.

    // in Scala 3:
    enum Command:
    case Forward(x: Int)
    case Down(x: Int)
    case Up(x: Int)

    // in Scala 2:
    sealed trait Command
    object Command {
    case class Forward(x: Int) extends Command
    case class Down(x: Int) extends Command
    case class Up(x: Int) extends Command
    }

    Now let's parse the input to a Command:

    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")

    info

    Here I have chosen to fail during the parsing method Command.from to keep the types as simple as possible. If an input file is not valid, we throw an exception.

    It's possible to delay the parsing error to the main method, and return an Option[Command] or Try[Command] Command.from

    Moving the sonar

    Now we need to create a method to compute the new position of a sonar given @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day20/index.html b/puzzles/day20/index.html index 334b916df..a78a70f4f 100644 --- a/puzzles/day20/index.html +++ b/puzzles/day20/index.html @@ -5,12 +5,12 @@ Day 20: Trench Map | Scala Center Advent of Code - - + +
    -

    Day 20: Trench Map

    Puzzle description

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

    Modeling and parsing the input

    The input is an image enhancement algorithm string, and an initial input image.

    The image is a black and white rectangle. We model the pixels with an +

    Day 20: Trench Map

    Puzzle description

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

    Modeling and parsing the input

    The input is an image enhancement algorithm string, and an initial input image.

    The image is a black and white rectangle. We model the pixels with an enumeration:

    enum Pixel:
    case Lit, Dark

    A pixel can either be lit, or dark.

    In the input text, lit pixels are represented by the character "#", whereas dark pixels are represented by the character "." (dot). We use pattern matching to parse them:

    object Pixel:
    def parse(char: Char): Pixel =
    char match
    case '#' => Pixel.Lit
    case '.' => Pixel.Dark
    end Pixel

    In case the input is malformed (ie, it contains a character other than "#" @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day21/index.html b/puzzles/day21/index.html index fcbb4162c..db21f71be 100644 --- a/puzzles/day21/index.html +++ b/puzzles/day21/index.html @@ -5,12 +5,12 @@ Day 21: Dirac Dice | Scala Center Advent of Code - - + +
    -

    Day 21: Dirac Dice

    by @sjrd

    Puzzle description

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

    Modeling and parsing the input

    As we did for previous solutions, we start by modeling the problem and parsing the input.

    type Cell = Int // from 0 to 9, to simplify computations

    case class Player(cell: Cell, score: Long)

    type Players = (Player, Player)

    def parseInput(input: String): Players =
    val lines = input.split("\n")
    (parsePlayer(lines(0)), parsePlayer(lines(1)))

    def parsePlayer(line: String): Player =
    line match
    case s"Player $num starting position: $cell" =>
    Player(cell.toInt - 1, 0L)

    The only thing worth noting is that we use numbers 0 to 9 for the cells (the "spaces") instead of 1 to 10. +

    Day 21: Dirac Dice

    by @sjrd

    Puzzle description

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

    Modeling and parsing the input

    As we did for previous solutions, we start by modeling the problem and parsing the input.

    type Cell = Int // from 0 to 9, to simplify computations

    case class Player(cell: Cell, score: Long)

    type Players = (Player, Player)

    def parseInput(input: String): Players =
    val lines = input.split("\n")
    (parsePlayer(lines(0)), parsePlayer(lines(1)))

    def parsePlayer(line: String): Player =
    line match
    case s"Player $num starting position: $cell" =>
    Player(cell.toInt - 1, 0L)

    The only thing worth noting is that we use numbers 0 to 9 for the cells (the "spaces") instead of 1 to 10. The only reason is that it simplifies computations for wrapping around.

    The deterministic die

    For the first part, with the deterministic die, we have to return the score of the losing player, while keeping tabs on how many times the die was rolled. We model the deterministic die as an instance of a class DeterministicDie:

    final class DeterministicDie {
    var throwCount: Int = 0
    private var lastValue: Int = 100

    def nextResult(): Int =
    throwCount += 1
    lastValue = (lastValue % 100) + 1
    lastValue
    }

    An instance of that class keeps track of what its last roll was, and of how many times it was thrown.

    Playing with the deterministic die

    We use the deterministic die in a tail-recursive function playWithDeterministicDie to play out a full game:

    @tailrec
    def playWithDeterministicDie(players: Players, die: DeterministicDie): Long =
    val diesValue = die.nextResult() + die.nextResult() + die.nextResult()
    val player = players(0)
    val newCell = (player.cell + diesValue) % 10
    val newScore = player.score + (newCell + 1)
    if newScore >= 1000 then
    players(1).score
    else
    val newPlayer = Player(newCell, newScore)
    playWithDeterministicDie((players(1), newPlayer), die)

    In that function, it is always players(0)'s turn. In the tail-recursive call, we swap out the two players, so that the next player to play always comes first.

    Other than that, the function is a direct translation from the rules of the game in the puzzle description:

    1. Throw the die 3 times, and add up the values
    2. Advance the player, wrapping around every 10 cells
    3. Compute the new score
    4. Detect the winning condition and return the loser's score, or continue with the next player

    If you are not familiar with modular arithmetics, it may not be obvious that the computation (player.cell + diesValue) % 10 is correct. @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day22/index.html b/puzzles/day22/index.html index e549843d3..66b75e461 100644 --- a/puzzles/day22/index.html +++ b/puzzles/day22/index.html @@ -5,12 +5,12 @@ Day 22: Reactor Reboot | Scala Center Advent of Code - - + +
    -

    Day 22: Reactor Reboot

    by @bishabosha

    Puzzle description

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

    Final Problem

    Modelling The Domain

    Basic Data Types

    We model the input as a series of steps, where each step has a +

    Day 22: Reactor Reboot

    by @bishabosha

    Puzzle description

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

    Final Problem

    Modelling The Domain

    Basic Data Types

    We model the input as a series of steps, where each step has a command (either "on" or "off"), and a cuboid area. Each cuboid is modelled by its dimensions in each of the x, y, and z dimensions. @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day23/index.html b/puzzles/day23/index.html index 94be9fc9f..d1bef0419 100644 --- a/puzzles/day23/index.html +++ b/puzzles/day23/index.html @@ -5,16 +5,16 @@ Day 23: Amphipod | Scala Center Advent of Code - - + +
    -

    Day 23: Amphipod

    by @adpi2

    Puzzle description

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

    Modeling and parsing the input

    We model Position as a case class containing two integer fields: x and y.

    Room is a enumeration of 4 cases from A to D and it is parameterized by the coordinate x of the room in the diagram.

    Likewise, Amphipod is an enumeration of 4 cases from A to D and it is parameterized by its energy cost and destination room.

    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)

    We model a situation as a case class of all the occupied positions and the size of the room (this will be needed for part 2).

    case class Situation(positions: Map[Position, Amphipod], roomSize: Int)

    We can parse the input file into the initial Situation with:

    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)

    where Amphipod.tryParse is:

    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

    Using Dijkstra's algorithm to solve the puzzle

    Dijkstra's algorithm is used for finding the shortest path between two nodes in a graph. +

    Day 23: Amphipod

    by @adpi2

    Puzzle description

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

    Modeling and parsing the input

    We model Position as a case class containing two integer fields: x and y.

    Room is a enumeration of 4 cases from A to D and it is parameterized by the coordinate x of the room in the diagram.

    Likewise, Amphipod is an enumeration of 4 cases from A to D and it is parameterized by its energy cost and destination room.

    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)

    We model a situation as a case class of all the occupied positions and the size of the room (this will be needed for part 2).

    case class Situation(positions: Map[Position, Amphipod], roomSize: Int)

    We can parse the input file into the initial Situation with:

    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)

    where Amphipod.tryParse is:

    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

    Using Dijkstra's algorithm to solve the puzzle

    Dijkstra's algorithm is used for finding the shortest path between two nodes in a graph. 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.

    - - + + \ No newline at end of file diff --git a/puzzles/day24/index.html b/puzzles/day24/index.html index cf43a1b45..414a31cad 100644 --- a/puzzles/day24/index.html +++ b/puzzles/day24/index.html @@ -5,14 +5,14 @@ Day 24: Arithmetic Logic Unit | Scala Center Advent of Code - - + +
    -

    Day 24: Arithmetic Logic Unit

    Puzzle description

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

    Solutions from the community

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

    - - + + \ No newline at end of file diff --git a/puzzles/day25/index.html b/puzzles/day25/index.html index 8bbce3861..1a776c324 100644 --- a/puzzles/day25/index.html +++ b/puzzles/day25/index.html @@ -5,14 +5,14 @@ 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. +

    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! See here for the expected format

    - - + + \ No newline at end of file diff --git a/puzzles/day3/index.html b/puzzles/day3/index.html index e0f38b016..1a8bcd02b 100644 --- a/puzzles/day3/index.html +++ b/puzzles/day3/index.html @@ -5,12 +5,12 @@ Day 3: Binary Diagnostic | Scala Center Advent of Code - - + +
    -

    Day 3: Binary Diagnostic

    by @sjrd

    Puzzle description

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

    Solution of Part 1

    Reading the input file

    To spice up our Advent of Code a bit, I chose to solve this puzzle using Scala.js rather than Scala/JVM. +

    Day 3: Binary Diagnostic

    by @sjrd

    Puzzle description

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

    Solution of Part 1

    Reading the input file

    To spice up our Advent of Code a bit, I chose to solve this puzzle using Scala.js rather than Scala/JVM. Concretely, that changes almost nothing, except how we read the input file. In Scala.js, we cannot use java.io.File to read files, and therefore neither scala.io.Source.fromFile. Instead, we can use Node.js' fs module, and in particular its readFileSync function.

    Since I do not want to add a dependency on an external library for a single method definition, I define a Scala.js facade type for fs.readFileSync myself:

    import scala.scalajs.js
    import scala.scalajs.js.annotation._

    @js.native @JSImport("fs", "readFileSync")
    def readFileSync(path: String, charset: String): String = js.native

    It declares that readFileSync is a native JavaScript function, found in the module "fs" under the name "readFileSync". @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day4/index.html b/puzzles/day4/index.html index 8f4f3587a..a259bd60a 100644 --- a/puzzles/day4/index.html +++ b/puzzles/day4/index.html @@ -5,12 +5,12 @@ Day 4: Giant Squid | Scala Center Advent of Code - - + +
    -

    Day 4: Giant Squid

    by @Sporarum, student at EPFL.

    Puzzle description

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

    Parsing

    Example Input
    14,30,18,8,3,10,77,4,48,67,28,38,63,43,62,12,68,88,54,32,17,21,83,64,97,53,24,2,60,96,86,23,20,93,65,34,45,46,42,49,71,9,61,16,31,1,29,40,59,87,95,41,39,27,6,25,19,58,80,81,50,79,73,15,70,37,92,94,7,55,85,98,5,84,99,26,66,57,82,75,22,89,74,36,11,76,56,33,13,72,35,78,47,91,51,44,69,0,90,52

    13 62 38 10 41
    93 59 60 74 75
    79 18 57 90 28
    56 76 34 96 84
    78 42 69 14 19

    96 38 62 8 7
    78 50 53 29 81
    88 45 34 58 52
    33 76 13 54 68
    59 95 10 80 63

    36 26 74 29 55
    43 87 46 70 21
    9 17 38 58 63
    56 79 85 51 2
    50 57 67 86 8

    29 78 3 24 79
    15 81 20 6 38
    97 41 28 42 82
    45 68 89 85 92
    48 33 40 62 4

    <elided>

    The input has two parts. +

    Day 4: Giant Squid

    by @Sporarum, student at EPFL.

    Puzzle description

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

    Parsing

    Example Input
    14,30,18,8,3,10,77,4,48,67,28,38,63,43,62,12,68,88,54,32,17,21,83,64,97,53,24,2,60,96,86,23,20,93,65,34,45,46,42,49,71,9,61,16,31,1,29,40,59,87,95,41,39,27,6,25,19,58,80,81,50,79,73,15,70,37,92,94,7,55,85,98,5,84,99,26,66,57,82,75,22,89,74,36,11,76,56,33,13,72,35,78,47,91,51,44,69,0,90,52

    13 62 38 10 41
    93 59 60 74 75
    79 18 57 90 28
    56 76 34 96 84
    78 42 69 14 19

    96 38 62 8 7
    78 50 53 29 81
    88 45 34 58 52
    33 76 13 54 68
    59 95 10 80 63

    36 26 74 29 55
    43 87 46 70 21
    9 17 38 58 63
    56 79 85 51 2
    50 57 67 86 8

    29 78 3 24 79
    15 81 20 6 38
    97 41 28 42 82
    45 68 89 85 92
    48 33 40 62 4

    <elided>

    The input has two parts. First, a list of all numbers from 1 to 99 drawn in a random order (without repetition). Then, after two newlines, a list of bingo boards, separated again by double newlines. We notice they also contain only numbers from 1 to 99.

    Since the numbers and the list of boards are all separated by double newlines, we can split our input into sections as follows:

    val inputSections: List[String] = input.split("\n\n").toList

    Once that's done, we can separate the parts by just using head and tail, thus giving us the numbers, and the list of boards, but those are still only text!

    Parsing Numbers

    For the numbers, since they are separated by , and nothing else, we can parse them with:

    val numbers: List[Int] = inputSections.head.split(',').map(_.toInt)

    Parsing Boards

    A board is a table of integers, and a table is a list of lines, where each line is also list. And so we have our type for the boards: List[List[Int]]!

    But not so fast, we would like to add some extra operations on boards, so we wrap it in a case class: @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day5/index.html b/puzzles/day5/index.html index 03e7b0f49..008d747ee 100644 --- a/puzzles/day5/index.html +++ b/puzzles/day5/index.html @@ -5,12 +5,12 @@ Day 5: Hydrothermal Venture | Scala Center Advent of Code - - + +
    -

    Day 5: Hydrothermal Venture

    by @tgodzik

    Puzzle description

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

    Solution of Part 1

    Parsing the file

    The first step as usual is to model the problem and to parse the input file.

    In this puzzle we are dealing with cartesian coordinates, so it would be good to +

    Day 5: Hydrothermal Venture

    by @tgodzik

    Puzzle description

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

    Solution of Part 1

    Parsing the file

    The first step as usual is to model the problem and to parse the input file.

    In this puzzle we are dealing with cartesian coordinates, so it would be good to add a Point class for convenience.

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

    Beside that we will also want to model a Vent which can have starting point and ending one:

    case class Vent(start: Point, end: Point)

    Let's try to convert the input to a sequence of vents.

    val allVents = input.linesIterator.map(Vent.apply).toSeq

    As you can see above I actually decide to put the parsing of the lines into the apply method of Vent, which we can implement as:

    object Vent:
    def apply(str: String) =
    str.split("->") match
    case Array(start, end) =>
    new Vent(Point(start), Point(end))
    case _ =>
    throw new java.lang.IllegalArgumentException(s"Wrong vent input $str")

    In the above code we split the line into the part before -> and after it, @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day6/index.html b/puzzles/day6/index.html index e2dd33856..52c2b7183 100644 --- a/puzzles/day6/index.html +++ b/puzzles/day6/index.html @@ -5,12 +5,12 @@ Day 6: Lanternfish | Scala Center Advent of Code - - + +
    -

    Day 6: Lanternfish

    by @julienrf

    Puzzle description

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

    Solution of Part 1

    For part 1, I implemented a "naive" solution by essentially translating the +

    Day 6: Lanternfish

    by @julienrf

    Puzzle description

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

    Solution of Part 1

    For part 1, I implemented a "naive" solution by essentially translating the problem statement into Scala.

    For instance, the problem statement contains:

    You can model each fish as a single number that represents the number of days until it creates a new lanternfish.

    So, my fish model is a case class with exactly one field containing the number of days until that fish creates a new fish:

    case class Fish(timer: Int)

    Then, we were asked to compute how a population of fish evolves after one @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day7/index.html b/puzzles/day7/index.html index 275dfdea2..573dd833c 100644 --- a/puzzles/day7/index.html +++ b/puzzles/day7/index.html @@ -5,12 +5,12 @@ Day 7: The Treachery of Whales | Scala Center Advent of Code - - + +
    -

    Day 7: The Treachery of Whales

    by @tgodzik

    Puzzle description

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

    Part 1: Fast and craby

    The whale is trying to swallow our submarine! Fortunately, there is a small +

    Day 7: The Treachery of Whales

    by @tgodzik

    Puzzle description

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

    Part 1: Fast and craby

    The whale is trying to swallow our submarine! Fortunately, there is a small armada of crabs, let's call it a crabmada, in their tiny crabmarines that want to help us out! However, the crabs are not particularly efficient and will need our help, we need to align all their little crabmarines using the least fuel @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day8/index.html b/puzzles/day8/index.html index 7c204da0b..561e2ba20 100644 --- a/puzzles/day8/index.html +++ b/puzzles/day8/index.html @@ -5,12 +5,12 @@ Day 8: Seven Segment Search | Scala Center Advent of Code - - + +
    -

    Day 8: Seven Segment Search

    by @bishabosha

    Puzzle description

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

    Solution of Part 1

    Modelling the Domain

    First we will model our problem so that we can parse the input into our model.

    Let's look at the representation of display digits from the problem: we see that +

    Day 8: Seven Segment Search

    by @bishabosha

    Puzzle description

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

    Solution of Part 1

    Modelling the Domain

    First we will model our problem so that we can parse the input into our model.

    Let's look at the representation of display digits from the problem: we see that there are a fixed number of possible digits for a display, and that each digit is made by activating some segments, where there are a fixed number of possible segments:

      0:      1:      2:      3:      4:
    aaaa .... aaaa aaaa ....
    b c . c . c . c b c
    b c . c . c . c b c
    .... .... dddd dddd dddd
    e f . f e . . f . f
    e f . f e . . f . f
    gggg .... gggg gggg ....

    5: 6: 7: 8: 9:
    aaaa aaaa aaaa aaaa aaaa
    b . b . . c b c b c
    b . b . . c b c b c
    dddd dddd .... dddd dddd
    . f e f . f e f . f
    . f e f . f e f . f
    gggg gggg .... gggg gggg

    Segment

    As there are a fixed range of possible display segments: a, b, c, d, e, f, g, we will model them with an enumeration.

    An enumeration is used to define a type consisting of a set of named values. @@ -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.

    - - + + \ No newline at end of file diff --git a/puzzles/day9/index.html b/puzzles/day9/index.html index c564373b9..0ef13817f 100644 --- a/puzzles/day9/index.html +++ b/puzzles/day9/index.html @@ -5,12 +5,12 @@ Day 9: Smoke Basin | Scala Center Advent of Code - - + +
    -

    Day 9: Smoke Basin

    by @VincenzoBaz

    Puzzle description

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

    Solution of Part 1

    Part 1 requires us to find all low points of the grid, where a low point is a +

    Day 9: Smoke Basin

    by @VincenzoBaz

    Puzzle description

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

    Solution of Part 1

    Part 1 requires us to find all low points of the grid, where a low point is a cell in the grid which contains a value smaller than the four adjacent values (up, down, left, right).

    I model the two dimensional grid containing height values using a case class called Heightmap. I provide it with a utility method which returns the list of @@ -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.

    - - + + \ No newline at end of file diff --git a/setup/index.html b/setup/index.html index 13d6e32bd..7f5e440aa 100644 --- a/setup/index.html +++ b/setup/index.html @@ -5,12 +5,12 @@ Setup | Scala Center Advent of Code - - + +
    -

    Setup

    There are many ways to get started with Scala and we will suggest that you try Scala CLI, developed by VirtusLab. +

    Setup

    There are many ways to get started with Scala and we will suggest that you try Scala CLI, developed by VirtusLab. Also, we recommend VS Code with the Metals extension for your IDE. With those two tools, you will be able to run, compile and develop your code.

    Of course, if you have already Scala installed, you can skip this part. You can come back to this documentation anytime if you want to try this setup.

    Install Scala CLI

    Scala CLI combines all the features you need to learn and use Scala in your scripts, @@ -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 diff --git a/sitemap.xml b/sitemap.xml index d55fb59b0..9c6ec32d6 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://scalacenter.github.io/scala-advent-of-code/2021/weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/weekly0.5https://scalacenter.github.io/scala-advent-of-code/weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day01weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day02weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day03weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day04weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day05weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day06weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day07weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day08weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day09weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day10weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day11weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day12weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day13weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day14weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day15weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day16weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day17weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day18weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day19weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day20weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day21weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day22weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day23weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day24weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day25weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day01weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day02weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day03weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day04weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day05weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day06weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day07weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day08weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day09weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day10weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day11weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day12weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day13weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day14weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day15weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day16weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day17weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day18weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day19weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day20weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day21weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day22weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day23weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day24weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day25weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day0weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day01weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day02weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day03weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day04weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day05weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day06weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day07weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day08weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day09weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day10weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day11weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day12weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day13weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day14weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day15weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day16weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day17weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day18weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day19weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day20weekly0.5https://scalacenter.github.io/scala-advent-of-code/introductionweekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day1weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day10weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day11weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day12weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day13weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day14weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day15weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day16weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day17weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day18weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day19weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day2weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day20weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day21weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day22weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day23weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day24weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day25weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day3weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day4weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day5weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day6weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day7weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day8weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day9weekly0.5https://scalacenter.github.io/scala-advent-of-code/setupweekly0.5 \ No newline at end of file +https://scalacenter.github.io/scala-advent-of-code/2021/weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/weekly0.5https://scalacenter.github.io/scala-advent-of-code/weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day01weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day02weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day03weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day04weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day05weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day06weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day07weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day08weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day09weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day10weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day11weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day12weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day13weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day14weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day15weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day16weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day17weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day18weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day19weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day20weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day21weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day22weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day23weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day24weekly0.5https://scalacenter.github.io/scala-advent-of-code/2022/puzzles/day25weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day01weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day02weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day03weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day04weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day05weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day06weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day07weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day08weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day09weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day10weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day11weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day12weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day13weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day14weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day15weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day16weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day17weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day18weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day19weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day20weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day21weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day22weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day23weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day24weekly0.5https://scalacenter.github.io/scala-advent-of-code/2023/puzzles/day25weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day0weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day01weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day02weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day03weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day04weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day05weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day06weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day07weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day08weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day09weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day10weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day11weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day12weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day13weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day14weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day15weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day16weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day17weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day18weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day19weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day20weekly0.5https://scalacenter.github.io/scala-advent-of-code/2024/puzzles/day21weekly0.5https://scalacenter.github.io/scala-advent-of-code/introductionweekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day1weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day10weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day11weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day12weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day13weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day14weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day15weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day16weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day17weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day18weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day19weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day2weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day20weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day21weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day22weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day23weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day24weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day25weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day3weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day4weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day5weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day6weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day7weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day8weekly0.5https://scalacenter.github.io/scala-advent-of-code/puzzles/day9weekly0.5https://scalacenter.github.io/scala-advent-of-code/setupweekly0.5 \ No newline at end of file