I spent two whole nights because I’m not familiar with matrices.
Part1, after using Gaussian elimination, search for all free variables. Part 2 uses bareiss to search all free variables after elimination. The upper bound of the free variable uses some techniques that I myself don’t quite understand to slightly reduce the search space, although I’m not sure myself whether this is optimization or negative optimization.
I’m not familiar with these matrix operations, but I have managed to implement them for the time being. At one point, optimization to avoid brute force search was considered, but all failed.
PS D:\advent-of-code-template> zig build solve -Dday=10 -Doptimize=ReleaseFast
[10/1] (0.116 ms) 411
[10/1] (10.541 ms) 16063
Total elapsed solution time: 10.657 ms
// Transposed Matrix
// Target | Btn0 Btn1 Btn2 Btn3
// Lgt0 [ 1 | 1 0 1 0 ] => b0 + b2 = 1
// Lgt1 [ 0 | 0 1 1 0 ] => b1 + b2 = 0
// Lgt2 [ 1 | 1 1 0 1 ] => b0 + b1 + b3 = 1
// Lgt3 [ 0 | 0 0 0 1 ] => b3 = 0
//
// Target
// v
// Row 0: 0...0001011 (Target:1, Buttons: 0, 2 active)
// Row 1: 0...0001100 (Target:0, Buttons: 1, 2 active)
// Row 2: 0...0010111 (Target:1, Buttons: 0, 1, 3 active)
// ^ ^^^^^
// Padding Coeffs
const std = @import("std");
pub const LinearSystemBit64 = struct {
rows: [64]u64,
max_row_id: u6,
max_col_id: u6,
pub const InitCursor = struct {
ref: *LinearSystemBit64,
current_col_id: u6,
pub inline fn setTargetBit(self: *@This(), row_id: usize) void {
self.ref.rows[row_id] |= 1;
}
pub inline fn nextCol(self: *@This()) void {
self.current_col_id += 1;
}
pub inline fn setColBit(self: *@This(), row_id: usize) void {
self.ref.rows[row_id] |= (@as(u64, 1) << self.current_col_id);
}
pub inline fn finish(self: *@This()) void {
self.ref.max_col_id = self.current_col_id;
self.* = undefined;
}
};
pub inline fn clearRows(self: *@This(), new_row_num: usize) InitCursor {
@memset((&self.rows)[0..new_row_num], 0);
self.max_row_id = @intCast(new_row_num - 1);
return .{ .ref = self, .current_col_id = 0 };
}
pub fn eliminateInplace(self: *@This()) struct {
rank: u6,
pivot_mask: u64,
} {
var pivot_row_id: u6 = 0;
var pivot_col_mask: u64 = 0;
for (1..@as(usize, self.max_col_id) + 1) |col_id| {
const col_mask: u64 = @as(u64, 1) << @as(u6, @intCast(col_id));
const row_id_to_swap: usize = blk: for (pivot_row_id..@as(usize, self.max_row_id) + 1) |r| {
if ((self.rows[r] & col_mask) != 0) {
break :blk r;
}
} else continue;
std.mem.swap(u64, &self.rows[pivot_row_id], &self.rows[row_id_to_swap]);
for (0..@as(usize, self.max_row_id) + 1) |r| {
if (r != pivot_row_id) {
if ((self.rows[r] & col_mask) != 0) {
self.rows[r] ^= self.rows[pivot_row_id];
}
}
}
pivot_col_mask |= col_mask;
pivot_row_id += 1;
}
return .{ .rank = pivot_row_id, .pivot_mask = pivot_col_mask };
}
pub fn minSolutionPopInplace(self: *@This()) ?u6 {
const eliminate_result = self.eliminateInplace();
const rank = eliminate_result.rank;
const pivot_mask = eliminate_result.pivot_mask;
for (rank..@as(usize, self.max_row_id) + 1) |r| {
if (self.rows[r] != 0) return null;
}
const full_var_mask = (@as(u64, std.math.maxInt(u64)) >> (63 - self.max_col_id)) - 1;
var free_mask = full_var_mask & ~pivot_mask;
var free_col_ids: [64]u6 = undefined;
var free_count: u6 = 0;
while (free_mask != 0) {
free_col_ids[free_count] = @intCast(@ctz(free_mask));
free_count += 1;
free_mask &= free_mask - 1;
}
var min_solution_pop: u6 = 63;
const combs = @as(u64, 1) << free_count;
for (0..combs) |comb_mask| {
var solution: u64 = 1;
var pop: u6 = 0;
for (0..free_count) |i| {
if (((comb_mask >> @as(u6, @intCast(i))) & 1) == 1) { // 检查组合的第 i 位
solution |= @as(u64, 1) << free_col_ids[i];
pop += 1;
}
}
for (0..rank) |r| {
if (@popCount(self.rows[r] & solution) % 2 == 1) {
pop += 1;
}
}
if (pop < min_solution_pop) min_solution_pop = pop;
}
return min_solution_pop;
}
};
pub fn part1(input: []const u8) !usize {
var line_it = std.mem.splitScalar(u8, input, '\n');
var sum: usize = 0;
var ls: LinearSystemBit64 = undefined;
while (line_it.next()) |line| {
if (line.len == 0) break;
var line_reader: std.Io.Reader = .fixed(line);
line_reader.toss(1);
const target = (try line_reader.takeDelimiter(']')).?;
var cursor = ls.clearRows(target.len);
for (target, 0..) |state, light_id| {
if (state == '#') cursor.setTargetBit(light_id);
}
while (line_reader.discardDelimiterInclusive('(')) |_| {
const button = (try line_reader.takeDelimiter(')')).?;
var light_id_it = std.mem.splitScalar(u8, button, ',');
cursor.nextCol();
while (light_id_it.next()) |light_id_str| {
cursor.setColBit(try std.fmt.parseUnsigned(u6, light_id_str, 10));
}
} else |err| switch (err) {
error.ReadFailed => return err,
error.EndOfStream => {},
}
cursor.finish();
sum += ls.minSolutionPopInplace() orelse std.debug.panic("{s}", .{line});
}
return sum;
}
pub const MiniNaturalLinearSystem = struct {
rows: [64][64]i64,
max_row_id: u6,
max_col_id: u6,
pub const InitCursor = struct {
ref: *MiniNaturalLinearSystem,
current_col_id: u6,
pub inline fn setTargetInRow(self: *@This(), row_id: usize, value: i64) void {
self.ref.rows[row_id][0] = value;
}
pub inline fn nextCol(self: *@This()) void {
self.current_col_id += 1;
}
pub inline fn setColInRow(self: *@This(), row_id: usize, value: i64) void {
self.ref.rows[row_id][self.current_col_id] = value;
}
pub inline fn finish(self: *@This()) void {
self.ref.max_col_id = self.current_col_id;
self.* = undefined;
}
};
pub inline fn clearRows(self: *@This(), new_row_num: usize) InitCursor {
@memset((&self.rows)[0..new_row_num], @as([64]i64, @splat(0)));
self.max_row_id = @intCast(new_row_num - 1);
return .{ .ref = self, .current_col_id = 0 };
}
pub fn eliminateInplace(self: *@This(), pivot_col_ids: *[64]u6) u6 {
var pivot_row_id: u6 = 0;
var prev_pivot: i64 = 1;
for (1..@as(usize, self.max_col_id) + 1) |col_id| {
const row_id_to_swap: usize = blk: for (pivot_row_id..@as(usize, self.max_row_id) + 1) |r| {
if ((self.rows[r][col_id]) != 0) {
break :blk r;
}
} else continue;
std.mem.swap([64]i64, &self.rows[pivot_row_id], &self.rows[row_id_to_swap]);
const pivot = self.rows[pivot_row_id][col_id];
pivot_col_ids[pivot_row_id] = @intCast(col_id);
for (pivot_row_id + 1..@as(usize, self.max_row_id) + 1) |r| {
const factor = self.rows[r][col_id];
const bareiss = struct {
inline fn apply(current: i64, current_pivot: i64, row_factor: i64, col_factor: i64, previous_pivot: i64) i64 {
return @divExact(current * current_pivot - row_factor * col_factor, previous_pivot);
}
};
self.rows[r][0] = bareiss.apply(self.rows[r][0], pivot, factor, self.rows[pivot_row_id][0], prev_pivot);
var c = col_id + 1;
while (c <= self.max_col_id) : (c += 1) {
self.rows[r][c] = bareiss.apply(self.rows[r][c], pivot, factor, self.rows[pivot_row_id][c], prev_pivot);
}
}
pivot_row_id += 1;
prev_pivot = pivot;
}
return pivot_row_id;
}
/// The free column in the solution is required to be pre-filled, while the pivot columns are required to be 0.
/// pivot_col_ids.len is rank.
/// Index: [0] [1] [2] [3] [4] [5]
/// Type: Target Pivot Free Pivot Free Pivot
/// Out In Out In Out
fn backSubstituteSolveCost(self: *const @This(), solution: *[64]i64, pivot_col_ids: []const u6) ?usize {
var r = pivot_col_ids.len;
while (r > 0) {
r -= 1;
const pivot_col_id: usize = pivot_col_ids[r];
var sum = self.rows[r][0]; // Target
for (pivot_col_id + 1..@as(usize, self.max_col_id) + 1) |c| {
sum -= self.rows[r][c] * solution[c];
}
const pivot_val = self.rows[r][pivot_col_id];
if (pivot_val == 0) @panic("Singular Matrix!");
if (@rem(sum, pivot_val) != 0) return null;
const res = @divExact(sum, pivot_val);
if (res < 0) return null;
solution[pivot_col_id] = res;
}
var total: usize = 0;
for (solution[1..@as(usize, self.max_col_id + 1)]) |val| {
total += @intCast(val);
}
return total;
}
fn minCostRecursive(
self: *const @This(),
pivot_col_ids: []const u6,
free_col_ids: []const u6,
depth: u6,
cols_physical_bounds: *const [64]?i64,
solution: *[64]i64,
) ?usize {
if (depth == free_col_ids.len) {
return self.backSubstituteSolveCost(solution, pivot_col_ids);
}
const current_free_col_id = free_col_ids[depth];
const free_min, const free_max = calc_bounds: {
var free_min: i64 = 0;
var free_max: i64 = cols_physical_bounds[current_free_col_id] orelse return minCostRecursive(
self,
pivot_col_ids,
free_col_ids,
depth + 1,
cols_physical_bounds,
solution,
);
if (depth == free_col_ids.len - 1) {
const r = pivot_col_ids.len - 1;
const pivot_col_id = pivot_col_ids[r];
const pivot = self.rows[r][pivot_col_id];
const coeff = self.rows[r][current_free_col_id];
const raw_target = self.rows[r][0];
var residue = raw_target;
for (0..depth) |prev_depth| {
const prev_free_col_id = free_col_ids[prev_depth];
const prev_val = solution[prev_free_col_id];
residue -= self.rows[r][prev_free_col_id] * prev_val;
}
const negator: i64 = if (pivot < 0) -1 else 0;
const aligned_residue = (residue ^ negator) - negator;
const aligned_coeff = (coeff ^ negator) - negator;
if (aligned_coeff > 0) {
const upper = @divTrunc(aligned_residue, aligned_coeff);
if (upper < free_max) free_max = upper;
} else if (aligned_coeff < 0) {
const lower = @divTrunc(-aligned_residue + -aligned_coeff - 1, -aligned_coeff);
if (lower > free_min) free_min = lower;
} else if (aligned_residue < 0) return null;
}
if (free_min > free_max) return null;
break :calc_bounds .{ free_min, free_max };
};
var f = free_min;
var min_local_cost: ?usize = null;
while (f <= free_max) : (f += 1) {
solution[current_free_col_id] = f;
if (minCostRecursive(
self,
pivot_col_ids,
free_col_ids,
depth + 1,
cols_physical_bounds,
solution,
)) |cost| {
if (blk: {
break :blk cost < min_local_cost orelse break :blk true;
}) min_local_cost = cost;
}
}
return min_local_cost;
}
pub fn minSolutionCostInplace(self: *@This()) ?usize {
var cols_physical_bounds: [64]?i64 = @splat(null);
for (0..@as(usize, self.max_row_id) + 1) |r| {
const target = self.rows[r][0];
if (target > 0) {
for (1..@as(usize, self.max_col_id) + 1) |c| {
const coeff = self.rows[r][c];
if (coeff > 0) {
const bound = @divTrunc(target, coeff);
if (blk: {
break :blk bound < cols_physical_bounds[c] orelse break :blk true;
}) {
cols_physical_bounds[c] = bound;
}
}
}
} else if (target == 0) {
for (1..@as(usize, self.max_col_id) + 1) |c| {
if (self.rows[r][c] > 0) cols_physical_bounds[c] = 0;
}
}
}
var pivot_col_ids: [64]u6 = undefined;
const rank = self.eliminateInplace(&pivot_col_ids);
for (rank..@as(usize, self.max_row_id) + 1) |r| {
if (self.rows[r][0] != 0) return null;
}
var solution: [64]i64 = @splat(0);
if (rank == self.max_col_id) {
return self.backSubstituteSolveCost(&solution, (&pivot_col_ids)[0..rank]);
}
var free_col_ids: [64]u6 = undefined;
var free_count: u6 = 0;
var p_id: u6 = 0;
for (1..@as(usize, self.max_col_id) + 1) |c| {
if (p_id < rank and pivot_col_ids[p_id] == c) {
p_id += 1;
continue;
}
free_col_ids[free_count] = @intCast(c);
free_count += 1;
}
return minCostRecursive(
self,
pivot_col_ids[0..rank],
free_col_ids[0..free_count],
0,
&cols_physical_bounds,
&solution,
);
}
};
pub fn part2(input: []const u8) !usize {
var line_it = std.mem.splitScalar(u8, input, '\n');
var sum: usize = 0;
var ls: MiniNaturalLinearSystem = undefined;
while (line_it.next()) |line| {
if (line.len == 0) break;
var line_reader: std.Io.Reader = .fixed(line);
line_reader.toss(1);
var cursor = ls.clearRows(try line_reader.discardDelimiterExclusive(']'));
while (std.mem.indexOfScalar(u8, try line_reader.peekGreedy(1), '(')) |delimiter_index| {
line_reader.toss(delimiter_index + 1);
const button = (try line_reader.takeDelimiter(')')).?;
var light_id_it = std.mem.splitScalar(u8, button, ',');
cursor.nextCol();
while (light_id_it.next()) |light_id_str| {
cursor.setColInRow(try std.fmt.parseUnsigned(u6, light_id_str, 10), 1);
}
}
_ = try line_reader.discardDelimiterInclusive('{');
const target = (try line_reader.takeDelimiter('}')).?;
var target_it = std.mem.splitScalar(u8, target, ',');
var row_id: u6 = 0;
while (target_it.next()) |target_val_str| {
cursor.setTargetInRow(row_id, try std.fmt.parseInt(i64, target_val_str, 10));
row_id += 1;
}
cursor.finish();
sum += ls.minSolutionCostInplace() orelse std.debug.panic("{s}\n", .{line});
}
return sum;
}