Advertisement
dekulan

AoC 2021, day 8 part 2, mostly complete

Jan 6th, 2022
1,422
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Prolog 17.43 KB | None | 0 0
  1. % -*- Prolog -*-
  2.  
  3. % Advent of Code 2021, Day 8, part 2 (SWI Prolog Solution)
  4. %
  5. % Example input
  6. %
  7. % acedgfb cdfbe gcdfa fbcad dab cefabd cdfgeb eafb cagedb ab |
  8. % cdfeb fcadb cdfeb cdbaf
  9.  
  10. % The main problem here is parsing the first part of the input and
  11. % using knowledge of how the 10 digits appear to determine:
  12. %
  13. % * which letters correspond to which segments; and
  14. % * which of the 10 readings correspond to which digits
  15. %
  16. % In part 1, it was pointed out for us that 'ab' above must refer to
  17. % the digit one, since that digit is the only one with exactly two
  18. % segments lit up.
  19. %
  20. % In this part, one problem is determining:
  21. %
  22. % * how the readings with 5 segments map to digits two, three and five
  23. % * how the readings with 6 segments map to digits zero, six and nine
  24. %
  25. % If we know that, then we can examine the second part of the input
  26. % and uniquely determine what digit that set of letters corresponds
  27. % to.
  28.  
  29.  
  30. % A simple (brute force) solution to the above would take all possible
  31. % permutations that assign letters to segments. We can then test those
  32. % assignments against what we know about what segments are in each
  33. % digit.
  34. %
  35. % It's not exactly trivial to code this in Prolog, but at least the
  36. % problem is decomposable into parts that can be worked on
  37. % individually.
  38.  
  39. % Representation
  40. %
  41. % We have two immediate problems with representation:
  42. %
  43. % * representing segment arrangement within digits
  44. % * representing letters within readings (and readings in general)
  45. %
  46. % Both of these require us to work with sets or some similar
  47. % structure. This is because, while the order of letters in the input
  48. % may differ, they all refer to the same set of letters/segments.
  49. %
  50. % The first part above is easiest. We can name each segment and then
  51. % use a contains clause to indicate inclusion. Here, vertical segments
  52. % are assigned names based on diagonal compass directions, whereas
  53. % horizontal ones are named 'top', 'mid' and 'bot':
  54.  
  55. % the order of clauses isn't important
  56. digit_lit(zero, top).
  57. digit_lit(zero, nw).
  58. digit_lit(zero, ne).
  59. digit_lit(zero, sw).
  60. digit_lit(zero, se).
  61. digit_lit(zero, bot).
  62.  
  63. digit_lit(one, ne).
  64. digit_lit(one, se).
  65.  
  66. digit_lit(two, top).
  67. digit_lit(two, ne).
  68. digit_lit(two, mid).
  69. digit_lit(two, sw).
  70. digit_lit(two, bot).
  71.  
  72. digit_lit(three, top).
  73. digit_lit(three, ne).
  74. digit_lit(three, mid).
  75. digit_lit(three, se).
  76. digit_lit(three, bot).
  77.  
  78. digit_lit(four, nw).
  79. digit_lit(four, ne).
  80. digit_lit(four, mid).
  81. digit_lit(four, se).
  82.  
  83. digit_lit(five, top).
  84. digit_lit(five, nw).
  85. digit_lit(five, mid).
  86. digit_lit(five, se).
  87. digit_lit(five, bot).
  88.  
  89. digit_lit(six, top).
  90. digit_lit(six, nw).
  91. digit_lit(six, mid).
  92. digit_lit(six, sw).
  93. digit_lit(six, se).
  94. digit_lit(six, bot).
  95.  
  96. digit_lit(seven, top).
  97. digit_lit(seven, ne).
  98. digit_lit(seven, se).
  99.  
  100. digit_lit(eight, top).
  101. digit_lit(eight, nw).
  102. digit_lit(eight, ne).
  103. digit_lit(eight, mid).
  104. digit_lit(eight, sw).
  105. digit_lit(eight, se).
  106. digit_lit(eight, bot).
  107.  
  108. digit_lit(nine, top).
  109. digit_lit(nine, nw).
  110. digit_lit(nine, ne).
  111. digit_lit(nine, mid).
  112. digit_lit(nine, se).
  113. digit_lit(nine, bot).
  114.  
  115.  
  116. % we can also add a relation between a digit and the number of
  117. % segments it has
  118. digit_segments(zero,  6).
  119. digit_segments(one,   2).
  120. digit_segments(two,   5).
  121. digit_segments(three, 5).
  122. digit_segments(four,  4).
  123. digit_segments(five,  5).
  124. digit_segments(six,   6).
  125. digit_segments(seven, 3).
  126. digit_segments(eight, 7).
  127. digit_segments(nine,  6).
  128.  
  129. % The above could also be calculated using findall (or setof, in case
  130. % we've accidentally listed a segment twice) on digit_lit and
  131. % counting the number of returned matches.
  132.  
  133. % Note that this gives us set-like structure for working with known
  134. % segments within each digit. Another way to do this would be to
  135. % assign a 7-bit binary number to the arrangement of segments, eg,
  136. % numbering the segments as follows:
  137. %
  138. %          top = 1
  139. %
  140. % nw = 2              ne = 4
  141. %
  142. %          mid = 8
  143. %
  144. % sw = 16             se = 32
  145. %
  146. %          bot = 64
  147.  
  148.  
  149. % This lets us use an alternative definition involving:
  150. %
  151. % digit name
  152. % digit reading
  153. % number of segments
  154. % binary number representing arrangement
  155. digit_info(zero,   0, 6, Arr) :- Arr is 1 + 2 + 4 + 16 + 32 + 64.
  156. digit_info(one,    1, 2, Arr) :- Arr is 4 + 32.
  157. digit_info(two,    2, 5, Arr) :- Arr is 1 + 4 + 8 + 16 + 64.
  158. digit_info(three,  3, 5, Arr) :- Arr is 1 + 4 + 8 + 32 + 64.
  159. digit_info(four,   4, 4, Arr) :- Arr is 2 + 4 + 8 + 32.
  160. digit_info(five,   5, 5, Arr) :- Arr is 1 + 2 + 8 + 32 + 64.
  161. digit_info(six,    6, 6, Arr) :- Arr is 1 + 2 + 8 + 16 + 32 + 64.
  162. digit_info(seven,  7, 3, Arr) :- Arr is 1 + 4 + 32.
  163. digit_info(eight,  8, 7, Arr) :- Arr is 127.
  164. digit_info(nine,   9, 6, Arr) :- Arr is 1 + 2 + 4 + 8 + 32 + 64.
  165.  
  166. % I'm not sure what sort of support this Prolog (SWI) has for bit
  167. % manipulation, but we may not need to use it explicitly. For example,
  168. % if our permutations mapping letters to segments represents segments
  169. % according to their binary encoding (2**something), then we might
  170. % only need to work in a forward direction (translating letters to
  171. % binary numbers, summing those, and comparing the result with the
  172. % known arrangements), and so won't have to ask questions like "is bit
  173. % x set" or perform manipulations like "flip bit x"
  174.  
  175. % A quick look at permutation.
  176.  
  177. % We want an input list and an output list. We also need a second
  178. % predicate that finds and removes the n'th value from the first list
  179.  
  180. % remove([H], H, []) :- !. % not needed since T can be []
  181. remove([H|T], H, T).
  182. remove([H|T], L, [H|R]) :- remove(T, L, R).
  183.  
  184. % The following fails on trying to permute an empty set, which is fine
  185. %
  186. % Satisfyingly, it works for ?- permute([a,b,c],[c,a,X]). (X = b)
  187.  
  188. permute([Any], [Any]).
  189. permute(In, [Item|Out]) :-
  190.     remove(In, Item, Rest),
  191.     permute(Rest, Out).
  192.  
  193. % This doesn't work, though:
  194. % ?- permute([a,b,c,c],[c,a,X,Y]).
  195. %
  196. % We get repeated output values because the input list has repeated
  197. % values. That's not a problem for our application, though.
  198.  
  199. % (OMG... I'm writing correct Prolog code!)
  200.  
  201. % A quick note about mapping readings to digits based on the above
  202. % segment permutations...
  203. %
  204. % You might think that you have to add extra logic to make sure that
  205. % once a digit is assigned to a reading, that that digit can't be used
  206. % again later. However, based on just general understanding of the
  207. % problem and the distinctness of the actual segment patterns, we can
  208. % actually combine validation of the input permutation with production
  209. % of the string (reading) to digit mapping.
  210. %
  211. % The two parts are:
  212. %
  213. % a) are letter to segment mappings correct for ...
  214. %    zero digit .. nine digit
  215. %
  216. % b) GIVEN that the above mappings are correct, create a list of
  217. %    reading to digit mappings
  218. %
  219. % Obviously, I am working under the assumption that only a single
  220. % permutation will satisfy the first part. If that is the case, then
  221. % the second part has to hold because the digit mappings all create
  222. % distinct binary representations...
  223.  
  224. % (not used; just trying things out)
  225. % This basically only counts up Segment values, but it could be the
  226. % basis for creating Permutation (element) <-> Segment pairs as a side
  227. % effect. (hence the "zip" naming)
  228. zip([], [], X, X).
  229. zip([_PHead|Perm], [SHead|Segment], Isum, Sum) :-
  230.     NextSum is SHead + Isum,
  231.     zip(Perm, Segment, NextSum, Sum).
  232.  
  233. % the name sum is taken by a predicate in the clpfd library and it
  234. % doesn't work the way you'd expect :(
  235. sum_list([], X, X).
  236. sum_list([SHead|Segment], Isum, Sum) :-
  237.     NextSum is SHead + Isum,
  238.     sum_list(Segment, NextSum, Sum).
  239.  
  240. % The obvious usage of the above is something like:
  241. %
  242. % permute(Letters, Permutation),
  243. % ...
  244.  
  245. % ah, not quite right... I need to translate letters in one step
  246. % (requiring an actual zip), then do the count in a separate step,
  247. % followed by something like:
  248. %
  249. % GIVEN this calculated arrangement for zero (...), does it have the
  250. % same stored arrangement for zero
  251. %
  252. % (and so on for one, two, three, ...)
  253. %
  254. % So we're reading the permutation, eg, [a,c,f,g,...] as
  255. % * a = 1
  256. % * c = 2
  257. % * f = 4
  258. % * g = 8
  259. % ...
  260.  
  261. % Then we look at each *reading* and see if, after the translation
  262. % above, it maps to some (each) known digit.
  263.  
  264. % Take a pair of translation lists (from, to), a symbol from the first
  265. % list, and output the corresponding symbol in the second list.
  266. %
  267. % eg translate([a,b,c,d], [1,2,4,8], c, Val). (Val = 4)
  268. translate([S|_Symbols], [V|_Values], S, V) :- !.
  269. translate([_|Syms], [_|Vals], S, V) :- translate(Syms,Vals,S,V).
  270.  
  271. % the above would actually have to be applied to all letters of the
  272. % permutation. Here's how to do that:
  273.  
  274. translate_all([], _From, _To, []).
  275. translate_all([I|In], From, To, [O|Out]) :-
  276.     translate(From, To, I, O),
  277.     translate_all(In, From, To, Out).
  278.  
  279. % Before I go further, I think I should write a pretty printer. There
  280. % are two ways to consider:
  281. %
  282. % * using # for lit segments, . for unlit ones
  283. % * as above, but with letters replacing #'s
  284. %
  285. % In order to avoiding writing two sets of predicates, we should
  286. % probably have a pair of inputs:
  287. %
  288. % * a set of canonically-named segments (eg, 7 = [top, ne, se])
  289. % * a "palette" used for painting that segment (eg, [b, c, a])
  290. %
  291.  
  292. pp(Segments, Palette) :-
  293.     write(" "),
  294.     (
  295.     translate(Segments, Palette, top, Top),
  296.     write(Top),
  297.     write(Top),
  298.     writeln(Top), !;
  299.     writeln("...")
  300.     ),
  301.  
  302.     % write two lines for each vertical segment
  303.     (
  304.     translate(Segments, Palette, nw, NW), !;
  305.     NW = "."
  306.     ),
  307.     (
  308.     translate(Segments, Palette, ne, NE), !;
  309.     NE = "."
  310.     ),
  311.     write(NW),
  312.     write("   "),
  313.     writeln(NE),
  314.     write(NW),
  315.     write("   "),
  316.     writeln(NE),
  317.  
  318.     write(" "),
  319.     (
  320.     translate(Segments, Palette, mid, Mid),
  321.     write(Mid),
  322.     write(Mid),
  323.     writeln(Mid), !;
  324.     writeln("...")
  325.     ),
  326.  
  327.     % write two lines for each vertical segment
  328.     (
  329.     translate(Segments, Palette, sw, SW), !;
  330.     SW = "."
  331.     ),
  332.     (
  333.     translate(Segments, Palette, se, SE), !;
  334.     SE = "."
  335.     ),
  336.     write(SW),
  337.     write("   "),
  338.     writeln(SE),
  339.     write(SW),
  340.     write("   "),
  341.     writeln(SE),
  342.  
  343.     write(" "),
  344.     (
  345.     translate(Segments, Palette, bot, Bot),
  346.     write(Bot),
  347.     write(Bot),
  348.     writeln(Bot), !;
  349.     writeln("...")
  350.     ).
  351.  
  352. % The above is fine, but there's a bit of preprocessing work needed
  353. % for giving segment names a canonical colour when printing known
  354. % digits. Of course, you can just print them using the hash palette.
  355. %
  356.  
  357. % pretty print a digit based on the digit_lit rules and # palette
  358. pp_digit_hash(Name) :-
  359.     % using findall returns Segs = [] if the predicate fails, but
  360.     % using setof causes unknown digit Name to fail
  361.     setof(Seg, digit_lit(Name, Seg), Segs),
  362.    
  363.     % Palette can be larger than segment list
  364.     pp(Segs, [ "#", "#", "#", "#", "#", "#", "#", "#", "#"]).
  365.  
  366. % As above, but using a canonical palette (top -> a, nw -> b, ...):
  367. pp_digit_canonical(Name) :-
  368.     setof(Seg, digit_lit(Name, Seg), Segs),
  369.     translate_all(Segs, [top, nw, ne, mid, sw, se, bot],
  370.           [a,b,c,d,e,f,g],
  371.           Palette),
  372.     pp(Segs,Palette).    
  373.  
  374.  
  375. % I have two different ways of representing digits, so let's make sure
  376. % that they agree
  377.  
  378. validate_digit_list([]).
  379. validate_digit_list([Name|T]) :-
  380.     digit_info(Name, _, _, Expect), % expected arrangement (binary number)
  381.     % !,
  382.  
  383.     setof(Seg, digit_lit(Name, Seg), Segs),
  384.     translate_all(Segs, [top, nw, ne, mid, sw, se, bot],
  385.           [1,2,4,8,16,32,64],
  386.           Digits),
  387.  
  388.     sum_list(Digits, Sum),
  389.  
  390.     write(Name),
  391.     write(" has digits: "),
  392.     write(Digits),
  393.     write(" (sum = "),
  394.     write(Sum),
  395.     writeln(")"),
  396.  
  397.     Sum == Expect,
  398.     % write("digit "),
  399.     write(Name),
  400.     writeln(" is OK"),
  401.     validate_digit_list(T).
  402.  
  403.  
  404. % There's something wrong with the following ... it calls
  405. % validate_digit_list too many times... calling validate_digit_list
  406. % manually works without repeating itself...
  407. validate_digits() :-
  408.     setof(Name, Seg^digit_lit(Name, Seg), Names),
  409.  
  410.     write("Testing digits: "),
  411.     writeln(Names),
  412.  
  413.    
  414.     % OK. So apparently setof was the culprit?
  415.     % findall(Name, digit_info(Name, _, _, _Arr), Names),
  416.     % findall(Name, digit_lit(Name, _Which), Names), % many duplicates!
  417.     % !,
  418.     validate_digit_list(Names).
  419.  
  420. % I changed Sum = Expect in validate_digit_list to Sum is Expect and
  421. % the strange behaviour went away. However, there were still problems.
  422. %
  423. % I had to change to eliminate relevance of the Seg variable...
  424.  
  425.  
  426. % another unused predicate. Succeeds when a list has no duplicates
  427. % (ie, all elements are distinct).
  428. distinct(Xs) :-
  429.     msort(Xs, Sorted),      % sort removes duplicates, msort doesn't.
  430.     sorted_distinct(Sorted).
  431.  
  432. % it was tricky to get this right (pairwise comparison, except at end)
  433. sorted_distinct([_]).       % [_] rather than []
  434. sorted_distinct([A,A|_]) :-
  435.     !, fail.                    % cut first! (alt A == B, !, fail.)
  436. sorted_distinct([_,B|T]) :-
  437.     sorted_distinct([B|T]).
  438.  
  439.  
  440. % Towards a solution
  441. %
  442. % Most of the pieces are written, but there's the thorny issue of
  443. % converting the text-based readings into lists or atoms.
  444. %
  445. % Ignoring that, the solution needs to:
  446. %
  447. % * start with a permutation of letters
  448. %
  449. % * pair up letters with segment names based on that permutation
  450. %
  451. % * translate segment names into numeric arrangement and sum
  452. %
  453. % * find the name (a digit) of the unique arrangement
  454. %
  455. % The last three steps are done for each reading.
  456. %
  457. % We can go directly from letters to the powers-of-two encodings of
  458. % the segments. So the permutation can be from letters in a canonical
  459. % order (a,b,c...) to the powers of two (1,2,4, etc.). That way, we
  460. % only have a single translate_all step
  461.  
  462. pp_string_as_ordinals([]) :- writeln("").
  463. pp_string_as_ordinals([H|T]) :- write(H), write(","), pp_string_as_ordinals(T).
  464.  
  465. % OK. The above doesn't work if you pass in a string (it's not
  466. % automatically treated as a list). We need to use string_codes/2, eg:
  467. %
  468. % ?- string_codes("foo", S), pp_string_as_ordinals(S).
  469. % 102,111,111,
  470. % S = [102, 111, 111].
  471.  
  472. % LHS is the calibration readings on the left, RHS is what we want to
  473. % find values for, then read them as a 4-digit decimal number
  474. solve_line(LHS, RHS, Total) :-
  475.     % convert letters to ascii codes
  476.     string_codes("abcdefg", Canonical),
  477.     % rather than use Digit mappings, use winning permutation
  478.     % (this enables us to reuse the pair_readings code in 2nd part)
  479.     solve_readings(LHS, Canonical, Permutation, _Digits),
  480.     pp_string_as_ordinals(Permutation),
  481.     % solve RHS using winning permutation, this time save Digits
  482.  
  483.     % Note that the following could be replaced with a call to:
  484.     %   pair_readings(RHS, Permutation, Digits)
  485.     %
  486.     % I'm calling solve_readings instead just to show that the same
  487.     % predicate can be used to solve both the left hand side and the
  488.     % right hand side. The difference between the calls is that
  489.     % Permutation is unbound on the first call, but bound on the
  490.     % second (so the call to permute simply verifies that Permutation
  491.     % is a permutation of Canonical)
  492.     solve_readings(RHS, Canonical, Permutation, Digits),
  493.  
  494.     sum_digits(Digits, 0, Total).
  495.  
  496. solve_readings(Readings, Canonical, Permutation, Digits) :-
  497.     permute(Canonical, Permutation),
  498.     pair_readings(Readings, Permutation, Digits).
  499.  
  500.  
  501. % convert ["one", "two", "three","four"] to decimal 1234. Call with
  502. % initial accumulator of 0. Example:
  503. %
  504. % ?- sum_digits([one,two,three], 0, T).
  505. % T = 123 ;
  506. % false.
  507.  
  508. % at bottom, instantiate final Total == Accumulator
  509. sum_digits([], Acc, Acc).
  510.  
  511. % Above could also have been written to terminate at [D]:
  512. % sum_digits([D], Acc, Total) :-
  513. %     digit_info(D, Val, _, _),
  514. %     Total is Acc * 10 + Val.
  515.  
  516. % Accumulate high digits on the way down, passing Total back up unchanged
  517. sum_digits([D|Ds], Acc, Total) :-
  518.     digit_info(D, Val, _, _),
  519.     Acc2 is Acc * 10 + Val, % Acc is initially zero
  520.     sum_digits(Ds, Acc2, Total).
  521.  
  522. % Letters comes from a reading
  523. % Permute is a permutation of [a, b, c, ...]
  524. %
  525. % eg, with canonical naming, no permutation:
  526. %
  527. % ?- pair_reading([c,f], [a,b,c,d,e,f,g], D).
  528. % D = one ;
  529. %
  530. % Also works with ascii codes, if both Letters and Permute follow the
  531. % same encoding:
  532. %
  533. % ?- pair_reading([97,99,102], [97,98,99,100,101,102,103], D).
  534. % D = seven ;
  535.  
  536. pair_reading(Letters, Permute, Digit) :-
  537.     translate_all(Letters, Permute, [1,2,4,8,16,32,64], Segments),
  538.     sum_list(Segments, 0, Arrangement),
  539.     digit_info(Digit, _, _, Arrangement).
  540.  
  541. pair_readings([],_,[]).
  542. pair_readings([H|T], P, [D|Ds]) :-
  543.     % convert letters to ascii codes
  544.     string_codes(H, HC),
  545.     pair_reading(HC, P, D),
  546.     pair_readings(T, P, Ds).
  547.  
  548. % OK. Fixed a typo, and I have a solution!
  549. % solve_line(["acedgfb", "cdfbe", "gcdfa", "fbcad", "dab", "cefabd", "cdfgeb",
  550. %     "eafb", "cagedb", "ab"],_,Digits).
  551. % 100,101,97,102,103,98,99,
  552. % Digits = [eight, five, two, three, seven, nine, six, four, zero|...] ;
  553. % false.
  554. %
  555. % Checking against example output...
  556. %
  557. % acedgfb: 8
  558. % cdfbe: 5
  559. % gcdfa: 2
  560. % fbcad: 3
  561. % dab: 7
  562. % cefabd: 9
  563. % cdfgeb: 6
  564. % eafb: 4
  565. % cagedb: 0
  566. % ab: 1
  567. %
  568. % Correct!
  569.  
  570. % Adding code (and new parameters) to solve the RHS:
  571. %
  572. % ?- solve_line(["acedgfb", "cdfbe", "gcdfa", "fbcad", "dab", "cefabd", "cdfgeb",
  573. % "eafb", "cagedb", "ab"],["cdfeb", "fcadb", "cdfeb", "cdbaf"],Digits).
  574. % 100,101,97,102,103,98,99,
  575. % Digits = 5353
  576. %
  577. % Also correct!
  578.  
  579. % To Do: write a parser to read the actual input and output the
  580. % solution, which is the sum of all the per-line Digits outputs above.
  581.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement