Advent of Code: Day 5

Author: Leo Nguyen Dec 5, 2021

There is a list of lines, which are defined by two ending points (coordinates are given to you). Let's say you draw all of them on a grid, can you find the number of points on the grid that have two or more lines through them? Assume only vertical and horizontal lines.

You can read the puzzle here.

This is my solution in Python:

def read(filename):
    file = open(filename)

    x1, y1, x2, y2 = [], [], [], []

    for line in file.read().splitlines():
        items = line.split()
        x1y1 = items[0]
        x2y2 = items[2]

        x1.append(int(x1y1.split(',')[0]))
        y1.append(int(x1y1.split(',')[1]))
        x2.append(int(x2y2.split(',')[0]))
        y2.append(int(x2y2.split(',')[1]))

    return x1, y1, x2, y2


def solve(x1, y1, x2, y2):
    # Create matrix of n x n
    m = max(x1 + x2) + 1
    n = max(y1 + y2) + 1
    a = [[0] * n for i in range(m)]

    # Iterate each line
    for i in range(len(x1)):
        # If same x
        if x1[i] == x2[i]:
            for y in range(min(y1[i], y2[i]), max(y1[i], y2[i]) + 1):
                a[x1[i]][y] += 1

        # If same y
        if y1[i] == y2[i]:
            for x in range(min(x1[i], x2[i]), max(x1[i], x2[i]) + 1):
                a[x][y1[i]] += 1

    # Count where m >= 2
    s = 0
    for row in a:
        s += sum([1 for number in row if number >= 2])

    print(s)


x1, y1, x2, y2 = read('input.txt')
solve(x1, y1, x2, y2)

Part 2 is very similar, only differs in that now let's assume the lines can be diagonal too. To simplify the problem, there are only perfectly 45°-angled lines. Now calculate the number of points that are struck by more than one line.

So my solution is modified as thus:

def read(filename):
    file = open(filename)

    x1, y1, x2, y2 = [], [], [], []

    for line in file.read().splitlines():
        items = line.split()
        point1 = items[0]
        point2 = items[2]

        x1.append(int(point1.split(',')[0]))
        y1.append(int(point1.split(',')[1]))
        x2.append(int(point2.split(',')[0]))
        y2.append(int(point2.split(',')[1]))

    return x1, y1, x2, y2


def solve(x1, y1, x2, y2):
    # Create matrix of n x n
    m = max(x1 + x2) + 1
    n = max(y1 + y2) + 1
    a = [[0] * n for i in range(m)]

    # Iterate each line
    for i in range(len(x1)):
        # Fill the matrix with points in between
        xstep = 0 if x2[i] == x1[i] else 1 if x2[i] > x1[i] else -1
        ystep = 0 if y2[i] == y1[i] else 1 if y2[i] > y1[i] else -1

        x, y = x1[i], y1[i]

        while True:
            a[x][y] += 1
            x += xstep
            y += ystep

            if x == x2[i] and y == y2[i]:
                a[x][y] += 1
                break

    # Count where m >= 2
    s = 0
    for row in a:
        s += sum([1 for number in row if number >= 2])

    print(s)


x1, y1, x2, y2 = read('input.txt')
solve(x1, y1, x2, y2)

Advent of Code: Day 4

Author: Leo Nguyen Dec 4, 2021

A giant squid appears.

No, really. On Day 4, a giant squid appears and wants to play Bingo with you. This time, there are several Bingo boards and you're given a list of numbers that will be drawn (cheating!). So your goal is to find out which board will win first. Thankfully, with technology's help, this matter shouldn't be too difficult.

def read(filename):
    file = open(filename)
    numbers = file.readline().split(',')
    lines = file.read().splitlines()

    rows = [line.split() for line in lines if line]
    boards = []

    while len(rows):
        boards.append(rows[:5])
        rows = rows[5:]

    return numbers, boards


def fill_board(board, number):
    for i in range(5):
        for j in range(5):
            if board[i][j] == number:
                board[i][j] = None


