Hmmm I see. Re-testing several of the days. Adding the header makes no difference for me, but not using the lambdas is a huge difference: 20x speed difference for some days. Quite the caveat emptor …
Good points, thanks!
I actually tried CLPFD first, too, but found that some queries did not terminate within seconds, so I went with CLPQ instead. ![]()
Unfortunately I don’t have as much time to spend on AoC this year, otherwise I would’ve been more involved!
RE clpfd, oddly I think it’s because you used 10^13 instead of the number written out. The former doesn’t terminate within a reasonable waiting time for me but the latter finishes very quickly. You can try it with my code to see for yourself; a bit weird.
Right, that works. ![]()
The solution using CLPQ is much faster, though. ![]()
Day 19
Missed many days, but today seems good for using tabling.
:- use_module(library(dcg/high_order)).
:- use_module(library(yall)).
:- use_module(library(aggregate)).
:- table possible_ways(_,_,sum).
towel(T) --> string_without(`,\n`, T).
towels(Ts) --> sequence(towel, `, `, Ts).
design(D) --> string_without(`\n`, D).
designs(Ds) --> sequence(design, `\n`, Ds).
towels_and_designs(Ts-Ds) --> towels(Ts), `\n\n`, designs(Ds0), { append(Ds, [[]], Ds0) }.
input(I) :- phrase_from_file(towels_and_designs(I), 'day19.txt').
possible_ways(_, [], 1).
possible_ways(Towels, Design, N) :-
aggregate_all(sum(W), (member(T, Towels),
append(T, Rest, Design),
possible_ways(Towels, Rest, W)), N).
solve(P1,P2) :-
input(Ts-Ds),
maplist(possible_ways(Ts), Ds, Ways),
include(<(0), Ways, Possible),
length(Possible, P1),
sum_list(Ways, P2).
Here is a belated Day 15 from me. Its fairly short but it was very tedious to write down. It is a good exercise and example of logical state management I think.
Here is a belated Day 16. The code is about concise as I can make it. Dijksktra was the only possible solution here because Part 2 requires all shortest paths between source and sink along with a record of all visited nodes. The code is very slow (about 90-130sec). Profiling suggest it spends all its time doing heap or rb lookups, so I’m not sure what can be done about that.
Here is my day 17. This questions is easy with clpfd, but difficult otherwise. It has the highest abandonment rate for Part2 so far this year.
:- use_module([library(clpfd), library(yall)]).
combo(Code,_,_,_,Code) :- Code #>= 0, 4 #> Code.
combo(Code0,A,B,C,Code) :- Code0 #> 3, I #= 1+mod(Code0,4), element(I,[A,B,C],Code).
execute([0,Y0|Rest],P,A0,B,C,XX,O) :- combo(Y0,A0,B,C,Y), A #= A0 // (2^Y), execute(Rest,P,A,B,C,XX,O).
execute([1,Y|Rest],P,A,B0,C,XX,O) :- B #= B0 xor Y, execute(Rest,P,A,B,C,XX,O).
execute([2,Y0|Rest],P,A,B0,C,XX,O) :- combo(Y0,A,B0,C,Y), B #= mod(Y,8), execute(Rest,P,A,B,C,XX,O).
execute([3,_|Rest],P,0,B,C,XX,O) :- execute(Rest,P,0,B,C,XX,O).
execute([3,Y|_],P0,A,B,C,XX,O) :- length(X,Y), append(X,P,P0), execute(P,P0,A,B,C,XX,O).
execute([4,_|Rest],P,A,B0,C,XX,O) :- B #= B0 xor C, execute(Rest, P,A,B,C,XX,O).
execute([5,Y0|Rest],P,A,B,C,XX,O) :- combo(Y0,A,B,C,Y), X #= Y mod 8, execute(Rest, P,A,B,C,[X|XX],O).
execute([6,Y0|Rest],P,A,B0,C,XX,O) :- combo(Y0,A,B0,C,Y), B #= A // (2^Y), execute(Rest,P,A,B,C,XX,O).
execute([7,Y0|Rest],P,A,B,C0,XX,O) :- combo(Y0,A,B,C0,Y), C #= A // (2^Y), execute(Rest,P,A,B,C,XX,O).
execute([], _,_,_,_,O0,O) :- reverse(O0,O).
solve(In, Part1, Part2) :-
read_file_to_string(In, S, []),
re_foldl([_{0:N},[N|V0],V0]>>true, "\\d+"/t, S, [A,B,C|Prog], [], []),
execute(Prog,Prog,A,B,C,[],Part1),
Part2 #> 0, execute(Prog,Prog,Part2,B,C,[],Prog), labeling([bisect],[Part2]).
Here is Day 18. The walk/3 predicate implements Djikstra again: I think its about as concise as it can be made in Prolog.
Here is my take on day 19:
:- table walk/2.
walk(S,N) :-
opts(Os),
aggregate_all(sum(N0),(member(O,Os), string_concat(O,T,S),
(T="" -> N0=1 ; walk(T,N0))), N).
solve(In, Part1, Part2) :-
read_file_to_string(In, S, []),
string_concat(S0, S2, S), string_concat(S1, "\n\n", S0),
re_foldl([_{0:X},[X|V0],V0]>>true, "[a-z]+", S1, Opts, [], []),
re_foldl([_{0:X},[X|V0],V0]>>true, "[a-z]+", S2, Pats, [], []),
retractall(opts(_)), asserta(opts(Opts)),
convlist([X,N]>>(walk(X,N),N\=0), Pats, Counts),
aggregate_all(count-sum(C), member(C,Counts), Part1-Part2).
Here is day 20. Its straightforward if you note that there is just one path through the maze. Much more difficult if you ignore that and attempt to solve the general problem.
path(X-Y, Prev, Acc, Final) :-
member(I-J, [0-1,1-0,-1-0,0-(-1)]), A is I+X, B is J+Y,
\+ p(A,B,'#'), A-B \= Prev,
!, path(A-B, X-Y, [X-Y|Acc], Final).
path(End, _, Acc, [End|Acc]).
solve(In, Part1, Part2) :-
read_file_to_string(In, S, []), string_chars(S,Cs0),
nth0(Cols, Cs0, '\n'), !, exclude(=('\n'), Cs0, Cs),
findall(I-J-C, (nth0(X,Cs,C), I is mod(X,Cols), J is X//Cols), Coo),
retractall(p(_,_,_)), maplist([I-J-C]>>asserta(p(I,J,C)), Coo),
p(X0,Y0,'S'), path(X0-Y0, none, [], Path),
lazy_findall(P1-P2,
( nth0(I1, Path, X-Y), nth0(I2, Path, I-J), I1<I2,
Man is abs(X-I) + abs(Y-J),
(2>=Man, abs(I1-I2) - Man >= 100->P1=1; P1=0),
(20>=Man, abs(I1-I2) - Man >= 100->P2=1; P2=0) ), Calc),
foldl([A-B,T1-T2,T3-T4]>>(T3 is T1+A, T4 is T2+B), Calc, 0-0, Part1-Part2).
@jan attempting to use aggregate_all/3 for this simple calculation uses about 6Gb of RAM for < 9000^2 items. Meanwhile lazy_findall/3 is great for memory but it is epically slow. Can you suggest a way to speed up the above (by orders of magnitude)?
I am not Jan so sorry about this.
Could you show how you used aggregate_all/3?
This is relevant because (from the docs and also easy to see in the source):
The Template values
count,sum(X),max(X),min(X),max(X,W)andmin(X,W)are processed incrementally rather than using findall/3 and run in constant memory.
Any other template just uses findall/3 internally.
aggregate_all(sum(P1)-sum(P2),
( nth0(I1, Path, X-Y), nth0(I2, Path, I-J), I1<I2,
Man is abs(X-I) + abs(Y-J),
(2>=Man, abs(I1-I2) - Man >= 100->P1=1; P1=0),
(20>=Man, abs(I1-I2) - Man >= 100->P2=1; P2=0) ), Part1-Part2)
My partial day 21 is here, day 22 is here and day 23 is here. I tried to learn lazy_list/3 but I’m not sure I can recommend it.
I’m missing Part 2’s for 21 and 24, but except for that this year is complete. I regret I may have to do those parts in Python. Especially with regard to 24, it felt like it should admit a neat Prolog solution, since its fault diagnostics of a binary circuit (AND, OR, XOR), but working out what CLP(B) actually does in code seem much more difficult than it needs to be. If someone else has a neat solution to 24 part 2, please post it!
Here is my solution for day 24, both parts:
% Using clpb for part1
:- use_module(library(clpb)).
day24 :-
part1(Wires, Final),
part2(Wires, Final).
part1(Wires, Final) :-
once(phrase_from_file(read_input(XYs, EPs, Wires), 'advent2024/day_24_binary.txt')),
length(XYs, LenXYs),
% Starts at zero, and is duplicated for x and y
% Is the final carry bit
Final is LenXYs / 2,
pairs_values(EPs, Es),
maplist(form_expr, Es, Exprs),
\+ \+ (
sat(*(Exprs)),
pairs_to_int(z, EPs, I),
writeln(I)
).
% https://adventofcode.com/2024/day/24
part2(Wires, Final) :-
same_length(Wires, WiresR),
% Crucial for performance
% "there are exactly four pairs of gates whose output wires have been swapped."
length(Twisteds, 8),
once(ripple_carry_adder(WiresR, 0, Final, _)),
match_wires(WiresR, Wires, Twisteds),
maplist(n_unwrap, Twisteds, TwistedsN),
msort(TwistedsN, TwistedsNS),
atomic_list_concat(TwistedsNS, ',', SwapWires),
writeln(SwapWires).
match_wires([], [], []).
match_wires([gwire(Group, Wire)|WiresR], Wires, Twisteds) :-
Wire = wire(Vars, Op, V),
( vars_perm(Vars, VarsP),
select(wire(VarsP, Op, V), Wires, Wires0),
TwistedsT = Twisteds
; Twisteds = [V,VN|TwistedsT],
Group1 is Group + 1,
Groups = [Group, Group1],
vars_perm(Vars, VarsP),
select(wire(VarsP, Op, VN), Wires, Wires1),
wire_in_groups(WiresR, Groups, wire(VarsNP, OpN, VN)),
% Find the other tangled wire in this pair
select(wire(VarsNP, OpN, V), Wires1, Wires2),
% Put back the untangled wire
Wires0 = [wire(VarsNP, OpN, VN)|Wires2]
),
match_wires(WiresR, Wires0, TwistedsT).
wire_in_groups([gwire(Group, Wire)|TGWires], Groups, WireR) :-
% The other tangled wire must be in this or the next Needed group,
% due to e.g. Carry1 and XorXY vars - crucial for performance
memberchk(Group, Groups),
( WireR = Wire
; wire_in_groups(TGWires, Groups, WireR)
).
% https://en.wikipedia.org/wiki/Adder_(electronics)#Ripple-carry_adder
% https://upload.wikimedia.org/wikipedia/commons/5/57/Fulladder.gif
ripple_carry_adder(Wires, C, Final, Carry0) :-
wires_needed_list(C, Final, Carry0, Carry1, Needed),
once(append(Needed, WiresT, Wires)),
( C < Final
-> C1 is C + 1,
ripple_carry_adder(WiresT, C1, Final, Carry1)
; WiresT = []
).
% These Needed wires are in order of will-be-ground vars, for performance
wires_needed_list(C, _Final, _Carry0, Carry1, Needed) :-
C is 0,
!,
Needed = [
gwire(C, wire(xy(C), xor, z(C))),
gwire(C, wire(xy(C), and, n(Carry1)))
].
wires_needed_list(C, Final, Carry0, _Carry1, Needed) :-
C is Final,
!,
% There will only be 1 z(Final) wire,
% so don't care about AndXY
Needed = [gwire(C, wire(nn(Carry0, _), or, z(C)))].
wires_needed_list(C, Final, Carry0, _Carry1, Needed) :-
C is Final - 1,
!,
Needed = [
gwire(C, wire(nn(XorXY, Carry0), xor, z(C))),
gwire(C, wire(xy(C), xor, n(XorXY))),
gwire(C, wire(xy(C), and, n(_AndXY))),
gwire(C, wire(nn(Carry0, XorXY), and, n(_Sum)))
].
wires_needed_list(C, _Final, Carry0, Carry1, Needed) :-
% Usual
Needed = [
gwire(C, wire(nn(XorXY, Carry0), xor, z(C))),
gwire(C, wire(xy(C), xor, n(XorXY))),
gwire(C, wire(xy(C), and, n(AndXY))),
gwire(C, wire(nn(Carry0, XorXY), and, n(ForOR))),
gwire(C, wire(nn(ForOR, AndXY), or, n(Carry1)))
].
% Don't need to distinguish x from y
vars_perm(xy(C), xy(C)).
vars_perm(nn(N1, N2), Perm) :-
( var(N1),
var(N2)
-> Perm = nn(N1, N2)
% Can swap N1 and N2
; member(Perm, [nn(N1, N2), nn(N2, N1)])
).
wire_vars(wire(xy(C), Op, z(C)), Op, [z(C), xy(C), xy(C)]).
wire_vars(wire(xy(C), Op, n(N)), Op, [n(N), xy(C), xy(C)]).
wire_vars(wire(nn(N1, N2), Op, z(C)), Op, [z(C), n(N1), n(N2)]).
wire_vars(wire(nn(N1, N2), Op, n(N)), Op, [n(N), n(N1), n(N2)]).
xyzs(C, Ps, L) :-
bagof(N-E,
(
member(N-E, Ps),
sub_atom(N, 0, _, _, C)
),
NBs
),
sort(NBs, L).
form_expr(e(Eq, E), Expr) :-
Expr = (Eq =:= E).
e_var_code(e(Eq, _E), C) :-
C is Eq + 0'0.
pairs_to_int(C, Ps, I) :-
xyzs(C, Ps, S),
reverse(S, R),
pairs_values(R, Vs),
maplist(e_var_code, Vs, Ds),
% Adding 0b for binary notation
number_codes(I, [0'0, 0'b|Ds]).
read_input(XYs, Exprs, Wires) -->
{ DE = d{} },
read_vals(DE, DV, XYs), nl,
read_combs(DV, _DC, Exprs, Wires).
read_vals(D, DF, [N-e(V, V)|XYs]) -->
word(N), ": ", !, binary(V), nl,
{ write_gate(N, D, D1, V) },
read_vals(D1, DF, XYs).
read_vals(D, D, []) --> [].
read_combs(D, DF, [Eq-e(VE, Expr)|Ps], [Wire|Wires]) -->
word(N1), !, " ", word(Op), " ", word(N2), " -> ", word(Eq), nl,
{ read_gate(N1, D, V1),
read_gate(N2, D, V2),
read_gate(Eq, D, VE),
sat_op(Op, V1, V2, Expr),
write_gate(N1, D, D1, V1),
write_gate(N2, D1, D2, V2),
write_gate(Eq, D2, D3, VE),
downcase_atom(Op, OpL),
n_wrap(Eq, EqW),
n_wrap(N1, NW1),
n_wrap(N2, NW2),
wire_vars(Wire, OpL, [EqW, NW1, NW2])
},
read_combs(D3, DF, Ps, Wires).
read_combs(D, D, [], []) --> [].
read_gate(N, D, V) :-
ignore(get_dict(N, D, V)).
write_gate(N, D, D1, V) :-
put_dict(N, D, V, D1).
sat_op('AND', V1, V2, Expr) :-
Expr = (V1 * V2).
sat_op('OR', V1, V2, Expr) :-
Expr = (V1 + V2).
sat_op('XOR', V1, V2, Expr) :-
Expr = (V1 # V2).
word(A) --> word_([H|T]),
{ atom_codes(A, [H|T]) }.
word_(L), [C] --> [C],
{ memberchk(C, [0':, 32, 9, 10, 13, 160]), !, L = [] }.
word_(L) --> [C], !, { L = [C|T] }, word_(T).
word_([]) --> [].
nl --> [10].
binary(0) --> "0".
binary(1) --> "1".
n_wrap(N, Term) :-
sub_atom(N, 0, 1, _, F),
sub_atom(N, 1, 2, 0, PI),
( memberchk(F, [x, y])
% Remove leading zero, e.g. 01 to 1
-> atom_number(PI, I),
% Don't care whether it is x or y
Term = xy(I)
; F = z
-> atom_number(PI, I),
Term = z(I)
; Term = n(N)
).
n_unwrap(n(N), N).
n_unwrap(z(C), N) :-
( C < 10
-> atomic_list_concat([z, 0, C], N)
; atomic_list_concat([z, C], N)
).
Results (I think each contestant gets a different file, so your results will differ):
?- time(day24).
53755311654662
dkr,ggk,hhh,htp,rhv,z05,z15,z20
% 933,656 inferences, 0.113 CPU in 0.113 seconds (99% CPU, 8271772 Lips)
true ;
% 137,851,580 inferences, 13.381 CPU in 13.404 seconds (100% CPU, 10301877 Lips)
false.
So, it takes under 1 second to find a solution, then another 14 seconds to check the rest of the problem space for other solutions.
Here is day 24, with improved performance for part 2:
% Using clpb for part1
:- use_module(library(clpb)).
day24 :-
part1(Wires, Final),
part2(Wires, Final).
part1(Wires, Final) :-
once(phrase_from_file(read_input(XYs, EPs, Wires), 'advent2024/day_24_binary.txt')),
length(XYs, LenXYs),
% Starts at zero, and is duplicated for x and y
% Is the final carry bit
Final is LenXYs / 2,
pairs_values(EPs, Es),
maplist(form_expr, Es, Exprs),
\+ \+ (
sat(*(Exprs)),
pairs_to_int(z, EPs, I),
writeln(I)
).
% https://adventofcode.com/2024/day/24
part2(Wires, Final) :-
same_length(Wires, WiresR),
% Crucial for performance
% "there are exactly four pairs of gates whose output wires have been swapped."
length(Twisteds, 8),
once(ripple_carry_adder(WiresR, 0, Final, _)),
match_wires(WiresR, Wires, Twisteds),
maplist(n_unwrap, Twisteds, TwistedsN),
msort(TwistedsN, TwistedsNS),
atomic_list_concat(TwistedsNS, ',', SwapWires),
writeln(SwapWires).
match_wires([], [], []).
match_wires([gwire(Group, Wire)|WiresR], Wires, Twisteds) :-
Wire = wire(Vars, Op, V),
( vars_perm(Vars, VarsP),
select(wire(VarsP, Op, V), Wires, Wires0),
TwistedsT = Twisteds
; Twisteds = [V,VN|TwistedsT],
% The other tangled wire must be in this same expected wire group,
% due to scope of the wire vars: XorXY, ForOR, AndXY, Carry0
% "(A gate can only be in at most one such pair; no gate's output was swapped multiple times.)"
wire_in_group(WiresR, Group, wire(VarsNP, OpN, VN)),
% Find the other tangled wire in this pair
select(wire(VarsNP, OpN, V), Wires, Wires1),
vars_perm(Vars, VarsP),
select(wire(VarsP, Op, VN), Wires1, Wires2),
% Put back the untangled other wire
Wires0 = [wire(VarsNP, OpN, VN)|Wires2]
),
match_wires(WiresR, Wires0, TwistedsT).
wire_in_group([gwire(Group, Wire)|TGWires], Group, WireR) :-
% Limit search to current group
( WireR = Wire
; wire_in_group(TGWires, Group, WireR)
).
% https://en.wikipedia.org/wiki/Adder_(electronics)#Ripple-carry_adder
% https://upload.wikimedia.org/wikipedia/commons/5/57/Fulladder.gif
ripple_carry_adder(Wires, C, Final, Carry0) :-
wires_needed_list(C, Final, Carry0, Carry1, Needed),
once(append(Needed, WiresT, Wires)),
( C < Final
-> C1 is C + 1,
ripple_carry_adder(WiresT, C1, Final, Carry1)
; WiresT = []
).
% These Needed wires are in order of will-be-ground vars, for performance
wires_needed_list(C, _Final, _Carry0, Carry1, Needed) :-
C is 0,
!,
Needed = [
gwire(C, wire(xy(C), xor, z(C))),
gwire(C, wire(xy(C), and, n(Carry1)))
].
wires_needed_list(C, Final, Carry0, _Carry1, Needed) :-
C is Final,
!,
% There will only be 1 z(Final) wire,
% so don't care about AndXY
Needed = [gwire(C, wire(nn(Carry0, _), or, z(C)))].
wires_needed_list(C, Final, Carry0, _Carry1, Needed) :-
C is Final - 1,
!,
Needed = [
gwire(C, wire(nn(XorXY, Carry0), xor, z(C))),
gwire(C, wire(xy(C), xor, n(XorXY))),
gwire(C, wire(xy(C), and, n(_AndXY))),
gwire(C, wire(nn(Carry0, XorXY), and, n(_Sum)))
].
wires_needed_list(C, _Final, Carry0, Carry1, Needed) :-
% Usual
Needed = [
gwire(C, wire(nn(XorXY, Carry0), xor, z(C))),
gwire(C, wire(xy(C), xor, n(XorXY))),
gwire(C, wire(xy(C), and, n(AndXY))),
gwire(C, wire(nn(Carry0, XorXY), and, n(ForOR))),
gwire(C, wire(nn(ForOR, AndXY), or, n(Carry1)))
].
% Don't need to distinguish x from y
vars_perm(xy(C), xy(C)).
vars_perm(nn(N1, N2), Perm) :-
( var(N1),
var(N2)
-> Perm = nn(N1, N2)
; % Can swap N1 and N2
member(Perm, [nn(N1, N2), nn(N2, N1)])
).
wire_vars(wire(xy(C), Op, z(C)), Op, [z(C), xy(C), xy(C)]).
wire_vars(wire(xy(C), Op, n(N)), Op, [n(N), xy(C), xy(C)]).
wire_vars(wire(nn(N1, N2), Op, z(C)), Op, [z(C), n(N1), n(N2)]).
wire_vars(wire(nn(N1, N2), Op, n(N)), Op, [n(N), n(N1), n(N2)]).
xyzs(C, Ps, L) :-
bagof(N-E,
(
member(N-E, Ps),
sub_atom(N, 0, _, _, C)
),
NBs
),
sort(NBs, L).
form_expr(e(Eq, E), Expr) :-
Expr = (Eq =:= E).
e_var_code(e(Eq, _E), C) :-
C is Eq + 0'0.
pairs_to_int(C, Ps, I) :-
xyzs(C, Ps, S),
reverse(S, R),
pairs_values(R, Vs),
maplist(e_var_code, Vs, Ds),
% Adding 0b for binary notation
number_codes(I, [0'0, 0'b|Ds]).
read_input(XYs, Exprs, Wires) -->
{ DE = d{} },
read_vals(DE, DV, XYs), nl,
read_combs(DV, _DC, Exprs, Wires).
read_vals(D, DF, [N-e(V, V)|XYs]) -->
word(N), ": ", !, binary(V), nl,
{ write_gate(N, D, D1, V) },
read_vals(D1, DF, XYs).
read_vals(D, D, []) --> [].
read_combs(D, DF, [Eq-e(VE, Expr)|Ps], [Wire|Wires]) -->
word(N1), !, " ", word(Op), " ", word(N2), " -> ", word(Eq), nl,
{ read_gate(N1, D, V1),
read_gate(N2, D, V2),
read_gate(Eq, D, VE),
sat_op(Op, V1, V2, Expr),
write_gate(N1, D, D1, V1),
write_gate(N2, D1, D2, V2),
write_gate(Eq, D2, D3, VE),
downcase_atom(Op, OpL),
n_wrap(Eq, EqW),
n_wrap(N1, NW1),
n_wrap(N2, NW2),
wire_vars(Wire, OpL, [EqW, NW1, NW2])
},
read_combs(D3, DF, Ps, Wires).
read_combs(D, D, [], []) --> [].
read_gate(N, D, V) :-
ignore(get_dict(N, D, V)).
write_gate(N, D, D1, V) :-
put_dict(N, D, V, D1).
sat_op('AND', V1, V2, Expr) :-
Expr = (V1 * V2).
sat_op('OR', V1, V2, Expr) :-
Expr = (V1 + V2).
sat_op('XOR', V1, V2, Expr) :-
Expr = (V1 # V2).
word(A) --> word_([H|T]),
{ atom_codes(A, [H|T]) }.
word_(L), [C] --> [C],
{ memberchk(C, [0':, 32, 9, 10, 13, 160]), !, L = [] }.
word_(L) --> [C], !, { L = [C|T] }, word_(T).
word_([]) --> [].
nl --> [10].
binary(0) --> "0".
binary(1) --> "1".
n_wrap(N, Term) :-
sub_atom(N, 0, 1, _, F),
sub_atom(N, 1, 2, 0, PI),
( memberchk(F, [x, y])
-> % Remove leading zero, e.g. 01 to 1
atom_number(PI, I),
% Don't care whether it is x or y
Term = xy(I)
; F = z
-> atom_number(PI, I),
Term = z(I)
; Term = n(N)
).
n_unwrap(n(N), N).
n_unwrap(z(C), N) :-
( C < 10
-> atomic_list_concat([z, 0, C], N)
; atomic_list_concat([z, C], N)
).
The tangled wire must be in the same expected wire group, because the wire variables (XorXY, ForOR, AndXY, Carry0) only have scope within that same wire group, and they will not be in other (forward) wire groups. So there is no need to check the next wire group also. This hugely improves the total execution time:
?- time(day24).
53755311654662
dkr,ggk,hhh,htp,rhv,z05,z15,z20
% 920,561 inferences, 0.084 CPU in 0.084 seconds (100% CPU, 10974142 Lips)
true ;
% 729,531 inferences, 0.072 CPU in 0.073 seconds (100% CPU, 10067432 Lips)
false.