{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Day 16\n", "\n", "- https://adventofcode.com/2019/day/16\n", "\n", "While I suspect this can actually be executed as a [Fast Fourier transform](https://en.wikipedia.org/wiki/Fast_Fourier_transform), I found using numpy matrix multiplications plenty fast enough for the first part.\n", "\n", "I create a $N x N$ matrix for the input signal, and a matching matrix of multiplication patterns (with judicious applications of [`np.tile()`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.tile.html) and [`np.repeat()`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.repeat.html)). Then both matrices can be multiplied, after which we sum each row, and take the last digit (after dropping the signs) as the output signal.\n", "\n", "Note that this is a $\\mathcal{O}(n^2)$ operation, and not an actual Fast Fourier transform; I only named the function `fft()` because it is the acronym for _Flawed Frequency Transmission_ :-P\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "\n", "def readsignal(line: str) -> np.array:\n", " return np.array([int(d) for d in line])\n", "\n", "\n", "_pattern = np.array([0, 1, 0, -1])\n", "\n", "\n", "def transform(signal: np.array, pattern: np.array = _pattern) -> np.array:\n", " n = signal.size\n", " matrix = np.tile(signal, (n, 1)) # N x N matrix\n", " patterns = np.zeros(matrix.shape, matrix.dtype)\n", " for i in range(1, n + 1):\n", " tilecount = int(np.ceil(n / ((i * pattern.size) - 1)))\n", " patterns[i - 1, :] = np.tile(np.repeat(pattern, i), tilecount)[1 : 1 + n]\n", " return np.abs((matrix * patterns).sum(axis=1)) % 10\n", "\n", "\n", "def fft(signal: np.array, repeats: int) -> np.array:\n", " for _ in range(repeats):\n", " signal = transform(signal)\n", " return signal\n", "\n", "\n", "def str8top(signal: np.array) -> str:\n", " return \"\".join(map(str, signal[:8]))\n", "\n", "\n", "assert str8top(fft(readsignal(\"12345678\"), 4)) == \"01029498\"\n", "part1_tests = {\n", " \"80871224585914546619083218645595\": \"24176176\",\n", " \"19617804207202209144916044189917\": \"73745418\",\n", " \"69317163492948606335995924319873\": \"52432133\",\n", "}\n", "for testnum, expected in part1_tests.items():\n", " assert str8top(fft(readsignal(testnum), 100)) == expected" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import aocd\n", "\n", "data = aocd.get_data(day=16, year=2019)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Part 1: 58100105\n" ] } ], "source": [ "print(\"Part 1:\", str8top(fft(readsignal(data), 100)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 2\n", "\n", "While my poor-mans `fft()` function is fast enough for part 1, part 2 demonstrates why you want to find an _actual_ FFT implementation. FFT is fast because it avoids a $\\mathcal{O}(n^2)$ multiplication matrix, giving you the results in $\\mathcal{O}(n\\log{}n)$ instead.\n", "\n", "The input data is 650 digits long, so for part 1 we had to process a 650x650 matrix multiplication, so roughly 422500 operations. For part 2 that turns into a 6.5 million digit monstrosity, and suddenly we are asking numpy to perform a 42250 billion calculations. We clearly need to find a similar shortcut!\n", "\n", "That shortcut is in the offset and the way the pattern is repeated. First note that for any digit $d$, the first $d - 1$ digits are ignored because they are multiplied by 0 in the pattern. Next, digit $d$ _and the next $d$ digits_ are all going to be `1`. At an offset 7 digits long, that means that the next several million digits are all set to 1, and we only have 6.5 million digits. We also _ignored_ the first `offset` digits by multiplying them with 0, so all we are doing is **_summing the digits starting at `offset` to determine the output digits_**.\n", "\n", "That's the per-digit sum. So, for `input[offset]`, the output digit `sum(input[offset:]) % 10`. The next digit at `offset + 1` is `sum(input[offset + 1:]) % 10`, and so on. So the digit at position $k + 1$ is the same value as $k$ but with the digit at position $k$ subtracted. So all we have to do is start at digit -1, add and add the digit at -2 to produce the second last digit. Then add digit -3 to create the next digit from the end. It's a cumulative sum of digits starting at the end, up to offset!\n", "\n", "1000 repeated cumulative sums of the digits from `offset` onwards is easy-peasy..\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "REPEAT = 10_000\n", "\n", "\n", "def offsetfft(signal: np.array, repeat: int = 100) -> np.array:\n", " # digits to number\n", " offset = (10 ** np.arange(6, -1, -1) * signal[:7]).sum()\n", " offsetsignal = np.tile(signal, REPEAT)[offset:]\n", " for _ in range(repeat):\n", " offsetsignal = np.cumsum(offsetsignal[::-1])[::-1] % 10\n", " return offsetsignal\n", "\n", "\n", "part2_tests = {\n", " \"03036732577212944063491565474664\": \"84462026\",\n", " \"02935109699940807407585447034323\": \"78725270\",\n", " \"03081770884921959731165446850517\": \"53553731\",\n", "}\n", "for testnum, expected in part2_tests.items():\n", " assert str8top(offsetfft(readsignal(testnum))) == expected" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Part 2: 41781287\n" ] } ], "source": [ "print(\"Part 2:\", str8top(offsetfft(readsignal(data), 100)))" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 2 }