🍯 Glaze

const std = @import("std");
const builtin = @import("builtin");

pub const invalid_index = std.math.inf_u32;
const Cell = struct {
    distance: u32,
    index: u32,
};

fn popNext(front: *std.ArrayList(Cell)) Cell {
    var min_distance: u32 = std.math.inf_u32;
    var pop: usize = std.math.inf_u32;
    for (front.items) |cell, array_index| {
        if (cell.distance < min_distance) {
            min_distance = cell.distance;
            pop = array_index;
        }
    }
    return front.swapRemove(pop);
}

pub const Map = struct {
    allocator: std.mem.Allocator,
    risk_levels: []u32,
    width: u32,
    height: u32,

    pub fn init(allocator: std.mem.Allocator, risk_levels: []u32, width: u32, height: u32) !Map {
        return Map{
            .allocator = allocator,
            .risk_levels = try allocator.dupe(u32, risk_levels),
            .width = width,
            .height = height,
        };
    }

    pub fn deinit(self: *Map) void {
        self.allocator.free(self.risk_levels);
    }

    pub inline fn neighbors(self: Map, index: u32) [4]u32 {
        const x: u32 = index % self.width;
        const y: u32 = index / self.width;
        return [4]u32{
            if (x > 0) index - 1 else invalid_index,
            if (x < self.width - 1) index + 1 else invalid_index,
            if (y > 0) index - self.width else invalid_index,
            if (y < self.height - 1) index + self.width else invalid_index,
        };
    }

    pub fn find_path(self: Map) !u32 {
        var front = try std.ArrayList(Cell).initCapacity(self.allocator, 10000);
        defer front.deinit();
        var visited = try self.allocator.alloc(bool, self.risk_levels.len);
        defer self.allocator.free(visited);
        std.mem.set(bool, visited, false);
        var distances = try self.allocator.alloc(u32, self.risk_levels.len);
        defer self.allocator.free(distances);
        std.mem.set(u32, distances, std.math.inf_u32);

        try front.append(Cell{
            .distance = 0,
            .index = 0,
        });

        var num_visited: u32 = 0;
        iterate: while (front.items.len > 0) {
            var cell = popNext(&front);
            if (visited[cell.index]) {
                continue :iterate;
            }
            visited[cell.index] = true;
            num_visited += 1;

            var nbors = self.neighbors(cell.index);
            for (nbors) |nbor| {
                if (nbor == invalid_index) {
                    continue;
                }

                var distance: u32 = cell.distance + self.risk_levels[nbor];
                if (distance < distances[nbor]) {
                    distances[nbor] = distance;
                }

                try front.append(Cell{
                    .distance = distance,
                    .index = nbor,
                });
            }

            if (num_visited % 10000 == 0) {
                std.debug.print("num_visited: {}/{}\n", .{ num_visited, self.risk_levels.len });
            }
        }

        var target: u32 = (self.width * self.height) - 1;
        return distances[target];
    }

    pub fn print_risk_levels(self: Map, split_width: u32) void {
        var y: u32 = 0;
        while (y < self.height) : (y += 1) {
            if (y > 0 and y % split_width == 0) {
                std.debug.print("\n", .{});
            }

            var x: u32 = 0;
            var base_i: u32 = y * self.width;
            while (x < self.width / split_width) : (x += 1) {
                const start = base_i + (x * split_width);
                const stop = start + split_width;
                std.debug.print(" {any}", .{self.risk_levels[start..stop]});
            }
            std.debug.print("\n", .{});
        }
    }

    pub fn expand(self: *Map, times: u32) !void {
        var old_width = self.width;
        var old_height = self.height;
        self.width *= times;
        self.height *= times;

        {
            var risk_levels = try self.allocator.alloc(u32, self.risk_levels.len * (times * times));
            var y: u32 = 0;
            while (y < old_height) : (y += 1) {
                var x: u32 = 0;
                while (x < old_width) : (x += 1) {
                    risk_levels[(y * self.width) + x] = self.risk_levels[(y * old_width) + x];
                }
            }
            self.allocator.free(self.risk_levels);
            self.risk_levels = risk_levels;
        }

        var i_y: u32 = 0;
        var base_i: u32 = 0;
        while (i_y < times) : (i_y += 1) {
            var i_x: u32 = if (i_y == 0) 1 else 0;
            while (i_x < times) : (i_x += 1) {
                var current_i = ((i_y * old_height) * self.width) + (i_x * old_width);

                var y: u32 = 0;
                while (y < old_height) : (y += 1) {
                    var x: u32 = 0;
                    while (x < old_width) : (x += 1) {
                        var internal_offset = (y * self.width) + x;
                        var current = self.risk_levels[base_i + internal_offset];
                        self.risk_levels[current_i + internal_offset] = if (current + 1 > 9) 1 else current + 1;
                    }
                }

                base_i += old_width;
            }
            base_i = i_y * (self.width * old_height);
        }

        // std.log.debug("expanded risk levels:", .{});
        // self.print_risk_levels(old_width);
    }
};

pub fn readInput(allocator: std.mem.Allocator, reader: anytype) !Map {
    var risk_levels = std.ArrayList(u32).init(allocator);
    defer risk_levels.deinit();

    var width: u32 = 0;
    var height: u32 = 0;
    while (true) {
        var line: []u8 = undefined;
        if (builtin.os.tag == .windows and !builtin.is_test) {
            // NOTE: Read another byte on windows due to two-byte eol.
            // NOTE: Check if in testing since tests only add single-byte eol in multiline strings.
            line = reader.readUntilDelimiterAlloc(allocator, '\r', 512) catch break;
            _ = try reader.readByte();
        } else {
            line = reader.readUntilDelimiterAlloc(allocator, '\n', 512) catch break;
        }
        defer allocator.free(line);

        if (line.len > width) {
            width = @intCast(u32, line.len);
        }
        height += 1;

        for (line) |char| {
            const risk: u32 = char - '0';
            try risk_levels.append(risk);
        }
    }

    return try Map.init(allocator, risk_levels.items, width, height);
}

pub fn task1(input: *Map) !u32 {
    return input.find_path();
}

pub fn task2(input: *Map) !u32 {
    try input.expand(5);
    return input.find_path();
}

pub fn main() !void {
    var buffer: [8000000]u8 = undefined;
    var fixed_buffer = std.heap.FixedBufferAllocator.init(&buffer);
    const allocator = fixed_buffer.allocator();

    const file = try std.fs.cwd().openFile("input.txt", .{ .read = true });
    defer file.close();

    var input = try readInput(allocator, file.reader());
    defer input.deinit();

    const task_1_result = try task1(&input);
    std.log.info("Task 1 result: {}", .{task_1_result});

    const task_2_result = try task2(&input);
    std.log.info("Task 2 result: {}", .{task_2_result});
}