diff --git a/src/aoc.zig b/src/aoc.zig index 7cb5388..9109ab3 100644 --- a/src/aoc.zig +++ b/src/aoc.zig @@ -6,7 +6,6 @@ pub const Input = struct { lines: []const []const u8 }; -// For now only .uint is needed pub const Result = union(enum) { uint: u64, int: i64, diff --git a/src/day11.zig b/src/day11.zig index 572e35d..aeaf1aa 100644 --- a/src/day11.zig +++ b/src/day11.zig @@ -195,7 +195,7 @@ pub fn part2(input: *aoc.Input) !aoc.Result { return .{ .uint = sum_min_distances(galaxies.items) }; } -const example_input = [_][]const u8{ +const example_input = [_][]u8{ "...#......", ".......#..", "#.........", diff --git a/src/day12.zig b/src/day12.zig new file mode 100644 index 0000000..9b5a586 --- /dev/null +++ b/src/day12.zig @@ -0,0 +1,362 @@ +const std = @import("std"); +const aoc = @import("./aoc.zig"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; + +const SpringState = enum { + Operational, Damaged, Unknown, + + fn from_char(char: u8) ?SpringState { + return switch (char) { + '.' => .Operational, + '#' => .Damaged, + '?' => .Unknown, + else => null + }; + } + + fn to_char(self: SpringState) u8 { + return switch (self) { + .Operational => '.', + .Damaged => '#', + .Unknown => '?', + }; + } +}; + +fn print_states(states: []const SpringState) void { + for (states) |state| { + std.debug.print("{c}", .{state.to_char()}); + } + std.debug.print("\n", .{}); +} + +const InputRow = struct { + states: []SpringState, + damaged_groups: []u32, + + fn print(self: InputRow) void { + for (self.states) |state| { + std.debug.print("{c}", .{state.to_char()}); + } + std.debug.print(" ", .{}); + for (0.., self.damaged_groups) |i, group_size| { + if (i != 0) { + std.debug.print(",", .{}); + } + std.debug.print("{}", .{group_size}); + } + std.debug.print("\n", .{}); + } +}; + +const Input = struct { + rows: ArrayList(InputRow), + + fn init(allocator: Allocator) Input { + return Input{ + .rows = ArrayList(InputRow).init(allocator) + }; + } + + fn deinit(self: Input) void { + const allocator = self.rows.allocator; + for (self.rows.items) |row| { + allocator.free(row.states); + allocator.free(row.damaged_groups); + } + self.rows.deinit(); + } + + fn print(self: Input) void { + for (self.rows.items) |row| { + row.print(); + } + } +}; + +fn parse_input_row(allocator: Allocator, line: []const u8) !InputRow { + const space_idx = std.mem.indexOfScalar(u8, line, ' ') orelse return error.InvalidFormat; + var states = try allocator.alloc(SpringState, space_idx); + errdefer allocator.free(states); + + const commas = std.mem.count(u8, line, ","); + var damaged_groups = try allocator.alloc(u32, commas + 1); + errdefer allocator.free(damaged_groups); + + for (0.., line[0..space_idx]) |i, char| { + states[i] = SpringState.from_char(char) orelse return error.InvalidFormat; + } + + var it = std.mem.splitScalar(u8, line[(space_idx+1)..], ','); + var idx: usize = 0; + while (it.next()) |number| { + damaged_groups[idx] = try std.fmt.parseInt(u32, number, 10); + idx += 1; + } + + return InputRow{ + .states = states, + .damaged_groups = damaged_groups, + }; +} + +fn parse_input(allocator: Allocator, lines: []const []const u8) !Input { + var parsed = Input.init(allocator); + errdefer parsed.deinit(); + + for (lines) |line| { + try parsed.rows.append(try parse_input_row(allocator, line)); + } + + return parsed; +} + +fn is_arrangement_valid(states: []const SpringState, groups: []const u32) bool { + var it = std.mem.tokenizeScalar(SpringState, states, .Operational); + var i: usize = 0; + + while (it.next()) |group| { + if (groups.len == i) return false; + if (group.len != groups[i]) return false; + i += 1; + } + + if (i != groups.len) { + return false; + } + + return true; +} + +fn count_valid_arrangements(allocator: Allocator, states: []const SpringState, groups: []const u32) !u32 { + var unknown_count: u32 = 0; + for (states) |state| { + if (state == .Unknown) { + unknown_count += 1; + } + } + + if (unknown_count == 0) { + if (is_arrangement_valid(states, groups)) { + return 1; + } else { + return 0; + } + } + + var unknown_indexes = try allocator.alloc(usize, unknown_count); + defer allocator.free(unknown_indexes); + var is_unknown_damaged = try allocator.alloc(bool, unknown_count); + defer allocator.free(is_unknown_damaged); + + @memset(is_unknown_damaged, false); + + var i: usize = 0; + for (0.., states) |index, state| { + if (state == .Unknown) { + unknown_indexes[i] = index; + i += 1; + } + } + + const arrangement = try allocator.dupe(SpringState, states); + defer allocator.free(arrangement); + @memcpy(arrangement, states); + + var count: u32 = 0; + for (0..std.math.pow(u32, 2, unknown_count)) |_| { + for (unknown_indexes, is_unknown_damaged) |index, is_damaged| { + arrangement[index] = if (is_damaged) .Damaged else .Operational; + } + + if (is_arrangement_valid(arrangement, groups)) { + count += 1; + } + + const carry = is_unknown_damaged[0]; + is_unknown_damaged[0] = !is_unknown_damaged[0]; + + if (carry) { + for (is_unknown_damaged[1..]) |*is_damaged| { + const was_set = is_damaged.*; + is_damaged.* = !is_damaged.*; + if (was_set) break; + } + } + } + + return count; +} + +pub fn part1(input: *aoc.Input) !aoc.Result { + const allocator = input.allocator; + const parsed = try parse_input(allocator, input.lines); + defer parsed.deinit(); + + var result: u32 = 0; + for (parsed.rows.items) |row| { + result += try count_valid_arrangements(allocator, row.states, row.damaged_groups); + } + + return .{ .uint = result }; +} + +fn unfold_input_row(allocator: Allocator, input_row: InputRow) !InputRow { + const multiplier = 5; + + const state_count = input_row.states.len; + var unfolded_states = try allocator.alloc(SpringState, (state_count + 1) * multiplier - 1); + errdefer allocator.free(unfolded_states); + + const group_count = input_row.damaged_groups.len; + var unfolded_groups = try allocator.alloc(u32, group_count * multiplier); + errdefer allocator.free(unfolded_groups); + + for (0..multiplier) |i| { + if (i != 0) { + unfolded_states[i*(state_count+1)-1] = .Unknown; + } + + @memcpy(unfolded_states[(i*(state_count+1))..(i*(state_count+1) + state_count)], input_row.states); + @memcpy(unfolded_groups[(i*group_count)..((i+1)*group_count)], input_row.damaged_groups); + } + + return InputRow{ + .states = unfolded_states, + .damaged_groups = unfolded_groups + }; +} + +fn unfold_input(allocator: Allocator, input: Input) !Input { + var unfolded = Input.init(allocator); + errdefer unfolded.deinit(); + + for (input.rows.items) |row| { + try unfolded.rows.append(try unfold_input_row(allocator, row)); + } + + return unfolded; +} + +const CacheKey = struct { + states: []const SpringState, + groups: []const u32, +}; +const CacheKeyContext = struct { + fn splitU32(value: u64) [8]u8 { + return .{ + @truncate(value >> 8*0), + @truncate(value >> 8*1), + @truncate(value >> 8*2), + @truncate(value >> 8*3), + @truncate(value >> 8*4), + @truncate(value >> 8*5), + @truncate(value >> 8*6), + @truncate(value >> 8*7), + }; + } + + pub fn hash(_: CacheKeyContext, key: CacheKey) u64 { + var h = std.hash.Fnv1a_64.init(); + h.update(&splitU32(@intFromPtr(key.states.ptr))); + h.update(&splitU32(@intCast(key.states.len))); + h.update(&splitU32(@intFromPtr(key.groups.ptr))); + h.update(&splitU32(@intCast(key.groups.len))); + return h.final(); + } + + pub fn eql(_: CacheKeyContext, a: CacheKey, b: CacheKey) bool { + return std.mem.eql(SpringState, a.states, b.states) and std.mem.eql(u32, a.groups, b.groups); + } +}; +const Cache = std.HashMap(CacheKey, u64, CacheKeyContext, 80); + +fn count_valid_arrangements_dp(cache: *Cache, states: []const SpringState, groups: []const u32) !u64 { + if (states.len == 0) { + if (groups.len == 0) { + // If there are no springs left AND expect no more groups, + // THEN this is a valid arrangment + return 1; + } else { + // If there are no springs left, but expected to have some groups, + // THEN this is not a valid arrangment + return 0; + } + } + + if (groups.len == 0) { + if (std.mem.indexOfScalar(SpringState, states, .Damaged) != null) { + // If we expect to see no more groups, but there are still some damanged springs left + // THEN this is not a valid arrangment + return 0; + } else { + // If we expect to see no more groups and we have no more damaged springs + // THEN this arrangment is valid + return 1; + } + } + + var cache_key = CacheKey{ .states = states, .groups = groups }; + if (cache.get(cache_key)) |cached_result| { + return cached_result; + } + + var result: u64 = 0; + if (states[0] == .Operational or states[0] == .Unknown) { + result += try count_valid_arrangements_dp(cache, states[1..], groups); + } + + const group_size = groups[0]; + if ( + (states[0] == .Damaged or states[0] == .Unknown) // If the leading spring is damanged + and (group_size <= states.len) // If the number of springs left is higher than the group size + and (std.mem.indexOfScalar(SpringState, states[0..group_size], .Operational) == null) // If contains continous group of broken springs + ) { + // If the damaged group ends with an operational spring + if (group_size == states.len) { + result += try count_valid_arrangements_dp(cache, states[group_size..], groups[1..]); + } else if (states[group_size] != .Damaged) { + result += try count_valid_arrangements_dp(cache, states[(group_size+1)..], groups[1..]); + } + } + + try cache.put(cache_key, result); + return result; +} + +pub fn part2(input: *aoc.Input) !aoc.Result { + const allocator = input.allocator; + const parsed = try parse_input(allocator, input.lines); + defer parsed.deinit(); + + const unfolded = try unfold_input(allocator, parsed); + defer unfolded.deinit(); + + var cache = Cache.init(allocator); + defer cache.deinit(); + + var result: u64 = 0; + for (unfolded.rows.items) |row| { + result += try count_valid_arrangements_dp(&cache, row.states, row.damaged_groups); + } + + return .{ .uint = result }; +} + +const example_input = [_][]const u8{ + "???.### 1,1,3", + ".??..??...?##. 1,1,3", + "?#?#?#?#?#?#?#? 1,3,1,6", + "????.#...#... 4,1,1", + "????.######..#####. 1,6,5", + "?###???????? 3,2,1", +}; + +test "part 1 example" { + try aoc.expectAnswerUInt(part1, 21, &example_input); +} + +test "part 2 example" { + try aoc.expectAnswerUInt(part2, 525152, &example_input); +} diff --git a/src/main.zig b/src/main.zig index 9884386..8884c14 100644 --- a/src/main.zig +++ b/src/main.zig @@ -32,6 +32,7 @@ const Days = [_]aoc.Day{ create_day(@import("day9.zig")), create_day(@import("day10.zig")), create_day(@import("day11.zig")), + create_day(@import("day12.zig")), }; fn kilobytes(count: u32) u32 { @@ -187,4 +188,5 @@ test { _ = @import("day9.zig"); _ = @import("day10.zig"); _ = @import("day11.zig"); + _ = @import("day12.zig"); }