const std = @import("std"); pub fn main() anyerror!void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); var gpa = &arena.allocator; var fields = try read_fields("input_fields", gpa); defer fields.deinit(); // for (fields.items) |f| { // std.log.debug("Field '{}' has {} limits", .{f.name, f.limits.items.len}); // for (f.limits.items) |i| { // std.log.debug(" {} - {}", .{i.min, i.max}); // } // } var tickets = try read_tickets("input", gpa); defer tickets.deinit(); var error_tickets = std.ArrayList(usize).init(gpa); defer error_tickets.deinit(); var error_rate : u64 = 0; for (tickets.items) |t, kt| { for (t.values) |v, kv| { if (!valid_for_any_field(v, fields.items[0..])) { std.log.debug("Field {} ({}) from ticket #{} is not valid for any field", .{kv, v, kt}); error_rate += v; try error_tickets.append(kt); break; } } } std.log.info("Error rate from nearby tickets: {}, from {} tickets", .{error_rate, tickets.items.len}); // Part 2, discard invalid tickets. while (error_tickets.popOrNull()) |v| { var t = tickets.orderedRemove(v); std.log.debug("Removed ticket #{}: '{}'\n{}, {}, {}, {}, {}, {}, {}, {}, {}, {}\n{}, {}, {}, {}, {}, {}, {}, {}, {}, {}", .{v, t, t.values[0], t.values[1], t.values[2], t.values[3], t.values[4], t.values[5], t.values[6], t.values[7], t.values[8], t.values[9], t.values[10], t.values[11], t.values[12], t.values[13], t.values[14], t.values[15], t.values[16], t.values[17], t.values[18], t.values[19]}); } std.log.debug("{} valid tickets to use for field position search", .{tickets.items.len}); try determine_field_positions(tickets.items[0..], fields.items[0..], gpa); for (fields.items) |f, idx| { std.log.info("Field '{}' has position {}", .{f.name, f.position.?}); } var my_ticket = Ticket { .values = [_]u64 { 61,151,59,101,173,71,103,167,127,157,137,73,181,97,179,149,131,139,67,53, }, }; var part2_result : u64 = 1; for (fields.items) |f, idx| { if (std.mem.startsWith(u8, f.name, "departure")) { part2_result *= my_ticket.values[f.position.?]; } } std.log.info("Product of all values for the departure codes on my ticket: {}", .{part2_result}); } pub fn determine_field_positions(tickets: []Ticket, fields: []Field, allocator: *std.mem.Allocator) !void { // While there are fields with unknown positions, loop through the fields from 0..len, // To improve performance, we could cache which fields are not valid for a given position // in the ticket values to avoid re-calculating across the list. // If the input tickets contain invalid examples, then this resolution with loop forever. // while(fields_with_unknown_positions_exist(fields)) { // for (fields) |f, f_idx| { // if (f.position != null) { // continue; // } // std.log.warn("Testing field '{}' ({}) to determine position", // .{f.name, f_idx}); // var pos : usize = 0; // while (pos < fields.len) : (pos += 1) { // if (already_known_position(fields, f_idx)) { // continue; // } // std.log.warn("Checking to see if all tickets match field {} in value index {}", // .{f.name, pos}); // if (all_tickets_fit_field_limits(tickets, &fields[f_idx], pos)) { // fields[f_idx].position = pos; // std.log.warn("Found position {} for field {}", // .{pos, f.name}); // break; // } // } // } // } // We have field.len positions to resolve. For each field, we build a list of possible // columns that could match. If after the first pass, fields with only a single possible // column are assigned that column, then those column numbers of removed from the // possibilities of the remaining fields. var completely_resolved_positions = std.ArrayList(usize).init(allocator); defer completely_resolved_positions.deinit(); var possible_positions_per_field = try allocator.alloc(std.ArrayList(usize), fields.len); defer allocator.free(possible_positions_per_field); for (fields) |f, idx| { possible_positions_per_field[idx] = std.ArrayList(usize).init(allocator); var ticket_value_index : usize = 0; while (ticket_value_index < fields.len) : (ticket_value_index += 1) { if (all_tickets_fit_field_limits(tickets, &fields[idx], ticket_value_index)) { std.log.warn("Adding {} as option for field {}", .{ticket_value_index, f.name}); try possible_positions_per_field[idx].append(ticket_value_index); } } std.log.warn("Field '{}' has possible ticket value indices:", .{f.name}); for (possible_positions_per_field[idx].items) |i| { std.log.warn(" {}", .{i}); } } var most_constrained_field_index : usize = 0; var min_possibilities : usize = std.math.maxInt(usize); // Check to make sure we can start to solve without making a decision of some sort for (fields) |f, idx| { if (possible_positions_per_field[idx].items.len < min_possibilities) { min_possibilities = possible_positions_per_field[idx].items.len; most_constrained_field_index = idx; } } std.debug.assert(min_possibilities == @as(usize, 1)); while (completely_resolved_positions.items.len < fields.len) { var ok = try process_solution_round(fields, possible_positions_per_field, &completely_resolved_positions); if (!ok) { std.log.warn("Unable to solve", .{}); break; } for (fields) |f, idx| { std.log.warn("After round, field {} has {} possibilities remaining.", .{f.name, possible_positions_per_field[idx].items.len}); for (possible_positions_per_field[idx].items) |i| { std.log.warn(" {}", .{i}); } } } // Set the position for each field for (fields) |f, idx| { std.log.warn("Field {} has {} remaining possibilities after solution rounds: {}", .{f.name, possible_positions_per_field[idx].items.len, possible_positions_per_field[idx].items[0]}); std.debug.assert(possible_positions_per_field[idx].items.len <= 1); fields[idx].position = possible_positions_per_field[idx].items[0]; } // Cleanup for (possible_positions_per_field) |p| { p.deinit(); } } fn process_solution_round(fields: []Field, possibilities: []std.ArrayList(usize), resolved: *std.ArrayList(usize)) !bool { // Check to make sure we can start to solve without making a decision of some sort for (fields) |f, idx| { if (possibilities[idx].items.len == 1) { std.log.warn("{} only solution for {}", .{possibilities[idx].items[0], f.name}); var already_in_resolved = in_array(idx, resolved); if (!already_in_resolved) { try resolved.append(idx); } // Remove the from possibilities of other fields var value_to_remove = possibilities[idx].items[0]; for (fields) |f2, idx2| { std.log.warn("Checking {} for removal of possibility {}", .{f2.name, value_to_remove}); if (idx2 == idx) { continue; } var need_to_remove = false; var remove_index : ?usize = null; if (possibilities[idx2].items.len == 1) { continue; } for (possibilities[idx2].items) |i, k| { std.log.warn("pos {}: {}", .{k, i}); if (i == value_to_remove) { std.log.warn("want to remove index {} (value: {}) from possibilities", .{k, i}); need_to_remove = true; remove_index = k; break; } } if (need_to_remove) { if (remove_index) |ri| { std.log.warn("Removed index {} from possibilities for {}", .{ri, f2.name}); _ = possibilities[idx2].orderedRemove(ri); } else { std.log.warn("Wanted to remove {} from possibilities for {}, but remove_index not set", .{idx2, f2.name}); } } } if (!already_in_resolved) { // Only process one removal per round break; } } } // @TODO Verifiy we still have at least another round left //std.debug.assert(min_possibilities == @as(usize, 1)); // if (min_possibilities > 1) { // std.log.warn("Unable to provide solution, minimum of two possibilities for all remaining fields.", .{}); // return false; // } var min_possibilities_remaining : usize = std.math.maxInt(usize); for (fields) |f, idx| { if (in_array(idx, resolved)) { continue; } min_possibilities_remaining = std.math.min(min_possibilities_remaining, possibilities[idx].items.len); } if (min_possibilities_remaining > 1) { std.log.warn("Unable to process more rounds, at least all non-resolved items have multiple solutions remaining", .{}); return false; } if (fields.len == resolved.items.len) { std.log.debug("Resolve items length matches field items length, should be done doing solution rounds", .{}); return false; } return true; } fn in_array(value: usize, a: *std.ArrayList(usize)) bool { for (a.items) |v, k| { if (value == v) { return true; } } return false; } fn all_tickets_fit_field_limits(tickets: []Ticket, field: *Field, index: usize) bool { for (tickets) |t, k| { //std.log.warn("Checking ticket for field '{}', index {}: {}", // .{field.name, index, t}); if (!field.in_limits(t.values[index])) { //std.log.warn("Ticket #{} has a value {} at index {} which does not match limits of field '{}'", //.{k, t.values[index], index, field.name}); return false; } } return true; } fn already_known_position(fields: []Field, position: usize) bool { for (fields) |f| { if (f.position) |p| { if (p == position) { return true; } } } return false; } fn fields_with_unknown_positions_exist(fields: []Field) bool { for (fields) |f| { if (f.position == null) { return true; } } return false; } pub fn read_tickets(filename: []const u8, a: *std.mem.Allocator) !std.ArrayList(Ticket) { var f = try std.fs.cwd().openFile(filename, .{}); var contents = try f.readToEndAlloc(a, std.math.maxInt(u32)); defer a.free(contents); var tickets = std.ArrayList(Ticket).init(a); errdefer tickets.deinit(); var it = std.mem.tokenize(contents, "\n"); while (it.next()) |line| { var ticket = std.mem.zeroes(Ticket); var count : u64 = 0; var ticket_it = std.mem.tokenize(line, ","); while (ticket_it.next()) |v| { ticket.values[count] = try std.fmt.parseUnsigned(u64, v, 10); count += 1; } try tickets.append(ticket); } return tickets; } const Ticket = struct { values: [20]u64, }; pub fn valid_for_any_field(value: u64, fields: []Field) bool { var r = false; for (fields) |f| { r = f.in_limits(value); // for (f.limits.items) |l| { // if (value >= l.min and value <= l.max) { // r = true; // //std.log.debug("{} is at least valid for field '{}', between {} and {}", // // .{value, f.name, l.min, l.max}); // break; // } // } if (r == true) { break; } } return r; } fn read_fields(filename: []const u8, a: *std.mem.Allocator) !std.ArrayList(Field) { var f = try std.fs.cwd().openFile(filename, .{}); var contents = try f.readToEndAlloc(a, std.math.maxInt(u32)); defer a.free(contents); var it = std.mem.tokenize(contents, "\n"); var fields = std.ArrayList(Field).init(a); errdefer fields.deinit(); var limits : std.ArrayList(Limit) = undefined; var name : []u8 = undefined; while (it.next()) |line| { var lit = std.mem.tokenize(line, ":"); limits = std.ArrayList(Limit).init(a); errdefer limits.deinit(); if (lit.next()) |n| { name = try a.dupe(u8, n); } if (lit.next()) |limit_line| { // split on the 'o', just cause. I don't think we have a handy split // for a complete string var limit_line_it = std.mem.tokenize(limit_line, "o"); while(limit_line_it.next()) | range_line | { var min: u64 = 0; var max: u64 = 0; var range_line_it = std.mem.tokenize(range_line, "-"); if (range_line_it.next()) |v| { min = try std.fmt.parseUnsigned(u64, std.mem.trim(u8, v, "r "), 10); } if (range_line_it.next()) |v| { max = try std.fmt.parseUnsigned(u64, std.mem.trim(u8, v, "r "), 10); } try limits.append(Limit {.min = min, .max = max}); } } try fields.append(Field {.name = name, .limits = limits, .allocator = a}); } return fields; } const Limit = struct { min: u64, max: u64, }; const Field = struct { name: []u8, limits: std.ArrayList(Limit), allocator: * std.mem.Allocator, position: ?u64 = null, pub fn deinit(self: *const Field) void { self.allocator.free(self.name); self.limits.deinit(); } pub fn in_limits(self: *const Field, value: u64) bool { //std.log.warn("Testing if {} fits in limits for field '{}'", // .{value, self.name}); for (self.limits.items) |l| { //std.log.warn("{}", .{l}); if (value >= l.min and value <= l.max) { return true; } } //std.log.warn("{} does not fit in limits for field '{}'", .{value, self.name}); return false; } }; test "valid_for_any_field" { var fields = try read_fields("test_input_fields", std.testing.allocator); std.testing.expect(valid_for_any_field(40, fields.items[0..])); std.testing.expect(valid_for_any_field(4, fields.items[0..]) == false); std.testing.expect(valid_for_any_field(55, fields.items[0..]) == false); std.testing.expect(valid_for_any_field(12, fields.items[0..]) == false); for(fields.items) |f| { f.deinit(); } fields.deinit(); } test "field_resolution" { var fields = try read_fields("test_input_fields2", std.testing.allocator); var tickets = [_]Ticket { Ticket { .values = [_]u64 {3, 9, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} }, Ticket { .values = [_]u64 {15, 1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} }, Ticket { .values = [_]u64 {5, 14, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} }, }; var fit = all_tickets_fit_field_limits(tickets[0..], &fields.items[1], 0); std.testing.expect(fit); fit = all_tickets_fit_field_limits(tickets[0..], &fields.items[0], 0); std.testing.expect(fit == false); try determine_field_positions(tickets[0..], fields.items[0..], std.testing.allocator); std.testing.expectEqual(@as(u64, 1), fields.items[0].position.?); std.testing.expectEqual(@as(u64, 0), fields.items[1].position.?); std.testing.expectEqual(@as(u64, 2), fields.items[2].position.?); for(fields.items) |f| { f.deinit(); } fields.deinit(); }