|
| 1 | +(ns y2024.d20 |
| 2 | + (:require |
| 3 | + [advent-of-code-clj.input :as input] |
| 4 | + [advent-of-code-clj.utils :as utils] |
| 5 | + [medley.core :as medley])) |
| 6 | + |
| 7 | +; # 2024, dag 20 |
| 8 | + |
| 9 | +; ## Del 1 |
| 10 | + |
| 11 | +; Vi skal finne korteste sti i en labyrint, men denne gangen er |
| 12 | +; twisten at man kan jukse én gang! |
| 13 | + |
| 14 | +; Så i stedet for å kunne enkelt bruke en shortest path-algoritme |
| 15 | +; må vi tenke litt annerledes. |
| 16 | + |
| 17 | +; Hva med å "tabulere" hele kartet baklengs fra mål, gi hver node |
| 18 | +; en "kost", og så finne alle mulige snarveier? Da kan man finne ut |
| 19 | +; hvor mye tid som spares på å ta snarveien... |
| 20 | + |
| 21 | +(def test-input "############### |
| 22 | +#...#...#.....# |
| 23 | +#.#.#.#.#.###.# |
| 24 | +#S#...#.#.#...# |
| 25 | +#######.#.#.### |
| 26 | +#######.#.#...# |
| 27 | +#######.#.###.# |
| 28 | +###..E#...#...# |
| 29 | +###.#######.### |
| 30 | +#...###...#...# |
| 31 | +#.#####.#.###.# |
| 32 | +#.#...#.#.#...# |
| 33 | +#.#.#.#.#.#.### |
| 34 | +#...#...#...### |
| 35 | +###############") |
| 36 | + |
| 37 | +; Vi starter med å samle opp alle mulige posisjoner: |
| 38 | + |
| 39 | +(defn coordinates [input] |
| 40 | + (let [coord-map (utils/coord-map-fixed (utils/text->matrix input)) |
| 41 | + start-node (key (medley/find-first (fn [[k v]] |
| 42 | + (= \S v)) |
| 43 | + coord-map)) |
| 44 | + end-node (key (medley/find-first (fn [[k v]] |
| 45 | + (= \E v)) |
| 46 | + coord-map))] |
| 47 | + {:start-node start-node |
| 48 | + :end-node end-node |
| 49 | + :possible-locations (keys (medley/filter-vals #{\. \S \E} coord-map))})) |
| 50 | + |
| 51 | +(update (coordinates test-input) :possible-locations #(take 10 %)) |
| 52 | + |
| 53 | +; Så kan vi gå fra slutt-noden og finne ut hvor mange steg man må ta: |
| 54 | + |
| 55 | +(defn node->cost [possible-locations from-node] |
| 56 | + (let [valid-location (set possible-locations)] |
| 57 | + (->> (utils/breadth-search |
| 58 | + (fn [node] |
| 59 | + (->> (apply utils/adjacent-hv node) |
| 60 | + (filter valid-location))) |
| 61 | + from-node) |
| 62 | + (map-indexed (fn [idx items] |
| 63 | + (into {} (map (fn [node] [node idx])) |
| 64 | + items))) |
| 65 | + (apply merge)))) |
| 66 | + |
| 67 | +(let [{p :possible-locations |
| 68 | + from :end-node} (coordinates test-input)] |
| 69 | + (node->cost p from)) |
| 70 | + |
| 71 | +; Neste steg er å finne alle mulige snarveier. En snarvei er lik |
| 72 | +; en mulig lokasjon som kan nå en annen mulig lokasjon selv om en |
| 73 | +; umulig lokasjon er mellom. |
| 74 | + |
| 75 | +(def dir-vectors |
| 76 | + (for [y [-1 0 1] |
| 77 | + x [-1 0 1] |
| 78 | + :when (not= x y) |
| 79 | + :when (or (zero? x) (zero? y))] |
| 80 | + [y x])) |
| 81 | + |
| 82 | +dir-vectors |
| 83 | + |
| 84 | +(defn shortcuts |
| 85 | + "For each possible location, can it reach a location two steps |
| 86 | + away with an impossible location in-between?" |
| 87 | + [possible-locations] |
| 88 | + (let [loc-set (set possible-locations)] |
| 89 | + (->> (for [loc-a possible-locations |
| 90 | + dir-v dir-vectors |
| 91 | + :let [next-v (mapv #(* % 2) dir-v) |
| 92 | + pos-1 (mapv + loc-a dir-v) |
| 93 | + pos-2 (mapv + loc-a next-v)] |
| 94 | + :when (and (not (loc-set pos-1)) |
| 95 | + (loc-set pos-2))] |
| 96 | + #{loc-a pos-2}) |
| 97 | + set))) |
| 98 | + |
| 99 | +; Nå ser vi at det er 44 mulige snarveier: |
| 100 | + |
| 101 | +(count (shortcuts (:possible-locations (coordinates test-input)))) |
| 102 | + |
| 103 | +; Nå kan vi gå igjennom hver mulige snarvei, og se hvor mye tid som |
| 104 | +; kan spares ved å bruke snarveien. Tiden spart finner vi ved å plusse |
| 105 | +; sammen de korteste lengdene til snarvei-nodene fra start og slutt: |
| 106 | + |
| 107 | +(defn cost-with-shortcut [node->cost-1 node->cost-2 shortcut] |
| 108 | + (let [c1 (mapv node->cost-1 shortcut) |
| 109 | + c2 (mapv node->cost-2 shortcut)] |
| 110 | + (+ (apply min c1) (apply min c2) 2))) |
| 111 | + |
| 112 | +; Vi forventer at vi sparer 64 steg på å hoppe mellom slutt-noden |
| 113 | +; og noden to steg til høyre: |
| 114 | + |
| 115 | +(let [{:keys [possible-locations start-node end-node]} (coordinates test-input) |
| 116 | + node->cost-from-start (node->cost possible-locations start-node) |
| 117 | + node->cost-from-end (node->cost possible-locations end-node) |
| 118 | + cost (node->cost-from-start end-node)] |
| 119 | + (- cost (cost-with-shortcut |
| 120 | + node->cost-from-start |
| 121 | + node->cost-from-end |
| 122 | + #{[7 7] [7 5]}))) |
| 123 | + |
| 124 | +; Dette ser jo ut til å fungere! Da kan vi prøve å løse del 1: |
| 125 | + |
| 126 | +(defn part-1 [required-amount-saved input] |
| 127 | + (let [{:keys [possible-locations start-node end-node]} (coordinates input) |
| 128 | + node->cost-from-start (node->cost possible-locations start-node) |
| 129 | + node->cost-from-end (node->cost possible-locations end-node) |
| 130 | + cost (node->cost-from-start end-node) |
| 131 | + shortcuts (shortcuts possible-locations)] |
| 132 | + (->> shortcuts |
| 133 | + (map #(- cost (cost-with-shortcut node->cost-from-start |
| 134 | + node->cost-from-end |
| 135 | + %))) |
| 136 | + (filter (fn [savings] (>= savings required-amount-saved))) |
| 137 | + count))) |
| 138 | + |
| 139 | +; For å teste kan vi se om vi finner de 5 snarveiene som er beskrevet i oppgaveteksten |
| 140 | +; som har en besparelse over 20 steg i test-dataene: |
| 141 | + |
| 142 | +(delay (part-1 20 test-input)) |
| 143 | + |
| 144 | +; Det ser ut til å fungere også, så da finner vi nok løsningen på del 1: |
| 145 | + |
| 146 | +(delay (part-1 100 (input/get-input 2024 20))) |
0 commit comments