I wanted to challenge myself by solving this in a multi-threaded way. Not necessary by any stretch for the default input, but it can save time for some of the fan-made expanded inputs out there, like this one: Aoc2024/bigboys/day3.7z at main · Error916/Aoc2024 · GitHub
You could find safe segments to multithread by searching for instances of do()
in the input so you know what the starting state is, but I took a different approach.
- Each worker starts with p2_enabled as indeterminate.
- They read from i = x * worker_no, to i + x - 1.
- Any muls read when enabled is indeterminate are accumulated in a special pool, to be checked when all the workers are finished.
- Otherwise you work as normal up to the boundary.
- Once all the workers finish, deal with the indeterminate results and make sure no commands are crossing the boundaries.
-module(day03).
-export([go/1, worker/1, run/2]).
go(Filename) ->
{ok, Data} = file:read_file(Filename),
run(Data).
run(Data) ->
Threads = erlang:system_info(schedulers),
run(Data, Threads).
run(Data, Threads) ->
PartLength = trunc(byte_size(Data) / Threads) + 1,
PartsWEnd = [ binary_part(Data, PartLength * N, PartLength) || N <- lists:seq(0, Threads-2) ],
FinalLength = byte_size(Data) - (PartLength * (Threads - 1)),
Parts = PartsWEnd ++ [binary_part(Data, PartLength * (Threads - 1), FinalLength)],
Keys = [ rpc:async_call(node(), ?MODULE, worker, [D]) || D <- Parts ],
Results = [ rpc:yield(Key) || Key <- Keys ],
accumulate(true, Results, tl(Parts)).
worker(Data) ->
worker(read_next(Data), tralse, 0, 0, 0, Data).
worker({A, B, Rest}, Enabled, P1, P2, Ind, _) ->
N = A * B,
case Enabled of
true ->
worker(read_next(Rest), Enabled, P1 + N, P2 + N, Ind, Rest);
false ->
worker(read_next(Rest), Enabled, P1 + N, P2, Ind, Rest);
tralse ->
worker(read_next(Rest), Enabled, P1 + N, P2, Ind + N, Rest)
end;
worker({enable, Rest}, _, P1, P2, Ind, _) ->
worker(read_next(Rest), true, P1, P2, Ind, Rest);
worker({disable, Rest}, _, P1, P2, Ind, _) ->
worker(read_next(Rest), false, P1, P2, Ind, Rest);
worker(eof, Enabled, P1, P2, Ind, LastRest) ->
{Enabled, P1, P2, Ind, LastRest}.
read_next(<<"mul(", NumbersAndRest/binary>>) ->
case read_numbers(NumbersAndRest) of
{A, B, Rest} ->
{A, B, Rest};
_ ->
read_next(NumbersAndRest)
end;
read_next(<<"don't()", Rest/binary>>) ->
{disable, Rest};
read_next(<<"do()", Rest/binary>>) ->
{enable, Rest};
read_next(<<_, Rest/binary>>) ->
read_next(Rest);
read_next(<<>>) ->
eof.
accumulate(Enabled, [{MaybeNextEnabled,P1,P2,Ind,Rest}|Results], Datas) ->
% Is something on the boundary?
{PrevRead, BoundRead, NextDatas} = case Datas of
[] -> {nil, nil, Datas};
[NextData|R] ->
Peek = binary_part(NextData, 0, 100),
{read_next(Peek), read_next(<<Rest/binary, Peek/binary>>), R}
end,
NextEnabled = case MaybeNextEnabled of
tralse -> Enabled;
_ -> MaybeNextEnabled
end,
{ActualP1, ActualP2, ActualInd} = if
PrevRead == BoundRead -> {P1,P2,Ind};
PrevRead /= BoundRead -> account_for_bound_read(NextEnabled, BoundRead, {P1,P2,Ind})
end,
P2WithInd = if
Enabled -> ActualP2 + ActualInd;
not Enabled -> ActualP2
end,
{NextP1, NextP2} = accumulate(NextEnabled, Results, NextDatas),
{ActualP1 + NextP1, P2WithInd + NextP2};
accumulate(_, [], _) ->
{0, 0}.
account_for_bound_read(Enabled, {A,B,_}, {P1, P2, Ind}) ->
N = A * B,
case Enabled of
true -> {P1 + N, P2 + N, Ind};
false -> {P1 + N, P2, Ind}
end;
account_for_bound_read(_, {enable, _}, {P1, P2, Ind}) ->
{P1, P2 + Ind, 0};
account_for_bound_read(_, {disable, _}, {P1, P2, _}) ->
{P1, P2, 0}.
read_numbers(Data) ->
maybe
{A, D2} ?= read_number(Data, $,),
{B, D3} ?= read_number(D2, $)),
{A, B, D3}
else
_ -> invalid
end.
-define(DIG(X), X >= $0 andalso X =< $9).
read_number(<<A, B, C, Term, Rest/binary>>, Term) when ?DIG(A) andalso ?DIG(B) andalso ?DIG(C) ->
{(A-$0) * 100 + (B-$0) * 10 + (C-$0), Rest};
read_number(<<A, B, Term, Rest/binary>>, Term) when ?DIG(A) andalso ?DIG(B) ->
{(A-$0) * 10 + (B-$0), Rest};
read_number(<<A, Term, Rest/binary>>, Term) when ?DIG(A) ->
{A-$0, Rest};
read_number(_, _) ->
invalid.
This ended up working quite well. On the input I linked above, my runtime went from about a second to only 115ms. More importantly, it was fun to think about this kind of parallellization.
As a bonus, I got to use one of my favorite jokes for the indeterminate state; true | false | tralse