diff --git a/inputs/11/real.txt b/inputs/11/real.txt new file mode 100644 index 0000000..3e15594 --- /dev/null +++ b/inputs/11/real.txt @@ -0,0 +1 @@ +510613 358 84 40702 4373582 2 0 1584 \ No newline at end of file diff --git a/inputs/11/test.txt b/inputs/11/test.txt new file mode 100644 index 0000000..528f9d5 --- /dev/null +++ b/inputs/11/test.txt @@ -0,0 +1 @@ +125 17 \ No newline at end of file diff --git a/solutions/11.zig b/solutions/11.zig new file mode 100644 index 0000000..2e85edb --- /dev/null +++ b/solutions/11.zig @@ -0,0 +1,161 @@ +const std = @import("std"); +const print = std.debug.print; +const util = @import("util.zig"); + +pub fn main() !void { + const response = try part_two(false, 75); + print("Response from running with 75 blinks is {}\n", .{response}); +} + +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("11", is_test_case); + const data = try util.readAllInputWithAllocator(input_file, allocator); + defer allocator.free(data); + + var start_stones = try parseData(data, allocator); + defer allocator.free(start_stones); + + for (0..25) |_| { + start_stones = try blinkStones(start_stones, allocator); + // print("DEBUG - after iteration {}, stones are {any}\n", .{ i, start_stones }); + } + return start_stones.len; +} + +fn parseData(data: []const u8, alloc: std.mem.Allocator) ![]u128 { + var accum = std.ArrayList(u128).init(alloc); + var it = std.mem.splitScalar(u8, data, ' '); + while (it.next()) |chunk| { + print("DEBUG - parsing {any} as int\n", .{chunk}); + try accum.append(try std.fmt.parseInt(u128, chunk, 10)); + } + print("DEBUG - initial values are: ", .{}); + for (accum.items) |stone_value| { + print("{} ", .{stone_value}); + } + print("\n", .{}); + return accum.toOwnedSlice(); +} + +fn blinkStones(stones: []u128, alloc: std.mem.Allocator) ![]u128 { + var accum = std.ArrayList(u128).init(alloc); + for (stones) |stone| { + const blinkedStones = try blinkStone(stone, alloc); + for (blinkedStones) |blinked_stone| { + try accum.append(blinked_stone); + } + alloc.free(blinkedStones); + } + alloc.free(stones); + return accum.toOwnedSlice(); +} + +fn blinkStone(stone: u128, alloc: std.mem.Allocator) ![]u128 { + // I don't like having to create an ArrayList here, but I don't know how to get around it: + // `return []u128{1}` gives `array literal requires address-of operator (&) to coerce to slice type '[]u128'` + // `return [_]u128{1}` gives the same error + // `return [1]u128{1}` gives `expected type '[]u128', found '*const [1]u128' + // And I can't declare the return type to be `[1]u128` because sometimes it's _not_ a single value + var al = std.ArrayList(u128).init(alloc); + if (stone == 0) { + try al.append(1); + return al.toOwnedSlice(); + } + const number_of_digits_in_stone = std.math.log10_int(stone) + 1; + // print("number of digits in {} is {}, ", .{ stone, number_of_digits_in_stone }); + if (number_of_digits_in_stone % 2 == 0) { + // print("which is even, so splitting it\n", .{}); + const half_number_of_digits = @divExact(number_of_digits_in_stone, 2); + const factor = std.math.pow(u128, 10, half_number_of_digits); + try al.append(stone / factor); + try al.append(stone % factor); + return al.toOwnedSlice(); + } else { + // print("which is odd, so multiplying it by 2024\n", .{}); + try al.append(stone * 2024); + return al.toOwnedSlice(); + } +} + +fn part_two(is_test_case: bool, blinks: u8) anyerror!u128 { + // Heh, I suspected that this would just be an optimization question :P + // Memoization ahoy! + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const input_file = try util.getInputFile("11", is_test_case); + const data = try util.readAllInputWithAllocator(input_file, allocator); + defer allocator.free(data); + + const start_stones = try parseData(data, allocator); + defer allocator.free(start_stones); + + var cache_data = std.AutoHashMap(Query, u128).init(allocator); + defer cache_data.deinit(); + var cache = Cache{ .data = cache_data }; + + var total: u128 = 0; + for (start_stones) |stone| { + total += try cache.get(Query{ .value = stone, .blinks = blinks }, allocator); + } + return total; +} + +const Query = struct { value: u128, blinks: u8 }; + +const Cache = struct { + data: std.AutoHashMap(Query, u128), + fn get(self: *Cache, query: Query, alloc: std.mem.Allocator) !u128 { + // "If you have a single stone with value `value`, how many stones will you have after `blinks` number of blinks?" + if (query.blinks == 0) { + return 1; + } + // A `getOrPut`-based solution - as outlined below - doesn't work. I'm _guessing_ this is because the + // pointer is invalid after recursively calling `try self.get(Query{...})`, because _they_ will mutate the + // HashMap, and so it's been "extended" such that the pointer doesn't point to the right place anymore. + // ...this memory volatility is getting really annoying! + // const response = try self.data.getOrPut(query); + // if (response.found_existing) { + // return response.value_ptr.*; + // } else { + // const blinked_stones = try blinkStone(query.value, alloc); + // var total: u32 = 0; + // for (blinked_stones) |blinked_stone| { + // total += try self.get(Query{ .value = blinked_stone, .blinks = query.blinks - 1 }, alloc); + // } + // response.value_ptr.* = total; + // alloc.free(blinked_stones); + // return total; + // } + // + if (self.data.contains(query)) { + return self.data.get(query).?; + } else { + const blinked_stones = try blinkStone(query.value, alloc); + var total: u128 = 0; + for (blinked_stones) |blinked_stone| { + total += try self.get(Query{ .value = blinked_stone, .blinks = query.blinks - 1 }, alloc); + } + try self.data.put(query, total); + alloc.free(blinked_stones); + return total; + } + } +}; + +test "part_one" { + const part_one_response = try part_one(true); + print("DEBUG - part_one_response is {}\n", .{part_one_response}); + try std.testing.expect(part_one_response == 55312); +} + +test "original_question_with_cache" { + const with_cache_response = try part_two(true, 25); + print("DEBUG - with_cache response is {}\n", .{with_cache_response}); + try std.testing.expect(with_cache_response == 55312); +}