~nilium/aoc2020

3729912ea84e668aed492f46f275db9bb201a4ed — Noel Cower 3 years ago 1f6e0f0 main
Add day 10 solution

There's more commentary in the code than the commit this time because
I spent about an hour or more trying to wrap my head around the
problem, then flailed around for a while longer with combinations of
runs of 1-sequences before getting a hint on Reddit about the
tribonacci sequence being useful for this. So, part 2 was dismal.
Interesting problem, but very tiring.
2 files changed, 229 insertions(+), 0 deletions(-)

M build.zig
A day10/src/main.zig
M build.zig => build.zig +1 -0
@@ 27,6 27,7 @@ const days = [_]Pkg{
    newPkg("day7"),
    newPkg("day8"),
    newPkg("day9"),
    newPkg("day10"),
};

fn dayBuild(day: Pkg, b: *Builder, target: Target, mode: std.builtin.Mode) void {

A day10/src/main.zig => day10/src/main.zig +228 -0
@@ 0,0 1,228 @@
const std = @import("std");
const io = std.io;
const Allocator = std.mem.Allocator;
const File = std.fs.File;

const ascI64 = std.sort.asc(i64);

fn closeFile(f: *File) void {
    f.close();
}

fn closeNop(f: *File) void {}

pub fn main() anyerror!void {
    const stdout = io.getStdOut().writer();

    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();

    const alloc = &arena.allocator;

    var args = std.process.args();
    _ = args.skip(); // skip arg0

    var numbers = std.ArrayList(i64).init(alloc);
    defer numbers.deinit();
    try numbers.append(0); // This line here factors into why part 2 was harder than it needed to be.

    // Read input file.
    {
        var buf: [4000]u8 = undefined;
        var file: File = undefined;
        var close = closeFile;
        if (args.nextPosix()) |path| {
            const cwd = std.fs.cwd();
            file = try cwd.openFile(path, .{});
        } else {
            file = io.getStdIn();
            close = closeNop;
        }
        defer close(&file);

        var stream = io.bufferedReader(file.reader());
        var rd = stream.reader();
        while (try rd.readUntilDelimiterOrEof(&buf, '\n')) |line| {
            const i = try std.fmt.parseInt(i64, line, 10);
            try numbers.append(i);
        }

        std.sort.sort(i64, numbers.items, {}, ascI64);
    }

    // Part 1:
    {
        const counts = differences(numbers.items);
        try stdout.print("Ones = {} Threes = {} Product = {}\n", .{
            counts.ones,
            counts.threes,
            counts.ones * counts.threes,
        });
    }

    // Part 2, or: I don't like this part.
    //
    // This has added commentary because I think it's a fun problem once it's
    // explained, but I made some dumb mistakes in writing the answer out.
    //
    // First, the tribonacci sequence is not my idea, I am not that smart and
    // I was going to reuse the Combination iterator I wrote previously for
    // this. Someone on reddit, bless them, mentioned that the number of
    // combinations for a run of numbers in this sequence lines up with the
    // tribonacci sequence. If you write this out by hand, it makes sense, but
    // is not a dot I would've connected on my own and especially not at 10pm
    // after a long day of debugging things.
    //
    // Seconds, if you look up where I first add the number zero to the list of
    // numbers, that wasn't there until the very end. This threw off the count
    // by a factor of two. This is because the first run of numbers was counted
    // incorrectly as {1,2,3} insted of {0,1,2,3}, so its factor was trib(3)
    // instead of trib(4).
    {
        const data = numbers.items;
        const count = combinations(data);
        try stdout.print("Count = {}\n", .{count});
    }
}

const Counter = struct {
    ones: usize = 0,
    threes: usize = 0,
};

fn differences(data: []const i64) Counter {
    var c = Counter{};

    var last: i64 = 0;
    for (data) |i| {
        const delta = i - last;
        last = i;
        switch (delta) {
            0 => {},
            1 => c.ones += 1,
            3 => c.threes += 1,
            else => {
                std.debug.warn("Unreachable code with delta {}", .{delta});
                unreachable;
            },
        }
    }

    return c;
}

test "part 1 example" {
    var sampleData = [_]i64{
        0,
        16,
        10,
        15,
        5,
        1,
        11,
        7,
        19,
        6,
        12,
        4,
    };

    std.sort.sort(i64, &sampleData, {}, ascI64);
    const counts = differences(&sampleData);

    // Part 1
    std.testing.expectEqual(@as(usize, 7), counts.ones);
    // +1 to account for the fact that we're not modifying the sample data in
    // the test to include max+3.
    std.testing.expectEqual(@as(usize, 5), counts.threes + 1);

    // Part 2
    std.testing.expectEqual(@as(i64, 8), combinations(&sampleData));
}

test "part 1 longer example" {
    var sampleData = [_]i64{
        0, // Implied 0 made explicit.
        28,
        33,
        18,
        42,
        31,
        14,
        46,
        20,
        48,
        47,
        24,
        23,
        49,
        45,
        19,
        38,
        39,
        11,
        1,
        32,
        25,
        35,
        8,
        17,
        7,
        9,
        4,
        2,
        34,
        10,
        3,
    };

    std.sort.sort(i64, &sampleData, {}, ascI64);
    const counts = differences(&sampleData);

    // Part 1
    std.testing.expectEqual(@as(usize, 22), counts.ones);
    std.testing.expectEqual(@as(usize, 10), counts.threes + 1);

    // Part 2
    std.testing.expectEqual(@as(i64, 19208), combinations(&sampleData));
}

fn trib(n: i64) i64 {
    if (n <= 0) return 0;
    switch (n) {
        1, 2 => return 1,
        else => {},
    }
    var c = [3]i64{ 0, 1, 1 };
    var i = n - 2;
    while (i > 0) : (i -= 1) {
        const sum = c[0] + c[1] + c[2];
        c[0] = c[1];
        c[1] = c[2];
        c[2] = sum;
    }
    return c[2];
}

test "tribonacci sequence" {
    const eq = std.testing.expectEqual;
    eq(@as(i64, 0), trib(0));
    eq(@as(i64, 1), trib(1));
    eq(@as(i64, 1), trib(2));
    eq(@as(i64, 2), trib(3));
    eq(@as(i64, 4), trib(4));
    eq(@as(i64, 7), trib(5));
    eq(@as(i64, 13), trib(6)); // NOTE: this never occurs in the dataset.
}

fn combinations(data: []const i64) i64 {
    var count: i64 = 1;
    var i: usize = 0;
    while (i < data.len - 1) {
        var j: usize = i + 1;
        while (j < data.len and data[j] == data[j - 1] + 1) : (j += 1) {}
        count *= trib(@intCast(i64, @truncate(u32, j - i)));
        i = j;
    }
    return count;
}