From dc0aa1ce3031ade3bb69c86492ecc7ccd08f9b86 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 13 Dec 2023 12:59:57 +0000 Subject: [PATCH] Day 13 (#15) Co-authored-by: Smaug123 Reviewed-on: https://gitea.patrickstevens.co.uk/patrick/advent-of-code-2023/pulls/15 --- .../AdventOfCode2023.FSharp.Bench/Inputs.fs | 2 +- .../AdventOfCode2023.FSharp.Bench/Program.fs | 2 +- .../AdventOfCode2023.FSharp.Bench/Run.fs | 13 ++ .../AdventOfCode2023.FSharp.Lib.fsproj | 1 + .../AdventOfCode2023.FSharp.Lib/Day13.fs | 191 ++++++++++++++++++ .../AdventOfCode2023.FSharp/Program.fs | 16 ++ AdventOfCode2023.FSharp/Test/Test.fsproj | 2 + AdventOfCode2023.FSharp/Test/TestDay13.fs | 68 +++++++ .../Test/samples/day13.txt | 15 ++ 9 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day13.fs create mode 100644 AdventOfCode2023.FSharp/Test/TestDay13.fs create mode 100644 AdventOfCode2023.FSharp/Test/samples/day13.txt diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Inputs.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Inputs.fs index 5646ffa..11956a7 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Inputs.fs +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Inputs.fs @@ -14,6 +14,6 @@ module Inputs = if isNull dir then failwith "reached root of filesystem without finding inputs dir" - Array.init 12 (fun day -> Path.Combine (dir.FullName, "inputs", $"day%i{day + 1}.txt") |> File.ReadAllText) + Array.init 13 (fun day -> Path.Combine (dir.FullName, "inputs", $"day%i{day + 1}.txt") |> File.ReadAllText) let inline day (i : int) = days.[i - 1] diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Program.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Program.fs index 8a2d560..7395103 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Program.fs +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Program.fs @@ -44,7 +44,7 @@ module Benchmarks = [] member _.Setup () = Run.shouldWrite <- false - [] + [] member val Day = 0 with get, set [] diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Run.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Run.fs index d3ffbe0..4746311 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Run.fs +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Bench/Run.fs @@ -173,6 +173,18 @@ module Run = if shouldWrite then Console.WriteLine output + let day13 (partTwo : bool) (input : string) = + if not partTwo then + let output = Day13.part1 input + + if shouldWrite then + Console.WriteLine output + else + let output = Day13.part2 input + + if shouldWrite then + Console.WriteLine output + let allRuns = [| day1 @@ -187,4 +199,5 @@ module Run = day10 day11 day12 + day13 |] diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj index faa1752..0388be6 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj @@ -23,6 +23,7 @@ + diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day13.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day13.fs new file mode 100644 index 0000000..dad9fd8 --- /dev/null +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day13.fs @@ -0,0 +1,191 @@ +namespace AdventOfCode2023 + +open System + +[] +module Day13 = + + let inline isPowerOf2 (i : uint32) = + // https://stackoverflow.com/a/600306/126995 + (i &&& (i - 1ul)) = 0ul + + let rowToInt (row : ReadOnlySpan) : uint32 = + let mutable mult = 1ul + let mutable answer = 0ul + + for c = row.Length - 1 downto 0 do + if row.[c] = '#' then + answer <- answer + mult + + mult <- mult * 2ul + + answer + + let colToInt (grid : ReadOnlySpan) (rowLength : int) (colNum : int) = + let mutable mult = 1ul + let mutable answer = 0ul + + for i = grid.Count '\n' - 1 downto 0 do + if grid.[i * (rowLength + 1) + colNum] = '#' then + answer <- answer + mult + + mult <- mult * 2ul + + answer + + let verifyReflection (group : ResizeArray<'a>) (smaller : int) (bigger : int) : bool = + let midPoint = (smaller + bigger) / 2 + + let rec isOkWithin (curr : int) = + if smaller + curr > midPoint then + true + else if group.[smaller + curr] = group.[bigger - curr] then + isOkWithin (curr + 1) + else + false + + if not (isOkWithin 0) then + false + else + + smaller = 0 || bigger = group.Count - 1 + + /// Find reflection among rows. + /// Returns 0 to indicate "no answer". + [] + let rec findRow (banAnswer : uint32) (rows : ResizeArray) (currentLine : int) : uint32 = + if currentLine = rows.Count - 1 then + 0ul + else + let mutable answer = UInt32.MaxValue + let mutable i = currentLine + + while i < rows.Count - 1 do + i <- i + 1 + + if currentLine % 2 <> i % 2 then + if rows.[i] = rows.[currentLine] then + if verifyReflection rows currentLine i then + let desiredAnswer = uint32 (((currentLine + i) / 2) + 1) + + if desiredAnswer <> banAnswer then + answer <- uint32 desiredAnswer + i <- Int32.MaxValue + + if answer < UInt32.MaxValue then + answer + else + findRow banAnswer rows (currentLine + 1) + + let render (rowBuf : ResizeArray<_>) (colBuf : ResizeArray<_>) (group : ReadOnlySpan) = + rowBuf.Clear () + colBuf.Clear () + let lineLength = group.IndexOf '\n' + + for col = 0 to lineLength - 1 do + colBuf.Add (colToInt group lineLength col) + + for row in StringSplitEnumerator.make' '\n' group do + if not row.IsEmpty then + rowBuf.Add (rowToInt row) + + /// Returns 0 to indicate "no solution". + let solve (banAnswer : uint32) (rowBuf : ResizeArray<_>) (colBuf : ResizeArray<_>) : uint32 = + match + findRow + (if banAnswer >= 100ul then + banAnswer / 100ul + else + UInt32.MaxValue) + rowBuf + 0 + with + | rowIndex when rowIndex > 0ul -> 100ul * rowIndex + | _ -> findRow banAnswer colBuf 0 + + /// Returns also the group with this gro + let peelGroup (s : ReadOnlySpan) : ReadOnlySpan = + let index = s.IndexOf "\n\n" + + if index < 0 then + // last group + s + else + s.Slice (0, index + 1) + + let part1 (s : string) = + let mutable s = s.AsSpan () + let rows = ResizeArray () + let cols = ResizeArray () + let mutable answer = 0ul + + while not s.IsEmpty do + let group = peelGroup s + + render rows cols group + // There's an obvious perf optimisation where we don't compute cols + // until we know there's no row answer. Life's too short. + answer <- answer + solve UInt32.MaxValue rows cols + + if group.Length >= s.Length then + s <- ReadOnlySpan.Empty + else + s <- s.Slice (group.Length + 1) + + answer + + let flipAt (rows : ResizeArray<_>) (cols : ResizeArray<_>) (rowNum : int) (colNum : int) : unit = + rows.[rowNum] <- + let index = 1ul <<< (cols.Count - colNum - 1) + + if rows.[rowNum] &&& index > 0ul then + rows.[rowNum] - index + else + rows.[rowNum] + index + + cols.[colNum] <- + let index = 1ul <<< (rows.Count - rowNum - 1) + + if cols.[colNum] &&& index > 0ul then + cols.[colNum] - index + else + cols.[colNum] + index + + let part2 (s : string) = + let mutable s = s.AsSpan () + let rows = ResizeArray () + let cols = ResizeArray () + let mutable answer = 0ul + + while not s.IsEmpty do + let group = peelGroup s + + render rows cols group + + let bannedAnswer = solve UInt32.MaxValue rows cols + + let mutable isDone = false + let mutable rowToChange = 0 + + while not isDone && rowToChange < rows.Count do + let mutable colToChange = 0 + + while not isDone && colToChange < cols.Count do + flipAt rows cols rowToChange colToChange + + match solve bannedAnswer rows cols with + | solved when solved > 0ul -> + isDone <- true + answer <- answer + solved + | _ -> + flipAt rows cols rowToChange colToChange + colToChange <- colToChange + 1 + + rowToChange <- rowToChange + 1 + + if group.Length >= s.Length then + s <- ReadOnlySpan.Empty + else + s <- s.Slice (group.Length + 1) + + answer diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/Program.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/Program.fs index 25640b4..a41eec9 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/Program.fs +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/Program.fs @@ -252,6 +252,22 @@ module Program = Console.WriteLine (part2.ToString ()) Console.Error.WriteLine ((1_000.0 * float sw.ElapsedTicks / float Stopwatch.Frequency).ToString () + "ms") + Console.WriteLine "=====Day 13=====" + + do + let input = Path.Combine (dir.FullName, "day13.txt") |> File.ReadAllText + + sw.Restart () + let part1 = Day13.part1 input + sw.Stop () + Console.WriteLine (part1.ToString ()) + Console.Error.WriteLine ((1_000.0 * float sw.ElapsedTicks / float Stopwatch.Frequency).ToString () + "ms") + sw.Restart () + let part2 = Day13.part2 input + sw.Stop () + Console.WriteLine (part2.ToString ()) + Console.Error.WriteLine ((1_000.0 * float sw.ElapsedTicks / float Stopwatch.Frequency).ToString () + "ms") + endToEnd.Stop () Console.Error.WriteLine ( diff --git a/AdventOfCode2023.FSharp/Test/Test.fsproj b/AdventOfCode2023.FSharp/Test/Test.fsproj index 5af2d07..18f4264 100644 --- a/AdventOfCode2023.FSharp/Test/Test.fsproj +++ b/AdventOfCode2023.FSharp/Test/Test.fsproj @@ -21,6 +21,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/AdventOfCode2023.FSharp/Test/TestDay13.fs b/AdventOfCode2023.FSharp/Test/TestDay13.fs new file mode 100644 index 0000000..a5c1ea8 --- /dev/null +++ b/AdventOfCode2023.FSharp/Test/TestDay13.fs @@ -0,0 +1,68 @@ +namespace AdventOfCode2023.Test + +open System + +open AdventOfCode2023 +open NUnit.Framework +open FsUnitTyped +open System.IO + +[] +module TestDay13 = + + [] + let ``rowToInt test`` () = + Day13.rowToInt ("#.##..##.".AsSpan ()) |> shouldEqual 358ul + + [] + let ``colToInt test`` () = + let s = + """#.##..##. +..#.##.#. +##......# +##......# +..#.##.#. +..##..##. +#.#.##.#. +""" + + Day13.colToInt (s.AsSpan ()) 9 0 + |> shouldEqual (List.sum [ 1 ; 8 ; 16 ; 64 ] |> uint32) + + [] + + let sample = Assembly.getEmbeddedResource typeof.Assembly "day13.txt" + + [] + let part1Sample () = + sample |> Day13.part1 |> shouldEqual 405ul + + [] + let part2Sample () = + sample |> Day13.part2 |> shouldEqual 400ul + + [] + let part1Actual () = + let s = + try + File.ReadAllText (Path.Combine (__SOURCE_DIRECTORY__, "../../inputs/day13.txt")) + with + | :? DirectoryNotFoundException + | :? FileNotFoundException -> + Assert.Inconclusive () + failwith "unreachable" + + Day13.part1 s |> shouldEqual 30158ul + + [] + let part2Actual () = + let s = + try + File.ReadAllText (Path.Combine (__SOURCE_DIRECTORY__, "../../inputs/day13.txt")) + with + | :? DirectoryNotFoundException + | :? FileNotFoundException -> + Assert.Inconclusive () + failwith "unreachable" + + Day13.part2 s |> shouldEqual 36474ul diff --git a/AdventOfCode2023.FSharp/Test/samples/day13.txt b/AdventOfCode2023.FSharp/Test/samples/day13.txt new file mode 100644 index 0000000..3b6b5cc --- /dev/null +++ b/AdventOfCode2023.FSharp/Test/samples/day13.txt @@ -0,0 +1,15 @@ +#.##..##. +..#.##.#. +##......# +##......# +..#.##.#. +..##..##. +#.#.##.#. + +#...##..# +#....#..# +..##..### +#####.##. +#####.##. +..##..### +#....#..#