def board_wins(board):
    # Check rows
    for i in range(5):
        win = True
        for j in range(5):
            if board[i][j] is not None:
                win = False
                break
        if win:
            return True

    # Check columns
    for j in range(5):
        win = True
        for i in range(5):
            if board[i][j] is not None:
                win = False
                break
        if win:
            return True

    return False


def solve(numbers, boards):
    for number in numbers:
        for board in boards:
            fill_board(board, number)

            if board_wins(board):
                summary = sum([sum([int(number) for number in row if number is not None]) for row in board])
                print(summary * int(number))
                return


numbers, boards = read('input.txt')
solve(numbers, boards)

In Part 2, turns out some in your crew apparently watched Squid Game, and they don't want to anger the squid for fear of bad things happening. So they want you to help it win. And this time we will try to determine which board will win last.

def read(filename):
    file = open(filename)
    numbers = file.readline().split(',')
    lines = file.read().splitlines()

    rows = [line.split() for line in lines if line]
    boards = []

    while len(rows):
        boards.append(rows[:5])
        rows = rows[5:]

    return numbers, boards


def fill_board(board, number):
    for i in range(5):
        for j in range(5):
            if board[i][j] == number:
                board[i][j] = None


def board_wins(board):
    if board[0][0] == '-1':
        return False

    # Check rows
    for i in range(5):
        win = True
        for j in range(5):
            if board[i][j] is not None:
                win = False
                break
        if win:
            return True

    # Check columns
    for j in range(5):
        win = True
        for i in range(5):
            if board[i][j] is not None:
                win = False
                break
        if win:
            return True

    return False


# Mark board removed by filling first number with '-1'
def remove_board(board):
    board[0][0] = '-1'


def solve(numbers, boards):
    boards_won = 0

    for number in numbers:
        for board in boards:
            fill_board(board, number)

            if board_wins(board):
                boards_won += 1

                if boards_won == len(boards):
                    summary = sum([sum([int(number) for number in row if number is not None]) for row in board])
                    print(summary * int(number))
                    return

                # Remove this board from boards
                remove_board(board)


numbers, boards = read('input.txt')
solve(numbers, boards)

Takeaways

Surprisingly, this game has provided me with some clarity on programming and how to optimize for this kind of competition. Specifically, here are the rules I set for myself in future days of Advent of Code:

  • Variables will contain data that match their names and descriptions.

  • Use functions. As a newcomer to Python, I tend to do what beginners do: write everything in one block from top to bottom. As it turns out, learning and using a more procedural style make all this more manageable and effective.

  • Take in strides. This means no longer frantically try to code as fast as I can. I made this mistake in Day 3, which made my coding experience worse and actually prolonged the time to solution. This time, with all experience equipped so far, I will read the problem carefully, take leisure in mapping out the solution, and code in professional business method, not code golf.

If you're reading this and want to join us, there's still time. If you've beaten Day 4, congratulations and thanks for the companionship.


Advent of Code: Day 3

Author: Leo Nguyen Dec 3, 2021

I gotta tell you, Day 3 is something else. The difficulty sharply jumped higher after the two easy previous day. Who would have thought?

The Puzzle

Day 3 deals with binary, which makes it hard. Usually every year the hard puzzles showed up quite later in the game, like Day 10 or 11. Basically, Part 1 gives you a list of binary numbers, and your goal is to construct a new binary number built from the most common bit at each position. Then do the same to construct a binary number but take from the least common bit instead. Multiply the decimal values of these two binaries to get the answer.

My solution is thus:

file = open('input.txt')
binaries = file.read().splitlines()

n = len(binaries[0])  # length of each binary

# Calculate 1's and 0's
ones = [0] * n
zeroes = [0] * n

for i in range(n):
    for binary in binaries:
        if binary[i] == '1':
            ones[i] += 1
        else:
            zeroes[i] += 1

# Build most and least common binaries
most_common = [''] * n
least_common = [''] * n

for i in range(n):
    if ones[i] > zeroes[i]:
        most_common[i] = '1'
        least_common[i] = '0'
    else:
        most_common[i] = '0'
        least_common[i] = '1'

