Skip to content

Commit f959c6e

Browse files
scarf005SethTisue
andauthored
docs(2024): day 23 writeup (#838)
* docs(2024): day 23 writeup * docs: Apply suggestions from code review Co-authored-by: Seth Tisue <seth@tisue.net> * docs: link to `groupMap` --------- Co-authored-by: Seth Tisue <seth@tisue.net>
1 parent f1c3a9d commit f959c6e

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed

docs/2024/puzzles/day23.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,186 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 23: LAN Party
44

5+
by [@scarf005](https://github.com/scarf005)
6+
57
## Puzzle description
68

79
https://adventofcode.com/2024/day/23
810

11+
## Solution summary
12+
13+
The puzzle involves finding triangles and [maximal cliques](https://en.wikipedia.org/wiki/Clique_(graph_theory)). The task is to determine:
14+
15+
- **Part 1**: Find the number of triangles in the graph.
16+
- **Part 2**: Find the size of the largest clique in the graph.
17+
18+
## Parsing the input
19+
20+
Both parts use undirected graphs, represented as:
21+
22+
```scala
23+
type Connection = Map[String, Set[String]]
24+
25+
def parse(input: String): Connection = input
26+
.split('\n')
27+
.toSet
28+
.flatMap { case s"$a-$b" => Set(a -> b, b -> a) } // 1)
29+
.groupMap(_._1)(_._2) // 2)
30+
```
31+
32+
- `1)`: both `a -> b` and `b -> a` are added to the graph so that the graph is undirected.
33+
- `2)`: a fancier way to write `groupBy(_._1).mapValues(_.map(_._2))`, [check the docs](https://www.scala-lang.org/api/3.x/scala/collection/IterableOps.html#groupMap-fffff03a) for details.
34+
35+
## Part 1
36+
37+
The goal is to find triangles that have a computer whose name starts with `t`.
38+
This could be checked by simply checking whether all three vertices are connected to each other, like:
39+
40+
```scala
41+
// connection: Connection
42+
43+
extension (a: String)
44+
inline infix def <->(b: String) =
45+
connection(a).contains(b) && connection(b).contains(a)
46+
47+
def isValidTriangle(vertices: Set[String]): Boolean = vertices.toList match
48+
case List(a, b, c) => a <-> b && b <-> c && c <-> a
49+
case _ => false
50+
```
51+
52+
Then it's just a matter of getting all neighboring vertices of each vertex and checking if they form a triangle:
53+
54+
```scala
55+
def part1(input: String) =
56+
val connection = parse(input)
57+
58+
extension (a: String)
59+
inline infix def <->(b: String) =
60+
connection(a).contains(b) && connection(b).contains(a)
61+
62+
def isValidTriangle(vertices: Set[String]): Boolean = vertices.toList match
63+
case List(a, b, c) => a <-> b && b <-> c && c <-> a
64+
case _ => false
65+
66+
connection
67+
.flatMap { (vertex, neighbors) =>
68+
neighbors
69+
.subsets(2) // 1)
70+
.map(_ + vertex) // 2)
71+
.withFilter(_.exists(_.startsWith("t")))
72+
.filter(isValidTriangle)
73+
}
74+
.toSet
75+
.size
76+
```
77+
78+
- `1)`: chooses two neighbors...
79+
- `2)` ...and adds the vertex itself to form a triangle.
80+
81+
## Part 2
82+
83+
This part is more complex, but there's a generalization of the problem: [finding the size of the largest clique in the graph](https://en.wikipedia.org/wiki/Clique_(graph_theory)). We'll skip the explanation of the algorithm, but here's the code:
84+
85+
```scala
86+
def findMaximumCliqueBronKerbosch(connections: Connection): Set[String] =
87+
def bronKerbosch(
88+
potential: Set[String],
89+
excluded: Set[String] = Set.empty,
90+
result: Set[String] = Set.empty,
91+
): Set[String] =
92+
if (potential.isEmpty && excluded.isEmpty) then result
93+
else
94+
// Choose pivot to minimize branching
95+
val pivot = (potential ++ excluded)
96+
.maxBy(vertex => potential.count(connections(vertex).contains))
97+
98+
val remaining = potential -- connections(pivot)
99+
100+
remaining.foldLeft(Set.empty[String]) { (currentMax, vertex) =>
101+
val neighbors = connections(vertex)
102+
val newClique = bronKerbosch(
103+
result = result + vertex,
104+
potential = potential & neighbors,
105+
excluded = excluded & neighbors,
106+
)
107+
if (newClique.size > currentMax.size) newClique else currentMax
108+
}
109+
110+
bronKerbosch(potential = connections.keySet)
111+
```
112+
113+
Then we could map them over to get the password:
114+
115+
```scala
116+
def part2(input: String) =
117+
val connection = parse(input)
118+
findMaximumCliqueBronKerbosch(connection).toList.sorted.mkString(",")
119+
```
120+
121+
## Final code
122+
123+
```scala
124+
type Connection = Map[String, Set[String]]
125+
126+
def parse(input: String): Connection = input
127+
.split('\n')
128+
.toSet
129+
.flatMap { case s"$a-$b" => Set(a -> b, b -> a) }
130+
.groupMap(_._1)(_._2)
131+
132+
def part1(input: String) =
133+
val connection = parse(input)
134+
135+
extension (a: String)
136+
inline infix def <->(b: String) =
137+
connection(a).contains(b) && connection(b).contains(a)
138+
139+
def isValidTriangle(vertices: Set[String]): Boolean = vertices.toList match
140+
case List(a, b, c) => a <-> b && b <-> c && c <-> a
141+
case _ => false
142+
143+
connection
144+
.flatMap { (vertex, neighbors) =>
145+
neighbors
146+
.subsets(2)
147+
.map(_ + vertex)
148+
.withFilter(_.exists(_.startsWith("t")))
149+
.filter(isValidTriangle)
150+
}
151+
.toSet
152+
.size
153+
154+
def part2(input: String) =
155+
val connection = parse(input)
156+
findMaximumCliqueBronKerbosch(connection).toList.sorted.mkString(",")
157+
158+
def findMaximumCliqueBronKerbosch(connections: Connection): Set[String] =
159+
def bronKerbosch(
160+
potential: Set[String],
161+
excluded: Set[String] = Set.empty,
162+
result: Set[String] = Set.empty,
163+
): Set[String] =
164+
if (potential.isEmpty && excluded.isEmpty) then result
165+
else
166+
// Choose pivot to minimize branching
167+
val pivot = (potential ++ excluded)
168+
.maxBy(vertex => potential.count(connections(vertex).contains))
169+
170+
val remaining = potential -- connections(pivot)
171+
172+
remaining.foldLeft(Set.empty[String]) { (currentMax, vertex) =>
173+
val neighbors = connections(vertex)
174+
val newClique = bronKerbosch(
175+
result = result + vertex,
176+
potential = potential & neighbors,
177+
excluded = excluded & neighbors,
178+
)
179+
if (newClique.size > currentMax.size) newClique else currentMax
180+
}
181+
182+
bronKerbosch(potential = connections.keySet)
183+
```
184+
9185
## Solutions from the community
10186
- [Solution](https://github.com/nikiforo/aoc24/blob/main/src/main/scala/io/github/nikiforo/aoc24/D23T2.scala) by [Artem Nikiforov](https://github.com/nikiforo)
11187
- [Solution](https://github.com/merlinorg/aoc2024/blob/main/src/main/scala/Day23.scala) by [merlinorg](https://github.com/merlinorg)

0 commit comments

Comments
 (0)