Solution for 08-02
This commit is contained in:
parent
43982a67a1
commit
c2955cd03e
2
NOTES.md
2
NOTES.md
@ -204,4 +204,4 @@ fn accumulate() ![]u32 {
|
||||
|
||||
## What's the point in `HashMap.getOrPut`?
|
||||
|
||||
`getOrPut` _doesn't_ actually `put` anything, it _only_ `get`s. See https://ziggit.dev/t/whats-the-point-in-hashmap-getorput/7547.
|
||||
`getOrPut` _doesn't_ actually `put` anything, it _only_ `get`s. See https://ziggit.dev/t/whats-the-point-in-hashmap-getorput/7547.
|
||||
|
@ -10,4 +10,8 @@ I've tried (in `main.zig`) to make a general-purpose executable that can be pass
|
||||
|
||||
So for now, run directly with (e.g.) `zig run solutions/01.zig`, and do the following manual changes:
|
||||
* Change `pub fn main() void {...}` in each solution-file to invoke the function you want run.
|
||||
* Change `isTestCase` from `true` to `false` when ready to get the real solution.
|
||||
* Change `isTestCase` from `true` to `false` when ready to get the real solution.
|
||||
|
||||
# Code Quality
|
||||
|
||||
AoC challenges almost always have a "twist" partway through, meaning that you can solve the second part by injecting one subtly-different piece of logic into the solution to the first part - a different way of calculating a value or identifying candidates. If I were trying to show off for an interview (and were more comfortable with the language!), I would do the refactoring "right" by factoring out the common setup and execution logic to sub-functions, so that `part_one` and `part_two` are each single-line invocations of a common `execute` function with differing functions passed as parameter. But this is just an exercise for myself to learn the language - I'd rather get to grips with challenging problems to learn techniques, than to learn the (language-agnostic) skills of refactoring that I am already _reasonably_ proficient with.
|
||||
|
173
solutions/08.zig
173
solutions/08.zig
@ -3,13 +3,17 @@ const print = std.debug.print;
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
const response = try part_one(false);
|
||||
const response = try part_two(false);
|
||||
print("{}\n", .{response});
|
||||
}
|
||||
|
||||
const Point = struct { x: usize, y: usize };
|
||||
const Point = struct { x: u16, y: u16 };
|
||||
|
||||
fn part_one(is_test_case: bool) !usize {
|
||||
return execute(is_test_case, findAntinodes);
|
||||
}
|
||||
|
||||
fn execute(is_test_case: bool, antinode_determination_function: fn (nodes: [2]Point, width: usize, height: usize, allocator: std.mem.Allocator) anyerror![]Point) !usize {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
@ -17,19 +21,19 @@ fn part_one(is_test_case: bool) !usize {
|
||||
const input_file = try util.getInputFile("08", is_test_case);
|
||||
const data = try util.readAllInputWithAllocator(input_file, allocator);
|
||||
defer allocator.free(data);
|
||||
print("DEBUG - created data", .{});
|
||||
print("DEBUG - created data\n", .{});
|
||||
|
||||
// In this problem I'm experimenting with not even parsing the input into lines, but just keeping a "line counter"
|
||||
// that is incremented whenever we hit a `\n` character
|
||||
var width: ?usize = null;
|
||||
var height: ?usize = null;
|
||||
var x: usize = 0;
|
||||
var y: usize = 0;
|
||||
var x: u16 = 0;
|
||||
var y: u16 = 0;
|
||||
var antennae = std.AutoHashMap(u8, std.ArrayList(Point)).init(allocator);
|
||||
defer antennae.deinit();
|
||||
|
||||
for (data) |c| {
|
||||
print("DEBUG - checking {c} at {}, {}\n", .{ c, x, y });
|
||||
// print("DEBUG - checking {c} at {}, {}\n", .{ c, x, y });
|
||||
switch (c) {
|
||||
'\n' => {
|
||||
if (width == null) {
|
||||
@ -47,6 +51,7 @@ fn part_one(is_test_case: bool) !usize {
|
||||
var result = try antennae.getOrPut(c);
|
||||
if (!result.found_existing) {
|
||||
result.value_ptr.* = std.ArrayList(Point).init(allocator);
|
||||
defer result.value_ptr.deinit();
|
||||
}
|
||||
try result.value_ptr.append(point);
|
||||
x += 1;
|
||||
@ -65,7 +70,7 @@ fn part_one(is_test_case: bool) !usize {
|
||||
const node_pairs = try pairs(v.items, allocator);
|
||||
defer allocator.free(node_pairs);
|
||||
for (node_pairs) |node_pair| {
|
||||
const antinodes = try findAntinodes(node_pair, width.?, height.?, allocator);
|
||||
const antinodes = try antinode_determination_function(node_pair, width.?, height.?, allocator);
|
||||
defer allocator.free(antinodes);
|
||||
for (antinodes) |antinode| {
|
||||
try all_antinodes.put(antinode, 1); // We don't actually need to put any value - just populating the key
|
||||
@ -81,6 +86,31 @@ fn part_one(is_test_case: bool) !usize {
|
||||
print("{}\n", .{antinode});
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// SERIOUS debugging here - visualizing the output!
|
||||
// var lines = std.ArrayList([]u8).init(allocator);
|
||||
// defer lines.deinit();
|
||||
// for (0..height.?) |_| {
|
||||
// var line = std.ArrayList(u8).init(allocator);
|
||||
// defer line.deinit();
|
||||
// for (0..width.?) |_| {
|
||||
// try line.append('.');
|
||||
// }
|
||||
// try lines.append(line.items);
|
||||
// }
|
||||
// var lines_items = lines.items;
|
||||
// var antinode_iterator_for_visualization = all_antinodes.keyIterator();
|
||||
// while (antinode_iterator_for_visualization.next()) |antinode| {
|
||||
// lines_items[antinode.y][antinode.x] = '#';
|
||||
// }
|
||||
// print("DEBUG - grid is\n", .{});
|
||||
// for (lines.items) |line| {
|
||||
// for (line) |c| {
|
||||
// print("{c}", .{c});
|
||||
// }
|
||||
// print("\n", .{});
|
||||
// }
|
||||
// End of debugging visualization
|
||||
return count;
|
||||
}
|
||||
|
||||
@ -97,7 +127,7 @@ fn pairs(nodes: []Point, allocator: std.mem.Allocator) ![][2]Point {
|
||||
return output.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn findAntinodes(nodes: [2]Point, width: usize, height: usize, allocator: std.mem.Allocator) ![]Point {
|
||||
fn findAntinodes(nodes: [2]Point, width: u16, height: u16, allocator: std.mem.Allocator) ![]Point {
|
||||
var response = std.ArrayList(Point).init(allocator);
|
||||
defer response.deinit();
|
||||
|
||||
@ -118,17 +148,134 @@ fn findAntinodes(nodes: [2]Point, width: usize, height: usize, allocator: std.me
|
||||
return response.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn antiNodeIsValid(antiNode: Point, width: usize, height: usize) bool {
|
||||
fn antiNodeIsValid(antiNode: Point, width: u16, height: u16) bool {
|
||||
// Don't technically need to check for >= 0 because that's already checked in `findAntinodes` (because otherwise
|
||||
// there would be integer overflow by daring to use a negative number :P ), but doesn't hurt to replicate it here -
|
||||
// otherwise a future reader might think we've forgotten it.
|
||||
return antiNode.x >= 0 and antiNode.y >= 0 and antiNode.x < width and antiNode.y < height;
|
||||
}
|
||||
|
||||
fn part_two(is_test_case: bool) !usize {
|
||||
return execute(is_test_case, findAntiNodesHarmonic);
|
||||
}
|
||||
|
||||
fn findAntiNodesHarmonic(nodes: [2]Point, width: usize, height: usize, allocator: std.mem.Allocator) ![]Point {
|
||||
print("DEBUG - checking antiNodesHarmonic for {} and {}\n", .{ nodes[0], nodes[1] });
|
||||
|
||||
// Approach:
|
||||
// * Find the vector V from nodes[0] to nodes[1]
|
||||
// * Find greatest-common-factor of V.x and V.y
|
||||
// * Use that to find the smallest integer-step <V'.x, V'.y>
|
||||
// * Iteratively (starting from n=0), check nodes[0] + n*V' for legality - then same for subtraction
|
||||
//
|
||||
// Not making a type for `Vector` because idk how to have a signed size, but it'd be a reasonable approach!
|
||||
const vector_x: i32 = @as(i32, nodes[1].x) - @as(i32, nodes[0].x);
|
||||
const vector_y: i32 = @as(i32, nodes[1].y) - @as(i32, nodes[0].y);
|
||||
const greatest_common_factor = gcf(magnitude(vector_x), magnitude(vector_y));
|
||||
const mini_vector_x = divide(vector_x, greatest_common_factor);
|
||||
const mini_vector_y = divide(vector_y, greatest_common_factor);
|
||||
print("DEBUG - vector_x is {}, mini_vector_x is {}, vector_y is {}, mini_vector_y is {}, gcd is {}\n", .{ vector_x, mini_vector_x, vector_y, mini_vector_y, greatest_common_factor });
|
||||
|
||||
var candidates = std.ArrayList(Point).init(allocator);
|
||||
defer candidates.deinit();
|
||||
var step: i32 = 0;
|
||||
while (true) : (step += 1) {
|
||||
var skips: usize = 0;
|
||||
const x_step: i32 = step * mini_vector_x;
|
||||
const y_step: i32 = step * mini_vector_y;
|
||||
print("DEBUG - step is {}, x_step is {}, y_step is {}\n", .{ step, x_step, y_step });
|
||||
|
||||
if (nodes[0].x < x_step or nodes[0].y < y_step or (x_step < 0 and (magnitude(x_step) + nodes[0].x >= width)) or (y_step < 0 and (magnitude(y_step) + nodes[0].y >= height))) {
|
||||
skips += 1;
|
||||
} else {
|
||||
const candidate_x: u16 = @intCast(nodes[0].x - x_step);
|
||||
const candidate_y: u16 = @intCast(nodes[0].y - y_step);
|
||||
try candidates.append(Point{ .x = candidate_x, .y = candidate_y });
|
||||
}
|
||||
|
||||
if (nodes[0].x + x_step >= width or nodes[0].y + y_step >= height or (x_step < 0 and magnitude(x_step) > nodes[0].x) or (y_step < 0 and magnitude(y_step) > nodes[0].y)) {
|
||||
skips += 1;
|
||||
} else {
|
||||
const candidate_x: u16 = @intCast(nodes[0].x + x_step);
|
||||
const candidate_y: u16 = @intCast(nodes[0].y + y_step);
|
||||
try candidates.append(Point{ .x = candidate_x, .y = candidate_y });
|
||||
}
|
||||
|
||||
if (skips == 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const response = candidates.toOwnedSlice();
|
||||
print("Response is {any}\n", .{response});
|
||||
return response;
|
||||
}
|
||||
|
||||
// There _must_ be something like this in the standard library, but I couldn't find it at a glance.
|
||||
fn magnitude(num: i32) u32 {
|
||||
if (num < 0) {
|
||||
return @intCast(-num);
|
||||
} else {
|
||||
return @intCast(num);
|
||||
}
|
||||
}
|
||||
|
||||
fn gcf(larger: u32, smaller: u32) u32 {
|
||||
print("DEBUG - calculating gcf for {} and {}\n", .{ larger, smaller });
|
||||
var a = larger;
|
||||
const b = smaller;
|
||||
if (b > a) {
|
||||
print("DEBUG - reversing them\n", .{});
|
||||
return gcf(b, a);
|
||||
}
|
||||
while (a > b) {
|
||||
print("DEBUG - subtracting {} from {} ", .{ b, a }); // Note no line-break!
|
||||
a -= b;
|
||||
print("to get {}\n", .{a});
|
||||
}
|
||||
if (a == b) {
|
||||
print("DEBUG - a == b, returning the value ({})\n", .{a});
|
||||
return a;
|
||||
} else {
|
||||
// a!>b and a!=b => a < b
|
||||
print("DEBUG - b is now larger than a ({}, {}), so starting again\n", .{ b, a });
|
||||
return gcf(b, a);
|
||||
}
|
||||
}
|
||||
|
||||
// Again - this _must_ exist somewhere in the standard library, can't believe I'm missing it
|
||||
fn divide(num: i32, denom: u32) i32 {
|
||||
const denom_as_i32: i32 = @intCast(denom);
|
||||
const divided_value: i32 = @divExact(num, denom_as_i32);
|
||||
return divided_value;
|
||||
}
|
||||
|
||||
const expect = std.testing.expect;
|
||||
|
||||
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 == 14);
|
||||
// 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 == 14);
|
||||
// }
|
||||
|
||||
test "greatest_common_factor" {
|
||||
try expect(gcf(18, 27) == 9);
|
||||
try expect(gcf(182664, 154875) == 177);
|
||||
}
|
||||
|
||||
test "magnitude" { // Pop pop!
|
||||
try expect(magnitude(5) == 5);
|
||||
try expect(magnitude(-32) == 32);
|
||||
}
|
||||
|
||||
test "divide" {
|
||||
try expect(divide(4, 2) == 2);
|
||||
try expect(divide(-4, 2) == -2);
|
||||
try expect(divide(25, 5) == 5);
|
||||
try expect(divide(-50, 2) == -25);
|
||||
}
|
||||
|
||||
test "part_two" {
|
||||
const part_two_response = try part_two(true);
|
||||
print("DEBUG - part_two_response is {}\n", .{part_two_response});
|
||||
try expect(part_two_response == 34);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user