Advent of Code 2024

I figured I was missing something like that. I haven't quite gotten my head around which methods are on the Regex itself and which are (extensions, I presume) on String et al.

My experience was similar. Part 1 was pretty inelegant. When I initially got part 2 I thought it would be harder, but once I started thinking about it actually ended up being a much simpler problem than part 1.

I would love to know why the same code for Day 4, part 1, runs so slowly in Swift compared to the same code in Python. The optimized Swift code takes more than 70 seconds to run, while the Python code produces an instant result.

Source code

Swift:

var grid: [[Character]] {
  data.split(separator: "\n").map { Array($0) }
}

func part1() -> Any {
    let rowCount = grid.count
    let columnCount = grid[0].count
    var count = 0

    // Loop through all characters in the grid
    for r in 0..<rowCount {
      for c in 0..<columnCount {
        // Check if the current character is 'X'
        if grid[r][c] != "X" { continue }
        
        // Check all 8 directions
        for dr in [-1, 0, 1] {
          for dc in [-1, 0, 1] {
            if dr == 0 && dc == 0 { continue }
            
            // Check if the pattern extends within bounds
            if !(0 <= r + 3 * dr && r + 3 * dr < rowCount &&
                 0 <= c + 3 * dc && c + 3 * dc < columnCount) { continue }
            
            // Check for 'MAS' pattern
            if grid[r + dr][c + dc] == "M" &&
               grid[r + 2 * dr][c + 2 * dc] == "A" &&
               grid[r + 3 * dr][c + 3 * dc] == "S" {
              count += 1
            }
          }
        }
      }
    }

    return count
  }

Python:

grid = open(0).read().splitlines()

count = 0

for r in range(len(grid)):
    for c in range(len(grid[0])):
        if grid[r][c] != "X": continue
        for dr in [-1, 0, 1]:
            for dc in [-1, 0, 1]:
                if dr == dc == 0: continue
                if not (0 <= r + 3 * dr < len(grid) and 0 <= c + 3 * dc < len(grid[0])): continue
                if grid[r + dr][c + dc] == "M" and grid[r + 2 * dr][c + 2 * dc] == "A" and grid[r + 3 * dr][c + 3 * dc] == "S":
                    count += 1

print(count)

Where is grid defined in your Swift source? Are you perhaps recomputing it on every access?

2 Likes

This property recomputes grid on every access so thats likely a large reason for the slow down.

1 Like

I updated the source code for Swift. The grid type is [[Character]] in Swift and an array of strings in Python.

1 Like

You are absolutely right! I completely overlooked this and was focused on the algorithm itself. :man_facepalming: Thanks to you, I can sleep peacefully tonight. :smiley:

I'm really appreciating the inline regexes this year. Thanks, Swift!

For any who are interested, my solutions are here.

3 Likes

Here is mine GitHub - Alex293/aoc-2024. I used the benchmark package to try différents approaches for the first day and optimise a bit day 4.

For now my biggest discovery is the difference of performance between inline regex and regexbuilder though I’m new to regexbuilder so I might have missed something.
edit: hopefully wrong see below

Also for the early exercises at least the biggest gains were from reading uint8 instead of string and manipulating indexes instead of properly parsing. I’ll look into switching the default file loading of the template to fread next

I can’t say enough how awesome is the benchmark package !

1 Like

Day 5 in the bag. Reading the other folks' solutions from day 4 made me realise that I'm still writing in 'C++' mode -- that is, leaf functions go first in the file, then functions that call those functions, and so on.

Flipping that so the 'orchestrating' functions come first makes the code read a lot better.

First year using swift, here are my solutions:

1 Like

Could you be more specific? What performance differences did you observe?

My mistake, with the builder one I look for Anchor.endOfLine in addition of what I look for the the inline version. I'll run the benchmark again later today to check if perfs matches

1 Like

Hi all! Doing this again this year, though I'm going to keep telling myself not to be competitive about it, as trying and failing to beat @mayoff last year was stressful. :grinning:

My solutions are here: https://github.com/gregomni/aoc2022

Nothing very interesting there yet this year, except that I have a generic Grid type that I've built in previous years that is very useful for problems like Day 4.

3 Likes

Hello everyone, I’m participating in Advent of Code for the first time this year! GitHub - mackoj/aoc-2024: Advent of Code 2024 in Swift. I’ll do my best to make it to the end.

I really enjoy reading other people’s solutions to learn new approaches and techniques.

Thanks

2 Likes

For day 7 part 1 I used binary to relatively quickly generate the list of possible operators.

For day 7 part 2 I didn't do anything quite so elegant...

Hidden for those with delicate sensibilities
func getAllPossibleOperatorsPart2(_ equation: Equation) -> [[Operator]] {
    return aReallySlowCombinatorialExplosion(size: equation.operators.count)
}

