Day 1: Historian Hysteria
In part 1, we need find the 1-norm of the distance between two (sorted) vectors. In the second part, we need to find all numbers that appear both in the first and second vector, then multiply those with their count in each vector. We get the following test data:
let test_input = [
3 4;
4 3;
2 5;
1 3;
3 9;
3 3]
= sort(test_input[:,1])
a = sort(test_input[:,2])
b @test total_distance(a, b) == 11
@test similarity_score(a, b) == 31
end
Parsing
We’re parsing two columns of integers.
tuples_to_matrix(v::Vector{NTuple{N, T}}) where {N, T} = reshape(reinterpret(T, v), (N, :))
read_input(io::IO) = let p = many(sequence(integer, integer)) >> fmap(Parser, tuples_to_matrix)
read(io, String) |> parse(p) |> result
end
Part 1
This becomes rather easy to solve if we first sort both input vectors.
total_distance(a, b) = sum(abs.(a .- b))
Part 2
Now we need to count how many times each number is in both
collections, and then find which numbers appear in both collections. We
could do this by keeping a Dict
of the counts, but if our
input collections are sorted, we don’t need to store any data, we can
just iterate through both sorted collections to compute the result. Then
it is interesting to see how we can express this succinctly.
The following implementation uses two non-standard iterator functions.
"""
count_sorted(lst)
Takes a sorted collection `lst` of item type `T` and counts consecutive repetitions
of items. Returns an iterator of `Tuple{T, Int}`.
"""
function count_sorted end
@test count_sorted([1, 1, 2, 2, 2, 3]) |> collect ==
1, 2), (2, 3), (3, 1)] [(
"""
zip_sorted(a, b; key=identity, map=identity, default=nothing)
Takes two collections `a` and `b` of item type `T`, that should be sorted by `key`.
Here, `key` is a function `T -> K`, `map` a function `T -> U`. Returns a sorted iterator of
`Tuple{K, U, U}`, replacing missing values with `default`.
"""
function zip_sorted end
@test zip_sorted([1, 2, 4, 6], [3, 4, 5, 6]; default=0) |> collect ==
1, 1, 0), (2, 2, 0), (3, 0, 3), (4, 4, 4), (5, 0, 5), (6, 6, 6)] [(
Now, zip_sorted
is a bit of a special combinator, but it
allows us to write down the answer to part two in a compact form. Also,
zip_sorted
is a kind of reduced elemental form of the kind
of concurrent iteration that we need.
function similarity_score(a, b)
sum(map(
splat(*),
zip_sorted(count_sorted(a), count_sorted(b);
=x->x[1], map=x->x[2], default=0)
key
))end
count_sorted
Now comes the painful bit. Implementing iterators in Julia is rather arduous.
struct CountSorted{V}
::V
vend
Base.IteratorSize(::CountSorted) = Base.SizeUnknown()
<<count-sorted-doc>>
count_sorted(v::V) where {V} = CountSorted(v)
struct CountSortedState{T, S}
::T
value::S
stateend
function Base.iterate(cs::CountSorted{V}, st::Nothing) where {V}
return nothing
end
function Base.iterate(cs::CountSorted{V}) where {V}
= iterate(cs.v)
n === nothing && return nothing
n iterate(cs, CountSortedState(n...))
end
function Base.iterate(cs::CountSorted{V}, st::CountSortedState{T, S}) where {V, T, S}
= st.value
item = st.state
state = 0
count
while item == st.value
+= 1
count = iterate(cs.v, state)
n === nothing && return ((st.value, count), nothing)
n = n
(item, state) end
return ((st.value, count), CountSortedState(item, state))
end
peekable
To implement zip_sorted
we need an iterator that
remembers the last obtained item.
struct Peekable{V}
::V
iterend
Base.IteratorSize(p::Peekable{V}) where V = IteratorSize(p.iter)
struct PeekableState{T,S}
::T
head::S
stateend
function Base.iterate(p::Peekable{V}) where {V}
= iterate(p.iter)
n === nothing && return nothing
n = n
(head, state) PeekableState(head, state))
(head, end
function Base.iterate(p::Peekable{V}, s::PeekableState{T,S}) where {T, V, S}
= iterate(p.iter, s.state)
n === nothing && return nothing
n = n
(head, state) PeekableState(head, state))
(head, end
zip_sorted
When we combine two iterators, we need to do bookkeeping logic for both iterators.
struct ZipSorted{It1,It2,K,M,T}
::Peekable{It1}
it1::Peekable{It2}
it2::K
key::M
map::T
defaultend
Base.IteratorSize(::ZipSorted) = Base.SizeUnknown()
<<zip-sorted-doc>>
zip_sorted(it1::It1, it2::It2; key::K = identity, map::M = identity, default::T = nothing) where {It1,It2,K,M,T} =
ZipSorted(Peekable(it1), Peekable(it2), key, map, default)
struct ZipSortedState{A,B}
::A
st1::B
st2end
function zip_sorted_helper(zs::ZipSorted{It1,It2,K,M}, st::ZipSortedState{A,B}) where {It1,It2,K,M,A,B}
= st.st1
st1 = st.st2
st2 key(st1.head) == zs.key(st2.head) &&
zs.return ((zs.key(st1.head), zs.map(st1.head), zs.map(st2.head)), st)
key(st1.head) < zs.key(st2.head) &&
zs.return ((zs.key(st1.head), zs.map(st1.head), zs.default), st)
return ((zs.key(st2.head), zs.default, zs.map(st2.head)), st)
end
function Base.iterate(zs::ZipSorted{It1,It2,K,M}) where {It1,It2,K,M}
= iterate(zs.it1)
n = iterate(zs.it2)
m === nothing && m === nothing && return nothing
n === nothing && return ((zs.key(m[1]), zs.default, zs.map(m[1])), ZipSortedState(nothing, m[2]))
n === nothing && return ((zs.key(n[1]), zs.map(n[1]), zs.default), ZipSortedState(n[2], nothing))
m zip_sorted_helper(zs, ZipSortedState(n[2], m[2]))
end
function Base.iterate(zs::ZipSorted{It1,It2,K,M}, st::ZipSortedState{Nothing,B}) where {It1,It2,K,M,B}
= iterate(zs.it2, st.st2)
n == nothing && return nothing
n = n
(v, s) return ((zs.key(v), zs.default, zs.map(v)), ZipSortedState(nothing, s))
end
function Base.iterate(zs::ZipSorted{It1,It2,K,M}, st::ZipSortedState{A,Nothing}) where {It1,It2,K,M,A}
= iterate(zs.it1, st.st1)
n == nothing && return nothing
n = n
(v, s) return ((zs.key(v), zs.map(v), zs.default), ZipSortedState(s, nothing))
end
function Base.iterate(zs::ZipSorted{It1,It2,K,M}, st::ZipSortedState{A,B}) where {It1,It2,K,M,A,B}
= st.st1
st1 = st.st2
st2 if zs.key(st1.head) == zs.key(st2.head)
= iterate(zs.it1, st1)
n = iterate(zs.it2, st2)
m === nothing && m === nothing && return nothing
n === nothing && return ((zs.key(m[1]), zs.default, zs.map(m[1])), ZipSortedState(nothing, m[2]))
n === nothing && return ((zs.key(n[1]), zs.map(n[1]), zs.default), ZipSortedState(n[2], nothing))
m = n[2]
st1 = m[2]
st2 elseif zs.key(st.st1.head) < zs.key(st.st2.head)
= iterate(zs.it1, st.st1)
n === nothing && return ((zs.key(st2.head), zs.default, zs.map(st2.head)), ZipSortedState(nothing, st2))
n = n[2]
st1 else
= iterate(zs.it2, st.st2)
n === nothing && return ((zs.key(st1.head), zs.map(st1.head), zs.default), ZipSortedState(st1, nothing))
n = n[2]
st2 end
zip_sorted_helper(zs, ZipSortedState(st1, st2))
end
Main
module Day01
using ..Monads
using ..Parsing
<<day01-parsing>>
<<day01-part1>>
<<count-sorted>>
<<peekable>>
<<zip-sorted>>
<<day01-part2>>
function main(io::IO)
= read_input(io)
input = sort(input[1,:])
a = sort(input[2,:])
b return (total_distance(a, b),
similarity_score(a, b))
end
end
using AOC2024.Day01: total_distance, similarity_score, count_sorted, zip_sorted
<<day01-spec>>
<<count-sorted-spec>>
<<zip-sorted-spec>>
Extra: Python
from collections.abc import Iterable, Generator, Callable
from typing import Protocol
import sys
class Ord(Protocol):
def __lt__(self, other, /) -> bool: ...
def count_sorted[T](lst: Iterable[T]) -> Generator[tuple[T, int]]:
= iter(lst)
it try:
= next(it)
value except StopIteration:
return
= 1
count
for x in it:
if x != value:
yield (value, count)
= x
value = 1
count else:
+= 1
count
yield (value, count)
def test_count_sorted():
assert list(count_sorted([1, 2, 2, 2, 3, 3])) == [(1, 1), (2, 3), (3, 2)]
assert list(count_sorted([])) == []
def identity[T](x: T) -> T:
return x
def merge_sorted[T, K: Ord, U](
a: Iterable[T],
b: Iterable[T],
key: Callable[[T], K],map: Callable[[T], U] = identity,
= None) -> Generator[tuple[K, U, U]]:
default: U
= iter(a)
it1 = iter(b)
it2
def advance_it1():
# if `a` is empty, yield from `b`
try:
= next(it1)
x except StopIteration:
yield from ((key(y), default, map(y)) for y in it2)
return
else:
return x
def advance_it2(x=None):
# if `b` is empty, yield from `a` (but we already took one!)
try:
= next(it2)
y except StopIteration:
if x is not None:
yield (key(x), map(x), default)
yield from ((key(x), map(x), default) for x in it1)
return
else:
return y
= (yield from advance_it1())
x if x is None:
return
= (yield from advance_it2(x))
y if y is None:
return
while True:
if key(x) == key(y):
yield (key(x), map(x), map(y))
= (yield from advance_it1())
x if x is None:
return
= (yield from advance_it2(x))
y if y is None:
return
elif key(x) < key(y):
yield (key(x), map(x), default)
= (yield from advance_it1())
x if x is None:
return
else:
yield (key(y), default, map(y))
= (yield from advance_it2())
y if y is None:
return
def test_merge_sorted():
assert list(merge_sorted([1, 2, 4], [2, 3, 4, 5], key=identity, default=0)) \
== [(1, 1, 0), (2, 2, 2), (3, 0, 3), (4, 4, 4), (5, 0, 5)]
assert list(merge_sorted(["a", "aaa"], ["b", "bb", "bbb"], key=len)) \
== [(1, "a", "b"), (2, None, "bb"), (3, "aaa", "bbb")]
def read_input(f):
= [tuple(map(int, l.split())) for l in f]
data = sorted(x[0] for x in data)
a = sorted(x[1] for x in data)
b return a, b
if __name__ == "__main__":
= read_input(sys.stdin)
a, b = sum(abs(x - y) for (x, y) in zip(a, b))
part1 = sum(k * x * y for (k, x, y) in
part2
merge_sorted(count_sorted(a), count_sorted(b),=lambda p: p[0], map=lambda p: p[1], default=0))
keyprint(f"Part 1: {part1}")
print(f"Part 2: {part2}")
Extra: Rust
use std::io;
use std::num::{ParseIntError};
use itertools::Itertools;
#[derive(Debug)]
enum Error {
io::Error),
IO(
ParseInt(ParseIntError)}
fn read_integer_pair(line: &str) -> Result<Vec<u32>, Error> {
.split(' ').map(|x| x.parse::<u32>()).collect::<Result<Vec<u32>, _>>().map_err(Error::ParseInt)
line}
fn main() -> Result<(), io::Error> {
let input: Vec<Vec<u32>> = io::stdin().lines().map(read_integer_pair).collect::<Result<Vec<_>, _>>()?;
println!("{:?}", input);
Ok(())
}
Extra: 2015 Day 19
The puzzle links back to a previous puzzle that is very hard.
module Year2015Day19
end