OSDN Git Service

Py 3 handles exception propagation a little differently?
[joypy/Thun.git] / docs / Advent_of_Code_2017_December_3rd.rst
1 Advent of Code 2017
2 ===================
3
4 December 3rd
5 ------------
6
7 You come across an experimental new kind of memory stored on an infinite
8 two-dimensional grid.
9
10 Each square on the grid is allocated in a spiral pattern starting at a
11 location marked 1 and then counting up while spiraling outward. For
12 example, the first few squares are allocated like this:
13
14 ::
15
16     17  16  15  14  13
17     18   5   4   3  12
18     19   6   1   2  11
19     20   7   8   9  10
20     21  22  23---> ...
21
22 While this is very space-efficient (no squares are skipped), requested
23 data must be carried back to square 1 (the location of the only access
24 port for this memory system) by programs that can only move up, down,
25 left, or right. They always take the shortest path: the Manhattan
26 Distance between the location of the data and square 1.
27
28 For example:
29
30 -  Data from square 1 is carried 0 steps, since it's at the access port.
31 -  Data from square 12 is carried 3 steps, such as: down, left, left.
32 -  Data from square 23 is carried only 2 steps: up twice.
33 -  Data from square 1024 must be carried 31 steps.
34
35 How many steps are required to carry the data from the square identified
36 in your puzzle input all the way to the access port?
37
38 Analysis
39 ~~~~~~~~
40
41 I freely admit that I worked out the program I wanted to write using
42 graph paper and some Python doodles. There's no point in trying to write
43 a Joy program until I'm sure I understand the problem well enough.
44
45 The first thing I did was to write a column of numbers from 1 to n (32
46 as it happens) and next to them the desired output number, to look for
47 patterns directly:
48
49 ::
50
51     1  0
52     2  1
53     3  2
54     4  1
55     5  2
56     6  1
57     7  2
58     8  1
59     9  2
60     10 3
61     11 2
62     12 3
63     13 4
64     14 3
65     15 2
66     16 3
67     17 4
68     18 3
69     19 2
70     20 3
71     21 4
72     22 3
73     23 2
74     24 3
75     25 4
76     26 5
77     27 4
78     28 3
79     29 4
80     30 5
81     31 6
82     32 5
83
84 There are four groups repeating for a given "rank", then the pattern
85 enlarges and four groups repeat again, etc.
86
87 ::
88
89             1 2
90           3 2 3 4
91         5 4 3 4 5 6
92       7 6 5 4 5 6 7 8
93     9 8 7 6 5 6 7 8 9 10
94
95 Four of this pyramid interlock to tile the plane extending from the
96 initial "1" square.
97
98 ::
99
100              2   3   |    4  5   |    6  7   |    8  9
101           10 11 12 13|14 15 16 17|18 19 20 21|22 23 24 25
102
103 And so on.
104
105 We can figure out the pattern for a row of the pyramid at a given "rank"
106 :math:`k`:
107
108 :math:`2k - 1, 2k - 2, ..., k, k + 1, k + 2, ..., 2k`
109
110 or
111
112 :math:`k + (k - 1), k + (k - 2), ..., k, k + 1, k + 2, ..., k + k`
113
114 This shows that the series consists at each place of :math:`k` plus some
115 number that begins at :math:`k - 1`, decreases to zero, then increases
116 to :math:`k`. Each row has :math:`2k` members.
117
118 Let's figure out how, given an index into a row, we can calculate the
119 value there. The index will be from 0 to :math:`k - 1`.
120
121 Let's look at an example, with :math:`k = 4`:
122
123 ::
124
125     0 1 2 3 4 5 6 7
126     7 6 5 4 5 6 7 8
127
128 .. code:: ipython3
129
130     k = 4
131
132 Subtract :math:`k` from the index and take the absolute value:
133
134 .. code:: ipython3
135
136     for n in range(2 * k):
137         print(abs(n - k),)
138
139
140 .. parsed-literal::
141
142     4
143     3
144     2
145     1
146     0
147     1
148     2
149     3
150
151
152 Not quite. Subtract :math:`k - 1` from the index and take the absolute
153 value:
154
155 .. code:: ipython3
156
157     for n in range(2 * k):
158         print(abs(n - (k - 1)), end=' ')
159
160
161 .. parsed-literal::
162
163     3 2 1 0 1 2 3 4 
164
165 Great, now add :math:`k`...
166
167 .. code:: ipython3
168
169     for n in range(2 * k):
170         print(abs(n - (k - 1)) + k, end=' ')
171
172
173 .. parsed-literal::
174
175     7 6 5 4 5 6 7 8 
176
177 So to write a function that can give us the value of a row at a given
178 index:
179
180 .. code:: ipython3
181
182     def row_value(k, i):
183         i %= (2 * k)  # wrap the index at the row boundary.
184         return abs(i - (k - 1)) + k
185
186 .. code:: ipython3
187
188     k = 5
189     for i in range(2 * k):
190         print(row_value(k, i), end=' ')
191
192
193 .. parsed-literal::
194
195     9 8 7 6 5 6 7 8 9 10 
196
197 (I'm leaving out details of how I figured this all out and just giving
198 the relevent bits. It took a little while to zero in of the aspects of
199 the pattern that were important for the task.)
200
201 Finding the rank and offset of a number.
202 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
203
204 Now that we can compute the desired output value for a given rank and
205 the offset (index) into that rank, we need to determine how to find the
206 rank and offset of a number.
207
208 The rank is easy to find by iteratively stripping off the amount already
209 covered by previous ranks until you find the one that brackets the
210 target number. Because each row is :math:`2k` places and there are
211 :math:`4` per rank each rank contains :math:`8k` places. Counting the
212 initial square we have:
213
214 :math:`corner_k = 1 + \sum_{n=1}^k 8n`
215
216 I'm not mathematically sophisticated enough to turn this directly into a
217 formula (but Sympy is, see below.) I'm going to write a simple Python
218 function to iterate and search:
219
220 .. code:: ipython3
221
222     def rank_and_offset(n):
223         assert n >= 2  # Guard the domain.
224         n -= 2  # Subtract two,
225                 # one for the initial square,
226                 # and one because we are counting from 1 instead of 0.
227         k = 1
228         while True:
229             m = 8 * k  # The number of places total in this rank, 4(2k).
230             if n < m:
231                 return k, n % (2 * k)
232             n -= m  # Remove this rank's worth.
233             k += 1
234
235 .. code:: ipython3
236
237     for n in range(2, 51):
238         print(n, rank_and_offset(n))
239
240
241 .. parsed-literal::
242
243     2 (1, 0)
244     3 (1, 1)
245     4 (1, 0)
246     5 (1, 1)
247     6 (1, 0)
248     7 (1, 1)
249     8 (1, 0)
250     9 (1, 1)
251     10 (2, 0)
252     11 (2, 1)
253     12 (2, 2)
254     13 (2, 3)
255     14 (2, 0)
256     15 (2, 1)
257     16 (2, 2)
258     17 (2, 3)
259     18 (2, 0)
260     19 (2, 1)
261     20 (2, 2)
262     21 (2, 3)
263     22 (2, 0)
264     23 (2, 1)
265     24 (2, 2)
266     25 (2, 3)
267     26 (3, 0)
268     27 (3, 1)
269     28 (3, 2)
270     29 (3, 3)
271     30 (3, 4)
272     31 (3, 5)
273     32 (3, 0)
274     33 (3, 1)
275     34 (3, 2)
276     35 (3, 3)
277     36 (3, 4)
278     37 (3, 5)
279     38 (3, 0)
280     39 (3, 1)
281     40 (3, 2)
282     41 (3, 3)
283     42 (3, 4)
284     43 (3, 5)
285     44 (3, 0)
286     45 (3, 1)
287     46 (3, 2)
288     47 (3, 3)
289     48 (3, 4)
290     49 (3, 5)
291     50 (4, 0)
292
293
294 .. code:: ipython3
295
296     for n in range(2, 51):
297         k, i = rank_and_offset(n)
298         print(n, row_value(k, i))
299
300
301 .. parsed-literal::
302
303     2 1
304     3 2
305     4 1
306     5 2
307     6 1
308     7 2
309     8 1
310     9 2
311     10 3
312     11 2
313     12 3
314     13 4
315     14 3
316     15 2
317     16 3
318     17 4
319     18 3
320     19 2
321     20 3
322     21 4
323     22 3
324     23 2
325     24 3
326     25 4
327     26 5
328     27 4
329     28 3
330     29 4
331     30 5
332     31 6
333     32 5
334     33 4
335     34 3
336     35 4
337     36 5
338     37 6
339     38 5
340     39 4
341     40 3
342     41 4
343     42 5
344     43 6
345     44 5
346     45 4
347     46 3
348     47 4
349     48 5
350     49 6
351     50 7
352
353
354 Putting it all together
355 ~~~~~~~~~~~~~~~~~~~~~~~
356
357 .. code:: ipython3
358
359     def row_value(k, i):
360         return abs(i - (k - 1)) + k
361     
362     
363     def rank_and_offset(n):
364         n -= 2  # Subtract two,
365                 # one for the initial square,
366                 # and one because we are counting from 1 instead of 0.
367         k = 1
368         while True:
369             m = 8 * k  # The number of places total in this rank, 4(2k).
370             if n < m:
371                 return k, n % (2 * k)
372             n -= m  # Remove this rank's worth.
373             k += 1
374     
375     
376     def aoc20173(n):
377         if n <= 1:
378             return 0
379         k, i = rank_and_offset(n)
380         return row_value(k, i)
381
382 .. code:: ipython3
383
384     aoc20173(23)
385
386
387
388
389 .. parsed-literal::
390
391     2
392
393
394
395 .. code:: ipython3
396
397     aoc20173(23000)
398
399
400
401
402 .. parsed-literal::
403
404     105
405
406
407
408 .. code:: ipython3
409
410     aoc20173(23000000000000)
411
412
413
414
415 .. parsed-literal::
416
417     4572225
418
419
420
421 Sympy to the Rescue
422 ===================
423
424 Find the rank for large numbers
425 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
426
427 Using e.g. Sympy we can find the rank directly by solving for the roots
428 of an equation. For large numbers this will (eventually) be faster than
429 iterating as ``rank_and_offset()`` does.
430
431 .. code:: ipython3
432
433     from sympy import floor, lambdify, solve, symbols
434     from sympy import init_printing
435     init_printing() 
436
437 .. code:: ipython3
438
439     k = symbols('k')
440
441 Since
442
443 :math:`1 + 2 + 3 + ... + N = \frac{N(N + 1)}{2}`
444
445 and
446
447 :math:`\sum_{n=1}^k 8n = 8(\sum_{n=1}^k n) = 8\frac{k(k + 1)}{2}`
448
449 We want:
450
451 .. code:: ipython3
452
453     E = 2 + 8 * k * (k + 1) / 2  # For the reason for adding 2 see above.
454     
455     E
456
457
458
459
460 .. math::
461
462     \displaystyle 4 k \left(k + 1\right) + 2
463
464
465
466 We can write a function to solve for :math:`k` given some :math:`n`...
467
468 .. code:: ipython3
469
470     def rank_of(n):
471         return floor(max(solve(E - n, k))) + 1
472
473 First ``solve()`` for :math:`E - n = 0` which has two solutions (because
474 the equation is quadratic so it has two roots) and since we only care
475 about the larger one we use ``max()`` to select it. It will generally
476 not be a nice integer (unless :math:`n` is the number of an end-corner
477 of a rank) so we take the ``floor()`` and add 1 to get the integer rank
478 of :math:`n`. (Taking the ``ceiling()`` gives off-by-one errors on the
479 rank boundaries. I don't know why. I'm basically like a monkey doing
480 math here.) =-D
481
482 It gives correct answers:
483
484 .. code:: ipython3
485
486     for n in (9, 10, 25, 26, 49, 50):
487         print(n, rank_of(n))
488
489
490 .. parsed-literal::
491
492     9 1
493     10 2
494     25 2
495     26 3
496     49 3
497     50 4
498
499
500 And it runs much faster (at least for large numbers):
501
502 .. code:: ipython3
503
504     %time rank_of(23000000000000)  # Compare runtime with rank_and_offset()!
505
506
507 .. parsed-literal::
508
509     CPU times: user 27.8 ms, sys: 5 µs, total: 27.8 ms
510     Wall time: 27.3 ms
511
512
513
514
515 .. math::
516
517     \displaystyle 2397916
518
519
520
521 .. code:: ipython3
522
523     %time rank_and_offset(23000000000000)
524
525
526 .. parsed-literal::
527
528     CPU times: user 216 ms, sys: 89 µs, total: 216 ms
529     Wall time: 215 ms
530
531
532
533
534 .. math::
535
536     \displaystyle \left( 2397916, \  223606\right)
537
538
539
540 After finding the rank you would still have to find the actual value of
541 the rank's first corner and subtract it (plus 2) from the number and
542 compute the offset as above and then the final output, but this overhead
543 is partially shared by the other method, and overshadowed by the time it
544 (the other iterative method) would take for really big inputs.
545
546 The fun thing to do here would be to graph the actual runtime of both
547 methods against each other to find the trade-off point.
548
549 It took me a second to realize I could do this...
550 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
551
552 Sympy is a *symbolic* math library, and it supports symbolic
553 manipulation of equations. I can put in :math:`y` (instead of a value)
554 and ask it to solve for :math:`k`.
555
556 .. code:: ipython3
557
558     y = symbols('y')
559
560 .. code:: ipython3
561
562     g, f = solve(E - y, k)
563
564 The equation is quadratic so there are two roots, we are interested in
565 the greater one...
566
567 .. code:: ipython3
568
569     g
570
571
572
573
574 .. math::
575
576     \displaystyle - \frac{\sqrt{y - 1}}{2} - \frac{1}{2}
577
578
579
580 .. code:: ipython3
581
582     f
583
584
585
586
587 .. math::
588
589     \displaystyle \frac{\sqrt{y - 1}}{2} - \frac{1}{2}
590
591
592
593 Now we can take the ``floor()``, add 1, and ``lambdify()`` the equation
594 to get a Python function that calculates the rank directly.
595
596 .. code:: ipython3
597
598     floor(f) + 1
599
600
601
602
603 .. math::
604
605     \displaystyle \left\lfloor{\frac{\sqrt{y - 1}}{2} - \frac{1}{2}}\right\rfloor + 1
606
607
608
609 .. code:: ipython3
610
611     F = lambdify(y, floor(f) + 1)
612
613 .. code:: ipython3
614
615     for n in (9, 10, 25, 26, 49, 50):
616         print(n, int(F(n)))
617
618
619 .. parsed-literal::
620
621     9 1
622     10 2
623     25 2
624     26 3
625     49 3
626     50 4
627
628
629 It's pretty fast.
630
631 .. code:: ipython3
632
633     %time int(F(23000000000000))  # The clear winner.
634
635
636 .. parsed-literal::
637
638     CPU times: user 60 µs, sys: 4 µs, total: 64 µs
639     Wall time: 67 µs
640
641
642
643
644 .. math::
645
646     \displaystyle 2397916
647
648
649
650 Knowing the equation we could write our own function manually, but the
651 speed is no better.
652
653 .. code:: ipython3
654
655     from math import floor as mfloor, sqrt
656     
657     def mrank_of(n):
658         return int(mfloor(sqrt(n - 1) / 2 - 0.5) + 1)
659
660 .. code:: ipython3
661
662     %time mrank_of(23000000000000)
663
664
665 .. parsed-literal::
666
667     CPU times: user 7 µs, sys: 1 µs, total: 8 µs
668     Wall time: 10 µs
669
670
671
672
673 .. math::
674
675     \displaystyle 2397916
676
677
678
679 Given :math:`n` and a rank, compute the offset.
680 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
681
682 Now that we have a fast way to get the rank, we still need to use it to
683 compute the offset into a pyramid row.
684
685 .. code:: ipython3
686
687     def offset_of(n, k):
688         return (n - 2 + 4 * k * (k - 1)) % (2 * k)
689
690 (Note the sneaky way the sign changes from :math:`k(k + 1)` to
691 :math:`k(k - 1)`. This is because we want to subract the
692 :math:`(k - 1)`\ th rank's total places (its own and those of lesser
693 rank) from our :math:`n` of rank :math:`k`. Substituting :math:`k - 1`
694 for :math:`k` in :math:`k(k + 1)` gives :math:`(k - 1)(k - 1 + 1)`,
695 which of course simplifies to :math:`k(k - 1)`.)
696
697 .. code:: ipython3
698
699     offset_of(23000000000000, 2397916)
700
701
702
703
704 .. math::
705
706     \displaystyle 223606
707
708
709
710 So, we can compute the rank, then the offset, then the row value.
711
712 .. code:: ipython3
713
714     def rank_of(n):
715         return int(mfloor(sqrt(n - 1) / 2 - 0.5) + 1)
716     
717     
718     def offset_of(n, k):
719         return (n - 2 + 4 * k * (k - 1)) % (2 * k)
720     
721     
722     def row_value(k, i):
723         return abs(i - (k - 1)) + k
724     
725     
726     def aoc20173(n):
727         k = rank_of(n)
728         i = offset_of(n, k)
729         return row_value(k, i)
730
731 .. code:: ipython3
732
733     aoc20173(23)
734
735
736
737
738 .. math::
739
740     \displaystyle 2
741
742
743
744 .. code:: ipython3
745
746     aoc20173(23000)
747
748
749
750
751 .. math::
752
753     \displaystyle 105
754
755
756
757 .. code:: ipython3
758
759     aoc20173(23000000000000)
760
761
762
763
764 .. math::
765
766     \displaystyle 4572225
767
768
769
770 .. code:: ipython3
771
772     %time aoc20173(23000000000000000000000000)  # Fast for large values.
773
774
775 .. parsed-literal::
776
777     CPU times: user 22 µs, sys: 2 µs, total: 24 µs
778     Wall time: 26.7 µs
779
780
781
782
783 .. math::
784
785     \displaystyle 2690062495969
786
787
788
789 A Joy Version
790 =============
791
792 At this point I feel confident that I can implement a concise version of
793 this code in Joy. ;-)
794
795 .. code:: ipython3
796
797     from notebook_preamble import J, V, define
798
799 ``rank_of``
800 ~~~~~~~~~~~
801
802 ::
803
804        n rank_of
805     ---------------
806           k
807
808 The translation is straightforward.
809
810 ::
811
812     int(floor(sqrt(n - 1) / 2 - 0.5) + 1)
813
814     rank_of == -- sqrt 2 / 0.5 - floor ++
815
816 .. code:: ipython3
817
818     define('rank_of -- sqrt 2 / 0.5 - floor ++')
819
820 ``offset_of``
821 ~~~~~~~~~~~~~
822
823 ::
824
825        n k offset_of
826     -------------------
827              i
828
829     (n - 2 + 4 * k * (k - 1)) % (2 * k)
830
831 A little tricky...
832
833 ::
834
835     n k dup 2 *
836     n k k 2 *
837     n k k*2 [Q] dip %
838     n k Q k*2 %
839
840     n k dup --
841     n k k --
842     n k k-1 4 * * 2 + -
843     n k*k-1*4     2 + -
844     n k*k-1*4+2       -
845     n-k*k-1*4+2
846
847     n-k*k-1*4+2 k*2 %
848     n-k*k-1*4+2%k*2
849
850 Ergo:
851
852 ::
853
854     offset_of == dup 2 * [dup -- 4 * * 2 + -] dip %
855
856 .. code:: ipython3
857
858     define('offset_of dup 2 * [dup -- 4 * * 2 + -] dip %')
859
860 ``row_value``
861 ~~~~~~~~~~~~~
862
863 ::
864
865        k i row_value
866     -------------------
867             n
868
869     abs(i - (k - 1)) + k
870
871     k i over -- - abs +
872     k i k    -- - abs +
873     k i k-1     - abs +
874     k i-k-1       abs +
875     k |i-k-1|         +
876     k+|i-k-1|
877
878 .. code:: ipython3
879
880     define('row_value over -- - abs +')
881
882 ``aoc2017.3``
883 ~~~~~~~~~~~~~
884
885 ::
886
887        n aoc2017.3
888     -----------------
889             m
890
891     n dup rank_of
892     n k [offset_of] dupdip
893     n k offset_of k
894     i             k swap row_value
895     k i row_value
896     m
897
898 .. code:: ipython3
899
900     define('aoc2017.3 dup rank_of [offset_of] dupdip swap row_value')
901
902 .. code:: ipython3
903
904     J('23 aoc2017.3')
905
906
907 .. parsed-literal::
908
909     2
910
911
912 .. code:: ipython3
913
914     J('23000 aoc2017.3')
915
916
917 .. parsed-literal::
918
919     105
920
921
922 .. code:: ipython3
923
924     V('23000000000000 aoc2017.3')
925
926
927 .. parsed-literal::
928
929                                                         • 23000000000000 aoc2017.3
930                                          23000000000000 • aoc2017.3
931                                          23000000000000 • dup rank_of [offset_of] dupdip swap row_value
932                           23000000000000 23000000000000 • rank_of [offset_of] dupdip swap row_value
933                           23000000000000 23000000000000 • -- sqrt 2 / 0.5 - floor ++ [offset_of] dupdip swap row_value
934                           23000000000000 22999999999999 • sqrt 2 / 0.5 - floor ++ [offset_of] dupdip swap row_value
935                        23000000000000 4795831.523312615 • 2 / 0.5 - floor ++ [offset_of] dupdip swap row_value
936                      23000000000000 4795831.523312615 2 • / 0.5 - floor ++ [offset_of] dupdip swap row_value
937                       23000000000000 2397915.7616563076 • 0.5 - floor ++ [offset_of] dupdip swap row_value
938                   23000000000000 2397915.7616563076 0.5 • - floor ++ [offset_of] dupdip swap row_value
939                       23000000000000 2397915.2616563076 • floor ++ [offset_of] dupdip swap row_value
940                                  23000000000000 2397915 • ++ [offset_of] dupdip swap row_value
941                                  23000000000000 2397916 • [offset_of] dupdip swap row_value
942                      23000000000000 2397916 [offset_of] • dupdip swap row_value
943                                  23000000000000 2397916 • offset_of 2397916 swap row_value
944                                  23000000000000 2397916 • dup 2 * [dup -- 4 * * 2 + -] dip % 2397916 swap row_value
945                          23000000000000 2397916 2397916 • 2 * [dup -- 4 * * 2 + -] dip % 2397916 swap row_value
946                        23000000000000 2397916 2397916 2 • * [dup -- 4 * * 2 + -] dip % 2397916 swap row_value
947                          23000000000000 2397916 4795832 • [dup -- 4 * * 2 + -] dip % 2397916 swap row_value
948     23000000000000 2397916 4795832 [dup -- 4 * * 2 + -] • dip % 2397916 swap row_value
949                                  23000000000000 2397916 • dup -- 4 * * 2 + - 4795832 % 2397916 swap row_value
950                          23000000000000 2397916 2397916 • -- 4 * * 2 + - 4795832 % 2397916 swap row_value
951                          23000000000000 2397916 2397915 • 4 * * 2 + - 4795832 % 2397916 swap row_value
952                        23000000000000 2397916 2397915 4 • * * 2 + - 4795832 % 2397916 swap row_value
953                          23000000000000 2397916 9591660 • * 2 + - 4795832 % 2397916 swap row_value
954                           23000000000000 22999994980560 • 2 + - 4795832 % 2397916 swap row_value
955                         23000000000000 22999994980560 2 • + - 4795832 % 2397916 swap row_value
956                           23000000000000 22999994980562 • - 4795832 % 2397916 swap row_value
957                                                 5019438 • 4795832 % 2397916 swap row_value
958                                         5019438 4795832 • % 2397916 swap row_value
959                                                  223606 • 2397916 swap row_value
960                                          223606 2397916 • swap row_value
961                                          2397916 223606 • row_value
962                                          2397916 223606 • over -- - abs +
963                                  2397916 223606 2397916 • -- - abs +
964                                  2397916 223606 2397915 • - abs +
965                                        2397916 -2174309 • abs +
966                                         2397916 2174309 • +
967                                                 4572225 • 
968
969
970 ::
971
972       rank_of == -- sqrt 2 / 0.5 - floor ++
973     offset_of == dup 2 * [dup -- 4 * * 2 + -] dip %
974     row_value == over -- - abs +
975
976     aoc2017.3 == dup rank_of [offset_of] dupdip swap row_value