nonisolated(unsafe) var sizeToOperators: [Int: [[Operator]]] = [:]
func aReallySlowCombinatorialExplosion(size: Int) -> [[Operator]] {
    if let cached = sizeToOperators[size] {
        return cached
    }
    if size <= 1 {
        return [[.add], [.mul], [.cat]]
    } else {
        let head = aReallySlowCombinatorialExplosion(size: 1)
        let tail = aReallySlowCombinatorialExplosion(size: size - 1)
        var results: [[Operator]] = []
        for headOperators in head {
            for tailOperators in tail {
                results.append(headOperators + tailOperators)
            }
        }
        sizeToOperators[size] = results
        return results
    }
}

I'm sure there's a simple, elegant, iterative algorithm to find that but I went for the 'beat the wall down with my forehead' option.

FWIW, using ternary for three operators works out just about the same as using binary for two.

Suppose you're trying to make goal 280 from sequence 11 6 16 20. That is, you want to find some operators , , and , so that 11 ⚀ 6 ⚁ 16 ⚂ 20 = 280.

Consider using + for the last operator : 11 ⚀ 6 ⚁ 16 + 20 = 280. Since we evaluate operators from left to right, this is equivalent to (11 ⚀ 6 ⚁ 16) + 20 = 280. So we can only replace with + if we can make 11 ⚀ 6 ⚁ 16 = 280 - 20 = 260. We should recursively try to make 260 from sequence 11 6 16, and if we succeed, then we can also make 280 from 11 6 16 20.

Note that we can skip the recursion unless 280 > 20, because we don't get negative numbers or zero as input and we cannot use - as an operator.

Also consider using * for the last operator : 11 ⚀ 6 ⚁ 16 * 20 = 280. This is equivalent to (11 ⚀ 6 ⚁ 16) * 20 = 280. So we can only replace with * if we can make 11 ⚀ 6 ⚁ 16 = 280 / 20 = 14. We should recursively try to make 14 from 11 6 16, and if we succeed, then we can also make 280 from 11 6 16 20.

Note that we can skip the recursion unless 280 is a multiple of 20 because we don't get fractions as input and we cannot use ÷ (division) as an operator.

Turning this into Swift code, we get a function like this:

func canMake(_ goal: Int, from inputs: ArraySlice<Int>) -> Bool {
    guard inputs.count > 1 else {
        return goal == inputs.first!
    }

    var inputs = inputs
    let last = inputs.removeLast()
    if goal > last && canMake(goal - last, from: inputs) {
        return true
    }
    if goal.isMultiple(of: last) && canMake(goal / last, from: inputs) {
        return true
    }
    return false
}

// Example use:
if canMake(280, from: [11, 6, 16, 20]) {
	answer += 280
}

For part 2, we get the new operator ||. We can handle this very similarly to how we handled + and *, except now we need to look at the decimal representations of the goal and of the last number in the sequence. If we're trying to make 12963 and the last number in the sequence is 3, then we can make 12963 as 1296 || 3, so we recursively ask whether we can make 1296 using the earlier part of the sequence.

func canMake(_ goal: Int, from inputs: ArraySlice<Int>) -> Bool {
    guard inputs.count > 1 else {
        return goal == inputs.first!
    }

    var inputs = inputs
    let last = inputs.removeLast()
    if goal > last && canMake(goal - last, from: inputs) {
        return true
    }
    if goal.isMultiple(of: last) && canMake(goal / last, from: inputs) {
        return true
    }

    let goalDigits = String(goal)
    let lastDigits = String(last)
    if goalDigits.count > lastDigits.count && goalDigits.hasSuffix(lastDigits) {
        let newGoal = Int(goalDigits.dropLast(lastDigits.count))!
        return canMake(newGoal, from: inputs)
    }

    return false
}
4 Likes

While working on day 7, I noticed something that hopefully someone here can explain. It doesn't seem to be possible to reference the + and * operator functions directly. Consider:

func t(f: (Int, Int) -> Int) {
    print(f(2, 3))
}
// Works:
t(f: +)

// Error: Expected expression after unary operator
let ops1: [(Int, Int) -> Int] = [+]
// Error: Cannot find operator '.+' in scope
let ops2: [(Int, Int) -> Int] = [Int.+]
// Error: Expected member name following '.'
let ops3: [(Int, Int) -> Int] = [Int.`+`]

I have to define a function plus to work around this. Why is that? Poking around the Swift code, I found that in IntegerTypes.swift.gyb there should be a method + defined in an extension to Int. So why can I use it in [3, 4].reduce(0, +) but can't add it to an array?

1 Like

This works:

let ops1: [(Int, Int) -> Int] = [(+)]

1 Like

You can also use quotientAndRemainder here to compute goal % last and goal / last.