From 0acad0121c954db25458c504f2f5d048cdabf47b Mon Sep 17 00:00:00 2001 From: chrisly42 Date: Sun, 24 Dec 2023 14:50:09 +0100 Subject: [PATCH] Day 24. This was a mad thing. I hate you all for using term solving tools under python. Thanks to the other kotlin guy who went the chinese remainder path and give a good amount of inspiration (strangely, the test input fails). I was thinking that some sort of modulo arithmetic could solve it, but it was too hard to figure out. What a waste of an otherwise nice xmas day. --- src/PrimesAndFactors.kt | 2 +- src/Utils.kt | 11 +++ src/aoc2023/Day24.kt | 196 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/aoc2023/Day24.kt diff --git a/src/PrimesAndFactors.kt b/src/PrimesAndFactors.kt index 69e3214..2eb9cec 100644 --- a/src/PrimesAndFactors.kt +++ b/src/PrimesAndFactors.kt @@ -47,7 +47,7 @@ fun primeFactors(n: Long, bits: BitSet): List { if (remainder == 1L) { return trivialFactors } - val factors = ArrayList(trivialFactors) + val factors = ArrayList(trivialFactors) if (!remainder.toBigInteger().isProbablePrime(20)) { val divisor = rhoBrent(remainder) factors.addAll(primeFactors(divisor, bits)) diff --git a/src/Utils.kt b/src/Utils.kt index cbcb20b..4e9e9b0 100644 --- a/src/Utils.kt +++ b/src/Utils.kt @@ -9,6 +9,17 @@ fun Long.lcm(b: Long): Long = this / gcd(b) * b fun Iterable.lcm(): Int = reduce(Int::lcm) fun Iterable.lcm(): Long = reduce(Long::lcm) +fun Iterable>.chineseRemainder(): Long { + val prod = map { it.second.toBigInteger() }.reduce(BigInteger::times) + var sum = BigInteger.ZERO + for ((rem, coprime) in this) { + val cobig = coprime.toBigInteger() + val p = prod / cobig + sum += rem.toBigInteger() * p.modInverse(cobig) * p + } + return (sum % prod).toLong() +} + /** * Reads lines from the given input txt file. */ diff --git a/src/aoc2023/Day24.kt b/src/aoc2023/Day24.kt new file mode 100644 index 0000000..21606ee --- /dev/null +++ b/src/aoc2023/Day24.kt @@ -0,0 +1,196 @@ +package aoc2023 + +import chineseRemainder +import primeFactors +import println +import readInput +import sieveOfErastosthenes +import kotlin.math.abs +import kotlin.math.sign + +/* +--- Day 24: Never Tell Me The Odds --- +It seems like something is going wrong with the snow-making process. Instead of forming snow, the water that's been absorbed into the air seems to be forming hail! +Maybe there's something you can do to break up the hailstones? +Due to strong, probably-magical winds, the hailstones are all flying through the air in perfectly linear trajectories. You make a note of each hailstone's position and velocity (your puzzle input). For example: +19, 13, 30 @ -2, 1, -2 +18, 19, 22 @ -1, -1, -2 +20, 25, 34 @ -2, -2, -4 +12, 31, 28 @ -1, -2, -1 +20, 19, 15 @ 1, -5, -3 + +Each line of text corresponds to the position and velocity of a single hailstone. The positions indicate where the hailstones are right now (at time 0). The velocities are constant and indicate exactly how far each hailstone will move in one nanosecond. +Each line of text uses the format px py pz @ vx vy vz. For instance, the hailstone specified by 20, 19, 15 @ 1, -5, -3 has initial X position 20, Y position 19, Z position 15, X velocity 1, Y velocity -5, and Z velocity -3. After one nanosecond, the hailstone would be at 21, 14, 12. +Perhaps you won't have to do anything. How likely are the hailstones to collide with each other and smash into tiny ice crystals? +To estimate this, consider only the X and Y axes; ignore the Z axis. Looking forward in time, how many of the hailstones' paths will intersect within a test area? (The hailstones themselves don't have to collide, just test for intersections between the paths they will trace.) +In this example, look for intersections that happen with an X and Y position each at least 7 and at most 27; in your actual data, you'll need to check a much larger test area. Comparing all pairs of hailstones' future paths produces the following results: +Hailstone A: 19, 13, 30 @ -2, 1, -2 +Hailstone B: 18, 19, 22 @ -1, -1, -2 +Hailstones' paths will cross inside the test area (at x=14.333, y=15.333). + +Hailstone A: 19, 13, 30 @ -2, 1, -2 +Hailstone B: 20, 25, 34 @ -2, -2, -4 +Hailstones' paths will cross inside the test area (at x=11.667, y=16.667). + +Hailstone A: 19, 13, 30 @ -2, 1, -2 +Hailstone B: 12, 31, 28 @ -1, -2, -1 +Hailstones' paths will cross outside the test area (at x=6.2, y=19.4). + +Hailstone A: 19, 13, 30 @ -2, 1, -2 +Hailstone B: 20, 19, 15 @ 1, -5, -3 +Hailstones' paths crossed in the past for hailstone A. + +Hailstone A: 18, 19, 22 @ -1, -1, -2 +Hailstone B: 20, 25, 34 @ -2, -2, -4 +Hailstones' paths are parallel; they never intersect. + +Hailstone A: 18, 19, 22 @ -1, -1, -2 +Hailstone B: 12, 31, 28 @ -1, -2, -1 +Hailstones' paths will cross outside the test area (at x=-6, y=-5). + +Hailstone A: 18, 19, 22 @ -1, -1, -2 +Hailstone B: 20, 19, 15 @ 1, -5, -3 +Hailstones' paths crossed in the past for both hailstones. + +Hailstone A: 20, 25, 34 @ -2, -2, -4 +Hailstone B: 12, 31, 28 @ -1, -2, -1 +Hailstones' paths will cross outside the test area (at x=-2, y=3). + +Hailstone A: 20, 25, 34 @ -2, -2, -4 +Hailstone B: 20, 19, 15 @ 1, -5, -3 +Hailstones' paths crossed in the past for hailstone B. + +Hailstone A: 12, 31, 28 @ -1, -2, -1 +Hailstone B: 20, 19, 15 @ 1, -5, -3 +Hailstones' paths crossed in the past for both hailstones. + +So, in this example, 2 hailstones' future paths cross inside the boundaries of the test area. +However, you'll need to search a much larger test area if you want to see if any hailstones might collide. Look for intersections that happen with an X and Y position each at least 200000000000000 and at most 400000000000000. Disregard the Z axis entirely. +Considering only the X and Y axes, check all pairs of hailstones' future paths for intersections. How many of these intersections occur within the test area? + +*/ +fun main() { + + val inlineTestInput = """ +19, 13, 30 @ -2, 1, -2 +18, 19, 22 @ -1, -1, -2 +20, 25, 34 @ -2, -2, -4 +12, 31, 28 @ -1, -2, -1 +20, 19, 15 @ 1, -5, -3 +""" + + data class Hailstone(val px: Long, val py: Long, val pz: Long, val vx: Int, val vy: Int, val vz: Int) + + fun part1(input: List, testx: LongRange, testy: LongRange): Int { + val hailstones = input.map { + val (x1, y1, z1, x2, y2, z2) = "(\\d+), (\\d+), (\\d+) @ ([-\\d]+), ([-\\d]+), ([-\\d]+)".toRegex() + .matchEntire(it)?.destructured!! + Hailstone(x1.toLong(), y1.toLong(), z1.toLong(), x2.toInt(), y2.toInt(), z2.toInt()) + } + var count = 0 + for (h1 in hailstones.indices) { + for (h2 in h1 + 1..hailstones.lastIndex) { + val s1 = hailstones[h1] + val s2 = hailstones[h2] + + val dx = (s2.px - s1.px).toDouble() + val dy = (s2.py - s1.py).toDouble() + + val d = s1.vx.toDouble() * s2.vy.toDouble() - + s1.vy.toDouble() * s2.vx.toDouble() + + val t = dx * s2.vy.toDouble() - dy * s2.vx.toDouble() + val u = dx * s1.vy.toDouble() - dy * s1.vx.toDouble() + + if (t * d.sign > 0 && u * d.sign > 0) { + val hx = (s1.px + (t * s1.vx.toDouble()) / d) + val hy = (s1.py + (t * s1.vy.toDouble()) / d) + if (hx.toLong() in testx && hy.toLong() in testy) count++ + } + } + } + return count + } + + fun part2(input: List): Long { + val hailstones = input.map { + val (x1, y1, z1, x2, y2, z2) = "(\\d+), (\\d+), (\\d+) @ ([-\\d]+), ([-\\d]+), ([-\\d]+)".toRegex() + .matchEntire(it)?.destructured!! + Hailstone(x1.toLong(), y1.toLong(), z1.toLong(), x2.toInt(), y2.toInt(), z2.toInt()) + }//.sortedBy { abs(it.vx) + abs(it.vy) + abs(it.vz) } + //.take(3) + + val h1 = hailstones[0] + val h2 = hailstones[1] + val h3 = hailstones[2] + + // rx = h1.px + t1*(h1.vx - rvx) = h2.px + t2*(h2.vx - rvx) = h3.px + t3*(h3.vx - rvx) + // ry = h1.py + t1*(h1.vy - rvy) = h2.py + t2*(h2.vy - rvy) = h3.py + t3*(h3.vy - rvy) + // rz = h1.pz + t1*(h1.vz - rvz) = h2.pz + t2*(h2.vz - rvz) = h3.pz + t3*(h3.vz - rvz) + + // [1] h1.px - h2.px = t2*(h2.vx - rvx) - t1*(h1.vx - rvx) + // [2] h1.px - h3.px = t3*(h3.vx - rvx) - t1*(h1.vx - rvx) + // [3] h2.px - h3.px = t3*(h3.vx - rvx) - t2*(h2.vx - rvx) + + // [4] h1.py - h2.py = t2*(h2.vy - rvy) - t1*(h1.vy - rvy) + // [5] h1.py - h3.py = t3*(h3.vy - rvy) - t1*(h1.vy - rvy) + // [6] h2.py - h3.py = t3*(h3.vy - rvy) - t2*(h2.vy - rvy) + + // [7] h1.pz - h2.pz = t2*(h2.vz - rvz) - t1*(h1.vz - rvz) + // [8] h1.pz - h3.pz = t3*(h3.vz - rvz) - t1*(h1.vz - rvz) + // [9] h2.pz - h3.pz = t3*(h3.vz - rvz) - t2*(h2.vz - rvz) + + val sieve = sieveOfErastosthenes(200) + for (rvx in -1000..1000) { + val speedMods = hailstones.mapNotNull { if (it.vx - rvx != 0) abs(it.vx - rvx).toLong() to (it.px % abs(it.vx - rvx)) else null } + + val factorMap = HashMap() + var good = true + reducePairs@ for ((speed, remain) in speedMods) { + for (pf in primeFactors(speed, sieve)) { + val sf = factorMap[pf] + val pr = remain % pf + if (sf != null && sf != pr) { + good = false + break@reducePairs + } + factorMap[pf] = pr + } + } + if (!good) continue + val reducedPairs = factorMap.map { it.value to it.key }.sortedBy { it.second } + + val rx = reducedPairs.chineseRemainder() + //val rx = 24 + + val bang1Time = (h1.px - rx) / (rvx - h1.vx) + val bang2Time = (h2.px - rx) / (rvx - h2.vx) + val h1px = h1.px + h1.vx * bang1Time + val h1py = h1.py + h1.vy * bang1Time + val h1pz = h1.pz + h1.vz * bang1Time + val h2px = h2.px + h2.vx * bang2Time + val h2py = h2.py + h2.vy * bang2Time + val h2pz = h2.pz + h2.vz * bang2Time + val rvy = (h2py - h1py) / (bang2Time - bang1Time) + val rvz = (h2pz - h1pz) / (bang2Time - bang1Time) + val ry = h1py - bang1Time * rvy + val rz = h1pz - bang1Time * rvz + return rx + ry + rz + } + return 0L + } + + // test if implementation meets criteria from the description, like: + val testInput = inlineTestInput.trim().reader().readLines() + //val testInput = readInput("aoc2023/Day24_test") + val testInputPart1Result = part1(testInput, 7L..27L, 7L..27L) + println("Part 1 Test: $testInputPart1Result") + val testInputPart2Result = part2(testInput) + println("Part 2 Test: $testInputPart2Result") + check(testInputPart1Result == 2) + //check(testInputPart2Result == 47L) + + val input = readInput("aoc2023/Day24") + part1(input, 200000000000000L..400000000000000L, 200000000000000L..400000000000000L).println() + part2(input).println() +}