From 284c402c3251427029d42f1ca1cd2748dd78e88d Mon Sep 17 00:00:00 2001 From: Akuli Date: Tue, 26 Dec 2023 13:45:17 +0200 Subject: [PATCH] Complete AoC 2023 (#482) --- README.md | 4 +- examples/aoc2023/day24/bigint.jou | 226 ++++++++++++++++ examples/aoc2023/day24/part1.jou | 204 ++++++++++++++ examples/aoc2023/day24/part2.jou | 356 +++++++++++++++++++++++++ examples/aoc2023/day24/sampleinput.txt | 5 + examples/aoc2023/day25/part1.jou | 282 ++++++++++++++++++++ examples/aoc2023/day25/sampleinput.txt | 13 + runtests.sh | 2 +- 8 files changed, 1089 insertions(+), 3 deletions(-) create mode 100644 examples/aoc2023/day24/bigint.jou create mode 100644 examples/aoc2023/day24/part1.jou create mode 100644 examples/aoc2023/day24/part2.jou create mode 100644 examples/aoc2023/day24/sampleinput.txt create mode 100644 examples/aoc2023/day25/part1.jou create mode 100644 examples/aoc2023/day25/sampleinput.txt diff --git a/README.md b/README.md index 5cbe0e75..cf2eca9a 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ def main() -> int: See the [examples](./examples/) and [tests](./tests/) directories for more example programs or read [the Jou tutorial](./doc/tutorial.md). -So far, Jou is usable enough to do [Advent of Code 2023](https://adventofcode.com/). -We'll see whether I get 50 stars with Jou this year. +So far, Jou is usable for writing small programs that don't have a lot of dependencies. +For example, I solved all problems of [Advent of Code 2023](https://adventofcode.com/2023/) in Jou. See [examples/aoc2023](./examples/aoc2023/) for the code. Goals: diff --git a/examples/aoc2023/day24/bigint.jou b/examples/aoc2023/day24/bigint.jou new file mode 100644 index 00000000..4ce4acac --- /dev/null +++ b/examples/aoc2023/day24/bigint.jou @@ -0,0 +1,226 @@ +import "stdlib/mem.jou" + + +class BigInt: + data: byte[48] # little endian, last byte includes sign bit + + +def bigint(value: long) -> BigInt: + # assumes little-endian CPU + result = BigInt{} + assert sizeof(value) < sizeof(result.data) + + if value < 0: + memset(&result.data, 0xff, sizeof(result.data)) + memcpy(&result.data, &value, sizeof(value)) + return result + + +def bigint_to_long(x: BigInt) -> long: + # assume that value fits into 64-bit long + # also assume little-endian + result: long + memcpy(&result, &x.data, sizeof(result)) + return result + + +# TODO: methods are kinda annoying because you'd need temporary values. +# e.g. foo.bar().baz() doesn't work, tries to do take address of bar() return value + + +# x+y +def bigadd(x: BigInt, y: BigInt) -> BigInt: + result = bigint(0) + carry_bit = 0 + + for i = 0; i < sizeof(x.data); i++: + result_byte = (x.data[i] as int) + (y.data[i] as int) + carry_bit + if result_byte >= 256: + carry_bit = 1 + else: + carry_bit = 0 + result.data[i] = result_byte as byte + + return result + + +def bigadd3(x: BigInt, y: BigInt, z: BigInt) -> BigInt: + return bigadd(bigadd(x, y), z) +def bigadd4(x: BigInt, y: BigInt, z: BigInt, zz: BigInt) -> BigInt: + return bigadd(bigadd(bigadd(x, y), z), zz) +def bigadd5(x: BigInt, y: BigInt, z: BigInt, zz: BigInt, zzz: BigInt) -> BigInt: + return bigadd(bigadd(bigadd(bigadd(x, y), z), zz), zzz) +def bigadd6(x: BigInt, y: BigInt, z: BigInt, zz: BigInt, zzz: BigInt, zzzz: BigInt) -> BigInt: + return bigadd(bigadd(bigadd(bigadd(bigadd(x, y), z), zz), zzz), zzzz) + + +# -x +def bigneg(x: BigInt) -> BigInt: + # Flipping all bits (~x) is almost same as negating the value. + # For example, -7 is f9ffffff... and ~7 is f8ffffff... + for i = 0; i < sizeof(x.data); i++: + x.data[i] = (0xff as byte) - x.data[i] + return bigadd(x, bigint(1)) + + +# x-y +def bigsub(x: BigInt, y: BigInt) -> BigInt: + return bigadd(x, bigneg(y)) + + +# Return values: +# x < y --> -1 +# x == y --> 0 +# x > y --> 1 +def bigcmp(x: BigInt, y: BigInt) -> int: + x_sign_bit = x.data[sizeof(x.data) - 1] / 128 + y_sign_bit = y.data[sizeof(y.data) - 1] / 128 + + if x_sign_bit != y_sign_bit: + return y_sign_bit - x_sign_bit + + for i = sizeof(x.data) - 1; i >= 0; i--: + if (x.data[i] as int) < (y.data[i] as int): + return -1 + if (x.data[i] as int) > (y.data[i] as int): + return 1 + + return 0 + + +# x == y +def bigeq(x: BigInt, y: BigInt) -> bool: + return bigcmp(x, y) == 0 + + +# Return values: +# positive --> 1 +# zero --> 0 +# negative --> -1 +def bigsign(x: BigInt) -> int: + return bigcmp(x, bigint(0)) + + +# |x| +def bigabs(x: BigInt) -> BigInt: + if bigsign(x) < 0: + return bigneg(x) + else: + return x + + +# x*y +def bigmul(x: BigInt, y: BigInt) -> BigInt: + result_sign = bigsign(x) * bigsign(y) + x = bigabs(x) + y = bigabs(y) + + result = bigint(0) + for i = 0; i < sizeof(x.data); i++: + for k = 0; i+k < sizeof(result.data); k++: + temp = (x.data[i] as int)*(y.data[k] as int) + + gonna_add = bigint(0) + gonna_add.data[i+k] = temp as byte + if i+k+1 < sizeof(gonna_add.data): + gonna_add.data[i+k+1] = (temp / 256) as byte + result = bigadd(result, gonna_add) + + if bigsign(result) == result_sign: + return result + else: + return bigneg(result) + + +# x / 256^n for x >= 0 +def shift_smaller(x: BigInt, n: int) -> BigInt: + assert bigsign(x) >= 0 + assert n >= 0 + + if n >= sizeof(x.data): + return bigint(0) + + memmove(&x.data, &x.data[n], sizeof(x.data) - n) + memset(&x.data[sizeof(x.data) - n], 0, n) + return x + + +# x * 256^n for x >= 0 +def shift_bigger(x: BigInt, n: int) -> BigInt: + assert bigsign(x) >= 0 + assert n >= 0 + + if n >= sizeof(x.data): + return bigint(0) + + memmove(&x.data[n], &x.data[0], sizeof(x.data) - n) + memset(&x.data, 0, n) + return x + + +# [x/y, x%y] +def bigdivmod(x: BigInt, y: BigInt) -> BigInt[2]: + assert not bigeq(y, bigint(0)) + + quotient = bigint(0) + remainder = bigabs(x) + yabs = bigabs(y) + + n = 0 + while bigcmp(shift_smaller(remainder, n), yabs) >= 0: + n++ + + assert n < sizeof(quotient.data) + while n --> 0: + # Find nth base-256 digit of result with trial and error. + d = 0 + bigger_y = shift_bigger(yabs, n) + while bigcmp(bigmul(bigger_y, bigint(d+1)), remainder) <= 0: + if d == 0: + d++ + else: + d *= 2 + d /= 2 + while bigcmp(bigmul(bigger_y, bigint(d+1)), remainder) <= 0: + d++ + + assert d < 256 + quotient.data[n] = d as byte + remainder = bigsub(remainder, bigmul(bigint(d), bigger_y)) + + if bigsign(x)*bigsign(y) < 0: + quotient = bigneg(quotient) + if bigsign(x) < 0: + remainder = bigneg(remainder) + + # When nonzero remainder, force its sign to be same sign as y, similar to jou % + if bigsign(remainder) != 0 and bigsign(remainder) != bigsign(y): + remainder = bigadd(remainder, y) + quotient = bigsub(quotient, bigint(1)) + + return [quotient, remainder] + + # Tests: + # + # for x = -100; x <= 100; x++: + # for y = -100; y <= 100; y++: + # if y != 0: + # result = bigdivmod(bigint(x), bigint(y)) + # assert x == (x/y)*y + (x%y) + # assert x == bigint_to_long(result[0])*y + bigint_to_long(result[1]) + # assert bigint_to_long(result[0]) == x / y + # assert bigint_to_long(result[1]) == x % y + + +# x / y +def bigdiv(x: BigInt, y: BigInt) -> BigInt: + pair = bigdivmod(x, y) + return pair[0] + + +# assert x % y == 0 +# x / y +def bigdiv_exact(x: BigInt, y: BigInt) -> BigInt: + pair = bigdivmod(x, y) + assert bigeq(pair[1], bigint(0)) + return pair[0] diff --git a/examples/aoc2023/day24/part1.jou b/examples/aoc2023/day24/part1.jou new file mode 100644 index 00000000..a43cacd2 --- /dev/null +++ b/examples/aoc2023/day24/part1.jou @@ -0,0 +1,204 @@ +import "stdlib/io.jou" +import "./bigint.jou" + + +def det(matrix: long[2][2]) -> BigInt: + a = matrix[0][0] + b = matrix[0][1] + c = matrix[1][0] + d = matrix[1][1] + return bigsub(bigmul(bigint(a), bigint(d)), bigmul(bigint(b), bigint(c))) + + +def dot(v1: long[2], v2: long[2]) -> BigInt: + return bigadd(bigmul(bigint(v1[0]), bigint(v2[0])), bigmul(bigint(v1[1]), bigint(v2[1]))) + + +def matrix_times_vector(matrix: long[2][2], vector: long[2]) -> BigInt[2]: + a = bigint(matrix[0][0]) + b = bigint(matrix[0][1]) + c = bigint(matrix[1][0]) + d = bigint(matrix[1][1]) + x = bigint(vector[0]) + y = bigint(vector[1]) + return [ + bigadd(bigmul(a, x), bigmul(b, y)), + bigadd(bigmul(c, x), bigmul(d, y)), + ] + + +# Returns the x and y of matrix*[x,y] = coeff_vector as fractions: +# +# [top of x, top of y, bottom] +def solve_linear_system_of_2_equations(matrix: long[2][2], coeff_vector: long[2]) -> BigInt[3]: + a = matrix[0][0] + b = matrix[0][1] + c = matrix[1][0] + d = matrix[1][1] + + determinant = det(matrix) + assert not bigeq(determinant, bigint(0)) # assume inverse matrix exists + + inverse_matrix_times_determinant = [ + [d, -b], + [-c, a], + ] + xy_times_determinant = matrix_times_vector(inverse_matrix_times_determinant, coeff_vector) + return [xy_times_determinant[0], xy_times_determinant[1], determinant] + + +class Rectangle: + x_min: long + x_max: long + y_min: long + y_max: long + + def contains(self, point: long[2]) -> bool: + x = point[0] + y = point[1] + return ( + self->x_min <= x and x <= self->x_max + and self->y_min <= y and y <= self->x_max + ) + + def contains_fraction(self, qx: BigInt, qy: BigInt, q: BigInt) -> bool: + qxmin = bigmul(q, bigint(self->x_min)) + qxmax = bigmul(q, bigint(self->x_max)) + qymin = bigmul(q, bigint(self->y_min)) + qymax = bigmul(q, bigint(self->y_max)) + + assert bigsign(q) > 0 + return ( + bigcmp(qxmin, qx) <= 0 and bigcmp(qx, qxmax) <= 0 + and bigcmp(qymin, qy) <= 0 and bigcmp(qy, qymax) <= 0 + ) + + +class Ray: + start: long[2] + dir: long[2] + + def intersects(self, other: Ray*, test_area: Rectangle) -> bool: + if bigeq(det([self->dir, other->dir]), bigint(0)): + # Rays go in parallel directions. + start_diff = [ + self->start[0] - other->start[0], + self->start[1] - other->start[1], + ] + if not bigeq(det([start_diff, self->dir]), bigint(0)): + # Rays are not aligned to go along the same line. + return False + + # Vectors go along the same line. Let's project everything to the line so this becomes 1 dimensional. + self_start = dot(self->dir, self->start) + self_dir = dot(self->dir, self->dir) + other_start = dot(self->dir, other->start) + other_dir = dot(self->dir, other->dir) + + assert bigsign(self_dir) > 0 + assert not bigeq(other_dir, bigint(0)) + assert not bigeq(self_start, other_start) + + if bigsign(other_dir) > 0: + # Rays go in the same direction. Eventually one ray will reach the start of the other. + if bigcmp(self_start, other_start) > 0: + return test_area.contains(self->start) + else: + return test_area.contains(other->start) + + if bigcmp(self_start, other_start) > 0: + # Rays point away from each other + return False + + # Rays point towards each other, so they meet somewhere in the middle. + # + # meet = self_start + t*self_dir (t is time) + # = other_start + t*other_dir + # + # Math gives a solution: + # + # t = p/q, p = other_start - self_start, q = self_dir - other_dir + q = bigsub(self_dir, other_dir) + assert bigsign(q) > 0 + qt = bigsub(other_start, self_start) + qx = bigadd(bigmul(q, bigint(self->start[0])), bigmul(qt, bigint(self->dir[0]))) + qy = bigadd(bigmul(q, bigint(self->start[1])), bigmul(qt, bigint(self->dir[1]))) + return test_area.contains_fraction(qx, qy, q) + + # Vectors are not parallel. They will intersect somewhere, but where? + # + # self->start + a*self->dir = other->start + b*other->dir + # + # a*self->dir[0] + b*(-other->dir[0]) = other->start[0] - self->start[0] + # a*self->dir[1] + b*(-other->dir[1]) = other->start[1] - self->start[1] + coeff_matrix = [ + [self->dir[0], -other->dir[0]], + [self->dir[1], -other->dir[1]], + ] + constant_vector = [ + other->start[0] - self->start[0], + other->start[1] - self->start[1], + ] + solve_result = solve_linear_system_of_2_equations(coeff_matrix, constant_vector) + qa = solve_result[0] + qb = solve_result[1] + q = solve_result[2] + + if bigsign(q) < 0: + qa = bigneg(qa) + qb = bigneg(qb) + q = bigneg(q) + + # rays do not extend backwards + if bigsign(qa) < 0 or bigsign(qb) < 0: + return False + + qx = bigadd(bigmul(q, bigint(self->start[0])), bigmul(qa, bigint(self->dir[0]))) + qy = bigadd(bigmul(q, bigint(self->start[1])), bigmul(qa, bigint(self->dir[1]))) + return test_area.contains_fraction(qx, qy, q) + + +def main() -> int: + f = fopen("sampleinput.txt", "r") + assert f != NULL + + rays: Ray[500] + nrays = 0 + + r: Ray + + x: long + y: long + z: long + dx: long + dy: long + dz: long + while fscanf(f, "%lld, %lld, %lld @ %lld, %lld, %lld\n", &x, &y, &z, &dx, &dy, &dz) == 6: + assert nrays < sizeof(rays)/sizeof(rays[0]) + rays[nrays++] = Ray{start = [x,y], dir = [dx,dy]} + + fclose(f) + + test_area = Rectangle{ + x_min = 7, + x_max = 27, + y_min = 7, + y_max = 27, +# x_min = 200000000000000L, +# x_max = 400000000000000L, +# y_min = 200000000000000L, +# y_max = 400000000000000L, + } + + result = 0 + for i = 0; i < nrays; i++: + # progress print for actual input + if nrays > 100: + printf("%d/%d\n", i, nrays) + + for k = i+1; k < nrays; k++: + if rays[i].intersects(&rays[k], test_area): + result++ + printf("%d\n", result) # Output: 2 + + return 0 diff --git a/examples/aoc2023/day24/part2.jou b/examples/aoc2023/day24/part2.jou new file mode 100644 index 00000000..b3a5733b --- /dev/null +++ b/examples/aoc2023/day24/part2.jou @@ -0,0 +1,356 @@ +import "stdlib/math.jou" +import "stdlib/mem.jou" +import "stdlib/io.jou" +import "./bigint.jou" + + +class Equation: + # sum(coeffs[i] * x_i) = rhs + nvariables: int + coeffs: BigInt* + rhs: BigInt + + def sub(self, var: int, value: BigInt) -> None: + self->rhs = bigsub(self->rhs, bigmul(self->coeffs[var], value)) + self->coeffs[var] = bigint(0) + + def has(self, var: int) -> bool: + assert 0 <= var and var < self->nvariables + return not bigeq(self->coeffs[var], bigint(0)) + + def print(self) -> None: + for v = 0; v < self->nvariables; v++: + if v > 0: + printf(" + ") + printf("%lld x%d", bigint_to_long(self->coeffs[v]), v) + printf(" = %lld\n", bigint_to_long(self->rhs)) + + +def add_with_coeff(dest: Equation*, src: Equation*, coeff: BigInt) -> None: + assert src->nvariables == dest->nvariables + for v = 0; v < src->nvariables; v++: + dest->coeffs[v] = bigadd(dest->coeffs[v], bigmul(coeff, src->coeffs[v])) + dest->rhs = bigadd(dest->rhs, bigmul(src->rhs, coeff)) + + +class EquationSolver: + eqs: Equation* + neqs: int + + def print(self) -> None: + printf("equations:\n") + for eq = &self->eqs[0]; eq < &self->eqs[self->neqs]; eq++: + printf(" ") + eq->print() + printf("\n") + + def count_equations_having_var(self, var: int) -> int: + n = 0 + for eq = &self->eqs[0]; eq < &self->eqs[self->neqs]; eq++: + if eq->has(var): + n++ + return n + + def eliminate_from_all_but_one_equation(self, var: int) -> None: + # Repeated Gaussian reduction in Euclidean algorithm style (sorry about word salad) + while self->count_equations_having_var(var) >= 2: + src = self->find_equation_with_smallest_nonzero_coeff_for_var(var) + assert src != NULL + assert not bigeq(src->coeffs[var], bigint(0)) + + for dest = self->eqs; dest < &self->eqs[self->neqs]; dest++: + if dest != src and dest->has(var): + # Pick c so that dest->coeffs[var] + c*src->coeffs[var] = 0. + # The perfect c isn't necessarly integer, so we pick a close value. + c = bigneg(bigdiv(dest->coeffs[var], src->coeffs[var])) + assert not bigeq(c, bigint(0)) + add_with_coeff(dest, src, c) + + def find_equation_with_smallest_nonzero_coeff_for_var(self, var: int) -> Equation*: + result: Equation* = NULL + for eq = &self->eqs[0]; eq < &self->eqs[self->neqs]; eq++: + if eq->has(var) and ( + result == NULL + or bigcmp(bigabs(result->coeffs[var]), bigabs(eq->coeffs[var])) > 0 + ): + result = eq + return result + + def all_equations_are_zero_equals_zero(self) -> bool: + for eq = &self->eqs[0]; eq < &self->eqs[self->neqs]; eq++: + if not bigeq(eq->rhs, bigint(0)): + return False + for v = 0; v < eq->nvariables; v++: + if not bigeq(eq->coeffs[v], bigint(0)): + return False + return True + + +def solve_linear_system_of_equations(eqs: Equation*, neqs: int) -> BigInt*: + nvariables = 0 + for i = 0; i < neqs; i++: + nvariables = max(nvariables, eqs[i].nvariables) + + # Copy all equations to avoid mutating input arguments, lol + solver = EquationSolver{neqs = neqs} + solver.eqs = malloc(sizeof(solver.eqs[0]) * neqs) + for i = 0; i < neqs; i++: + eq = &solver.eqs[i] + eq->nvariables = nvariables + eq->coeffs = calloc(sizeof(eq->coeffs[0]), nvariables) + memcpy(eq->coeffs, eqs[i].coeffs, sizeof(eqs[i].coeffs[0]) * eqs[i].nvariables) + eq->rhs = eqs[i].rhs + + # Each final equation only has one unknown variable + variables from equations after it + final_equations: Equation* = malloc(sizeof(final_equations[0]) * nvariables) + + for var = 0; var < nvariables; var++: + #printf("Solver: variable %d/%d\n", var, nvariables) + + if solver.count_equations_having_var(var) == 0: + # This variable could be anything. We will set it to zero. + # I really only want to solve the first two variables, so this is fine. + coeffs: BigInt* = calloc(sizeof(coeffs[0]), nvariables) + coeffs[var] = bigint(1) + final_equations[var] = Equation{nvariables = nvariables, coeffs = coeffs, rhs = bigint(0)} + else: + solver.eliminate_from_all_but_one_equation(var) + eq = solver.find_equation_with_smallest_nonzero_coeff_for_var(var) + assert eq != NULL + final_equations[var] = *eq + *eq = solver.eqs[--solver.neqs] + + assert solver.all_equations_are_zero_equals_zero() + + for i = 0; i < solver.neqs; i++: + free(solver.eqs[i].coeffs) + free(solver.eqs) + + values: BigInt* = calloc(sizeof(values[0]), nvariables) + for var = nvariables - 1; var >= 0; var--: + # Substitute known values into final equations + final = final_equations[var] + for known = var+1; known < nvariables; known++: + final.sub(known, values[known]) + + for v = 0; v < nvariables; v++: + if v != var: + assert bigeq(final.coeffs[v], bigint(0)) + + values[var] = bigdiv_exact(final.rhs, final.coeffs[var]) + free(final.coeffs) + + free(final_equations) + return values + + +def test_equation_solver() -> None: + # 4x + 2y = 116 + # 10x - y = 86 + # -100x + 10y = -860 + coeffs1 = [bigint(4), bigint(2)] + coeffs2 = [bigint(10), bigint(-1)] + coeffs3 = [bigint(-100), bigint(10)] + eqs = [ + Equation{nvariables = 2, coeffs = coeffs1, rhs = bigint(116)}, + Equation{nvariables = 2, coeffs = coeffs2, rhs = bigint(86)}, + Equation{nvariables = 2, coeffs = coeffs3, rhs = bigint(-860)}, + ] + + result = solve_linear_system_of_equations(eqs, 3) + printf("x=%lld y=%lld\n", bigint_to_long(result[0]), bigint_to_long(result[1])) # Output: x=12 y=34 + free(result) + + +class MovingPoint: + start: long[3] + speed: long[3] + + +# We don't need the whole input, first N lines are enough +def read_input(N: int) -> MovingPoint*: + f = fopen("sampleinput.txt", "r") + assert f != NULL + + result: MovingPoint* = malloc(N * sizeof(result[0])) + for i = 0; i < N; i++: + x: long + y: long + z: long + dx: long + dy: long + dz: long + assert fscanf(f, "%lld, %lld, %lld @ %lld, %lld, %lld\n", &x, &y, &z, &dx, &dy, &dz) == 6 + result[i] = MovingPoint{start = [x,y,z], speed = [dx,dy,dz]} + + fclose(f) + return result + + +def get_paired_index(N: int, small: int, big: int) -> int: + counter = N + for i = 0; i < N; i++: + for j = i+1; j < N; j++: + if i == small and j == big: + return counter + counter++ + assert False + + +# Returned equations describe the moments in time t_1,...,t_N where the moving points align. +# Specifically: +# +# for each of XY plane, XZ plane, YZ plane: +# for all moving point indexes i Equation*: + nvariables = N + N*(N-1)/2 # N normal variables, (n choose 2) paired variables + + eqs: Equation* = calloc(sizeof(eqs[0]), 3*N*N*N + 1) # more than big enough + assert eqs != NULL + *neqs = 0 + + planes = [[0,1], [0,2], [1,2]] + for planeidx = 0; planeidx < 3; planeidx++: + plane = planes[planeidx] + for i = 0; i < N; i++: + for j = i+1; j < N; j++: + for k = j+1; k < N; k++: + eq = &eqs[(*neqs)++] + eq->nvariables = nvariables + eq->coeffs = calloc(sizeof(eq->coeffs[0]), nvariables) + + six = bigint(moving_points[i].start[plane[0]]) + siy = bigint(moving_points[i].start[plane[1]]) + sjx = bigint(moving_points[j].start[plane[0]]) + sjy = bigint(moving_points[j].start[plane[1]]) + skx = bigint(moving_points[k].start[plane[0]]) + sky = bigint(moving_points[k].start[plane[1]]) + + vix = bigint(moving_points[i].speed[plane[0]]) + viy = bigint(moving_points[i].speed[plane[1]]) + vjx = bigint(moving_points[j].speed[plane[0]]) + vjy = bigint(moving_points[j].speed[plane[1]]) + vkx = bigint(moving_points[k].speed[plane[0]]) + vky = bigint(moving_points[k].speed[plane[1]]) + + titj_var = get_paired_index(N, i, j) + titk_var = get_paired_index(N, i, k) + tjtk_var = get_paired_index(N, j, k) + + # These can't be written as Jou code, because they contain unknown variables: + # p1 = [six + t_i*vix, siy + t_i*viy] + # p2 = [sjx + t_j*vjx, sjy + t_j*vjy] + # p3 = [skx + t_k*vkx, sky + t_k*vky] + # + # To ensure straight line, we do: + # + # | x of p1 - x of p2 x of p3 - x of p2 | + # | | = 0 + # | y of p1 - y of p2 y of p3 - y of p2 | + # + # With sympy (Python library), I expanded this to: + # + # -six*sjy + six*sky - six*t_j*vjy + six*t_k*vky + siy*sjx - siy*skx + siy*t_j*vjx - siy*t_k*vkx - sjx*sky + sjx*t_i*viy - sjx*t_k*vky + sjy*skx - sjy*t_i*vix + sjy*t_k*vkx - skx*t_i*viy + skx*t_j*vjy + sky*t_i*vix - sky*t_j*vjx - t_i*t_j*vix*vjy + t_i*t_j*viy*vjx + t_i*t_k*vix*vky - t_i*t_k*viy*vkx - t_j*t_k*vjx*vky + t_j*t_k*vjy*vkx + eq->coeffs[i] = bigadd4(bigmul(sjx, viy), bigneg(bigmul(sjy, vix)), bigneg(bigmul(skx, viy)), bigmul(sky, vix)) + eq->coeffs[j] = bigadd4(bigmul(siy, vjx), bigneg(bigmul(six, vjy)), bigmul(skx, vjy), bigneg(bigmul(sky, vjx))) + eq->coeffs[k] = bigadd4(bigmul(six, vky), bigneg(bigmul(siy, vkx)), bigneg(bigmul(sjx, vky)), bigmul(sjy, vkx)) + eq->coeffs[titj_var] = bigsub(bigmul(viy, vjx), bigmul(vix, vjy)) + eq->coeffs[titk_var] = bigsub(bigmul(vix, vky), bigmul(viy, vkx)) + eq->coeffs[tjtk_var] = bigsub(bigmul(vjy, vkx), bigmul(vjx, vky)) + eq->rhs = bigadd6(bigmul(six, sjy), bigneg(bigmul(six, sky)), bigneg(bigmul(siy, sjx)), bigmul(siy, skx), bigmul(sjx, sky), bigneg(bigmul(sjy, skx))) + + return eqs + + +def lerp(src_min: long, src_max: long, dest_min: long, dest_max: long, value: long) -> long: + weight1 = value - src_min + weight2 = src_max - value + + # weighted average: (dest_max*weight1 + dest_min*weight2)/(weight1 + weight2) + # multiplications are too big for longs, but values themselves seem to fit + top = bigadd(bigmul(bigint(weight1), bigint(dest_max)), bigmul(bigint(weight2), bigint(dest_min))) + bottom = weight1 + weight2 + return bigint_to_long(bigdiv_exact(top, bigint(bottom))) + + +def test_lerp() -> None: + # 0-------1-------2-------3-------4-------5 + # ^ + # + # 100-----120-----140-----160-----180-----200 + # ^ + printf("%lld\n", lerp(0, 5, 100, 200, 2)) # Output: 140 + + # You can extrapolate outside the input range: + printf("%lld\n", lerp(0, 5, 100, 200, -1)) # Output: 80 + + +def lerp3D(src_min: long, src_max: long, dest_min: long[3], dest_max: long[3], value: long) -> long[3]: + return [ + lerp(src_min, src_max, dest_min[0], dest_max[0], value), + lerp(src_min, src_max, dest_min[1], dest_max[1], value), + lerp(src_min, src_max, dest_min[2], dest_max[2], value), + ] + + +def main() -> int: + test_equation_solver() + test_lerp() + + N = 5 + moving_points = read_input(N) + + neqs: int + eqs = create_equations(moving_points, N, &neqs) + + printf("Created %d equations\n", neqs) # Output: Created 30 equations + +# for i = 0; i < neqs; i++: +# eqs[i].print() + + solution = solve_linear_system_of_equations(eqs, neqs) + + # Output: 5 3 4 6 1 + printf( + "%lld %lld %lld %lld %lld\n", + bigint_to_long(solution[0]), + bigint_to_long(solution[1]), + bigint_to_long(solution[2]), + bigint_to_long(solution[3]), + bigint_to_long(solution[4]), + ) + + t1 = bigint_to_long(solution[0]) + t2 = bigint_to_long(solution[1]) + + point_at_t1 = [ + moving_points[0].start[0] + t1*moving_points[0].speed[0], + moving_points[0].start[1] + t1*moving_points[0].speed[1], + moving_points[0].start[2] + t1*moving_points[0].speed[2], + ] + point_at_t2 = [ + moving_points[1].start[0] + t2*moving_points[1].speed[0], + moving_points[1].start[1] + t2*moving_points[1].speed[1], + moving_points[1].start[2] + t2*moving_points[1].speed[2], + ] + + point_initially = lerp3D(t1, t2, point_at_t1, point_at_t2, 0) + printf("%lld\n", point_initially[0] + point_initially[1] + point_initially[2]) # Output: 47 + + free(moving_points) + for i = 0; i < neqs; i++: + free(eqs[i].coeffs) + free(eqs) + free(solution) + + return 0 diff --git a/examples/aoc2023/day24/sampleinput.txt b/examples/aoc2023/day24/sampleinput.txt new file mode 100644 index 00000000..35963dc2 --- /dev/null +++ b/examples/aoc2023/day24/sampleinput.txt @@ -0,0 +1,5 @@ +19, 13, 30 @ -2, 1, -2 +18, 19, 22 @ -1, -1, -2 +20, 25, 34 @ -2, -2, -4 +12, 31, 28 @ -1, -2, -1 +20, 19, 15 @ 1, -5, -3 diff --git a/examples/aoc2023/day25/part1.jou b/examples/aoc2023/day25/part1.jou new file mode 100644 index 00000000..725c5df8 --- /dev/null +++ b/examples/aoc2023/day25/part1.jou @@ -0,0 +1,282 @@ +import "stdlib/ascii.jou" +import "stdlib/math.jou" +import "stdlib/process.jou" +import "stdlib/str.jou" +import "stdlib/io.jou" +import "stdlib/mem.jou" + + +def print_path(path: int*, end: int) -> None: + for i = 0; path[i] != end; i++: + printf("%d -> ", path[i]) + printf("%d\n", end) + + +class Graph: + num_nodes: int + matrix: int** + weights: int* # Initially every node has weight 1, the weights are added when merging + + def free(self) -> None: + for i = 0; i < self->num_nodes; i++: + free(self->matrix[i]) + free(self->matrix) + free(self->weights) + + def visualize_with_graphviz(self) -> None: + assert not WINDOWS + + f = fopen("/tmp/aoc25-graph.txt", "w") + assert f != NULL + + fprintf(f, "digraph G {\n") + for i = 0; i < self->num_nodes; i++: + fprintf(f, "node%d [label=\"%d (weight=%d)\"]\n", i, i, self->weights[i]) + + for i = 0; i < self->num_nodes; i++: + for k = 0; k < i; k++: + if self->matrix[i][k] != 0: + fprintf(f, " node%d -> node%d [label=\"%d\", dir=none]\n", i, k, self->matrix[i][k]) + + fprintf(f, "}\n") + fclose(f) + system("dot -T png /tmp/aoc25-graph.txt -o /tmp/aoc25-graph.png && open /tmp/aoc25-graph.png") + + def connect(self, a: int, b: int) -> None: + assert 0 <= a and a < self->num_nodes + assert 0 <= b and b < self->num_nodes + self->matrix[a][b]++ + self->matrix[b][a]++ + + def disconnect(self, a: int, b: int) -> None: + assert 0 <= a and a < self->num_nodes + assert 0 <= b and b < self->num_nodes + assert self->matrix[a][b] > 0 + assert self->matrix[b][a] > 0 + self->matrix[a][b]-- + self->matrix[b][a]-- + + def merge_nodes(self, a: int, b: int) -> None: + assert 0 <= a and a < self->num_nodes + assert 0 <= b and b < self->num_nodes + + # Add weights + self->weights[a] += self->weights[b] + + # Add all b's connections to a, except the possible a <--> b connection + for n = 0; n < self->num_nodes; n++: + if n != a: + self->matrix[a][n] += self->matrix[b][n] + self->matrix[n][a] += self->matrix[n][b] + + # Disconnect b from everything + for n = 0; n < self->num_nodes; n++: + self->matrix[n][b] = 0 + self->matrix[b][n] = 0 + + self->num_nodes-- + + # Now the matrix has zeros in b's row and column. Delete that row and column. + # This changes names of other, unrelated nodes. That can't be avoided. + free(self->matrix[b]) + self->matrix[b] = self->matrix[self->num_nodes] + for i = 0; i < self->num_nodes; i++: + self->matrix[i][b] = self->matrix[i][self->num_nodes] + self->weights[b] = self->weights[self->num_nodes] + + def copy(self) -> Graph: + result = *self + n = self->num_nodes + + result.matrix = malloc(sizeof(result.matrix[0]) * n) + for i = 0; i < n; i++: + row: int* = malloc(sizeof(row[0]) * n) + memcpy(row, self->matrix[i], sizeof(row[0]) * n) + result.matrix[i] = row + + result.weights = malloc(sizeof(result.weights[0]) * n) + memcpy(result.weights, self->weights, sizeof(self->weights[0]) * n) + + return result + + # Returns shortest path, or NULL if there is no path. + # Returned path always starts with a and ends with b. There is no -1 termination or similar. + def dijkstra_algorithm(self, a: int, b: int) -> int*: + assert a != b + + where_we_came_from: int* = malloc(sizeof(where_we_came_from[0]) * self->num_nodes) + distances: int* = calloc(sizeof(distances[0]), self->num_nodes) + nodes_with_distance_set: int* = calloc(sizeof(nodes_with_distance_set[0]), self->num_nodes) + known_shortest: bool* = calloc(sizeof(known_shortest[0]), self->num_nodes) + + for i = 0; i < self->num_nodes; i++: + distances[i] = -1 + + # Make dijkstra go from b to a, so that we don't need to reverse when we're done + nodes_with_distance_set[0] = b + distances[b] = 0 + n_nodes_with_distance_set = 1 + + while not known_shortest[a]: + # pick the node with unknown-yet-to-be-smallest distance, whose distance is smallest + current = -1 + for i = 0; i < n_nodes_with_distance_set; i++: + n = nodes_with_distance_set[i] + if (not known_shortest[n]) and (current == -1 or distances[n] < distances[current]): + current = n + if current == -1: + # no path + free(where_we_came_from) + free(distances) + free(nodes_with_distance_set) + free(known_shortest) + return NULL + + # for some reason that i don't understand, the distance of the node we picked is known to be smallest + known_shortest[current] = True + + # update neighbor distances, if visiting through current makes them shorter + for neighbor = 0; neighbor < self->num_nodes; neighbor++: + if self->matrix[current][neighbor] > 0: + d = distances[current] + 1 # 1 = weight of edge + if distances[neighbor] == -1: + distances[neighbor] = d + nodes_with_distance_set[n_nodes_with_distance_set++] = neighbor + where_we_came_from[neighbor] = current + elif distances[neighbor] > d: + distances[neighbor] = d + where_we_came_from[neighbor] = current + + result: int* = malloc(sizeof(result[0]) * self->num_nodes) + p = result + current = a + while current != b: + *p++ = current + current = where_we_came_from[current] + *p++ = b + + free(where_we_came_from) + free(distances) + free(nodes_with_distance_set) + free(known_shortest) + + return result + + # If this returns True, the nodes will always be in the same region after breaking 3 connections. + def nodes_are_strongly_connected(self, a: int, b: int) -> bool: + assert a != b + + copy = self->copy() + + for i = 0; i < 3; i++: + path = copy.dijkstra_algorithm(a, b) + if path == NULL: + copy.free() + return False + + for k = 0; path[k] != b; k++: + copy.disconnect(path[k], path[k+1]) + free(path) + + # We disconnected 3 distinct paths, is it still connected? + path = copy.dijkstra_algorithm(a, b) + if path != NULL: + # Yes, it is --> strongly connected + free(path) + copy.free() + return True + copy.free() + return False + + def find_a_strong_connection(self, hint: int*) -> int[2]: + for i = *hint; i < *hint + self->num_nodes; i++: + a = i % self->num_nodes + for b = a+1; b < self->num_nodes; b++: + if self->nodes_are_strongly_connected(a, b): + *hint = max(a, b) + return [a, b] + return [-1, -1] + + +def main() -> int: + max_nodes = 10000 + node_names: byte[10]* = malloc(sizeof(node_names[0]) * max_nodes) + num_nodes = 0 + + f = fopen("sampleinput.txt", "r") + assert f != NULL + + line: byte[1000] + while fgets(line, sizeof(line) as int, f) != NULL: + colon = strchr(line, ':') + assert colon != NULL + *colon = ' ' + + words = split_by_ascii_whitespace(line) + for w = words; *w != NULL; w++: + found = False + for i = 0; i < num_nodes; i++: + if strcmp(node_names[i], *w) == 0: + found = True + break + if not found: + assert strlen(*w) < 10 + s: byte[10] + strcpy(s, *w) + node_names[num_nodes++] = s + + free(words) + + graph = Graph{num_nodes = num_nodes} + graph.matrix = calloc(num_nodes, sizeof(graph.matrix[0])) + for i = 0; i < num_nodes; i++: + row: int* = calloc(num_nodes, sizeof(row[0])) + graph.matrix[i] = row + + graph.weights = malloc(num_nodes * sizeof(graph.weights[0])) + for i = 0; i < num_nodes; i++: + graph.weights[i] = 1 + + rewind(f) + while fgets(line, sizeof(line) as int, f) != NULL: + colon = strchr(line, ':') + assert colon != NULL + *colon = ' ' + + words = split_by_ascii_whitespace(line) + assert words[0] != NULL + + start_node = -1 + for i = 0; i < num_nodes; i++: + if strcmp(node_names[i], words[0]) == 0: + start_node = i + break + + for w = words; *w != NULL; w++: + for i = 0; i < num_nodes; i++: + if strcmp(node_names[i], *w) == 0: + graph.connect(start_node, i) + break + + free(words) + + fclose(f) + free(node_names) + + i = 0 + hint = 0 + + while True: + nodes = graph.find_a_strong_connection(&hint) + if nodes[0] == -1: + break + + graph.merge_nodes(nodes[0], nodes[1]) +# printf("Merged %d and %d, now there are %d nodes\n", nodes[0], nodes[1], graph.num_nodes) + + #graph.visualize_with_graphviz() + + assert graph.num_nodes == 2 + printf("%d\n", graph.weights[0] * graph.weights[1]) # Output: 54 + graph.free() + return 0 diff --git a/examples/aoc2023/day25/sampleinput.txt b/examples/aoc2023/day25/sampleinput.txt new file mode 100644 index 00000000..bbfda0b9 --- /dev/null +++ b/examples/aoc2023/day25/sampleinput.txt @@ -0,0 +1,13 @@ +jqt: rhn xhk nvd +rsh: frs pzl lsr +xhk: hfx +cmg: qnr nvd lhk bvb +rhn: xhk bvb hfx +bvb: xhk hfx +pzl: lsr hfx nvd +qnr: nvd +ntq: jqt hfx bvb xhk +nvd: lhk +lsr: lhk +rzs: qnr cmg lsr rsh +frs: qnr lhk lsr diff --git a/runtests.sh b/runtests.sh index 1c8adc9d..996e3964 100755 --- a/runtests.sh +++ b/runtests.sh @@ -224,7 +224,7 @@ function run_test() counter=0 skipped=0 -for joufile in examples/*.jou examples/aoc2023/day*/*.jou tests/*/*.jou; do +for joufile in examples/*.jou examples/aoc2023/day*/part*.jou tests/*/*.jou; do if ! [[ $joufile == *"$file_filter"* ]]; then # Skip silently, without showing that this is skipped. # This produces less noisy output when you select only a few tests.