this post was submitted on 10 Dec 2023
21 points (95.7% liked)

Advent Of Code

158 readers
25 users here now

An unofficial home for the advent of code community on programming.dev!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

AoC 2024

Solution Threads

M T W T F S S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 18 20 21 22
23 24 25

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 1 year ago
MODERATORS
21
submitted 11 months ago* (last edited 11 months ago) by [email protected] to c/[email protected]
 

Day 10: Pipe Maze

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ , pastebin, or github (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)

FAQ


🔒 Thread is locked until there's at least 100 2 star entries on the global leaderboard

🔓 Unlocked after 40 mins

you are viewing a single comment's thread
view the rest of the comments
[–] [email protected] 3 points 11 months ago (1 children)

The squeezing component in part 2 made this really interesting.

I had a thought on a naïve solution consisting of expanding the input grid, painting all the walked pipes, and then doing a flood fill from the outside of the expanded map. There are a lot cleverer ways to do it, but the idea stuck with me and so...

The code's a bit of a mess, but I actually kind of like the result. It visualizes really well and still runs both parts in under 8 seconds, so it's not even particularly slow considering how it does it.

E.g;
Picture of solution output

RubyA snippet from the expansion/flood fill;

def flood_fill(used: [])
  new_dim = @dim * 3
  new_map = Array.new(new_dim.size, '.')

  puts "Expanding #{@dim} to #{new_dim}, with #{used.size} visited pipes." if $args.verbose

  # Mark all real points as inside on the expanded map
  (0..(@dim.y - 1)).each do |y|
    (0..(@dim.x - 1)).each do |x|
      expanded_point = Point.new x * 3 + 1, y * 3 + 1
      new_map[expanded_point.y * new_dim.x + expanded_point.x] = 'I'
    end
  end

  # Paint all used pipes onto the expanded map
  used.each do |used_p|
    expanded_point = Point.new used_p.x * 3 + 1, used_p.y * 3 + 1

    new_map[expanded_point.y * new_dim.x + expanded_point.x] = '#'
    offsets = @links[used_p].connections
    offsets.shift

    offsets.each do |offs|
      diff = offs - used_p
      new_map[(expanded_point.y + diff.y) * new_dim.x + (expanded_point.x + diff.x)] = '#'
    end
  end

  puts "Flooding expanded map..." if $args.verbose

  # Flood fill the expanded map from the top-left corner
  to_visit = [Point.new(0, 0)]
  until to_visit.empty?
    at = to_visit.shift
    new_map[at.y * new_dim.x + at.x] = ' '

    (-1..1).each do |off_y|
      (-1..1).each do |off_x|
        next if (off_x.zero? && off_y.zero?) || !(off_x.zero? || off_y.zero?)

        off_p = at + Point.new(off_x, off_y)
        next if off_p.x < 0 || off_p.y < 0 \
          || off_p.x >= new_dim.x || off_p.y >= new_dim.y \
          || to_visit.include?(off_p)

        val = new_map[off_p.y * new_dim.x + off_p.x]
        next unless %w[. I].include? val

        to_visit << off_p
      end
    end
  end

  return new_map, new_dim
end

[–] [email protected] 2 points 11 months ago (1 children)

That's an interesting approach!

I wonder why it runs so slow, though, as far as I can tell the flooding code is just a BFS on the grid, so should be linear in number of cells?

[–] [email protected] 2 points 11 months ago (1 children)

With the fully expanded map for the actual input it ends up working a 420x420 tile grid, and it has to do both value lookups as well as mutations into that, alongside inclusion testing for the search array (which could probably be made cheaper by building it as a set). It ends up somewhat expensive simply on the number of tests.

The sample I posted the picture of runs in 0.07s wall time though.

[–] [email protected] 1 points 11 months ago* (last edited 11 months ago) (1 children)

Maybe you are adding the same point multiple times to to_visit. I don't know ruby but couldn't see a check for visited points before adding, and to_visit appears to be an array instead of set, which can store the same point multiple times.

[–] [email protected] 1 points 11 months ago* (last edited 11 months ago)

There's a next if [...] to_visit.include?(off_p), and I also only visit points that haven't been flood filled yet (next unless %w[. I].include? val), so there shouldn't be any superfluous testing going on.

Went back and did a quick test of thing, and yep, converting the to_visit array to a set pulls execution time down to ~600ms. But the code becomes much messier.
Going to move the mutation of the map down to the point where I pick a point for visitation instead, so I can re-use the check for already flooded tiles instead.