OSDN Git Service

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