• Jour 1
  • Jour 2
  • Jour 3
  • Jour 4
  • Jour 5
  • Jour 6
  • Jour 7
  • Jour 8
  • Jour 9
  • Jour 10
  • Jour 11
  • Jour 12
  • Jour 13
  • Jour 14
  • Jour 15
  • Jour 16
  • Jour 17
  • Jour 18
  • Jour 19
  • Jour 21
  • Jour 22
  • Jour 23
  • Accueil
  • Articles
  • Notes
  • Livres
  • Auteur
🇺🇸 en 🇨🇳 zh 🇮🇳 ml

Nathaniel Thomas

Advent of Code 2024 en Haskell

21 décembre 2024

Je fais AoC en Haskell pour apprendre le langage. Voici mes solutions.

Jour 1

import Data.List
import qualified Data.Map as Map

f xs =
  let x1s = sort $ map fst xs
      x2s = sort $ map snd xs
      diff x y = abs (x - y)
   in sum $ zipWith diff x1s x2s

counter = Map.fromListWith (+) . map (,1)

sim xs =
  let c = counter (map snd xs)
   in sum [x * Map.findWithDefault 0 x c | x <- map fst xs]

main = do
  l <- readFile "data1.txt"
  let xs = [(read x, read y) | [x, y] <- map words (lines l)]
  print (f xs)
  print (sim xs)

Assez propre, je ne pense pas pouvoir l’améliorer.

Jour 2

allSame [] = True
allSame (x : xs) = all (== x) xs

monotonic xs = allSame (zipWith (\x y -> signum (x - y)) xs (tail xs))

diffValid (x : y : xs)
  | abs (x - y) >= 1 && abs (x - y) <= 3 = diffValid (y : xs)
  | otherwise = False
diffValid _ = True

isSafe xs = monotonic xs && diffValid xs

without xs i = [x | (x, j) <- zip xs [1 ..], j /= i]

isSafeDamp xs = isSafe xs || any (isSafe . without xs) [1 .. length xs]

main = do
  content <- readFile "data2.txt"
  let parsed = map (\l -> map read (words l)) (lines content) :: [[Int]]
  let nSafe = length (filter isSafe parsed)
  let nSafeDamp = length (filter isSafeDamp parsed)
  print ("nombre d'éléments sûrs : " ++ (show nSafe))
  print ("nombre d'éléments sûrs (amortissement) : " ++ (show nSafeDamp))

C’est également assez propre et direct.

Jour 3

import Data.List
import Debug.Trace

