Skip to content

Commit 90cdf76

Browse files
committed
Day 18: Union-Find
1 parent ff24655 commit 90cdf76

File tree

6 files changed

+201
-126
lines changed

6 files changed

+201
-126
lines changed

hs/aoc2024.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ library
5757
containers ^>=0.7,
5858
heap ^>=1.0.4,
5959
megaparsec ^>=9.7.0,
60+
monad-loops ^>=0.4.3,
6061
parallel ^>=3.2.2.0,
6162
primitive ^>=0.9.0.0,
6263
split ^>=0.2.5,

hs/src/Day18.hs

+53-33
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66
module Day18 (part1, part1', part2, part2') where
77

88
import Common (readEntire)
9-
import Data.List.NonEmpty (NonEmpty ((:|)))
10-
import Data.List.NonEmpty qualified as NonEmpty (cons, toList)
11-
import Data.Set (Set)
12-
import Data.Set qualified as Set (empty, fromList, insert, member, notMember)
9+
import Control.Monad (ap, join, liftM2)
10+
import Control.Monad.Loops (firstM)
11+
import Control.Monad.ST (runST)
12+
import Data.Function (on)
13+
import Data.Functor (($>))
14+
import Data.IntSet qualified as IntSet (empty, fromList, insert, notMember)
15+
import Data.List (scanl')
16+
import Data.Maybe (listToMaybe)
1317
import Data.Text (Text)
1418
import Data.Text qualified as T (lines, stripPrefix)
1519
import Data.Text.Read (Reader)
1620
import Data.Text.Read qualified as T (decimal)
21+
import Data.Vector.Unboxed.Mutable qualified as MV (generate, length, read, write)
1722

1823
coord :: (Integral a) => Reader (a, a)
1924
coord input = do
@@ -28,39 +33,54 @@ part1 = part1' 70 1024
2833
part1' :: Int -> Int -> Text -> Either String Int
2934
part1' size n input = do
3035
coords <- mapM (readEntire coord) . take n $ T.lines input
31-
case go size $ Set.fromList coords of
32-
Just path -> Right $ length path - 1
33-
Nothing -> Left "no solution"
34-
35-
go :: Int -> Set (Int, Int) -> Maybe (NonEmpty (Int, Int))
36-
go size visited = go' visited [(0, 0) :| []] []
36+
maybe (Left "no solution") Right $ go (IntSet.fromList $ 0 : map index coords) [((0, 0), 0)] []
3737
where
38-
go' visited' (path@(pos@(x, y) :| _) : queue1) queue2
39-
| pos `Set.member` visited' = go' visited' queue1 queue2
40-
| pos == (size, size) = Just path
41-
| otherwise =
42-
go' (Set.insert pos visited') queue1 $
43-
[ NonEmpty.cons pos' path
44-
| pos'@(x', y') <- [(x - 1, y), (x, y - 1), (x, y + 1), (x + 1, y)],
45-
0 <= x' && x' <= size && 0 <= y' && y' <= size
46-
]
47-
++ queue2
48-
go' _ _ [] = Nothing
49-
go' visited' [] queue2 = go' visited' (reverse queue2) []
38+
index (x, y) = x * (size + 1) + y
39+
go visited (((x, y), t) : queue) queue'
40+
| x == size && y == size = Just t
41+
| otherwise = go (foldl' (flip $ IntSet.insert . index) visited next) queue $ map (,t + 1) next ++ queue'
42+
where
43+
next =
44+
[ pos'
45+
| pos'@(x', y') <- [(x - 1, y), (x, y - 1), (x, y + 1), (x + 1, y)],
46+
0 <= x' && x' <= size && 0 <= y' && y' <= size && index pos' `IntSet.notMember` visited
47+
]
48+
go _ _ [] = Nothing
49+
go visited [] queue = go visited (reverse queue) []
5050

5151
part2 :: Text -> Either String (Int, Int)
5252
part2 = part2' 70
5353

5454
part2' :: Int -> Text -> Either String (Int, Int)
55-
part2' size input = mapM (readEntire coord) (T.lines input) >>= go' Set.empty
55+
part2' size input = do
56+
candidates <-
57+
reverse
58+
. filter (uncurry $ IntSet.notMember . index)
59+
. (zip `ap` scanl' (flip $ IntSet.insert . index) IntSet.empty)
60+
<$> mapM (readEntire coord) (T.lines input)
61+
let obstacles0 = maybe IntSet.empty (uncurry $ IntSet.insert . index) $ listToMaybe candidates
62+
maybe (Left "No solution") (Right . fst) $ runST $ do
63+
acc <- MV.generate (join (*) $ size + 1) id
64+
let root key = MV.read acc key >>= root' key
65+
root' key value
66+
| key == value = pure value
67+
| otherwise = do
68+
value' <- root value
69+
MV.write acc key value' $> value'
70+
union i j = join $ MV.write acc <$> root i <*> root j
71+
sequence_
72+
[ (union `on` index) pos pos'
73+
| pos@(x, y) <- join (liftM2 (,)) [0 .. size],
74+
index pos `IntSet.notMember` obstacles0,
75+
pos' <- [(x, y + 1) | y < size] ++ [(x + 1, y) | x < size],
76+
index pos' `IntSet.notMember` obstacles0
77+
]
78+
flip firstM candidates $ \(pos@(x, y), obstacles) -> do
79+
sequence_
80+
[ (union `on` index) pos pos'
81+
| pos' <- [(x - 1, y) | x > 0] ++ [(x, y - 1) | y > 0] ++ [(x, y + 1) | y < size] ++ [(x + 1, y) | x < size],
82+
index pos' `IntSet.notMember` obstacles
83+
]
84+
(==) <$> root 0 <*> root (MV.length acc - 1)
5685
where
57-
go' visited (candidate : rest) =
58-
case go size visited' of
59-
Just path ->
60-
let path' = Set.fromList $ NonEmpty.toList path
61-
(skip, rest') = span (`Set.notMember` path') rest
62-
in go' (visited' <> Set.fromList skip) rest'
63-
Nothing -> Right candidate
64-
where
65-
visited' = Set.insert candidate visited
66-
go' _ _ = Left "no solution"
86+
index (x, y) = x * (size + 1) + y

py/aoc2024/day18.py

+49-32
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"""
44

55
from collections import deque
6-
from typing import Iterable
6+
from itertools import islice
7+
from typing import Generator
78

89
SAMPLE_INPUT = """
910
5,4
@@ -34,53 +35,69 @@
3435
"""
3536

3637

37-
def _parse(data: str) -> list[tuple[int, int]]:
38-
return [
39-
(int(line[: (i := line.index(","))]), int(line[i + 1 :]))
40-
for line in data.splitlines()
41-
if "," in line
42-
]
38+
def _parse(data: str) -> Generator[tuple[int, int]]:
39+
for line in data.splitlines():
40+
if "," not in line:
41+
continue
42+
x, y = line.split(",", maxsplit=1)
43+
yield int(x), int(y)
4344

4445

45-
def findpath(obstacles: Iterable[tuple[int, int]], size: int) -> list[tuple[int, int]]:
46-
visited, queue = set(obstacles), deque(([(0, 0)],))
46+
def part1(data: str, size: int = 70, n: int = 1024) -> int | None:
47+
"""
48+
>>> part1(SAMPLE_INPUT, 6, 12)
49+
22
50+
"""
51+
visited, queue = set(islice(_parse(data), n)), deque((((0, 0), 0),))
52+
visited.add((0, 0))
4753
while queue:
48-
path = queue.popleft()
49-
x, y = pos = path[-1]
54+
(x, y), _ = pos, t = queue.popleft()
5055
if x == size and y == size:
51-
return path
52-
if pos in visited:
53-
continue
54-
visited.add(pos)
56+
return t
5557
for pos in ((x - 1, y), (x, y - 1), (x, y + 1), (x + 1, y)):
5658
x, y = pos
57-
if 0 <= x <= size and 0 <= y <= size:
58-
queue.append(path + [(x, y)])
59+
if 0 <= x <= size and 0 <= y <= size and pos not in visited:
60+
visited.add(pos)
61+
queue.append((pos, t + 1))
5962
return None
6063

6164

62-
def part1(data: str, size: int = 70, n: int = 1024) -> int:
63-
"""
64-
>>> part1(SAMPLE_INPUT, 6, 12)
65-
22
66-
"""
67-
return len(findpath(_parse(data)[:n], size)) - 1
65+
def _root[T](sets: dict[T, T], key: T) -> T:
66+
value = sets.setdefault(key, key)
67+
while key != value:
68+
sets[key], _ = key, value = value, sets.setdefault(value, value)
69+
return value
6870

6971

70-
def part2(data: str, size: int = 70) -> str:
72+
def part2(data: str, size: int = 70) -> str | None:
7173
"""
7274
>>> part2(SAMPLE_INPUT, 6)
7375
'6,1'
7476
"""
75-
obstacles, i = _parse(data), 0
76-
while True:
77-
path = findpath(obstacles[: i + 1], size)
78-
if path is None:
79-
x, y = obstacles[i]
77+
obstacles, sets = {}, {}
78+
for pos in _parse(data):
79+
if pos not in obstacles:
80+
obstacles[pos] = None
81+
for x in range(size + 1):
82+
for y in range(size + 1):
83+
pos = x, y
84+
if pos in obstacles:
85+
continue
86+
_root(sets, pos)
87+
for pos2 in ((x, y + 1), (x + 1, y)):
88+
x2, y2 = pos2
89+
if 0 <= x2 <= size and 0 <= y2 <= size and pos2 not in obstacles:
90+
sets[_root(sets, pos)] = _root(sets, pos2)
91+
for pos in list(reversed(obstacles.keys())):
92+
del obstacles[pos]
93+
x, y = pos
94+
for pos2 in ((x - 1, y), (x, y - 1), (x, y + 1), (x + 1, y)):
95+
x2, y2 = pos2
96+
if 0 <= x2 <= size and 0 <= y2 <= size and pos2 not in obstacles:
97+
sets[_root(sets, pos)] = _root(sets, pos2)
98+
if _root(sets, (0, 0)) == _root(sets, (size, size)):
8099
return f"{x},{y}"
81-
path = set(path)
82-
while obstacles[i] not in path:
83-
i += 1
100+
return None
84101

85102

86103
parts = (part1, part2)

rs/benches/criterion.rs

+2-6
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,8 @@ fn aoc2024_bench(c: &mut Criterion) -> io::Result<()> {
121121

122122
let data = get_day_input(18)?;
123123
let mut g = c.benchmark_group("day 18");
124-
g.bench_function("part 1", |b| {
125-
b.iter(|| day18::Default::part1(black_box(&data)))
126-
});
127-
g.bench_function("part 2", |b| {
128-
b.iter(|| day18::Default::part2(black_box(&data)))
129-
});
124+
g.bench_function("part 1", |b| b.iter(|| day18::part1(black_box(&data))));
125+
g.bench_function("part 2", |b| b.iter(|| day18::part2(black_box(&data))));
130126
g.finish();
131127

132128
let data = get_day_input(19)?;

0 commit comments

Comments
 (0)