Skip to content

Commit

Permalink
Dynamic programming
Browse files Browse the repository at this point in the history
  • Loading branch information
ephemient committed Dec 12, 2023
1 parent 85e624f commit bb076d5
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 136 deletions.
71 changes: 21 additions & 50 deletions hs/src/Day12.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,35 @@
Module: Day12
Description: <https://adventofcode.com/2023/day/12 Day 12: Hot Springs>
-}
{-# LANGUAGE BlockArguments, LambdaCase, MultiWayIf, OverloadedStrings, TransformListComp, ViewPatterns #-}
{-# LANGUAGE MultiWayIf, OverloadedStrings, ViewPatterns #-}
module Day12 (part1, part2) where

import Common (readEntire)
import Control.Monad (forM_, when)
import Control.Monad.State (MonadState, evalState, execStateT, gets, modify, modify')
import Control.Monad.Trans (lift)
import Control.Parallel.Strategies (parMap, rseq)
import Data.List (foldl', maximumBy, inits, tails, scanl')
import Data.Map (Map)
import qualified Data.Map as Map (empty, insert, lookup)
import Data.Ord (comparing)
import Data.List (foldl')
import Data.Text (Text)
import qualified Data.Text as T (all, any, breakOn, count, drop, dropAround, dropEnd, dropWhile, dropWhileEnd, head, index, intercalate, length, lines, null, split, tail, take, unlines, unpack, words)
import qualified Data.Text as T (any, drop, findIndex, index, intercalate, length, lines, split, take, words)
import qualified Data.Text.Read as T (decimal)

choose :: Int -> Int -> Int
n `choose` r = foldl' f 1 $ zip [1..r] [n, n - 1..] where
f k (a, b) | (q, 0) <- (k * b) `divMod` a = q
infix 1 `choose`
import Data.Vector.Unboxed (Vector)
import qualified Data.Vector.Unboxed as V (drop, generate, sum, take)

solutions :: Text -> [Int] -> Int
solutions s xs = evalState (solutions' s xs) Map.empty where
solutions' (T.dropAround (== '.') -> s) xs = gets (Map.lookup (s, xs)) >>= flip maybe pure do
let m = sum xs
x:xs' = xs
maxRun = maximumBy (comparing T.length) $ T.split (/= '#') s
result <- if
| T.count "#" s > m || m > T.length s - T.count "." s || m + length xs - 1 > T.length s
-> pure 0
| T.null s || null xs -> pure 1
| (leftS, rightS) <- T.breakOn "." s, not $ T.null rightS -> flip execStateT 0 $
forM_
[ (leftXs, rightXs)
| (leftXs, rightXs, acc) <- zip3 (inits xs) (tails xs) $ scanl' ((+) . succ) (-1) xs
, then takeWhile by acc <= T.length leftS
] $ \(leftXs, rightXs) -> lift (solutions' leftS leftXs) >>= \case
0 -> pure ()
left -> lift (solutions' rightS rightXs) >>= modify' . (+) . (left *)
| T.all (/= '#') s -> pure $ T.length s - m + 1 `choose` length xs
| T.length maxRun > maximum xs -> pure 0
| not $ T.null maxRun, (leftS, rightS) <- T.breakOn maxRun s -> flip execStateT 0 $
forM_
[ (T.dropEnd (dx + 1) leftS, leftXs, T.drop (x' - dx + 1) rightS, rightXs)
| (leftXs, x' : rightXs, acc) <- zip3 (inits xs) (tails xs) $ scanl' ((+) . succ) 0 xs
, dx <- [max 0 $ x' - T.length rightS..x' - T.length maxRun]
, then takeWhile by acc + dx <= T.length leftS
, dx + 1 > T.length leftS || leftS `T.index` (T.length leftS - dx - 1) /= '#'
, x' - dx >= T.length rightS || rightS `T.index` (x' - dx) /= '#'
] $ \(leftS, leftXs, rightS, rightXs) -> lift (solutions' leftS leftXs) >>= \case
0 -> pure ()
left -> lift (solutions' rightS rightXs) >>= modify' . (+) . (left *)
| otherwise -> flip execStateT 0 $ do
when (x == T.length s || s `T.index` x /= '#') $
lift (solutions' (T.drop (x + 1) s) xs') >>= modify' . (+)
when (T.head s /= '#') $ lift (solutions' (T.tail s) xs) >>= modify' . (+)
modify $ Map.insert (s, xs) result
pure result
solutions string (reverse -> run0:runs) =
V.sum . maybe id (V.take . succ) (T.findIndex (== '#') string) $ foldl' f v0 runs where
v0 = V.generate (T.length string) $ \i -> if
| i + run0 > T.length string -> 0
| i /= 0, string `T.index` (i - 1) == '#' -> 0
| T.any (== '.') . T.take run0 $ T.drop i string -> 0
| T.any (== '#') $ T.drop (i + run0) string -> 0
| otherwise -> 1
f v run = V.generate (T.length string) $ \i -> if
| i + run >= T.length string -> 0
| i /= 0, string `T.index` (i - 1) == '#' -> 0
| T.any (== '.') . T.take run $ T.drop i string -> 0
| string `T.index` (i + run) == '#' -> 0
| otherwise -> V.sum .
maybe id (V.take . succ) (T.findIndex (== '#') $ T.drop (i + run + 1) string) $
V.drop (i + run + 1) v

part1 :: Text -> Int
part1 = sum . parMap rseq part1' . T.lines where
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,39 @@ class Day12(input: String) {
lhs to rhs.split(',').map { it.toIntOrNull() ?: return@mapNotNull null }
}

suspend fun part1(): Long = input.parSum(::calculate)
suspend fun part1(): Long = input.parSum { (string, runs) -> calculate(string, runs).toLong() }

suspend fun part2(): Long = input.parSum { (string, runs) ->
calculate(List(5) { string }.joinToString(",") to List(5) { runs }.flatten())
calculate(List(5) { string }.joinToString(","), List(5) { runs }.flatten()).toLong()
}

companion object {
private fun calculate(input: Pair<String, List<Int>>): Long {
val memo = mutableMapOf<Pair<String, List<Int>>, Long>()
return DeepRecursiveFunction<Pair<String, List<Int>>, Long> { (string, runs) ->
val trimmed = string.trim('.')
memo.getOrPut(trimmed to runs) {
val m = runs.sum()
when {
trimmed.count { it == '#' } > m ||
m > trimmed.length - trimmed.count { it == '.' } ||
m + runs.size - 1 > trimmed.length
-> 0
trimmed.isEmpty() || runs.isEmpty() -> 1
else -> if (
trimmed.subSequence(1, runs[0]).all { it != '.' } &&
trimmed.getOrNull(runs[0]) != '#'
) { callRecursive(trimmed.drop(runs[0] + 1) to runs.subList(1, runs.size)) } else { 0 } +
if (!trimmed.startsWith('#')) { callRecursive(trimmed.drop(1) to runs) } else { 0 }
private fun calculate(string: String, runs: List<Int>): Int {
val arr = runs.subList(0, runs.lastIndex).asReversed().fold(
IntArray(string.length) { i ->
if (
i + runs.last() > string.length ||
string.getOrElse(i - 1) { '.' } == '#' ||
(i until i + runs.last()).any { string[it] == '.' } ||
(i + runs.last() until string.length).any { string[it] == '#' }
) 0 else 1
}
) { arr, run ->
IntArray(string.length) { i ->
if (i + run > string.length ||
string.getOrElse(i - 1) { '.' } == '#' ||
(i until (i + run).coerceAtMost(string.length)).any { string[it] == '.' } ||
string.getOrElse(i + run) { '.' } == '#'
) return@IntArray 0
var acc = 0
for (j in i + run + 1 until string.length) {
acc += arr[j]
if (string[j] == '#') break
}
acc
}
}(input)
}
return arr.take(string.indexOf('#') + 1).sum()
}
}
}
47 changes: 25 additions & 22 deletions py/aoc2023/day12.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
Day 12: Hot Springs
"""

from functools import cache

SAMPLE_INPUT = """
???.### 1,1,3
.??..??...?##. 1,1,3
Expand All @@ -14,26 +12,31 @@
"""


def _solve(string, runs):
@cache
def solve_helper(string, runs):
m = sum(runs)
if (
m < string.count("#")
or m > len(string) - string.count(".")
or m + len(runs) - 1 > len(string)
):
return 0
if not string or not runs:
return 1
m = 0
if "." not in string[0 : runs[0]] and not string[runs[0] :].startswith("#"):
m += solve_helper(string[runs[0] + 1 :].strip("."), runs[1:])
if not string.startswith("#"):
m += solve_helper(string[1:], runs)
return m

return solve_helper(string.strip("."), runs)
def _solve(string: str, runs: list[int]):
counts = [
i + runs[-1] <= len(string)
and string[i - 1 : i] != "#"
and "." not in string[i : i + runs[-1]]
and "#" not in string[i + runs[-1] :]
for i in range(len(string))
]
for run in runs[-2::-1]:
counts = [
i + run < len(string)
and string[i - 1 : i] != "#"
and "." not in string[i : i + run]
and "#" not in string[i + run : i + run + 1]
and sum(
counts[
i + run + 1 : string.index("#", i + run + 1) + 1
if "#" in string[i + run + 1 :]
else len(string)
]
)
for i in range(len(string))
]
total = sum(counts[: string.index("#") + 1 if "#" in string else len(string)])
return total


def part1(data):
Expand Down
8 changes: 7 additions & 1 deletion rs/benches/criterion.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use aoc2023::{day1, day10, day11, day2, day3, day4, day5, day6, day7, day8, day9};
use aoc2023::{day1, day10, day11, day12, day2, day3, day4, day5, day6, day7, day8, day9};
use criterion::{black_box, Criterion};
use std::env;
use std::fs;
Expand Down Expand Up @@ -80,6 +80,12 @@ fn aoc2023_bench(c: &mut Criterion) -> io::Result<()> {
g.bench_function("part 2", |b| b.iter(|| day11::part2(black_box(&data))));
g.finish();

let data = get_day_input(12)?;
let mut g = c.benchmark_group("day 12");
g.bench_function("part 1", |b| b.iter(|| day12::part1(black_box(&data))));
g.bench_function("part 2", |b| b.iter(|| day12::part2(black_box(&data))));
g.finish();

Ok(())
}

Expand Down
92 changes: 49 additions & 43 deletions rs/src/day12.rs
Original file line number Diff line number Diff line change
@@ -1,55 +1,61 @@
use itertools::Itertools;
use std::collections::HashMap;

struct Solver<'a> {
memo: HashMap<(&'a str, &'a [usize]), usize>,
}

impl<'a> Solver<'a> {
fn solve(&mut self, string: &'a str, runs: &'a [usize]) -> usize {
let string = string.trim_matches('.');
if let Some(&result) = self.memo.get(&(string, runs)) {
return result;
}
let m = runs.iter().sum::<usize>();
let result = if m < string.chars().filter(|&c| c == '#').count()
|| m > string.chars().filter(|&c| c != '.').count()
|| m + runs.len() > string.len() + 1
{
0
} else if string.is_empty() || runs.is_empty() {
1
} else {
let x = runs[0];
(if string[..x].chars().any(|c| c == '.') || string[x..].starts_with('#') {
0
} else {
self.solve(&string[(x + 1).min(string.len())..], &runs[1..])
}) + if string.starts_with('#') {
0
} else {
self.solve(&string[1..], runs)
}
};
self.memo.insert((string, runs), result);
result
}
}

fn solve<const N: usize>(line: &str) -> Option<usize> {
let (lhs, rhs) = line.split_once(' ')?;
let rhs = rhs
.split(',')
.map(|x| x.parse().ok())
.collect::<Option<Vec<_>>>()?;
let string = Itertools::intersperse([&lhs; N].into_iter(), &"?")
.flat_map(|s| s.chars())
.collect::<Vec<_>>();
let runs = [&rhs; N].into_iter().flatten().copied().collect::<Vec<_>>();
let last_run = runs.last()?;
let counts = runs.iter().rev().skip(1).fold(
(0..string.len())
.map(|i| {
if i + last_run > string.len()
|| i != 0 && string[i - 1] == '#'
|| string[i..i + last_run].iter().any(|&c| c == '.')
|| string[i + last_run..].iter().any(|&c| c == '#')
{
0
} else {
1
}
})
.collect::<Vec<_>>(),
|counts, run| {
(0..string.len())
.map(|i| {
if i + run >= string.len()
|| i != 0 && string[i - 1] == '#'
|| string[i..i + run].iter().any(|&c| c == '.')
|| string[i + run] == '#'
{
0
} else {
counts[i + run + 1..]
.iter()
.zip(
string[i + run + 1..]
.iter()
.take_while(|&&c| c != '#')
.chain([&'#']),
)
.map(|(&count, _)| count)
.sum()
}
})
.collect()
},
);
Some(
Solver {
memo: HashMap::new(),
}
.solve(
&Itertools::intersperse([lhs; N].into_iter(), "?").collect::<String>(),
&[&rhs; N].into_iter().flatten().copied().collect::<Vec<_>>(),
),
counts
.into_iter()
.zip(string.into_iter().take_while(|&c| c != '#').chain(['#']))
.map(|(count, _)| count)
.sum(),
)
}

Expand Down

0 comments on commit bb076d5

Please sign in to comment.