Skip to content

Commit ed23b92

Browse files
committed
add Day 10: Adapter Array
1 parent bba2f3c commit ed23b92

File tree

16 files changed

+849
-21
lines changed

16 files changed

+849
-21
lines changed

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ If you are into programming, logic, maybe also a little into competition, this o
2020
| 6 |Custom Customs |Whoop, whoop, simple one again. What is Eric doing?|
2121
| 7 |Handy Haversacks |Bags inside bags, inside bags. Challenge deals with clean parsing and recursive search.|
2222
| 8 |Handheld Halting |Finally, we got a CPU emulator in 2020. Quite primitive, but still!|
23+
| 9 |Encoding Error |Handling of a bunch of numbers with attributes to check.|
24+
| 10 |Adapter Array |Part 2 turns out to be the hardest puzzle so far! It's not about path finding...|
2325

2426
## My logbook of 2020
2527

@@ -127,4 +129,37 @@ in front of the number to check can easily be derived by the Kotlin fun `windowe
127129
check out of the box.
128130
In part 2 a sequence of continuous numbers needs to be found that sums up to part 1's solution.
129131
In my cleaner version, a running fold operation produces a sequence of min/max/sum triples starting at a given position
130-
which can be taken until the summed value is greater than the target.
132+
which can be taken until the summed value is greater than the target.
133+
134+
### Day 10: Adapter Array
135+
This one got me! I immediately had a path finding algorithm in my head when I speed-read through part 1 - but that
136+
turned out to be fatal...
137+
We have a number of "jolt" values for adapter ratings given. Each adapter can be plugged into a predecessor adapter
138+
with a rating 1 through 3 lower than its own.
139+
What I did not get, was the part clearly stating that *all* adapters have to be used for part 1. So, basically sorting
140+
the given values and adding a "0 rating" to the front (the outlet) and a final "max+3 rating" (the device) to the end
141+
and then getting the differences (gaps!) between them was all.
142+
Simply a one-liner!
143+
144+
`
145+
val gaps = (adapters + listOf(0, adapters.maxOrNull()!! + 3)).sorted().windowed(size = 2, step = 1).map { it[1] - it[0] }
146+
`
147+
148+
for all the gaps and a `gaps.count { it == 1 } * gaps.count { it == 3 }` for the solution!
149+
I did not see that for over half an hour, trying to get my path finding algorithm to work. Sad.
150+
151+
Part 2 though was a math challenge. How many combinations are there to use the adapters (not necessarily all!)?
152+
Looking at the gaps and having part 1 in mind revealed the pattern that there are only 1-jolt and 3-jolt gaps.
153+
154+
Let's make this thought experience:
155+
156+
If we had *only* 3-jolt gaps, there would be only one "path" of adapters from 0 to the end. No other adapter in-between
157+
would fit.
158+
159+
For the 1-jolt gaps, you can look at it like this: how many ways are there to "hop through" the gaps when the maximum
160+
step size is 3? For example: 1-1-1 has 4 possible ways to get through: a single 3-jolt hop, 2 - 1, 1 - 2 or 1 - 1 - 1
161+
162+
So the answer to the overall paths possible is the product of all 1-jolt gap runs possibilities (broken by any amount
163+
of 3-jolt gaps)!
164+
How many are there: turns out, this is the number of compositions of the length as a sum of the values 1, 2 and 3! In math
165+
this is called an A-restricted composition!

puzzles/2020/day10.txt

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
54
2+
91
3+
137
4+
156
5+
31
6+
70
7+
143
8+
51
9+
50
10+
18
11+
1
12+
149
13+
129
14+
151
15+
95
16+
148
17+
41
18+
144
19+
7
20+
125
21+
155
22+
14
23+
114
24+
108
25+
57
26+
118
27+
147
28+
24
29+
25
30+
73
31+
26
32+
8
33+
115
34+
44
35+
12
36+
47
37+
106
38+
120
39+
132
40+
121
41+
35
42+
105
43+
60
44+
9
45+
6
46+
65
47+
111
48+
133
49+
38
50+
138
51+
101
52+
126
53+
39
54+
78
55+
92
56+
53
57+
119
58+
136
59+
154
60+
140
61+
52
62+
15
63+
90
64+
30
65+
40
66+
64
67+
67
68+
139
69+
76
70+
32
71+
98
72+
113
73+
80
74+
13
75+
104
76+
86
77+
27
78+
61
79+
157
80+
79
81+
122
82+
59
83+
150
84+
89
85+
158
86+
107
87+
77
88+
112
89+
5
90+
83
91+
58
92+
21
93+
2
94+
66

src/main/kotlin/AdventOfCode.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ abstract class Day(val day: Int, private val year: Int = 2020, val title: String
7878

7979
fun solve() {
8080
header
81-
runWithTiming(1) { part1 }
82-
runWithTiming(2) { part2 }
81+
runWithTiming("1") { part1 }
82+
runWithTiming("2") { part2 }
8383
}
8484

8585
fun <T> T.show(prompt: String = "", maxLines: Int = 10): T {
@@ -138,16 +138,17 @@ abstract class Day(val day: Int, private val year: Int = 2020, val title: String
138138
}
139139
}
140140

141-
private inline fun runWithTiming(part: Int, f: () -> Any?) {
142-
var result: Any?
143-
val millis = measureTimeMillis { result = f() }
144-
val duration = if (millis < 1000) "$millis ms" else "${"%.3f".format(millis / 1000.0)} s"
145-
println("\nSolution $part: (took $duration)\n$result")
146-
}
147141
}
148142

149143
}
150144

145+
inline fun runWithTiming(part: String, f: () -> Any?) {
146+
var result: Any?
147+
val millis = measureTimeMillis { result = f() }
148+
val duration = if (millis < 1000) "$millis ms" else "${"%.3f".format(millis / 1000.0)} s"
149+
println("\nSolution $part: (took $duration)\n$result")
150+
}
151+
151152
@Suppress("unused")
152153
class ParserContext(private val columnSeparator: Regex, private val line: String) {
153154
val cols: List<String> by lazy { line.split(columnSeparator) }
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import utils.productAsLong
2+
import utils.restrictedCompositionsOf
3+
import utils.runsOf
4+
5+
class Day10 : Day(10, title = "Adapter Array") {
6+
7+
private val adapterRatings = inputAsInts
8+
private val maxRating = (adapterRatings.maxOrNull()!! + 3).show("Max Jolt")
9+
private val allRatings = (adapterRatings + listOf(0, maxRating)).sorted()
10+
private val joltDifferences = allRatings.windowed(2, 1).map { it[1] - it[0] }
11+
12+
override fun part1() = joltDifferences.count { it == 1 } * joltDifferences.count { it == 3 }
13+
14+
override fun part2() = joltDifferences.runsOf(1).map { numberOfPossibleWays(it) }.productAsLong()
15+
16+
private fun numberOfPossibleWays(length: Int) =
17+
restrictedCompositionsOf(length, restriction = 1..3).count()
18+
19+
// the "brute force" path finding using a cache works, too
20+
private val cache = mutableMapOf<Int, Long>()
21+
22+
private fun countWays(inJolts: Int = 0): Long {
23+
if (inJolts == maxRating) return 1L
24+
val fits = allRatings.filter { rating -> inJolts in rating - 3 until rating }
25+
return fits.sumOf { cache.getOrPut(it) { countWays(it) } }
26+
}
27+
28+
fun part2BruteForce() = countWays()
29+
30+
}
31+
32+
fun main() {
33+
with (Day10()) {
34+
solve()
35+
runWithTiming("2 - using plain recursive path calculation") { part2BruteForce() }
36+
}
37+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package utils
2+
3+
// https://en.wikipedia.org/wiki/Composition_(combinatorics)
4+
5+
/**
6+
* In mathematics, a composition of an integer n is a way of writing n as the sum of a sequence of (strictly)
7+
* positive integers.
8+
* @param n the number to be composed
9+
* @param minParts the minimum number of parts for the compositions created
10+
* @param maxParts the maximum number of parts for the compositions created
11+
*/
12+
fun compositionsOf(n: Int, minParts: Int = 0, maxParts: Int = Int.MAX_VALUE): Sequence<List<Int>> =
13+
when {
14+
minParts > maxParts || maxParts <= 0 || minParts < 0 -> emptySequence()
15+
n <= 0 -> emptySequence()
16+
n < minParts -> emptySequence()
17+
maxParts == 1 -> sequenceOf(listOf(n))
18+
else -> sequence {
19+
val newMin = (minParts - 1).coerceAtLeast(0)
20+
val newMax = maxParts - 1
21+
(n downTo 1).forEach { v ->
22+
if (n == v && newMin == 0)
23+
yield(listOf(n))
24+
else
25+
compositionsOf(n - v, newMin, newMax).forEach { yield(listOf(v) + it) }
26+
}
27+
}
28+
}
29+
30+
fun kCompositionsOf(n: Int, k: Int): Sequence<List<Int>> = compositionsOf(n, k, k)
31+
32+
fun restrictedCompositionsOf(
33+
n: Int,
34+
restriction: IntRange,
35+
minParts: Int = 0,
36+
maxParts: Int = Int.MAX_VALUE
37+
): Sequence<List<Int>> =
38+
when {
39+
minParts > maxParts || maxParts <= 0 || minParts < 0 -> emptySequence()
40+
n <= 0 -> emptySequence()
41+
n < minParts * restriction.first.coerceAtLeast(1) -> emptySequence()
42+
maxParts == 1 -> if (n in restriction) sequenceOf(listOf(n)) else emptySequence()
43+
else -> sequence {
44+
val newMin = (minParts - 1).coerceAtLeast(0)
45+
val newMax = maxParts - 1
46+
(n.coerceAtMost(restriction.last) downTo restriction.first.coerceAtLeast(1)).forEach { v ->
47+
if (v == n && newMin == 0)
48+
yield(listOf(n))
49+
else
50+
restrictedCompositionsOf(n - v, restriction, newMin, newMax).forEach { yield(listOf(v) + it) }
51+
}
52+
}
53+
}
54+
55+
fun restrictedCompositionsOf(n: Int, parts: List<Int>): Sequence<List<Int>> {
56+
val availableParts = parts.toMutableList().apply { sortDescending() }
57+
58+
fun restrictedCompositions(n: Int): Sequence<List<Int>> = when {
59+
n <= 0 -> emptySequence()
60+
availableParts.isEmpty() -> emptySequence()
61+
availableParts.sum() < n -> emptySequence()
62+
else -> sequence {
63+
val possible = availableParts.filter { it <= n }
64+
possible.forEach { v ->
65+
if (v == n)
66+
yield(listOf(n))
67+
else {
68+
availableParts -= v
69+
restrictedCompositions(n - v).forEach { yield(listOf(v) + it) }
70+
availableParts += v
71+
}
72+
}
73+
}
74+
}
75+
76+
return restrictedCompositions(n)
77+
}
78+
79+
/**
80+
* In mathematics, a composition of an integer n is a way of writing n as the sum of a sequence of (strictly)
81+
* positive integers. Two sequences that differ in the order of their terms define different compositions of
82+
* their sum, while they are considered to define the same partition of that number.
83+
* @param n the number to be composed
84+
* @param maxParts the maximum number of parts for the partitions created
85+
* @param maxN limit the highest used number to maxN
86+
*/
87+
fun partitionsOf(n: Int, minParts: Int = 0, maxParts: Int = Int.MAX_VALUE, maxN: Int = n): Sequence<Collection<Int>> =
88+
when {
89+
minParts > maxParts || maxParts <= 0 || minParts < 0 -> emptySequence()
90+
n <= 0 -> emptySequence()
91+
n < minParts -> emptySequence()
92+
maxParts == 1 -> if (n <= maxN) sequenceOf(listOf(n)) else emptySequence()
93+
else -> sequence {
94+
val newMin = (minParts - 1).coerceAtLeast(0)
95+
val newMax = maxParts - 1
96+
(n.coerceAtMost(maxN) downTo 1).forEach { v ->
97+
if (v == n && newMin == 0)
98+
yield(listOf(n))
99+
else {
100+
partitionsOf(n - v, newMin, newMax, v).forEach { yield(listOf(v) + it) }
101+
}
102+
}
103+
}
104+
}
105+
106+
fun kPartitionsOf(n: Int, k: Int): Sequence<Collection<Int>> = partitionsOf(n, k, k)
107+
108+
fun restrictedPartitionsOf(n: Int, parts: List<Int>): Sequence<List<Int>> {
109+
110+
fun restrictedPartitions(n: Int, availableParts: List<Int>): Sequence<List<Int>> = when {
111+
n <= 0 -> sequenceOf(emptyList())
112+
availableParts.sum() < n -> emptySequence()
113+
availableParts.last() > n -> emptySequence()
114+
else -> sequence {
115+
val firstFitting = availableParts.indexOfFirst { it <= n }
116+
(firstFitting until availableParts.size).forEach { idx ->
117+
val v = availableParts[idx]
118+
restrictedPartitions(n - v, availableParts.subList(idx + 1, availableParts.size)).forEach {
119+
yield(listOf(v) + it)
120+
}
121+
}
122+
}
123+
}
124+
125+
return restrictedPartitions(n, parts.sortedDescending())
126+
}
127+
128+
/**
129+
* A weak composition of an integer n is similar to a composition of n, but allowing terms of the sequence
130+
* to be zero: it is a way of writing n as the sum of a sequence of non-negative integers.
131+
* @param n the number to be composed
132+
* @param k the number of parts for the weak compositions created
133+
* @param maxN limit the highest used number to maxN
134+
*/
135+
fun weakCompositionsOf(n: Int, k: Int, maxN: Int = n): Sequence<List<Int>> =
136+
when {
137+
k <= 0 -> emptySequence()
138+
k == 1 -> if (n <= maxN) sequenceOf(listOf(n)) else emptySequence()
139+
else -> sequence {
140+
(n.coerceAtMost(maxN) downTo 0).forEach { v ->
141+
weakCompositionsOf(n - v, k - 1, maxN).forEach { yield(listOf(v) + it) }
142+
}
143+
}
144+
}
145+

0 commit comments

Comments
 (0)