Day 7: Bridge Repair
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Iβm way behind, but Iβm trying to learn F#.
Iβm using the library Combinatorics in dotnet, which Iβve used in the past, generate in this case every duplicating possibility of the operations. I the only optimization that I did was to use a function to concatenate numbers without converting to strings, but that didnβt actually help much.
I have parser helpers that use ReadOnlySpans over strings to prevent unnecessary allocations. However, here Iβm adding to a C# mutable list and then converting to an FSharp (linked) list, which this language is more familiar with. Not optimal, but runtime was pretty good.
Iβm not terribly good with F#, but I think I did ok for this challenge.
F#
// in another file:
let concatenateLong (a:Int64) (b:Int64) : Int64 =
let rec countDigits (n:int64) =
if n = 0 then 0
else 1 + countDigits (n / (int64 10))
let bDigits = if b = 0 then 1 else countDigits b
let multiplier = pown 10 bDigits |> int64
a * multiplier + b
// challenge file
type Operation = {Total:Int64; Inputs:Int64 list }
let parse (s:ReadOnlySpan<char>) : Operation =
let sep = s.IndexOf(':')
let total = Int64.Parse(s.Slice(0, sep))
let inputs = System.Collections.Generic.List<Int64>()
let right:ReadOnlySpan<char> = s.Slice(sep + 1).Trim()
// because the Split function on a span returns a SpanSplitEnumerator, which is a ref-struct and can only live on the stack,
// I can't use the F# list syntax here
for range in right.Split(" ") do
inputs.Add(Int64.Parse(sliceRange right range))
{Total = total; Inputs = List.ofSeq(inputs) }
let part1Ops = [(+); (*)]
let execute ops input =
input
|> PSeq.choose (fun op ->
let total = op.Total
let inputs = op.Inputs
let variations = Variations(ops, inputs.Length - 1, GenerateOption.WithRepetition)
variations
|> Seq.tryFind (fun v ->
let calcTotal = (inputs[0], inputs[1..], List.ofSeq(v)) |||> List.fold2 (fun acc n f -> f acc n)
calcTotal = total
)
|> Option.map(fun _ -> total)
)
|> PSeq.fold (fun acc n -> acc + n) 0L
let part1 input =
(read input parse)
|> execute part1Ops
let part2Ops = [(+); (*); concatenateLong]
let part2 input = (read input parse) |> execute part2Ops
The Gen0 garbage collection looks absurd, but Gen0 is generally considered βfreeβ.
Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|
Part1 | 19.20 ms | 0.372 ms | 0.545 ms | 17843.7500 | 156.2500 | 106.55 MB |
Part2 | 17.94 ms | 0.355 ms | 0.878 ms | 17843.7500 | 156.2500 | 106.55 MB |
V2 - concatenate numbers did little for the runtime, but did help with Gen1 garbage, but not the overall allocation.
Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|
Part1 | 17.34 ms | 0.342 ms | 0.336 ms | 17843.7500 | 125.0000 | 106.55 MB |
Part2 | 17.24 ms | 0.323 ms | 0.270 ms | 17843.7500 | 93.7500 | 106.55 MB |
Kotlin
I finally got around to doing day 7. I try the brute force method (takes several seconds), but Iβm particularly proud of my sequence generator for operation permutations.
The Collection
method is in the file Utils.kt
, which can be found in my repo.
Solution
import kotlin.collections.any
import kotlin.math.pow
fun main() {
fun part1(input: List<String>): Long {
val operations = setOf(CalibrationOperation.Plus, CalibrationOperation.Multiply)
return generalizedSolution(input, operations)
}
fun part2(input: List<String>): Long {
val operations = setOf(CalibrationOperation.Plus, CalibrationOperation.Multiply, CalibrationOperation.Concat)
return generalizedSolution(input, operations)
}
val testInput = readInput("Day07_test")
check(part1(testInput) == 3749L)
check(part2(testInput) == 11387L)
val input = readInput("Day07")
part1(input).println()
part2(input).println()
}
fun parseInputDay7(input: List<String>) = input.map {
val calibrationResultAndInput = it.split(':')
calibrationResultAndInput[0].toLong() to calibrationResultAndInput[1].split(' ').filter { it != "" }.map { it.toLong() }
}
fun generalizedSolution(input: List<String>, operations: Set<CalibrationOperation>): Long {
val parsedInput = parseInputDay7(input)
val operationsPermutations = CalibrationOperation.operationPermutationSequence(*operations.toTypedArray()).take(calculatePermutationsNeeded(parsedInput, operations)).toList()
return sumOfPossibleCalibrationEquations(parsedInput, operationsPermutations)
}
fun calculatePermutationsNeeded(parsedInput: List<Pair<Long, List<Long>>>, operations: Set<CalibrationOperation>): Int {
val highestNumberOfOperations = parsedInput.maxOf { it.second.size - 1 }
return (1..highestNumberOfOperations).sumOf { operations.size.toDouble().pow(it).toInt() }
}
fun sumOfPossibleCalibrationEquations(parsedInput: List<Pair<Long, List<Long>>>, operationPermutationCollection: Collection<OperationPermutation>): Long {
val permutationsGrouped = operationPermutationCollection.groupBy { it.size }
return parsedInput.sumOf { (equationResult, equationInput) ->
if (permutationsGrouped[equationInput.size - 1]!!.any { operations ->
equationResult == equationInput.drop(1)
.foldIndexed(equationInput[0]) { index, acc, lng -> operations[index](acc, lng) }
}) equationResult else 0
}
}
typealias OperationPermutation = List<CalibrationOperation>
sealed class CalibrationOperation(val operation: (Long, Long) -> Long) {
operator fun invoke(a: Long, b: Long) = operation(a, b)
object Plus : CalibrationOperation({ a: Long, b: Long -> a + b })
object Multiply : CalibrationOperation({ a: Long, b: Long -> a * b })
object Concat : CalibrationOperation({ a: Long, b: Long -> "$a$b".toLong() })
companion object {
fun operationPermutationSequence(vararg operations: CalibrationOperation) = sequence<OperationPermutation> {
val cache = mutableListOf<OperationPermutation>()
val calculateCacheRange = { currentLength: Int ->
val sectionSize = operations.size.toDouble().pow(currentLength - 1).toInt()
val sectionStart = (1 until currentLength - 1).sumOf { operations.size.toDouble().pow(it).toInt() }
sectionStart..(sectionStart + sectionSize - 1)
}
// Populate the cache with initial values for permutation length 1.
operations.forEach { operation -> yield(listOf(operation).also { cache.add(it) }) }
var currentLength = 2
var offset = 0
var cacheRange = calculateCacheRange(currentLength)
var rotatingOperations = operations.toList()
yieldAll(
generateSequence {
if (cacheRange.count() == offset) {
rotatingOperations = rotatingOperations.rotated(1)
if (rotatingOperations.first() == operations.first()) {
currentLength++
}
offset = 0
cacheRange = calculateCacheRange(currentLength)
}
val cacheSlice = cache.slice(cacheRange)
return@generateSequence (cacheSlice[offset] + rotatingOperations.first()).also {
cache += it
offset++
}
}
)
}
}
}
Julia
Took quite some time to debug but in the end I think itβs a nice solution using base 2 and 3 numbers counting up to check all operator combinations.
Code
function readInput(inputFile::String)::Vector{Vector{Int}}
f = open(inputFile,"r")
lines::Vector{String} = readlines(f)
close(f)
equations::Vector{Vector{Int}} = []
function getValues(line::String)
return map(sp->parse(Int,sp),(sp=split(line," ");sp[1]=sp[1][1:end-1];sp))
end
map(l->push!(equations,getValues(l)),lines)
return equations
end
function checkEq(eq::Vector{Int},withConCat::Bool)::Bool
function calcEq(eq::Vector{Int},operators::Vector{Int},withConCat::Bool)::Int
res::Int = eq[2]
for (i,op) in enumerate(operators)
if op == 0 #+
res += eq[i+2]
elseif op ==1 #*
res *= eq[i+2]
else #op==2 ||
res = parse(Int,string(res)*string(eq[i+2]))
end
end
return res
end
opInt::Int = 0
operators = Vector{Int}(undef,length(eq)-2)
while opInt < (withConCat ? 3^(length(eq)-2) : 2^(length(eq)-2))
withConCat==true ? operators=digits(opInt,base=3,pad=length(eq)-2) : operators=digits(opInt,base=2,pad=length(eq)-2)
#calcEq(eq,operators,withConCat)==eq[1] ? (return true) : opInt -= 1
calcEq(eq,operators,withConCat)==eq[1] ? (return true) : opInt += 1
end
return false
end
function calcTotCalRes(equations::Vector{Vector{Int}},withConCat::Bool)::Int
totCalRes::Int = 0
for e in equations
checkEq(e,withConCat) ? totCalRes+=e[1] : nothing
end
return totCalRes
end
@info "Part 1"
println("result: $(calcTotCalRes(readInput("day07Input"),false))")
@info "Part 2"
println("result: $(calcTotCalRes(readInput("day07Input"),true))")
Lisp
Could probably go much faster if I kept track of calculations to not repeat, but 4 seconds for part 2 on my old laptop is good enough for me. Also, not really a big change from part 1 to part 2.
Part 1 and 2
(defstruct calibration result inputs)
(defun p1-process-line (line)
(let ((parts (str:words line)))
(make-calibration :result (parse-integer (car parts) :junk-allowed t)
:inputs (mapcar #'parse-integer (cdr parts)))))
(defun apply-opperators (c opps)
(let ((accum (car (calibration-inputs c))))
(loop for o in opps
for v in (cdr (calibration-inputs c))
until (> accum (calibration-result c))
if (eql o 'ADD)
do (setf accum (+ accum v))
else if (eql o 'MUL)
do (setf accum (* accum v))
else
do (setf accum (+ v (* accum (expt 10 (1+ (floor (log v 10)))))))
finally (return accum)
)))
(defun generate-operators (item-count)
(labels ((g-rec (c results)
(if (< c 1)
results
(g-rec (1- c) (loop for r in results
collect (cons 'ADD r)
collect (cons 'MUL r))))))
(g-rec (1- item-count) '((ADD) (MUL)))))
(defun generate-ops-hash (c gen-ops)
(let ((h (make-hash-table)))
(dotimes (x c)
(setf (gethash (+ 2 x) h) (funcall gen-ops (+ 1 x))))
h))
(defun validate-calibration (c ops-h)
(let ((r (calibration-result c))
(ops (gethash (length (calibration-inputs c)) ops-h)))
(loop for o in ops
for v = (apply-opperators c o)
when (= v r)
return t)))
(defun run-p1 (file)
(let ((calibrations (read-file file #'p1-process-line))
(ops (generate-ops-hash 13 #'generate-operators)))
(loop for c in calibrations
when (validate-calibration c ops)
sum (calibration-result c))))
(defun generate-operators-p2 (item-count)
(labels ((g-rec (c results)
(if (< c 1)
results
(g-rec (1- c) (loop for r in results
collect (cons 'ADD r)
collect (cons 'MUL r)
collect (cons 'CAT r))))))
(g-rec (1- item-count) '((ADD) (MUL) (CAT)))))
(defun run-p2 (file)
(let ((calibrations (read-file file #'p1-process-line))
(ops (generate-ops-hash 13 #'generate-operators-p2)))
(loop for c in calibrations
when (validate-calibration c ops)
sum (calibration-result c))))
TypeScript
I wrote my own iterator because Iβm a big dummy. Also brute forced (~8s). Might be worth adding a cache to skip all the questions that have been computed / done.
Solution
import { AdventOfCodeSolutionFunction } from "./solutions";
function MakeCombination<T>(choices: Array<T>, state: Array<number>): Array<T> {
return state.map((v) => choices[v]);
}
function MakeStateArray(length: number) {
const newArray = [];
while (length-- > 0)
newArray.push(0);
return newArray;
}
function IncrementState(state: Array<number>, max: number): [state: Array<number>, overflow: boolean] {
state[0]++;
for (let index = 0; index < state.length; index++) {
if (state[index] == max) {
state[index] = 0;
if (index + 1 == state.length)
return [state, true];
state[index + 1]++;
}
}
return [state, false];
}
function GenerateCombinations<T>(choices: Array<T>, length: number): Array<Array<T>> {
const states = MakeStateArray(length);
const combinations: Array<Array<T>> = [];
let done = false
while (!done) {
combinations.push(MakeCombination(choices, states));
done = IncrementState(states, choices.length)[1];
}
return combinations;
}
enum Op {
MUL = "*",
ADD = "+",
CON = "|",
}
function ApplyOp(a: number, b: number, op: Op): number {
switch (op) {
case Op.MUL:
return a * b;
case Op.ADD:
return a + b;
case Op.CON:
return Number(`${a}${b}`);
}
}
function ApplyOperatorsToNumbers(numbers: Array<number>, ops: Array<Op>): number {
let prev = ApplyOp(numbers[0], numbers[1], ops[0]);
for (let index = 2; index < numbers.length; index++) {
prev = ApplyOp(prev, numbers[index], ops[index - 1])
}
return prev;
}
export const solution_7: AdventOfCodeSolutionFunction = (input) => {
const numbers = // [{target: 123, numbers: [1, 2, 3, ...]}, ...]
input.split("\n")
.map(
(v) => v.trim()
.split(":")
.map(v => v.trim().split(" ").map(v => Number(v)))
).map(
(v) => {
return { target: v[0][0], numbers: v[1] }
}
);
let part_1 = 0;
let part_2 = 0;
for (let index = 0; index < numbers.length; index++) {
const target = numbers[index].target;
const numbs = numbers[index].numbers;
// GenerateCombinations(["+", "*"], 2) => [["+", "+"], ["+", "*"], ["*", "+"], ["*", "*"]]
const combinations = GenerateCombinations([Op.ADD, Op.MUL], numbs.length - 1);
// part 1 calculations
for (let combinationIndex = 0; combinationIndex < combinations.length; combinationIndex++) {
const combination = combinations[combinationIndex];
const result = ApplyOperatorsToNumbers(numbs, combination);
if (result == target) {
part_1 += result;
break;
}
}
const combinations2 = GenerateCombinations([Op.ADD, Op.MUL, Op.CON], numbs.length - 1);
// part 2 calculations
for (let combinationIndex = 0; combinationIndex < combinations2.length; combinationIndex++) {
const combination = combinations2[combinationIndex];
const result = ApplyOperatorsToNumbers(numbs, combination);
if (result == target) {
part_2 += result;
break;
}
}
}
return {
part_1,
part_2,
}
}