findMuls :: String -> [Int]
findMuls "" = []
findMuls s@(_ : s')
  | "mul(" `isPrefixOf` s = case parseArgs (drop 3 s) of
      Just ((n1, n2), rest) -> (n1 * n2) : findMuls rest
      Nothing -> findMuls s'
  | otherwise = findMuls s'

findMuls2 :: String -> Bool -> [Int]
findMuls2 [] _ = []
findMuls2 s@(_ : s') enabled
  | "do()" `isPrefixOf` s = findMuls2 (drop 4 s) True
  | "don't()" `isPrefixOf` s = findMuls2 (drop 7 s) False
  | "mul(" `isPrefixOf` s = case (enabled, parseArgs (drop 3 s)) of
      (True, Just ((n1, n2), rest)) -> (n1 * n2) : findMuls2 rest enabled
      _ -> findMuls2 s' enabled
  | otherwise = findMuls2 s' enabled

parseArgs :: String -> Maybe ((Int, Int), String)
parseArgs s = case break (== ')') s of
  (front, ')' : rest) -> case reads (front ++ ")") of
    [(val, "")] -> Just (val, rest)
    _ -> Nothing
  _ -> Nothing

main :: IO ()
main = do
  content <- readFile "data3.txt"
  let total = sum (findMuls content)
  let total2 = sum (findMuls2 content True)
  print $ "total: " ++ show total
  print $ "total avec drapeau: " ++ show total2

Pas aussi élégant que les autres. J’aurais probablement pu utiliser des expressions régulières ou un package de parsing.

Jour 4

import Data.Array
import Data.List

countXmas [] = 0
countXmas s@(_ : s')
  | "XMAS" `isPrefixOf` s = 1 + countXmas (drop 4 s)
  | otherwise = countXmas s'

diagonals grid = [diagonal arr i | i <- [0 .. m + n - 2]]
  where
    m = length grid
    n = length $ head grid
    arr = listArray ((0, 0), (m - 1, n - 1)) (concat grid)
    diagonal arr k = [arr ! (i, k - i) | i <- [max 0 (k - n + 1) .. min k (m - 1)]]

antiDiagonals = diagonals . map reverse

countPatterns grid = sum (map countXmas grid) + sum (map (countXmas . reverse) grid)

countAllDirections grid =
  let gridT = transpose grid
      gridDiag = diagonals grid
      gridADiag = antiDiagonals grid
      horCount = countPatterns grid
      verCount = countPatterns gridT
      diagCount = countPatterns gridDiag
      antiDiagCount = countPatterns gridADiag
   in horCount + verCount + diagCount + antiDiagCount

isMasXShape (i, j) grid =
  grid !! i !! j == 'M'
    && grid !! (i) !! (j + 2) == 'S'
    && grid !! (i + 1) !! (j + 1) == 'A'
    && grid !! (i + 2) !! (j) == 'M'
    && grid !! (i + 2) !! (j + 2) == 'S'

countMasXShape grid = length [() | i <- [0 .. m - 3], j <- [0 .. n - 3], isMasXShape (i, j) grid]
  where
    m = length grid
    n = length $ head grid

countMasXAllDirections grid =
  let normalCount = countMasXShape grid
      horizontalFlipCount = countMasXShape (map reverse grid)
      verticalFlipCount = countMasXShape (transpose grid)
      bothFlipCount = countMasXShape (map reverse $ transpose grid)
   in normalCount + horizontalFlipCount + verticalFlipCount + bothFlipCount

main = do
  content <- readFile "data4.txt"
  let grid = lines content
  -- Partie 1
  print $ countAllDirections grid
  -- Partie 2
  print $ countMasXAllDirections grid

Bien que le nombre de lignes de code soit élevé, je pense que cette solution est conceptuellement très simple.

Jour 5

import qualified Data.Graph as G
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import qualified Data.Set as Set
import System.IO

main = do
  contents <- readFile "data5.txt"
  let (ruleLines, updateLines) = break null (lines contents)
      rules = map parseRule ruleLines
      updates = map parseUpdate (drop 1 updateLines)
      (correctUpdates, incorrectUpdates) = partition (isSorted rules) updates
      sortedUpdates = map (sortAccordingTo rules) incorrectUpdates
      middleSum = sum $ map middle correctUpdates
      middleSum2 = sum $ map middle sortedUpdates
  -- partie 1
  print middleSum
  -- partie 2
  print middleSum2

parseRule s = let [x, y] = splitOn '|' s in (read x, read y)

parseUpdate s = map read $ splitOn ',' s

splitOn delim s = case break (== delim) s of
  (a, _ : b) -> a : splitOn delim b
  (a, "") -> [a]

isSorted rules update =
  let indices = Map.fromList $ zip update [0 ..]
   in all
        (\(x, y) -> Map.lookup x indices < Map.lookup y indices)
        [(x, y) | (x, y) <- rules, x `elem` update, y `elem` update]

sortAccordingTo rules update =
  let edges = [(n, n, [y | (x, y) <- rules, x == n, y `elem` update]) | n <- update]
      (graph, nodeFromVertex, _) = G.graphFromEdges edges
      sortedVertices = G.topSort graph
      sortedNodes = map (\v -> let (n, _, _) = nodeFromVertex v in n) sortedVertices
   in sortedNodes

middle xs = xs !! (length xs `div` 2)

L’API Graph était pratique pour ce problème !

Jour 6

Voici ma première solution :

import Control.Monad.ST
import Data.Array
import Data.Array.ST
import qualified Data.Set as Set
import Debug.Trace

data Elem = Visited | Blocked | Free deriving (Show, Eq)

type Grid = [[Elem]]

data Direction = U | D | L | R deriving (Show, Eq, Ord)

type Pos = (Int, Int)

type State = (Pos, Direction)

turnRight :: Direction -> Direction
turnRight d
  | d == U = R
  | d == R = D
  | d == D = L
  | d == L = U

move (i, j) d
  | d == U = (i - 1, j)
  | d == R = (i, j + 1)
  | d == D = (i + 1, j)
  | d == L = (i, j - 1)

toGrid :: [String] -> Grid
toGrid [] = []
toGrid (l : ls) = (foldr repl [] l) : (toGrid ls)
  where
    repl c acc
      | c == '#' = Blocked : acc
      | otherwise = Free : acc

findGuard grid = head [(i, j) | i <- [0 .. length grid - 1], j <- [0 .. length (head grid) - 1], grid !! i !! j == '^']

countVisited grid = length (filter (== Visited) (elems grid))

simulate grid pos d m n states
  | (pos, d) `Set.member` states = (countVisited grid, True)
  | i' < 0 || i' >= m || j' < 0 || j' >= n = (countVisited grid', False)
  | grid ! (i', j') == Blocked = simulate grid' pos (turnRight d) m n states'
  | otherwise = simulate grid' (move pos d) d m n states'
  where
    states' = Set.insert (pos, d) states
    (i', j') = move pos d
    grid' = (grid // [(pos, Visited)])

countCycles grid pos d m n = length $ filter snd $ map (\(g, idx) -> simulateWithLog g pos d m n Set.empty idx total) (zip grids [1 ..])
  where
    grids = [grid // [((i, j), Blocked)] | i <- [0 .. m - 1], j <- [0 .. n - 1], grid ! (i, j) /= Blocked]
    total = length grids
    simulateWithLog grid pos d m n states idx total = trace (logProgress idx total) $ simulate grid pos d m n states
    logProgress idx total = show idx ++ "/" ++ show total

main = do
  content <- readFile "data6.txt"
  let charGrid = lines content
  let grid = toGrid charGrid
  let pos = findGuard charGrid
  let (m, n) = (length grid, length $ head grid)
  let inds = [(i, j) | i <- [0 .. m - 1], j <- [0 .. n - 1]]
  let gridArr = array ((0, 0), (m - 1, n - 1)) (zip inds $ concat grid)
  print $ fst $ simulate gridArr pos U m n Set.empty
  print $ countCycles gridArr pos U m n

Mais elle était beaucoup trop lente. Le principal problème était que simulate utilisait des tableaux immuables, ce qui nécessitait des copies à chaque assignation. Cela peut être corrigé en utilisant des tableaux mutables, au prix d’une complexité accrue :

countVisitedST :: STArray s (Int, Int) Elem -> ST s Int
countVisitedST mGrid = do
  elems <- getElems mGrid
  return $ length (filter (== Visited) elems)

simulate :: Array (Int, Int) Elem -> (Int, Int) -> Direction -> Int -> Int -> Set.Set State -> (Int, Bool)
simulate grid pos d m n states = runST $ do
  mGrid <- thaw grid :: ST s (STArray s (Int, Int) Elem)
  simulateST mGrid pos d m n states

simulateST :: STArray s (Int, Int) Elem -> (Int, Int) -> Direction -> Int -> Int -> Set.Set State -> ST s (Int, Bool)
simulateST mGrid pos@(i, j) d m n states
  | (pos, d) `Set.member` states = do
      writeArray mGrid pos Visited
      visitedCount <- countVisitedST mGrid
      return (visitedCount, True)
  | i' < 0 || i' >= m || j' < 0 || j' >= n = do
      writeArray mGrid pos Visited
      visitedCount <- countVisitedST mGrid
      return (visitedCount, False)
  | otherwise = do
      currentElem <- readArray mGrid (i', j')
      if currentElem == Blocked
        then simulateST mGrid pos (turnRight d) m n states'
        else do
          writeArray mGrid pos Visited
          simulateST mGrid (move pos d) d m n states'
  where
    states' = Set.insert (pos, d) states
    (i', j') = move pos d

ce qui donne la bonne réponse en un temps raisonnable.

Jour 7

concatMapFuncs :: [(a -> a -> a)] -> [a] -> [a]
concatMapFuncs fs [x] = [x]
concatMapFuncs fs (x : xs) = concatMap applyAll (concatMapFuncs fs xs)
  where
    applyAll y = map (\f -> f x y) fs

evalResults ops target xs = target `elem` (concatMapFuncs ops xs)

-- inverser les arguments car les données sont inversées
intConcat x y = read (show y ++ show x)

splitLine c s = let (pre, _ : _ : post) = break (== c) s in (pre, post)

main = do
  content <- readFile "data7.txt"
  let contentLines = lines content
  let splitLines = map (splitLine ':') contentLines
  -- inverser pour que la priorité soit de gauche à droite
  let parsedLines = [(read target, reverse $ map read $ words nums) | (target, nums) <- splitLines]
  let calibratedLines = filter (uncurry (evalResults [(+), (*)])) parsedLines
  let calibratedLinesConcat = filter (uncurry (evalResults [(+), (*), intConcat])) parsedLines
  let result = sum $ map fst calibratedLines
  let result2 = sum $ map fst calibratedLinesConcat
  -- partie 1
  print $ result
  -- partie 2
  print $ result2

J’aime cette solution car elle montre à quel point les fonctions d’ordre supérieur peuvent être expressives.

Jour 8

import Data.Array
import Data.List
import qualified Data.Map as Map

type Loc = (Int, Int)

instance Num Loc where
  (x1, y1) + (x2, y2) = (x1 + x2, y1 + y2)
  (x1, y1) - (x2, y2) = (x1 - x2, y1 - y2)
  (x1, y1) * (x2, y2) = (x1 * x2, y1 * y2)
  fromInteger n = (fromInteger n, fromInteger n)

freqLocations grid = foldr (\(pos, c) acc -> Map.insertWith (++) c [pos] acc) Map.empty nonEmptyGrid
  where
    nonEmptyGrid = filter (\(_, c) -> c /= '.') $ assocs grid

pairs [] = []
pairs (x : xs) = [(x, y) | y <- xs] ++ pairs xs

scale n p = p * fromInteger n

inBounds grid (i, j) =
  let ((minI, minJ), (maxI, maxJ)) = bounds grid
   in i >= minI && i <= maxI && j >= minJ && j <= maxJ

-- Partie 1
placeAntinodes inBounds (p1, p2) = filter inBounds [p1 - dist, p2 + dist]
  where
    dist = p2 - p1

-- Partie 2
placeAntinodes2 inBounds (p1, p2) = (genNodes spaceBefore) ++ (genNodes spaceAfter)
  where
    genNodes spacingRule = takeWhile inBounds $ map spacingRule [0 ..]
    dist = p2 - p1
    spaceBefore n = p1 - (scale n dist)
    spaceAfter n = p2 + (scale n dist)

antinodes grid locMap placeAntinodes = concatMap process (Map.elems locMap)
  where
    process locs = concatMap placeAntinodes (pairs locs)

main = do
  content <- readFile "data8.txt"
  let gridList = lines content
  let (m, n) = (length gridList, length $ head gridList)
  let grid = listArray ((0, 0), (m - 1, n - 1)) (concat gridList)
  let locMap = freqLocations grid
  let placer1 = placeAntinodes (inBounds grid)
  let placer2 = placeAntinodes2 (inBounds grid)
  let allAntinodes1 = antinodes grid locMap placer1
  let allAntinodes2 = antinodes grid locMap placer2
  -- partie 1
  print $ length $ nub allAntinodes1
  -- partie 2
  print $ length $ nub allAntinodes2

J’aimerais pouvoir surcharger l’opérateur * pour la multiplication scalaire.

Jour 9

import Data.Char
import Data.List (group)
import Debug.Trace

readDisk :: [Int] -> [Int]
readDisk xs = let (_, _, disk) = foldl process (True, 0, []) xs in disk
  where
    process (isFile, curId, disk) x =
      if isFile
        then
          (False, curId + 1, disk ++ (replicate x curId))
        else (True, curId, disk ++ (replicate x (-1)))

compress disk = (take filesize $ compress' disk revDisk) ++ (replicate (length disk - filesize) (-1))
  where
    compress' [] _ = []
    compress' _ [] = []
    compress' (f : fs) (b : bs)
      | f == -1 = b : compress' fs bs
      | otherwise = f : compress' fs (b : bs)
    revDisk = filter (>= 0) $ reverse disk
    filesize = length revDisk

compressBlocks :: [Int] -> [Int]
compressBlocks disk = concat $ compress' groups
  where
    compress' [] = []
    compress' gs =
      let (lastG, initG) = (last gs, init gs)
       in if head lastG == -1
            then (compress' initG) ++ [lastG]
            else case place lastG initG of
              Just gs' -> compress' $ group $ concat $ gs' ++ [replicate (length lastG) (-1)]
              Nothing -> compress' initG ++ [lastG]
    place _ [] = Nothing
    place x (f : fs)
      | (head f == -1) && length x <= length f = Just ((fill f x) : fs)
      | otherwise = do
          rest <- place x fs
          return (f : rest)
    groups = group disk
    back = filter ((>= 0) . head) groups
    fill f b = b ++ (drop (length b) f)

checkSum compressedDisk = sum [i * x | (i, x) <- zip [0 ..] compressedDisk, x /= -1]

showDisk disk = map (\x -> if x == -1 then '.' else intToDigit x) disk

main = do
  content <- readFile "data9.txt"
  let nums = map digitToInt content :: [Int]
  let disk = readDisk nums
  let compressed = compress disk
  let compressedBlocks = compressBlocks disk
  let cs = checkSum compressed
  let cs2 = checkSum compressedBlocks
  print $ cs
  print $ cs2

Je ne suis pas très fier de la solution compressBlocks. Elle est compliquée et lente. Peut-être que ce problème n’est pas adapté à la programmation fonctionnelle, mais cela pourrait aussi être une question de compétence de ma part.

Jour 10

import Data.Array
import Data.Char
import Data.List
import qualified Data.Set as Set

inBounds grid (i, j) =
  let ((minI, minJ), (maxI, maxJ)) = bounds grid
   in i >= minI && i <= maxI && j >= minJ && j <= maxJ

trailheads grid = [(i, j) | ((i, j), x) <- assocs grid, x == 0]

count9 grid positions = length $ filter (\pos -> grid ! pos == 9) positions

dfs grid (i, j) = dfs' [] [(i, j)]
  where
    dfs' seen [] = seen
    dfs' seen (cur : stack) = dfs' (cur : seen) (neighbors cur ++ stack)
    neighbors (i, j) =
      [ (i', j')
      | (i', j') <- diffs (i, j),
        inBounds grid (i', j'),
        grid ! (i', j') - grid ! (i, j) == 1
      ]
    diffs (i, j) = [(i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)]

-- Ici, nous nous intéressons au nombre de 9 atteignables, uniques en index
trailheadScore grid (i, j) = count9 grid $ nub $ dfs grid (i, j)

-- Ici, nous nous intéressons au nombre de façons d'atteindre un 9, pas unique en index
trailheadRating grid (i, j) = count9 grid $ dfs grid (i, j)

main = do
  content <- readFile "data10.txt"
  let contentLines = lines content
  let (m, n) = (length contentLines, length $ head contentLines)
  let grid = listArray ((0, 0), (m - 1, n - 1)) (map digitToInt $ concat contentLines)
  let ths = trailheads grid
  let score1 = sum $ map (trailheadScore grid) ths
  let score2 = sum $ map (trailheadRating grid) ths
  putStrLn $ "Partie 1: " ++ (show score1)
  putStrLn $ "Partie 2: " ++ (show score2)

C’est sympa de voir que les parties 1 et 2 ne diffèrent que de quelques caractères !

Jour 11

import qualified Data.Map as Map

type Cache = Map.Map (Int, Int) Int

blinkLen :: Int -> Int -> Cache -> (Int, Cache)
blinkLen 0 _ cache = (1, cache)
blinkLen n 0 cache = blinkLen (n - 1) 1 cache)
blinkLen n x cache =
  case Map.lookup (n, x) cache of
    Just result -> (result, cache)
    Nothing ->
      let (result, newCache) =
            if evenLength x
              then evenBlink x cache
              else blinkLen (n - 1) (x * 2024) cache
          updatedCache = Map.insert (n, x) result newCache
       in (result, updatedCache)
  where
    evenBlink x cache =
      let (x1, x2) = split x
          (res1, cache1) = blinkLen (n - 1) x1 cache
          (res2, cache2) = blinkLen (n - 1) x2 cache1
       in (res1 + res2, cache2)
    evenLength x = length (show x) `mod` 2 == 0
    split x = (read $ take halfLen xStr, read $ drop halfLen xStr)
      where
        xStr = show x
        halfLen = length xStr `div` 2

main = do
  content <- readFile "data11.txt"
  let nums = map read $ words content :: [Int]
  let (result, cache) = foldl (\(acc, cache) x -> let (res, newCache) = blinkLen 25 x cache in (acc + res, newCache)) (0, Map.empty) nums
  putStrLn $ "Partie 1: " ++ show result
  let (result2, _) = foldl (\(acc, cache) x -> let (res, newCache) = blinkLen 75 x cache in (acc + res, newCache)) (0, cache) nums
  putStrLn $ "Partie 2: " ++ show result2

Le code ne s’exécutait pas assez rapidement sans mise en cache, malheureusement.

Jour 12

import Data.Array
import Data.List (group, sortOn)
import Data.Maybe (fromMaybe)
import qualified Data.Set as Set

type Loc = (Int, Int)

type Grid = Array Loc Char

directions :: [Loc]
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

inBounds :: Grid -> Loc -> Bool
inBounds grid (i, j) =
  let ((minI, minJ), (maxI, maxJ)) = bounds grid
   in i >= minI && i <= maxI && j >= minJ && j <= maxJ

dfs grid start value visited = dfs' [start] visited []
  where
    dfs' [] visited acc = (acc, visited)
    dfs' (current : stack) visited acc
      | current `Set.member` visited = dfs' stack visited acc
      | otherwise = dfs' (neighbors ++ stack) (Set.insert current visited) (current : acc)
      where
        neighbors =
          [ (i + di, j + dj)
          | (di, dj) <- directions,
            let (i, j) = current,
            let neighbor = (i + di, j + dj),
            inBounds grid neighbor,
            grid ! neighbor == value
          ]

findRegions :: Grid -> [[Loc]]
findRegions grid = findRegions' (indices grid) Set.empty []
  where
    findRegions' [] _ acc = acc
    findRegions' (loc : locs) visited acc
      | loc `Set.member` visited = findRegions' locs visited acc
      | otherwise =
          let value = grid ! loc
              (region, newVisited) = dfs grid loc value visited
           in findRegions' locs newVisited (region : acc)

perimeter grid (i, j) = length $ perimeterIndices grid (i, j)

perimeterIndices grid (i, j) = filter isDifferentOrOutOfBounds neighbors
  where
    neighbors = [(i + di, j + dj) | (di, dj) <- directions]
    isDifferentOrOutOfBounds (ni, nj) =
      not (inBounds grid (ni, nj)) || grid ! (ni, nj) /= grid ! (i, j)

nSides grid region =
  sum (map (vertSideRows (-1)) [0 .. n])
    + sum (map (vertSideRows 1) [0 .. n])
    + sum (map (horSideRows (-1)) [0 .. m])
    + sum (map (horSideRows 1) [0 .. m])
  where
    countSides sides = length $ filter (\xs -> head xs) $ group sides
    vertSideRows dj j = contGroups $ map fst $ filter (\(i, j) -> invalid (i, j + dj)) (colArray ! j)
    horSideRows di i = contGroups $ map snd $ filter (\(i, j) -> invalid (i + di, j)) (rowArray ! i)
    ((0, 0), (m, n)) = bounds grid
    invalid (ni, nj) =
      not (inBounds grid (ni, nj)) || not (inRegion (ni, nj))
    inRegion pos = pos `Set.member` regionSet
    regionSet = Set.fromList region
    colArray = listArray (0, n) [sortOn fst $ filter ((== j) . snd) region | j <- [0 .. n]]
    rowArray = listArray (0, m) [sortOn snd $ filter ((== i) . fst) region | i <- [0 .. m]]
    contGroups [] = 0
    contGroups [x] = 1
    contGroups (x1 : x2 : xs)
      | x2 - x1 == 1 = contGroups (x2 : xs)
      | otherwise = 1 + contGroups (x2 : xs)

main :: IO ()
main = do
  content <- readFile "data12.txt"
  let gridList = lines content
  let (m, n) = (length gridList, length $ head gridList)
  let grid = listArray ((0, 0), (m - 1, n - 1)) (concat gridList)
  let regions = findRegions grid
  let areas = map length regions
  let perimeters = map (\r -> sum $ map (perimeter grid) r) regions
  putStrLn $ "Partie 1: " ++ show (sum $ zipWith (*) areas perimeters)
  let sides = map (nSides grid) regions
  putStrLn $ "Partie 2: " ++ show (sum $ zipWith (*) areas sides)

Il m’a fallu un peu de temps pour comprendre la partie 2. Je ne suis pas sûr qu’il existe une meilleure solution.

Jour 13

import Data.Maybe
import Debug.Trace
import Text.Parsec
import Text.Parsec.String (Parser)

{-
A =
ax bx
ay by
b = [px, py]
Ax=b => x = A^-1 b
-}
solve ((ax, ay), (bx, by), (px, py)) =
  let a = fromIntegral ax
      b = fromIntegral bx
      c = fromIntegral ay
      d = fromIntegral by
      p = fromIntegral px
      q = fromIntegral py
      det = a * d - b * c
   in if det == 0
        then Nothing
        else
          let (invA11, invA12, invA21, invA22) = (d / det, -b / det, -c / det, a / det)
              x = invA11 * p + invA12 * q
              y = invA21 * p + invA22 * q
              roundedX = round x
              roundedY = round y
           in if (fromIntegral roundedX * a + fromIntegral roundedY * b == p) && (fromIntegral roundedX * c + fromIntegral roundedY * d == q)
                then Just (3 * roundedX + roundedY)
                else Nothing

minTokenCost xs = sum $ map (maybe 0 id . solve) xs

parseTestCase :: Parser ((Int, Int), (Int, Int), (Int, Int))
parseTestCase = do
  _ <- string "Bouton A: X+"
  ax <- many1 digit
  _ <- string ", Y+"
  ay <- many1 digit
  _ <- newline
  _ <- string "Bouton B: X+"
  bx <- many1 digit
  _ <- string ", Y+"
  by <- many1 digit
  _ <- newline
  _ <- string "Récompense: X="
  px <- many1 digit
  _ <- string ", Y="
  py <- many1 digit
  _ <- newline
  return ((read ax, read ay), (read bx, read by), (read px, read py))

parseTestCases :: Parser [((Int, Int), (Int, Int), (Int, Int))]
parseTestCases = many (parseTestCase <* optional newline)

readTestCases :: String -> Either ParseError [((Int, Int), (Int, Int), (Int, Int))]
readTestCases = parse parseTestCases ""

main = do
  input <- readFile "data13.txt"
  let delta = 10000000000000
  case readTestCases input of
    Left err -> print err
    Right testCases -> do
      putStrLn $ "Partie 1: " ++ show (minTokenCost testCases)
      let testCasesPart2 = map (\((ax, ay), (bx, by), (px, py)) -> ((ax, ay), (bx, by), (px + delta, py + delta))) testCases
      putStrLn $ "Partie 2: " ++ show (minTokenCost testCasesPart2)

Comme cela se réduit à un simple problème d’algèbre linéaire, la solution est conceptuellement facile. Mais écrire des opérations matricielles à la main en Haskell est peu pratique.

Jour 14

import Control.Monad.ST
import Data.Array
import Data.Array.ST
import Data.List
import Debug.Trace
import Text.Parsec
import Text.Parsec.String (Parser)

number :: Parser Int
number = do
  sign <- optionMaybe (char '-')
  digits <- many1 digit
  let num = read digits
  return $ case sign of
    Just _ -> -num
    Nothing -> num

parseLine :: Parser ((Int, Int), (Int, Int))
parseLine = do
  _ <- string "p="
  px <- number
  _ <- char ','
  py <- number
  _ <- spaces
  _ <- string "v="
  vx <- number
  _ <- char ','
  vy <- number
  return ((py, px), (vy, vx))

parseData :: Parser [((Int, Int), (Int, Int))]
parseData = parseLine `sepBy` newline

zeros m n = listArray ((0, 0), (m - 1, n - 1)) [0 | i <- [0 .. m - 1], j <- [0 .. n - 1]]

buildGrid :: [((Int, Int), (Int, Int))] -> Int -> Int -> Array (Int, Int) Int
buildGrid nums m n = runSTArray $ do
  arr <- newArray ((0, 0), (m - 1, n - 1)) 0
  mapM_ (assign arr) nums
  return arr
  where
    assign arr ((x, y), _) = do
      current <- readArray arr (x, y)
      writeArray arr (x, y) (current + 1)

printGrid :: Array (Int, Int) Int -> IO ()
printGrid grid = do
  let ((minX, minY), (maxX, maxY)) = bounds grid
  mapM_ (printRow minY maxY) [minX .. maxX]
  where
    printRow minY maxY x = do
      let row = [if grid ! (x, y) == 0 then '.' else head (show (grid ! (x, y))) | y <- [minY .. maxY]]
      putStrLn row

timestep _ _ [] = []
timestep m n (((pi, pj), (vi, vj)) : rest) = (wrap (pi + vi) (pj + vj) m n, (vi, vj)) : (timestep m n rest)
  where
    wrap i j m n = ((i + m) `mod` m, (j + n) `mod` n)

quadrantSums grid m n =
  let mid dim = ((dim - 1) `div` 2)
      (midI, midJ) = (mid m, mid n)
   in [ sum $ [grid ! (i, j) | i <- [0 .. midI - 1], j <- [0 .. midJ - 1]],
        sum $ [grid ! (i, j) | i <- [0 .. midI - 1], j <- [midJ + 1 .. n - 1]],
        sum $ [grid ! (i, j) | i <- [midI + 1 .. m - 1], j <- [0 .. midJ - 1]],
        sum $ [grid ! (i, j) | i <- [midI + 1 .. m - 1], j <- [midJ + 1 .. n - 1]]
      ]

hasCenter :: Int -> Int -> Int -> [((Int, Int), (Int, Int))] -> Bool
hasCenter m n thresh nums = thresh <= (length $ filter inCenter nums)
  where
    inCenter ((i, j), _) = i >= m `div` 4 && i <= 3 * (m `div` 4) && j >= n `div` 4 && j <= 3 * (n `div` 4)

main = do
  content <- readFile "data14.txt"
  let (m, n) = (103, 101)
  let niter = 100
  case parse parseData "" content of
    Left err -> print err >> return ()
    Right nums -> do
      putStrLn ""
      let states = iterate (timestep m n) nums
      let finalState = states !! niter
      let finalGrid = buildGrid finalState m n
      let qsums = quadrantSums finalGrid m n
      putStrLn $ "Partie 1: " ++ (show $ product qsums)
      let thresh = length nums `div` 2
      let (firstIndex, num) = head $ filter (\(i, num) -> hasCenter m n thresh num) $ zip [0 ..] states
      printGrid $ buildGrid num m n
      putStrLn $ "Partie 2: " ++ (show i)

Jour 15

import Control.Monad.ST
import Data.Array
import Data.Array.ST
import Debug.Trace

type Loc = (Int, Int)

type Grid = Array Loc Char

type Dir = Char

inBounds :: Grid -> Loc -> Bool
inBounds grid (i, j) =
  let ((iMin, jMin), (iMax, jMax)) = bounds grid
   in i >= iMin && i <= iMax && j >= jMin && j <= jMax

inBoundsMut :: (Ix i) => STArray s (i, i) e -> (i, i) -> ST s Bool
inBoundsMut grid (i, j) = do
  ((iMin, jMin), (iMax, jMax)) <- getBounds grid
  return $ i >= iMin && i <= iMax && j >= jMin && j <= jMax

nextPos dir (i, j) = case dir of
  '^' -> (i - 1, j)
  'v' -> (i + 1, j)
  '<' -> (i, j - 1)
  '>' -> (i, j + 1)

move :: Grid -> Loc -> Loc -> Maybe Grid
move grid (i1, j1) (i2, j2)
  | inBounds grid (i1, j1) && inBounds grid (i2, j2) =
      let (didMove, grid') = runST $ do
            gridMut <- thaw grid
            didMove <- moveST gridMut (i1, j1) (i2, j2)
            grid' <- freeze gridMut
            return (didMove, grid')
       in if didMove then Just grid' else Nothing
  | otherwise = Nothing

moveST :: STArray s Loc Char -> Loc -> Loc -> ST s Bool
moveST gridMut (i1, j1) (i2, j2) = do
  elem1 <- readArray gridMut (i1, j1)
  elem2 <- readArray gridMut (i2, j2)
  case (elem1, elem2) of
    ('.', _) -> return True
    (_, '#') -> return False
    ('@', '.') -> do
      writeArray gridMut (i2, j2) '@'
      writeArray gridMut (i1, j1) '.'
      return True
    ('O', '.') -> do
      writeArray gridMut (i2, j2) 'O'
      writeArray gridMut (i1, j1) '.'
      return True
    (x, 'O') -> do
      let (i3, j3) = (i2 + (i2 - i1), j2 + (j2 - j1))
      inb <- inBoundsMut gridMut (i3, j3)
      if inb
        then do
          valid <- moveST gridMut (i2, j2) (i3, j3)
          if not valid
            then return False
            else do
              writeArray gridMut (i2, j2) x
              writeArray gridMut (i1, j1) '.'
              return True
        else return False

moveRobot grid (i, j) dir =
  let (i', j') = nextPos dir (i, j)
   in if not (inBounds grid (i', j'))
        then (grid, (i, j))
        else case move grid (i, j) (i', j') of
          Just g -> (g, (i', j'))
          Nothing -> (grid, (i, j))

printGrid :: Grid -> IO ()
printGrid grid = do
  let ((iMin, jMin), (iMax, jMax)) = bounds grid
  mapM_
    ( \i -> do
        mapM_ (\j -> putStr [grid ! (i, j)]) [jMin .. jMax]
        putStrLn ""
    )
    [iMin .. iMax]

traceGrid :: Grid -> a -> a
traceGrid grid expr = trace (unlines gridLines) expr
  where
    ((iMin, jMin), (iMax, jMax)) = bounds grid
    gridLines = [[grid ! (i, j) | j <- [jMin .. jMax]] | i <- [iMin .. iMax]]

score grid = sum $ map (\((i, j), _) -> 100 * i + j) $ filter (\(_, x) -> x == 'O') $ assocs grid

wideGrid grid m n = listArray ((0, 0), (m - 1, 2 * n - 1)) grid'
  where
    grid' = concatMap repl (assocs grid)
    repl (_, x)
      | x == '#' = "##"
      | x == 'O' = "[]"
      | x == '.' = ".."
      | x == '@' = "@."

findPos grid = head [pos | (pos, x) <- assocs grid, x == '@']

gpsCoordSum grid = sum [100 * i + j | ((i, j), x) <- assocs grid, x == '[']

moveW :: Array Loc Char -> Dir -> Array Loc Char
moveW grid dir = case (grid ! (i, j), grid ! (i', j')) of
  ('@', '#') -> grid
  ('@', '.') -> grid // [((i, j), '.'), ((i', j'), '@')]
  ('@', '[') ->
    let grid' = moveBox grid (i', j') dir
     in if grid' ! (i', j') == '.'
          then
            grid' // [((i, j), '.'), ((i', j'), '@')]
          else grid
  ('@', ']') ->
    let grid' = moveBox grid (i', j' - 1) dir
     in if grid' ! (i', j') == '.'
          then
            grid' // [((i, j), '.'), ((i', j'), '@')]
          else grid
  x -> error (show x)
  where
    (i, j) = findPos grid
    (i', j') = nextPos dir (i, j)
    -- déplacer la boîte à (i,j) dans la direction dir
    moveBox :: Array Loc Char -> Loc -> Dir -> Array Loc Char
    moveBox grid (i, j) dir
      -- si on monte, cela peut être soit entre
      -- []
      -- []   (monte)
      -- [][]
      --  []  (monte)
      -- dans le premier cas, on déplace la boîte supérieure et on déplace la boîte actuelle s'il y a de la place
      -- dans le second cas, on déplace les deux boîtes puis la boîte actuelle s'il y a de la place
      | boxSpaceEmpty grid (i, j) dir =
          if grid ! (i, j) /= '['
            then
              error "doit être un crochet gauche"
            else case dir of
              '^' ->
                grid
                  // [ ((i, j), '.'),
                       ((i, j + 1), '.'),
                       ((i - 1, j), '['),
                       ((i - 1, j + 1), ']')
                     ]
              'v' ->
                grid
                  // [ ((i, j), '.'),
                       ((i, j + 1), '.'),
                       ((i + 1, j), '['),
                       ((i + 1, j + 1), ']')
                     ]
              '<' ->
                grid
                  // [ ((i, j), ']'),
                       ((i, j + 1), '.'),
                       ((i, j - 1), '[')
                     ]
              '>' ->
                grid
                  // [ ((i, j), '.'),
                       ((i, j + 1), '['),
                       ((i, j + 2), ']')
                     ]
      | boxSpaceBlocked grid (i, j) dir = grid
      | otherwise =
          let boxes = getNeighBoxes grid (i, j) dir
              grid' = foldr (\pos g -> moveBox g pos dir) grid boxes :: Array Loc Char
           in if boxSpaceEmpty grid' (i, j) dir
                then
                  moveBox grid' (i, j) dir
                else grid'

    getNeighBoxes grid (i, j) dir
      | grid ! (i, j) /= '[' = error "doit être un crochet gauche"
      | otherwise = case dir of
          '^' ->
            if grid ! (i - 1, j) == '['
              then [(i - 1, j)]
              else
                if grid ! (i - 1, j) == ']' && grid ! (i - 1, j + 1) == '['
                  then [(i - 1, j - 1), (i - 1, j + 1)]
                  else
                    if grid ! (i - 1, j) == ']'
                      then [(i - 1, j - 1)]
                      else
                        if grid ! (i - 1, j + 1) == '['
                          then [(i - 1, j + 1)]
                          else []
          'v' ->
            if grid ! (i + 1, j) == '['
              then [(i + 1, j)]
              else
                if grid ! (i + 1, j) == ']' && grid ! (i + 1, j + 1) == '['
                  then [(i + 1, j - 1), (i + 1, j + 1)]
                  else
                    if grid ! (i + 1, j) == ']'
                      then [(i + 1, j - 1)]
                      else
                        if grid ! (i + 1, j + 1) == '['
                          then [(i + 1, j + 1)]
                          else []
          '>' -> if grid ! (i, j + 2) == '[' then [(i, j + 2)] else []
          '<' -> if grid ! (i, j - 1) == ']' then [(i, j - 2)] else []

    boxDisplacingSpaces grid (i, j) dir
      | grid ! (i, j) /= '[' = error "doit être un crochet gauche"
      | otherwise = case dir of
          '^' -> [(i - 1, j), (i - 1, j + 1)]
          'v' -> [(i + 1, j), (i + 1, j + 1)]
          '>' -> [(i, j + 2)]
          '<' -> [(i, j - 1)]

    boxSpaceEmpty grid (i, j) dir =
      all
        (\p -> inBounds grid p && grid ! p == '.')
        (boxDisplacingSpaces grid (i, j) dir)

    boxSpaceBlocked grid (i, j) dir =
      any
        (\p -> not (inBounds grid p) || grid ! p == '#')
        (boxDisplacingSpaces grid (i, j) dir)

main :: IO ()
main = do
  rawContent <- readFile "data15.txt"
  let (gridSpec, _ : movesSpec) = break null (lines rawContent)
  let moves = concat movesSpec
  let (m, n) = (length gridSpec, length $ head gridSpec)
  let grid = listArray ((0, 0), (m - 1, n - 1)) (concat gridSpec)
  let (i, j) = fst $ head $ filter (\(_, x) -> x == '@') $ assocs grid
  let (finalGrid, finalPos) = foldl' (\(grid, pos) d -> moveRobot grid pos d) (grid, (i, j)) moves
  print $ score finalGrid
  let wgrid = wideGrid grid m n
  let final = foldl' moveW wgrid moves
  print $ gpsCoordSum final

Pas très fier de cette solution. C’est désordonné, long, et la première partie ne nécessitait pas de tableaux mutables. Je pourrais revenir et l’améliorer.

Jour 16

{-# LANGUAGE TupleSections #-}

import Data.Array
import Data.Heap (MinPrioHeap)
import qualified Data.Heap as H
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import qualified Data.Set as Set
import System.IO

type Dir = Int

type Loc = (Int, Int)

type Grid = Array Loc Char

type State = (Loc, Dir)

type Cost = Int

directionsForward = [(0, 1), (1, 0), (0, -1), (-1, 0)]

directionsBackward = map (\(x, y) -> (-x, -y)) directionsForward

parseGrid :: [String] -> (Grid, Loc, Loc)
parseGrid linesInput =
  let m = length linesInput
      n = length (head linesInput)
      gridList = concat linesInput
      grid = listArray ((0, 0), (m - 1, n - 1)) gridList
      findPos x = head [(i, j) | i <- [0 .. m - 1], j <- [0 .. n - 1], grid ! (i, j) == x]
      start = findPos 'S'
      end = findPos 'E'
   in (grid, start, end)

isValid :: Grid -> Loc -> Bool
isValid grid loc = inBounds grid loc && grid ! loc /= '#'

inBounds :: Grid -> Loc -> Bool
inBounds grid (i, j) =
  let ((iMin, jMin), (iMax, jMax)) = bounds grid
   in i >= iMin && i <= iMax && j >= jMin && j <= jMax

dijkstra :: Grid -> [State] -> [Loc] -> Map.Map State Cost
dijkstra grid starts directions = go initialQueue Map.empty
  where
    initialQueue = H.fromList [(0, s) | s <- starts]

    go :: MinPrioHeap Cost State -> Map.Map State Cost -> Map.Map State Cost
    go pq seenMap =
      case H.view pq of
        Nothing -> seenMap
        Just ((currentCost, currentState), pq') ->
          if Map.member currentState seenMap
            then go pq' seenMap
            else
              let seenMap' = Map.insert currentState currentCost seenMap
                  (loc@(x, y), dir) = currentState
                  (dx, dy) = directions !! dir
                  newLoc = (x + dx, y + dy)
                  newStates =
                    if isValid grid newLoc
                      then [(currentCost + 1, (newLoc, dir))]
                      else []
                  turnDirs = [(dir + 1) `mod` 4, (dir + 3) `mod` 4]
                  newStates' = newStates ++ [(currentCost + 1000, (loc, newDir)) | newDir <- turnDirs]
                  updatedPQ = foldr H.insert pq' newStates'
               in go updatedPQ seenMap'

countBestPathTiles grid start end minCost costFromStart costToEnd =
  let startsFromStart = [(start, 0)]
      startsFromEnd = [(end, d) | d <- [0 .. 3]]
      gridBounds = range (bounds grid)
      bestTiles =
        Set.fromList
          [ loc
          | loc <- gridBounds,
            grid ! loc /= '#',
            any
              ( \d ->
                  let cost_s = Map.findWithDefault maxBound (loc, d) costFromStart
                      cost_e = Map.findWithDefault maxBound (loc, d) costToEnd
                   in cost_s + cost_e == minCost
              )
              [0 .. 3]
          ]
   in Set.size bestTiles

main :: IO ()
main = do
  content <- readFile "data16.txt"
  let gridLines = lines content
  let (grid, start, end) = parseGrid gridLines
  let startsFromStart = [(start, 0)]
  let costFromStart = dijkstra grid startsFromStart directionsForward
  let startsFromEnd = [(end, d) | d <- [0 .. 3]]
  let costToEnd = dijkstra grid startsFromEnd directionsBackward
  let minCost = minimum [Map.findWithDefault maxBound (end, d) costFromStart | d <- [0 .. 3]]
  putStrLn $ "Score minimum : " ++ show minCost
  let tileCount = countBestPathTiles grid start end minCost costFromStart costToEnd
  putStrLn $ "Nombre de tuiles du meilleur chemin : " ++ show tileCount

Celui-ci nécessitait quelques astuces de programmation dynamique pour que la partie 2 s’exécute dans les temps. Nous calculons les distances les plus courtes depuis le départ et jusqu’à la tuile d’arrivée pour chaque tuile. Nous pouvons ensuite utiliser cela pour calculer rapidement tous les chemins optimaux.

Jour 17

import Data.Array
import Data.Bits
import Data.List (foldl', intercalate)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import System.IO
import Text.Parsec
import Text.Parsec.String (Parser)

type Regs = Array Int Int

type Op = Int

type Inst = (Int, Int)

type Prog = [Inst]

type Func = Regs -> [Int] -> Op -> Int -> (Regs, [Int], Int)

[a, b, c] = [0 .. 2]

comboOperand :: Regs -> Int -> Int
comboOperand regs op
  | op <= 3 = op
  | op == 4 = regs ! a
  | op == 5 = regs ! b
  | op == 6 = regs ! c
  | otherwise = error "Opérande invalide"

adv, bxl, bst, jnz, bxc, outOp, bdv, cdv :: Func
adv regs out op ip = (regs // [(a, res)], out, ip + 1)
  where
    res = regs ! a `div` (2 ^ comboOperand regs op)
bxl regs out op ip = (regs // [(b, res)], out, ip + 1)
  where
    res = regs ! b `xor` op
bst regs out op ip = (regs // [(b, comboOperand regs op `mod` 8)], out, ip + 1)
jnz regs out op ip = (regs, out, ip')
  where
    ip' = if regs ! a /= 0 then op else ip + 1
bxc regs out op ip = (regs // [(b, regs ! b `xor` regs ! c)], out, ip + 1)
outOp regs out op ip = (regs, o : out, ip + 1)
  where
    o = comboOperand regs op `mod` 8
bdv regs out op ip = (regs // [(b, res)], out, ip + 1)
  where
    res = regs ! a `div` (2 ^ comboOperand regs op)
cdv regs out op ip = (regs // [(c, res)], out, ip + 1)
  where
    res = regs ! a `div` (2 ^ comboOperand regs op)

opFuncs :: [Func]
opFuncs = [adv, bxl, bst, jnz, bxc, outOp, bdv, cdv]

-- Parseur pour les valeurs initiales des registres
parseRegs :: Parser Regs
parseRegs = do
  string "Registre A: "
  aVal <- read <$> many1 digit
  newline
  string "Registre B: "
  bVal <- read <$> many1 digit
  newline
  string "Registre C: "
  cVal <- read <$> many1 digit
  many newline
  return $ listArray (0, 2) [aVal, bVal, cVal]

-- Parseur pour le programme
parseProg :: Parser Prog
parseProg = do
  string "Programme: "
  sepBy parseInst (char ',')

-- Parseur pour une seule instruction
parseInst :: Parser Inst
parseInst = do
  opcode <- read <$> many1 digit
  char ','
  operand <- read <$> many1 digit
  return (opcode, operand)

-- Parseur combiné pour l'entrée complète
parseInput :: Parser (Regs, Prog)
parseInput = do
  regs <- parseRegs
  prog <- parseProg
  return (regs, prog)

runProg :: Prog -> Regs -> [Int]
runProg prog regs = reverse out
  where
    progLen = length prog
    notDone (_, _, ip) = ip < progLen
    exec (regs', out, ip) =
      let (opCode, operand) = prog !! ip
          func = opFuncs !! opCode
       in func regs' out operand ip
    (_, out, _) = head $ dropWhile notDone $ iterate exec (regs, [], 0)

cycleFunc :: Int -> Int
cycleFunc a =
  let b1 = a .&. 7
      b2 = b1 `xor` 5
      c = a `shiftR` b2
      b3 = b2 `xor` 6
      a' = a `shiftR` 3
      b4 = b3 `xor` c
   in b4 .&. 7

dfs :: [Int] -> [Int]
dfs prog = concatMap (\a -> findPath a 10 a (tail prog)) initialSet
  where
    possibleAs :: Map Int [Int]
    possibleAs = Map.fromListWith (++) [(cycleFunc a, [a]) | a <- [0 .. 1023]]

    initialSet :: [Int]
    initialSet = case prog of
      [] -> []
      (x : _) -> Map.findWithDefault [] x possibleAs

    findPath :: Int -> Int -> Int -> [Int] -> [Int]
    findPath path pathI cur [] =
      if (cur `shiftR` 3) == 0 then [path] else []
    findPath path pathI cur (t : ts) =
      let cur' = cur `shiftR` 3
          candidates = Map.findWithDefault [] t possibleAs
          matches = filter (\a0 -> (a0 .&. 0x7F) == cur') candidates
       in concatMap
            ( \a ->
                let newA = (a `shiftR` 7) .&. 0x7
                    newPath = path .|. (newA `shiftL` pathI)
                 in findPath newPath (pathI + 3) a ts
            )
            matches

main :: IO ()
main = do
  content <- readFile "data17.txt"
  case parse parseInput "" content of
    Left err -> print err
    Right (regs, prog) -> do
      putStr "Partie 1: "
      putStrLn $ intercalate "," $ map show $ runProg prog regs
      let flatProg = concat [[x, y] | (x, y) <- prog]
      putStrLn $ "Partie 2: " ++ show (minimum (dfs flatProg))

La partie 2 était assez difficile. La force brute n’a pas fonctionné, donc j’ai dû inspecter manuellement le programme et trouver une méthode de recherche plus intelligente. Je n’ai pas non plus pu déboguer assez rapidement avec Haskell (problème de compétence), donc ma solution est une traduction directe de mon code de débogage en Python.

Code de débogage en Python
from collections import defaultdict

def run_specific_program(A):
    """Le programme donné, directement traduit en Python pour validation."""
    B = 0
    C = 0
    output = []

    while True:
        # Instruction 1: bst 4 -> B = A % 8
        # obtenir les 3 derniers bits de A
        B = (A & 0b111) ^ 0b101

        # Instruction 2: bxl 5 -> B ^= 5
        # B <- B ^ 0b101

        # Instruction 3: cdv 5 -> C = A // (2 ** B)
        # L'opérande 5 fait référence au registre B
        C = A >> B
        # C = A // (2 ** B)

        # Instruction 4: bxl 6 -> B ^= 6
        # 0b110
        # B ^= 0b110

        # Instruction 5: adv 3 -> A = A // 8
        A = A >> 3
        # A = A // 8

        # Instruction 6: bxc 6 -> B ^= C
        # B ^= C

        # Instruction 7: out 5 -> sortie B % 8
        output.append(str((B ^ C ^ 0b110) & 0b111))

        # Instruction 8: jnz 0 -> si A != 0, sauter au début
        if A == 0:
            break  # Arrêter le programme

    return ",".join(output)

def cycle(A):
    """Une seule itération de boucle du programme donné, traduit en Python."""
    B = A % 8  # & 0b111
    B ^= 5  # 0b101
    # 0 <= B <= 7
    C = A >> B
    B ^= 6
    A = A >> 3
    B ^= C
    out = B % 8  # & 0b111
    return out

def dfs(prog):
    possible_As = defaultdict(list)
    # cycle est uniquement une fonction des 10 premiers bits de A
    for A in range(1 << 10):
        out = cycle(A)
        possible_As[out].append(A)

    def find_path(path, path_i, cur, prog, prog_i) -> list[int]:
        # trouver tous les prochains 3 bits possibles de A étant donné les 7 derniers
        if prog_i == len(prog):
            if (cur >> 3) == 0:
                # le programme s'est terminé avec A == 0
                return [path]
            else:
                # le programme ne s'est pas terminé à temps
                return []
        cur = cur >> 3
        nexts = []
        target = prog[prog_i]
        for A0 in possible_As[target]:
            if A0 & 0b1111111 == cur:
                nexts.append(A0)

        ret = []
        for A in nexts:
            new_A = (A & (0b111 << 7)) >> 7
            cur_path = path | (new_A << path_i)
            ret.extend(find_path(cur_path, path_i + 3, A, prog, prog_i + 1))
        return ret

    init_set = possible_As[prog[0]]
    ret = []
    for cur in init_set:
        ret.extend(find_path(cur, 10, cur, prog, 1))
    return ret

prog = [2, 4, 1, 5, 7, 5, 1, 6, 0, 3, 4, 6, 5, 5, 3, 0]
test = [3, 6, 3, 7, 0, 7, 0, 3, 0]
res = dfs(prog)
print(len(res))
print(min(res))

for r in res:
    result = run_specific_program(r)
    print(result)

Jour 18

import Control.Monad
import Control.Monad.ST
import Data.Array
import Data.Array.ST
import Data.Foldable (toList)
import qualified Data.Map as Map
import Data.Maybe (isJust)
import Data.Sequence (Seq (..), (|>))
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import Debug.Trace
import System.IO
import System.IO.Unsafe (unsafePerformIO)

readPair s =
  let (pre, _ : post) = break (== ',') s
   in (read post, read pre) -- convert x,y to i,j

printGrid grid = do
  let ((iMin, jMin), (iMax, jMax)) = bounds grid
  mapM_
    ( \i -> do
        mapM_ (\j -> putStr [grid ! (i, j)]) [jMin .. jMax]
        putStrLn ""
    )
    [iMin .. iMax]

bfs grid start target = bfs' (Seq.singleton (start, 0)) Set.empty
  where
    bfs' Seq.Empty _ = Nothing
    bfs' ((current@(i, j), len) :<| rest) visited
      | current == target = Just len
      | current `Set.member` visited = bfs' rest visited
      | otherwise =
          bfs' (rest <> Seq.fromList neighbors) (Set.insert current visited)
      where
        neighbors = [((i + di, j + dj), len + 1) | (di, dj) <- directions, inBounds (i + di, j + dj), grid ! (i + di, j + dj) /= '#']
        inBounds (i, j) =
          let ((minI, minJ), (maxI, maxJ)) = bounds grid
           in i >= minI && i <= maxI && j >= minJ && j <= maxJ

directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

showPair (i, j) = show j ++ "," ++ show i

main = do
  content <- readFile "data18.txt"
  let pairs = map readPair (lines content)
  let (m, n) = (71, 71)
  let nPairs = 1024
  let gridInit = listArray ((0, 0), (m - 1, n - 1)) ['.' | i <- [0 .. m - 1], j <- [0 .. n - 1]]
  let grid = gridInit // [(p, '#') | p <- (take nPairs pairs)]
  case bfs grid (0, 0) (m - 1, n - 1) of
    Just len -> putStrLn $ "Partie 1: " ++ (show len)
    Nothing -> error "impossible de trouver la sortie"

  let total = length pairs
  let restPairs = drop nPairs pairs
  let grids = scanl addObstacle (head restPairs, grid) (zip [1025 ..] (tail restPairs))
        where
          addObstacle (_, g) (n, p) = unsafePerformIO $ do
            putStr $ "\r" ++ show n ++ "/" ++ show total ++ " paires ajoutées"
            hFlush stdout
            return (p, g // [(p, '#')])
  let result = (showPair . fst . head) $ dropWhile (\(_, g) -> isJust $ bfs g (0, 0) (m - 1, n - 1)) grids
  putStrLn ""
  print $ result

Un peu lent, mais ça fonctionne.

Jour 19

import Control.Monad (void)
import qualified Control.Monad.State as S
import Data.List (isPrefixOf)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe, mapMaybe)
import Text.Parsec
import Text.Parsec.String (Parser)

type Pattern = String

type Towel = String

type Cache = Map.Map String Bool

parsePatternOrTowel :: Parser String
parsePatternOrTowel = many1 (noneOf ", \n")

parsePatterns :: Parser [Pattern]
parsePatterns = parsePatternOrTowel `sepBy` (char ',' <* spaces)

parseTowels :: Parser [Towel]
parseTowels = many1 (parsePatternOrTowel <* spaces)

parseInput :: Parser ([Pattern], [Towel])
parseInput = do
  patterns <- parsePatterns
  void newline
  void newline
  towels <- parseTowels
  return (patterns, towels)

main :: IO ()
main = do
  content <- readFile "data19.txt"
  case parse parseInput "" content of
    Left err -> print err
    Right (patterns, towels) -> do
      putStrLn $ "Partie 1: " ++ (show $ length $ filter (\t -> countPossible patterns t > 0) towels)
      putStrLn $ "Partie 2: " ++ (show $ sum $ map (countPossible patterns) towels)

countPossible patterns towel = S.evalState (go patterns towel) Map.empty
  where
    go _ "" = return 1
    go patterns towel = do
      cache <- S.get
      case Map.lookup towel cache of
        Just result -> return result
        Nothing -> do
          let cands = filter (`isPrefixOf` towel) patterns
          let checkCandidate p = go patterns (drop (length p) towel)
          result <- sum <$> mapM checkCandidate cands
          S.modify (Map.insert towel result)
          return result

J’ai appris à utiliser la monade State. Cool !

## Jour 20
import Control.Monad
import Data.Array
import qualified Data.Map as Map
import Data.Sequence (Seq (..), (|>))
import qualified Data.Sequence as Seq
import qualified Data.Set as Set

inBounds grid (i, j) =
  let ((minI, minJ), (maxI, maxJ)) = bounds grid
   in i >= minI && i <= maxI && j >= minJ && j <= maxJ

distanceFrom grid start = go (Seq.singleton (start, 0)) Set.empty Map.empty
  where
    go Seq.Empty _ dists = listArray (bounds grid) (map (\i -> Map.findWithDefault maxBound i dists) (indices grid))
    go ((pos@(i, j), d) :<| rest) seen dists
      | pos `Set.member` seen = go rest seen dists
      | otherwise = go (rest <> Seq.fromList (neighbors grid pos)) (Set.insert pos seen) (Map.insert pos d dists)
      where
        neighbors grid (i, j) =
          [((i + di, j + dj), d + 1) | (di, dj) <- directions, inBounds grid (i + di, j + dj), grid ! (i + di, j + dj) /= '#']

distWithSkip grid distFromStart distToEnd (skipStart, skipEnd, skipLen) =
  distFromStart ! skipStart + skipLen + distToEnd ! skipEnd

directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

findPaths grid maxDist start = bfs' [] (Seq.singleton (start, 0)) Set.empty
  where
    bfs' acc Seq.Empty _ = acc
    bfs' acc ((current@(i, j), len) :<| rest) visited
      | len > maxDist = bfs' acc rest (Set.insert current visited)
      | current `Set.member` visited = bfs' acc rest visited
      | otherwise =
          let acc' = if grid ! current /= '#' then ((current, len) : acc) else acc
           in bfs' acc' (rest <> Seq.fromList neighbors) (Set.insert current visited)
      where
        neighbors = [((i + di, j + dj), len + 1) | (di, dj) <- directions, inBounds grid (i + di, j + dj)]

barriers grid cheatTime =
  let allPaths = [pos | (pos, x) <- assocs grid, x /= '#']
      reachablePaths = findPaths grid cheatTime
   in [(s, e, l) | s <- allPaths, (e, l) <- reachablePaths s]

gridFind grid target = head [pos | (pos, x) <- assocs grid, x == target]

computeSavings grid cheatTime thresh =
  let start = gridFind grid 'S'
      end = gridFind grid 'E'
      fromStart = distanceFrom grid start
      toEnd = distanceFrom grid end
      origTime = fromStart ! end
      possibleSkips = barriers grid cheatTime
      savings = map ((\x -> origTime - x) . distWithSkip grid fromStart toEnd) possibleSkips
      validSavings = filter (>= thresh) savings
   in length validSavings

main = do
  content <- readFile "data20.txt"
  let contentLines = lines content
  let (m, n) = (length contentLines, length $ head contentLines)
  let grid = listArray ((0, 0), (m - 1, n - 1)) (concat contentLines)
  let cheatTime = 2 :: Int
  let thresh = 100
  putStrLn $ "Partie 1 : " ++ (show $ computeSavings grid cheatTime thresh)
  let cheatTime2 = 20
  putStrLn $ "Partie 2 : " ++ (show $ computeSavings grid cheatTime2 thresh)

J’aime cette solution—elle est très fonctionnelle— mais elle est lente pour la partie 2.

Jour 21

import Data.Char (isDigit)
import Data.IORef
import Data.List (zip4)
import Data.Map (Map)
import qualified Data.Map as M
import System.IO.Unsafe (unsafePerformIO)

type Keypad = [String]

type Pos = (Int, Int)

dirKeypad :: Keypad
dirKeypad = [".^A", "<v>"]

dirKeypadInv :: Map Char Pos
dirKeypadInv = buildInv dirKeypad

keypad :: Keypad
keypad = ["789", "456", "123", ".0A"]

keypadInv :: Map Char Pos
keypadInv = buildInv keypad

buildInv :: Keypad -> Map Char Pos
buildInv kp =
  M.fromList
    [ (c, (r, cx))
    | (r, row) <- zip [0 ..] kp,
      (cx, c) <- zip [0 ..] row
    ]

inBounds :: Keypad -> Pos -> Bool
inBounds kp (r, c) = r >= 0 && r < length kp && c >= 0 && c < length (head kp)

neighbors :: Keypad -> Pos -> [(Pos, Char)]
neighbors kp (r, c) =
  let ds = [(0, 1), (0, -1), (1, 0), (-1, 0)]
      dch = ['>', '<', 'v', '^']
   in [ ((r + dr, c + dc), ch)
      | ((dr, dc), ch) <- zip ds dch,
        inBounds kp (r + dr, c + dc),
        (kp !! (r + dr)) !! (c + dc) /= '.'
      ]

getDirections :: Keypad -> Pos -> Pos -> [String]
getDirections kp start target = bfs [(start, "")]
  where
    bfs [] = error "Aucun chemin trouvé"
    bfs q =
      let found = [path | (p, path) <- q, p == target]
       in if not (null found)
            then found
            else
              let nxt =
                    [ (npos, path ++ [dir])
                    | (pos, path) <- q,
                      (npos, dir) <- neighbors kp pos
                    ]
               in bfs nxt

getCodeDirections :: Keypad -> Map Char Pos -> Pos -> String -> [String]
getCodeDirections kp kpinv st keys =
  let sc = (kp !! fst st) !! snd st
   in foldl
        ( \dirs (sK, tK) ->
            let nds = getDirections kp (kpinv M.! sK) (kpinv M.! tK)
             in [d ++ nd ++ "A" | d <- dirs, nd <- nds]
        )
        [""]
        (zip (sc : keys) keys)

{-# NOINLINE memoMap #-}
memoMap :: IORef (Map (Pos, Pos, Int) Int)
memoMap = unsafePerformIO (newIORef M.empty)

shortestDirectionsPairwise :: Pos -> Pos -> Int -> Int
shortestDirectionsPairwise start end depth = unsafePerformIO $ do
  m <- readIORef memoMap
  case M.lookup (start, end, depth) m of
    Just v -> return v
    Nothing -> do
      let dirs = getDirections dirKeypad start end
          dirsAct = map (++ "A") dirs
      let val =
            if depth == 1
              then minimum (map length dirsAct)
              else
                let ps = map (\d -> zip ('A' : d) d) dirsAct
                 in minimum
                      [ sum
                          [ shortestDirectionsPairwise
                              (dirKeypadInv M.! sC)
                              (dirKeypadInv M.! eC)
                              (depth - 1)
                          | (sC, eC) <- pair
                          ]
                      | pair <- ps
                      ]
      writeIORef memoMap (M.insert (start, end, depth) val m)
      return val

shortestPath :: String -> Int -> Int
shortestPath code padDepth =
  let initDirs = getCodeDirections keypad keypadInv (3, 2) code
   in minimum $
        map
          ( \d ->
              let pairs = zip ('A' : d) d
               in sum
                    [ shortestDirectionsPairwise
                        (dirKeypadInv M.! sC)
                        (dirKeypadInv M.! eC)
                        padDepth
                    | (sC, eC) <- pairs
                    ]
          )
          initDirs

parseNum :: String -> Int
parseNum = read . filter isDigit

part1 :: [String] -> Int
part1 codes =
  let nums = map parseNum codes
      lens = map (`shortestPath` 2) codes
   in sum (zipWith (*) nums lens)

part2 :: [String] -> Int
part2 codes =
  let nums = map parseNum codes
      lens = map (`shortestPath` 25) codes
   in sum (zipWith (*) nums lens)

input :: String
input =
  unlines
    [ "789A",
      "968A",
      "286A",
      "349A",
      "170A"
    ]

main :: IO ()
main = do
  let args = lines input
  print (part1 args)
  print (part2 args)

Extrêmement rapide grâce à l’IO non sécurisée 🔥. Cette solution a été traduite du code Python que j’ai utilisé pour le débogage.

Code de débogage Python
from functools import cache

dir_keypad = [list(".^A"), list("<v>")]
dir_keypad_inv = {
    k: (i, j) for i, row in enumerate(dir_keypad) for j, k in enumerate(row)
}
keypad = [
    list("789"),
    list("456"),
    list("123"),
    list(".0A"),
]
keypad_inv = {k: (i, j) for i, row in enumerate(keypad) for j, k in enumerate(row)}

def keypad_get(keypad, pos):
    i, j = pos
    return keypad[i][j]

def get_directions(cur_keypad, start, target):
    q = [(start, "")]

    def in_bounds(pos):
        i, j = pos
        return 0 <= i < len(cur_keypad) and 0 <= j < len(cur_keypad[0])

    def neighbors(pos):
        i, j = pos
        directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        direction_chars = [">", "<", "v", "^"]
        return [
            ((i + di, j + dj), dir)
            for (di, dj), dir in zip(directions, direction_chars)
            if in_bounds((i + di, j + dj)) and cur_keypad[i + di][j + dj] != "."
        ]

    while True:
        target_paths = [path for pos, path in q if pos == target]
        if target_paths:
            return target_paths
        n = []
        for p, path in q:
            for neigh, dir in neighbors(p):
                n.append((neigh, path + dir))
        if not n:
            raise Exception
        q = n

def get_code_directions(cur_keypad, cur_keypad_inv, start, keys):
    directions = [""]
    start_c = cur_keypad[start[0]][start[1]]
    for start_k, target_k in zip(start_c + keys, keys):
        start, target = cur_keypad_inv[start_k], cur_keypad_inv[target_k]
        new_dir = get_directions(cur_keypad, start, target)
        new_directions = []
        for d in directions:
            for nd in new_dir:
                new_directions.append(d + nd + "A")
        directions = new_directions
    return directions

@cache
def shortest_directions_pairwise(start, end, depth):
    dirs = get_directions(dir_keypad, start, end)
    dirs_with_activation = [d + "A" for d in dirs]
    if depth == 1:
        return min(len(d) for d in dirs_with_activation)

    dir_pairs = [list(zip("A" + d, d)) for d in dirs_with_activation]
    return min(
        sum(
            shortest_directions_pairwise(
                dir_keypad_inv[start], dir_keypad_inv[end], depth - 1
            )
            for start, end in pair
        )
        for pair in dir_pairs
    )

def shortest_path(code, n_directional_pads=2):
    init_dirs = get_code_directions(keypad, keypad_inv, (3, 2), code)
    minlen = float("inf")
    for dir in init_dirs:
        pairs = list(zip("A" + dir, dir))
        minlen = min(
            minlen,
            sum(
                shortest_directions_pairwise(
                    dir_keypad_inv[start], dir_keypad_inv[end], n_directional_pads
                )
                for start, end in pairs
            ),
        )
    return minlen

def parse_num(code):
    return int("".join(filter(str.isdigit, code)))

def part1(codes):
    nums = map(parse_num, codes)
    path_lens = map(shortest_path, codes)
    return sum(n * length for n, length in zip(nums, path_lens))

def part2(codes):
    def s(c):
        return shortest_path(c, 25)

    nums = map(parse_num, codes)
    path_lens = map(s, codes)
    return sum(n * length for n, length in zip(nums, path_lens))

input = """789A
968A
286A
349A
170A"""
args = input.split("\n")
print(part1(args))
print(part2(args))

Jour 22

import Data.Bits (xor)
import Data.List
import qualified Data.Map as Map
import Data.Ord (comparing)
import qualified Data.Set as Set

mix secret val = secret `xor` val

prune secret = secret `mod` 16777216

nextSecret secret =
  let x1 = prune $ mix secret (64 * secret)
      x2 = prune $ mix x1 (x1 `div` 32)
      x3 = prune $ mix x2 (x2 * 2048)
   in x3

secrets :: Int -> [Int]
secrets x = iterate nextSecret x

secret2k x = (secrets x) !! 2000

changes xs = zipWith (-) (tail xs) xs

slidingWindow (a : b : c : d : xs) = (a, b, c, d) : slidingWindow (b : c : d : xs)
slidingWindow _ = []

prices xs = map (`mod` 10) xs

changeMap n start =
  let secrets' = take n $ secrets start
      prices' = prices secrets'
      changes' = changes prices'
      changeWindows = slidingWindow changes'
   in Map.fromList $ reverse $ zip changeWindows (drop 4 prices')

main = do
  content <- lines <$> readFile "data22.txt"
  let nums = map read content :: [Int]
  print $ sum $ map secret2k nums
  let changeMaps = map (changeMap 2000) nums
  let allKeys = foldr Set.union Set.empty $ map Map.keysSet changeMaps
  let totalBananas key = sum $ map (\m -> Map.findWithDefault 0 key m) changeMaps
  let soldPrices = Set.toList allKeys
  let maxKey = maximumBy (comparing totalBananas) soldPrices
  putStrLn $ "Partie 2: " ++ (show $ totalBananas maxKey)

Celui-ci était assez simple. La solution est un peu lente cependant.

Jour 23

import Control.Monad (guard)
import Data.List (isPrefixOf, nub, sort, sortOn)
import qualified Data.Map as M
import qualified Data.Set as S

type Graph = M.Map String (S.Set String)

fromEdges :: [(String, String)] -> Graph
fromEdges es =
  M.fromListWith S.union $
    concatMap (\(a, b) -> [(a, S.singleton b), (b, S.singleton a)]) es

triangles :: Graph -> [[String]]
triangles g =
  nub $
    sort
      [ sort [a, b, c]
      | a <- M.keys g,
        b <- S.toList (g M.! a),
        c <- S.toList (g M.! b),
        c `S.member` (g M.! a)
      ]

bronKerbosch :: Graph -> S.Set String -> S.Set String -> S.Set String -> [S.Set String]
bronKerbosch g r p x
  | S.null p && S.null x = [r]
  | otherwise = concatMap expand candidates
  where
    unionPx = S.union p x
    pivot =
      if not (S.null unionPx)
        then S.findMin unionPx
        else error "Pivot vide inattendu"
    pivotNeighbors = M.findWithDefault S.empty pivot g
    candidates = S.difference p pivotNeighbors
    expand v = bronKerbosch g (S.insert v r) (S.intersection (g M.! v) p) (S.intersection (g M.! v) x)

main :: IO ()
main = do
  input <- lines <$> readFile "data23.txt"
  let edges = [let (a, _ : b) = break (== '-') l in (a, b) | l <- input]
      g = fromEdges edges
  let tris = triangles g
      validTris = filter (any (isPrefixOf "t")) tris
  print $ length validTris

  let allNodes = S.fromList (M.keys g)
      cliques = bronKerbosch g S.empty allNodes S.empty
      maxSize = maximum $ map S.size cliques
      largestCliques = filter ((== maxSize) . S.size) cliques
      password = sort $ S.toList $ head largestCliques
  putStrLn $ concat $ zipWith (\i s -> if i == 0 then s else "," ++ s) [0 ..] password

Je n’avais jamais entendu parler de l’algorithme de Bron-Kerbosch auparavant !


←
Ce site web
Simulateur de Portefeuille Basé sur le Ratio de Sharpe
→

back to top