|
| 1 | +""" |
| 2 | +If we are presented with the first k terms of a sequence it is impossible to say with |
| 3 | +certainty the value of the next term, as there are infinitely many polynomial functions |
| 4 | +that can model the sequence. |
| 5 | +
|
| 6 | +As an example, let us consider the sequence of cube |
| 7 | +numbers. This is defined by the generating function, |
| 8 | +u(n) = n3: 1, 8, 27, 64, 125, 216, ... |
| 9 | +
|
| 10 | +Suppose we were only given the first two terms of this sequence. Working on the |
| 11 | +principle that "simple is best" we should assume a linear relationship and predict the |
| 12 | +next term to be 15 (common difference 7). Even if we were presented with the first three |
| 13 | +terms, by the same principle of simplicity, a quadratic relationship should be |
| 14 | +assumed. |
| 15 | +
|
| 16 | +We shall define OP(k, n) to be the nth term of the optimum polynomial |
| 17 | +generating function for the first k terms of a sequence. It should be clear that |
| 18 | +OP(k, n) will accurately generate the terms of the sequence for n ≤ k, and potentially |
| 19 | +the first incorrect term (FIT) will be OP(k, k+1); in which case we shall call it a |
| 20 | +bad OP (BOP). |
| 21 | +
|
| 22 | +As a basis, if we were only given the first term of sequence, it would be most |
| 23 | +sensible to assume constancy; that is, for n ≥ 2, OP(1, n) = u(1). |
| 24 | +
|
| 25 | +Hence we obtain the |
| 26 | +following OPs for the cubic sequence: |
| 27 | +
|
| 28 | +OP(1, n) = 1 1, 1, 1, 1, ... |
| 29 | +OP(2, n) = 7n-6 1, 8, 15, ... |
| 30 | +OP(3, n) = 6n^2-11n+6 1, 8, 27, 58, ... |
| 31 | +OP(4, n) = n^3 1, 8, 27, 64, 125, ... |
| 32 | +
|
| 33 | +Clearly no BOPs exist for k ≥ 4. |
| 34 | +
|
| 35 | +By considering the sum of FITs generated by the BOPs (indicated in red above), we |
| 36 | +obtain 1 + 15 + 58 = 74. |
| 37 | +
|
| 38 | +Consider the following tenth degree polynomial generating function: |
| 39 | +
|
| 40 | +1 - n + n^2 - n^3 + n^4 - n^5 + n^6 - n^7 + n^8 - n^9 + n^10 |
| 41 | +
|
| 42 | +Find the sum of FITs for the BOPs. |
| 43 | +""" |
| 44 | + |
| 45 | + |
| 46 | +from typing import Callable, List, Union |
| 47 | + |
| 48 | +Matrix = List[List[Union[float, int]]] |
| 49 | + |
| 50 | + |
| 51 | +def solve(matrix: Matrix, vector: Matrix) -> Matrix: |
| 52 | + """ |
| 53 | + Solve the linear system of equations Ax = b (A = "matrix", b = "vector") |
| 54 | + for x using Gaussian elimination and back substitution. We assume that A |
| 55 | + is an invertible square matrix and that b is a column vector of the |
| 56 | + same height. |
| 57 | + >>> solve([[1, 0], [0, 1]], [[1],[2]]) |
| 58 | + [[1.0], [2.0]] |
| 59 | + >>> solve([[2, 1, -1],[-3, -1, 2],[-2, 1, 2]],[[8], [-11],[-3]]) |
| 60 | + [[2.0], [3.0], [-1.0]] |
| 61 | + """ |
| 62 | + size: int = len(matrix) |
| 63 | + augmented: Matrix = [[0 for _ in range(size + 1)] for _ in range(size)] |
| 64 | + row: int |
| 65 | + row2: int |
| 66 | + col: int |
| 67 | + col2: int |
| 68 | + pivot_row: int |
| 69 | + ratio: float |
| 70 | + |
| 71 | + for row in range(size): |
| 72 | + for col in range(size): |
| 73 | + augmented[row][col] = matrix[row][col] |
| 74 | + |
| 75 | + augmented[row][size] = vector[row][0] |
| 76 | + |
| 77 | + row = 0 |
| 78 | + col = 0 |
| 79 | + while row < size and col < size: |
| 80 | + # pivoting |
| 81 | + pivot_row = max( |
| 82 | + [(abs(augmented[row2][col]), row2) for row2 in range(col, size)] |
| 83 | + )[1] |
| 84 | + if augmented[pivot_row][col] == 0: |
| 85 | + col += 1 |
| 86 | + continue |
| 87 | + else: |
| 88 | + augmented[row], augmented[pivot_row] = augmented[pivot_row], augmented[row] |
| 89 | + |
| 90 | + for row2 in range(row + 1, size): |
| 91 | + ratio = augmented[row2][col] / augmented[row][col] |
| 92 | + augmented[row2][col] = 0 |
| 93 | + for col2 in range(col + 1, size + 1): |
| 94 | + augmented[row2][col2] -= augmented[row][col2] * ratio |
| 95 | + |
| 96 | + row += 1 |
| 97 | + col += 1 |
| 98 | + |
| 99 | + # back substitution |
| 100 | + for col in range(1, size): |
| 101 | + for row in range(col): |
| 102 | + ratio = augmented[row][col] / augmented[col][col] |
| 103 | + for col2 in range(col, size + 1): |
| 104 | + augmented[row][col2] -= augmented[col][col2] * ratio |
| 105 | + |
| 106 | + # round to get rid of numbers like 2.000000000000004 |
| 107 | + return [ |
| 108 | + [round(augmented[row][size] / augmented[row][row], 10)] for row in range(size) |
| 109 | + ] |
| 110 | + |
| 111 | + |
| 112 | +def interpolate(y_list: List[int]) -> Callable[[int], int]: |
| 113 | + """ |
| 114 | + Given a list of data points (1,y0),(2,y1), ..., return a function that |
| 115 | + interpolates the data points. We find the coefficients of the interpolating |
| 116 | + polynomial by solving a system of linear equations corresponding to |
| 117 | + x = 1, 2, 3... |
| 118 | +
|
| 119 | + >>> interpolate([1])(3) |
| 120 | + 1 |
| 121 | + >>> interpolate([1, 8])(3) |
| 122 | + 15 |
| 123 | + >>> interpolate([1, 8, 27])(4) |
| 124 | + 58 |
| 125 | + >>> interpolate([1, 8, 27, 64])(6) |
| 126 | + 216 |
| 127 | + """ |
| 128 | + |
| 129 | + size: int = len(y_list) |
| 130 | + matrix: Matrix = [[0 for _ in range(size)] for _ in range(size)] |
| 131 | + vector: Matrix = [[0] for _ in range(size)] |
| 132 | + coeffs: Matrix |
| 133 | + x_val: int |
| 134 | + y_val: int |
| 135 | + col: int |
| 136 | + |
| 137 | + for x_val, y_val in enumerate(y_list): |
| 138 | + for col in range(size): |
| 139 | + matrix[x_val][col] = (x_val + 1) ** (size - col - 1) |
| 140 | + vector[x_val][0] = y_val |
| 141 | + |
| 142 | + coeffs = solve(matrix, vector) |
| 143 | + |
| 144 | + def interpolated_func(var: int) -> int: |
| 145 | + """ |
| 146 | + >>> interpolate([1])(3) |
| 147 | + 1 |
| 148 | + >>> interpolate([1, 8])(3) |
| 149 | + 15 |
| 150 | + >>> interpolate([1, 8, 27])(4) |
| 151 | + 58 |
| 152 | + >>> interpolate([1, 8, 27, 64])(6) |
| 153 | + 216 |
| 154 | + """ |
| 155 | + return sum( |
| 156 | + round(coeffs[x_val][0]) * (var ** (size - x_val - 1)) |
| 157 | + for x_val in range(size) |
| 158 | + ) |
| 159 | + |
| 160 | + return interpolated_func |
| 161 | + |
| 162 | + |
| 163 | +def question_function(variable: int) -> int: |
| 164 | + """ |
| 165 | + The generating function u as specified in the question. |
| 166 | + >>> question_function(0) |
| 167 | + 1 |
| 168 | + >>> question_function(1) |
| 169 | + 1 |
| 170 | + >>> question_function(5) |
| 171 | + 8138021 |
| 172 | + >>> question_function(10) |
| 173 | + 9090909091 |
| 174 | + """ |
| 175 | + return ( |
| 176 | + 1 |
| 177 | + - variable |
| 178 | + + variable ** 2 |
| 179 | + - variable ** 3 |
| 180 | + + variable ** 4 |
| 181 | + - variable ** 5 |
| 182 | + + variable ** 6 |
| 183 | + - variable ** 7 |
| 184 | + + variable ** 8 |
| 185 | + - variable ** 9 |
| 186 | + + variable ** 10 |
| 187 | + ) |
| 188 | + |
| 189 | + |
| 190 | +def solution(func: Callable[[int], int] = question_function, order: int = 10) -> int: |
| 191 | + """ |
| 192 | + Find the sum of the FITs of the BOPS. For each interpolating polynomial of order |
| 193 | + 1, 2, ... , 10, find the first x such that the value of the polynomial at x does |
| 194 | + not equal u(x). |
| 195 | + >>> solution(lambda n: n ** 3, 3) |
| 196 | + 74 |
| 197 | + """ |
| 198 | + data_points: List[int] = [func(x_val) for x_val in range(1, order + 1)] |
| 199 | + |
| 200 | + polynomials: List[Callable[[int], int]] = [ |
| 201 | + interpolate(data_points[:max_coeff]) for max_coeff in range(1, order + 1) |
| 202 | + ] |
| 203 | + |
| 204 | + ret: int = 0 |
| 205 | + poly: int |
| 206 | + x_val: int |
| 207 | + |
| 208 | + for poly in polynomials: |
| 209 | + x_val = 1 |
| 210 | + while func(x_val) == poly(x_val): |
| 211 | + x_val += 1 |
| 212 | + |
| 213 | + ret += poly(x_val) |
| 214 | + |
| 215 | + return ret |
| 216 | + |
| 217 | + |
| 218 | +if __name__ == "__main__": |
| 219 | + print(f"{solution() = }") |
0 commit comments