changeset 67:b4d590e70ccd

Day 13 Part 2
author Lewin Bormann <lbo@spheniscida.de>
date Sat, 23 Dec 2023 21:36:45 +0100
parents 2746741a49f6
children cd6e3d6c1338
files 2023/day13.ml
diffstat 1 files changed, 76 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/2023/day13.ml	Sat Dec 23 19:58:38 2023 +0100
+++ b/2023/day13.ml	Sat Dec 23 21:36:45 2023 +0100
@@ -13,6 +13,8 @@
 let ix_of_rc field r c = (r * field.cols) + c
 let rc_of_ix field ix = (ix / field.cols, ix mod field.cols)
 let tile_at field r c = field.tiles.(ix_of_rc field r c)
+let get_column field c = Array.init field.rows ~f:(fun r -> tile_at field r c)
+let get_row field r = Array.init field.cols ~f:(fun c -> tile_at field r c)
 
 module Parse = struct
   let maybe p = p >>| (fun _ -> ()) <|> return ()
@@ -32,9 +34,6 @@
 end
 
 module Part1 = struct
-  let get_column field c = Array.init field.rows ~f:(fun r -> tile_at field r c)
-  let get_row field r = Array.init field.cols ~f:(fun c -> tile_at field r c)
-
   (* check if mirror axis exists between columns c and c+1 (one-based) *)
   let check_mirror_v field c =
     let rec check_with_off field c off =
@@ -96,17 +95,86 @@
     List.map symmetries ~f:symmetry_to_score |> List.fold ~init:0 ~f:( + )
 end
 
+module Part2 = struct
+  (* throws on index error, ensure that there is only one differing element *)
+  let rec find_different_pos ?(ix = 0) a b =
+    if equal_tile a.(ix) b.(ix) then ix else find_different_pos a b ~ix:(ix + 1)
+
+  (* tracking is a bit more complex than it seems: when comparing two rows/columns,
+       the result is either No smudge: we can continue looking; One smudge: we found the smudge
+       but there can't be a second one; or more than one difference, in which case we're not
+     looking at a valid symmetry axis. *)
+  type continue = No_smudge | One_smudge of int | Symmetry_fail
+
+  (* find the difference between two rows/columns, and detect a smudge if present. *)
+  let find_diff a b =
+    match
+      Array.fold2_exn a b ~init:0 ~f:(fun acc a b ->
+          if equal_tile a b then acc else acc + 1)
+    with
+    | 0 -> No_smudge
+    | 1 ->
+        let ix = find_different_pos a b in
+        One_smudge ix
+    | _ -> Symmetry_fail
+
+  let find_smudge getter max field ix =
+    let rec find_with_off field ix off =
+      if ix - off - 1 < 0 || ix + off >= max then No_smudge
+      else
+        let left = getter field (ix - off - 1)
+        and right = getter field (ix + off)
+        and rest = find_with_off field ix (off + 1) in
+        match find_diff left right with
+        | One_smudge ix -> (
+            match rest with No_smudge -> One_smudge ix | _ -> Symmetry_fail)
+        | No_smudge -> rest
+        | Symmetry_fail -> Symmetry_fail
+    in
+    find_with_off field ix 0
+
+  (* find a smudge in column c (one-based). *)
+  let find_smudge_v field c = find_smudge get_column field.cols field c
+
+  (* find a smudge in row r (one-based). *)
+  let find_smudge_h field r = find_smudge get_row field.rows field r
+
+  let find_any_smudge max smudgefinder field =
+    let open Option in
+    let indices = List.range 1 (max + 1) in
+    List.find_map indices ~f:(fun ix ->
+        match smudgefinder field ix with One_smudge _ -> Some ix | _ -> None)
+
+  (* find a new vertical symmetry axis in a field, ignoring exactly one smudge. *)
+  let find_any_smudge_v field = find_any_smudge field.cols find_smudge_v field
+
+  (* find a new horizontal symmetry axis in a field, ignoring exactly one smudge. *)
+  let find_any_smudge_h field = find_any_smudge field.rows find_smudge_h field
+
+  (* find a new symmetry axis in a field, ignoring exactly one smudge. *)
+  let find_new_symmetry field =
+    let open Part1 in
+    match (find_any_smudge_h field, find_any_smudge_v field) with
+    | Some h, Some v -> { horizontal = Some h; vertical = Some v }
+    | Some h, None -> { horizontal = Some h; vertical = None }
+    | None, Some v -> { horizontal = None; vertical = Some v }
+    | None, None -> assert false
+
+  (* find all new symmetries in a list of fields. *)
+  let find_all_symmetries fields = List.map fields ~f:find_new_symmetry
+end
+
 let () =
   let inp = In_channel.(input_all stdin) in
   let fields =
     Angstrom.parse_string ~consume:All Parse.parse_fields inp
     |> Result.ok_or_failwith
   in
-  (* let fields_str = List.map fields ~f:show_field |> String.concat ~sep:"\n\n" in *)
   let symmetries = Part1.check_all fields in
-  (* let symmetries_str =
-     List.map symmetries ~f:Part1.show_symmetry |> String.concat ~sep:"\n" *)
   let score = Part1.symmetries_to_score symmetries in
-  Out_channel.((*printf "field: %s\n" fields_str;*)
-               printf "score = %d\n" score)
+  let new_symmetries = Part2.find_all_symmetries fields in
+  let new_score = Part1.symmetries_to_score new_symmetries in
+  Out_channel.(
+    printf "score = %d\n" score;
+    printf "new score = %d\n" new_score)