433 lines
16 KiB
Zig
433 lines
16 KiB
Zig
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();
|
|
|
|
}
|