print(int(''.join(most_common), 2) * int(''.join(least_common), 2))

Part 2 of the puzzle upped the ante: Iterating each bit from left to right, you're to keep the binaries that have the same most common bit at each position, then discard the rest in the input. Rinse and repeat until you have one binary left. Do the same but with the least commot bit instead, then produce a second binary. Again, multiply their decimal values to get the answer.

The trick is to reuse the calculation of most and least common binaries from Part 1.

Here's my code for Part 2:

def calculate_commons(binaries):
    n = len(binaries[0])  # length of each binary

    # Calculate 1's and 0's
    ones = [0] * n
    zeroes = [0] * n

    for i in range(n):
        for binary in binaries:
            if binary[i] == '1':
                ones[i] += 1
            else:
                zeroes[i] += 1

    # Build most and least common binaries
    most_common = [''] * n
    least_common = [''] * n

    for i in range(n):
        if ones[i] >= zeroes[i]:
            most_common[i] = '1'
            least_common[i] = '0'
        else:
            most_common[i] = '0'
            least_common[i] = '1'

    return most_common, least_common


file = open('input.txt')
binaries = file.read().splitlines()

n = len(binaries[0])  # length of each binary

# Oxygen
oxygen_keep = binaries

for i in range(n):
    most_common, least_common = calculate_commons(oxygen_keep)
    keep = []

    for binary in oxygen_keep:
        if binary[i] == most_common[i]:
            keep.append(binary)

    oxygen_keep = keep

    if len(oxygen_keep) == 1:
        break

# CO2
co2_keep = binaries

for i in range(n):
    most_common, least_common = calculate_commons(co2_keep)
    keep = []

    for binary in co2_keep:
        if binary[i] == least_common[i]:
            keep.append(binary)

    co2_keep = keep

    if len(co2_keep) == 1:
        break

print(int(''.join(oxygen_keep), 2) * int(''.join(co2_keep), 2))

This took me more than an hour to solve. But I'm happy because I lasted to the end.

Happy coding!


Advent of Code: Day 2

Author: Leo Nguyen Dec 2, 2021

Just like clockwork, Day 2 opened precisely at midnight of Eastern Standard Time.

The Puzzle

The puzzle, called Dive, has an input file that contains a list of commands. Each command has a word and a value, which represent a movement and how much to move, respectively. The words can be: forward to increase forward (horizontal) position, down to increase depth, and up to decrease depth. For example, this input:

forward 5
down 5
forward 8
up 3
down 8
forward 2

tells us that, if you move according to these commands from your starting point, you will be 15 forward and 10 deep. And the puzzle expects you to give it the product of these two numbers (150 in this case).

Here’s my code to solve it:

file = open('input.txt')
lines = file.read().splitlines()

x = 0
depth = 0

for line in lines:
    items = line.split()
    command = items[0]
    value = int(items[1])

    match command:
        case 'forward':
            x += value
        case 'down':
            depth += value
        case 'up':
            depth -= value

print(x * depth)

After Part 1 was solved, Part 2 presented a new element: aim. Similar to Part 1, but it has some modifications:

  • down X increases your aim by X units.

  • up X decreases your aim by X units.

  • forward X does two things:

    • It increases your horizontal position by X units.

    • It increases your depth by your aim multiplied by X.

So the code evolved:

file = open('input.txt')
lines = file.read().splitlines()

x = 0
depth = 0
aim = 0

for line in lines:
    items = line.split()
    command = items[0]
    value = int(items[1])

    match command:
        case 'forward':
            x += value
            depth += aim * value
        case 'down':
            aim += value
        case 'up':
            aim -= value

print(x * depth)

Huge Community

Man does this Advent of Code have a huge community! It has a DJ performing live when the puzzle started. Day 2 has people visualizing the problems like this one here. Folks have formed clubs, private leaderboards, and thinktanks to brainstorm and solve these puzzles together.

I expect to last ‘til December 25. Do you?