Solution to 13-2

This commit is contained in:
Jack Jackson 2025-01-10 18:26:02 -08:00
parent e45052d14a
commit 956109235d
4 changed files with 1570 additions and 0 deletions

1279
inputs/13/real.txt Normal file

File diff suppressed because it is too large Load Diff

15
inputs/13/test.txt Normal file
View File

@ -0,0 +1,15 @@
Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=8400, Y=5400
Button A: X+26, Y+66
Button B: X+67, Y+21
Prize: X=12748, Y=12176
Button A: X+17, Y+86
Button B: X+84, Y+37
Prize: X=7870, Y=6450
Button A: X+69, Y+23
Button B: X+27, Y+71
Prize: X=18641, Y=10279

View File

@ -2,6 +2,8 @@ const std = @import("std");
const print = std.debug.print;
const util = @import("util.zig");
// Part Two seems just plain unfun. _Might_ come back to it, but...ugh.
pub fn main() !void {
const response = try part_one(false);
print("{}\n", .{response});

274
solutions/13.zig Normal file
View File

@ -0,0 +1,274 @@
const std = @import("std");
const print = std.debug.print;
const util = @import("util.zig");
const expect = std.testing.expect;
pub fn main() !void {
const response = try part_two(false);
print("{}\n", .{response});
}
const CaseError = error{ ArithmeticError, InsolubleError, ZeroDivisionError };
const Solution = struct { a: u32, b: u32 };
const SolutionTakeTwo = struct { a: u64, b: u64 };
const Case = struct {
ax: u32,
ay: u32,
bx: u32,
by: u32,
prizex: u32,
prizey: u32,
pub fn from_lines(a_line: []const u8, b_line: []const u8, prize_line: []const u8) anyerror!Case {
// Looks like there are no regexes in Zig :shrug:
print("DEBUG - parsing a_line: {s}\n", .{a_line});
const start_of_ax: usize = 12;
const end_of_ax: usize = std.mem.indexOf(u8, a_line, ",").?;
const start_of_ay: usize = std.mem.indexOf(u8, a_line, "Y").? + 2;
const ax = try std.fmt.parseInt(u32, a_line[start_of_ax..end_of_ax], 10);
const ay = try std.fmt.parseInt(u32, a_line[start_of_ay..a_line.len], 10);
const start_of_bx: usize = 12;
const end_of_bx: usize = std.mem.indexOf(u8, b_line, ",").?;
const start_of_by: usize = std.mem.indexOf(u8, b_line, "Y").? + 2;
const bx = try std.fmt.parseInt(u32, b_line[start_of_bx..end_of_bx], 10);
const by = try std.fmt.parseInt(u32, b_line[start_of_by..b_line.len], 10);
const start_of_prizex: usize = 9;
const end_of_prizex: usize = std.mem.indexOf(u8, prize_line, ",").?;
const start_of_prizey: usize = std.mem.indexOf(u8, prize_line, "Y").? + 2;
const prizex = try std.fmt.parseInt(u32, prize_line[start_of_prizex..end_of_prizex], 10);
const prizey = try std.fmt.parseInt(u32, prize_line[start_of_prizey..prize_line.len], 10);
return Case{ .ax = ax, .ay = ay, .bx = bx, .by = by, .prizex = prizex, .prizey = prizey };
}
pub fn solve(self: *const Case) CaseError!Solution {
// Logic:
// * Increment b (since b is cheaper than a) until x and y values are >= targets
// * If x and y values are equal to the targets, _and_ b <= 100, then we're done - return
// * Repeatedly:
// * Increment a, then decrement b until _at least one_ x or y value is <= targets
// * If both x and y are equal to targets, and b <= 100, then return
// * Else:
// * Increment b until both x and y are >= targets
// * If a is 100, the Case is insoluble. Else, loop (so a will get incremented as the loop starts again)
var a: u32 = 0;
var b: u32 = 0;
b = @max(self.prizex / self.bx, self.prizey / self.by);
// There's probably a way to do this in a single pass, but :shrug:
if (self.bx * b < self.prizex or self.by * b < self.prizey) {
b += 1;
}
// This _shouldn't_ be necessary, but I don't trust myself with Zig arithmetic yet!
if (self.bx * b < self.prizex or self.by * b < self.prizey) {
print("Arithmetic error while operating on {}\n", .{self});
return CaseError.ArithmeticError;
}
if (self.bx * b == self.prizex and self.by * b == self.prizey and b <= 100) {
return .{ .a = 0, .b = b };
}
while (true) {
// Could _probably do this as part of the `while` loop definition, but it makes more sense to my brain
// as the first statement like this.
a += 1;
while (self.ax * a + self.bx * b > self.prizex or self.ay * a + self.by * b > self.prizey) : (b -= 1) {
if (b == 0) {
return CaseError.InsolubleError;
}
}
if (self.ax * a + self.bx * b == self.prizex and self.ay * a + self.by * b == self.prizey and b <= 100) {
return .{ .a = a, .b = b };
} else {
while (self.ax * a + self.bx * b < self.prizex and self.ay * a + self.by * b < self.prizey) : (b += 1) {}
if (a == 100) {
return CaseError.InsolubleError;
} // else - continue loop, increment a, keep trying
}
}
}
};
const CaseTakeTwo = struct {
ax: u64,
ay: u64,
bx: u64,
by: u64,
prizex: u64,
prizey: u64,
pub fn from_lines(a_line: []const u8, b_line: []const u8, prize_line: []const u8) anyerror!CaseTakeTwo {
// Looks like there are no regexes in Zig :shrug:
print("DEBUG - parsing a_line: {s}\n", .{a_line});
const start_of_ax: usize = 12;
const end_of_ax: usize = std.mem.indexOf(u8, a_line, ",").?;
const start_of_ay: usize = std.mem.indexOf(u8, a_line, "Y").? + 2;
const ax = try std.fmt.parseInt(u64, a_line[start_of_ax..end_of_ax], 10);
const ay = try std.fmt.parseInt(u64, a_line[start_of_ay..a_line.len], 10);
const start_of_bx: usize = 12;
const end_of_bx: usize = std.mem.indexOf(u8, b_line, ",").?;
const start_of_by: usize = std.mem.indexOf(u8, b_line, "Y").? + 2;
const bx = try std.fmt.parseInt(u64, b_line[start_of_bx..end_of_bx], 10);
const by = try std.fmt.parseInt(u64, b_line[start_of_by..b_line.len], 10);
const start_of_prizex: usize = 9;
const end_of_prizex: usize = std.mem.indexOf(u8, prize_line, ",").?;
const start_of_prizey: usize = std.mem.indexOf(u8, prize_line, "Y").? + 2;
const prizex = try std.fmt.parseInt(u64, prize_line[start_of_prizex..end_of_prizex], 10) + 10000000000000;
const prizey = try std.fmt.parseInt(u64, prize_line[start_of_prizey..prize_line.len], 10) + 10000000000000;
return CaseTakeTwo{ .ax = ax, .ay = ay, .bx = bx, .by = by, .prizex = prizex, .prizey = prizey };
}
pub fn solve(self: *const CaseTakeTwo) CaseError!SolutionTakeTwo {
// The iterative approach doesn't work here :P take the exact numerical approach.
// A little algebraic rearrangement gives us:
//
// ```
// b ( x_1 y_2 - x_2 y_1) = x_1 t_y - y_1 t_x
// ```
//
// So, if the LHS factor does not divide the RHS factor, we know the case is insoluble;
// and if they _do_ divide, this gives us an equation for b.
// (Question - there could be cases where there are multiple solutions, what would that look like? Cross that
// bridge when we come to it.)
// Because heaven forbid we have a filthy <gasp> *NEGATIVE NUMBER* in our data.
// Won't somebody please think of the children!
if ((self.ax * self.by) >= (self.bx * self.ay)) {
const lhs = (self.ax * self.by) - (self.bx * self.ay);
if (lhs == 0) {
print("SOMETHING WEIRD HAPPENED - zero-division in case {}\n", .{self});
return CaseError.ZeroDivisionError;
}
if (self.ax * self.prizey <= self.ay * self.prizex) {
// i.e. lhs and rhs would have different signs
return CaseError.InsolubleError;
}
const rhs = (self.ax * self.prizey - self.ay * self.prizex);
if (rhs % lhs != 0) {
return CaseError.InsolubleError;
}
// We know that lhs divides rhs exactly, so...
const b = @divExact(rhs, lhs);
print("DEBUG - b is {}\n", .{b});
const a_calc_numerator = self.prizex - (b * self.bx);
const a_calc_denominator = self.ax;
if (a_calc_numerator % a_calc_denominator != 0) {
return CaseError.InsolubleError;
}
const a = @divExact(a_calc_numerator, a_calc_denominator);
return SolutionTakeTwo{ .a = a, .b = b };
} else {
const lhs = (self.bx * self.ay) - (self.ax * self.by);
if (lhs == 0) {
print("SOMETHING WEIRD HAPPENED - zero-division in case {}\n", .{self});
return CaseError.ZeroDivisionError;
}
if (self.ay * self.prizex <= self.ax * self.prizey) {
// i.e. lhs and rhs would have different signs
return CaseError.InsolubleError;
}
const rhs = (self.ay * self.prizex - self.ax * self.prizey);
if (rhs % lhs != 0) {
return CaseError.InsolubleError;
}
// We know that lhs divides rhs exactly, so...
const b = @divExact(rhs, lhs);
print("DEBUG - b is {}\n", .{b});
const a_calc_numerator = self.prizex - (b * self.bx);
const a_calc_denominator = self.ax;
if (a_calc_numerator % a_calc_denominator != 0) {
return CaseError.InsolubleError;
}
const a = @divExact(a_calc_numerator, a_calc_denominator);
return SolutionTakeTwo{ .a = a, .b = b };
}
}
};
fn part_one(is_test_case: bool) anyerror!u128 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const input_file = try util.getInputFile("13", is_test_case);
const data = try util.readAllInputWithAllocator(input_file, allocator);
defer allocator.free(data);
var it = std.mem.splitScalar(u8, data, '\n');
var cases = std.ArrayList(Case).init(allocator);
defer cases.deinit();
while (true) {
const a_line = it.next().?;
const b_line = it.next().?;
const prize_line = it.next().?;
_ = it.next();
try cases.append(try Case.from_lines(a_line, b_line, prize_line));
if (it.peek() == null) {
break;
}
}
var total: u32 = 0;
for (cases.items) |case| {
print("Solution to case {}", .{case});
if (case.solve()) |solution| {
print(" is {}\n", .{solution});
total += solution.a * 3 + solution.b;
} else |_| {
print(" is impossible\n", .{});
}
}
return total;
}
fn part_two(is_test_case: bool) anyerror!u128 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const input_file = try util.getInputFile("13", is_test_case);
const data = try util.readAllInputWithAllocator(input_file, allocator);
defer allocator.free(data);
var it = std.mem.splitScalar(u8, data, '\n');
var cases = std.ArrayList(CaseTakeTwo).init(allocator);
defer cases.deinit();
while (true) {
const a_line = it.next().?;
const b_line = it.next().?;
const prize_line = it.next().?;
_ = it.next();
try cases.append(try CaseTakeTwo.from_lines(a_line, b_line, prize_line));
if (it.peek() == null) {
break;
}
}
var total: u64 = 0;
for (cases.items) |case| {
print("Solution to case {}", .{case});
if (case.solve()) |solution| {
print(" is {}\n", .{solution});
total += solution.a * 3 + solution.b;
} else |_| {
print(" is impossible\n", .{});
}
}
return total;
}
test "part_one" {
const part_one_response = try part_one(true);
print("DEBUG - part_one_response is {}\n", .{part_one_response});
try expect(part_one_response == 480);
}