12 votes

Day 9: Smoke Basin

Today's problem description: https://adventofcode.com/2021/day/9

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

18 comments

  1. [2]
    spit-evil-olive-tips
    Link
    Part 1 my main stumbling block here was self-inflicted. I thought I'd be clever, consider the 4 neighboring squares without regard to overflowing the edges, and use Python's "easier to ask...
    Part 1

    my main stumbling block here was self-inflicted. I thought I'd be clever, consider the 4 neighboring squares without regard to overflowing the edges, and use Python's "easier to ask forgiveness than permission" approach and just catch IndexError and ignore it as a way of handling falling off the side of the map.

    of course, what I didn't realize at first is that when I'm at x=0 or y=0, the neighbor computation will generate -1 as an index, which is perfectly valid in Python, and means the other edge of the map. so I was inadvertently wrapping around in cases where I shouldn't have been.

    and frustratingly, this bug caused no problems at all with the example input, only with my actual input. so I played some printf debugging games of having it print out what it thought were the local minima, and then comparing that by eyeball against the map. once I realized the bug I added the explicit check for if neighbor_x < 0 or neighbor_y < 0 and the rest Just Worked.

    def main():
        with open('009.txt') as file:
            lines = file.readlines()
    
        map = []
        for line in lines:
            row = [int(height) for height in line.strip()]
            map.append(row)
    
        print(find_local_minima(map))
    
    
    def find_local_minima(map):
        total = 0
        for x, row in enumerate(map):
            for y, height in enumerate(row):
                neighbors = [
                    (x + 1, y),
                    (x - 1, y),
                    (x, y + 1),
                    (x, y - 1),
                ]
    
                is_local_minimum = True
                for neighbor_x, neighbor_y in neighbors:
                    if neighbor_x < 0 or neighbor_y < 0:
                        continue
    
                    try:
                        neighbor_value = map[neighbor_x][neighbor_y]
                        if height >= neighbor_value:
                            is_local_minimum = False
                    except IndexError:
                        pass
    
                if is_local_minimum:
                    total += height + 1
    
        return total
    
    main()
    
    Part 2

    I ended up needing "what are the neighbors of this cell?" in two places, so I split that out into its own function, and this time implemented my own bounds checking for the 4 edges of the map rather than using IndexErrors. this means that callers of get_neighbors can use the return value as-is without needing bounds-checking of their own.

    the basin idea made sense to me as a recursive definition - a basin is the low point, plus all the points around it that aren't 9s, plus all the points around them that aren't 9s, and so on.

    any recursive algorithm can be implemented in a non-recursive / iterative fashion, and I often find these implementations to be clearer / more understandable. so that's what I did.

    my find_basin function keeps a queue of pending points to be considered, starting with the single bottom point. any time I find a neighbor point that belongs to the current basin, I add it to the set of basin points, as well as the queue of pending points whose neighbors should be considered. when I run out of pending points, I know I've explored the entire basin.

    def main():
        with open('009.txt') as file:
            lines = file.readlines()
    
        map = []
        for line in lines:
            row = [int(height) for height in line.strip()]
            map.append(row)
    
        minima = find_local_minima(map)
    
        basins = []
        for minimum in minima:
            basin = find_basin(map, minimum)
            basins.append(basin)
    
        sorted_basins = list(sorted(basins, reverse=True, key=lambda basin: len(basin)))
        print(len(sorted_basins[0]) * len(sorted_basins[1]) * len(sorted_basins[2]))
    
    
    def find_basin(map, bottom):
        points = set([bottom])
        pending = [bottom]
    
        while pending:
            current = pending.pop()
            current_x, current_y = current
            current_value = map[current_x][current_y]
    
            for neighbor in get_neighbors(map, current_x, current_y):
                neighbor_x, neighbor_y = neighbor
                neighbor_value = map[neighbor_x][neighbor_y]
                if neighbor_value != 9 and neighbor_value > current_value:
                    if neighbor not in points:
                        points.add(neighbor)
                        pending.append(neighbor)
    
        return points
    
    
    def find_local_minima(map):
        minima = []
        for x, row in enumerate(map):
            for y, height in enumerate(row):
                neighbors = get_neighbors(map, x, y)
    
                is_local_minimum = True
                for neighbor_x, neighbor_y in neighbors:
                    neighbor_value = map[neighbor_x][neighbor_y]
                    if height >= neighbor_value:
                        is_local_minimum = False
    
                if is_local_minimum:
                    minima.append((x, y))
    
        return minima
    
    
    def get_neighbors(map, x, y):
        neighbors = [
            (x + 1, y),
            (x - 1, y),
            (x, y + 1),
            (x, y - 1),
        ]
    
        actual_neighbors = []
        for neighbor_x, neighbor_y in neighbors:
            if neighbor_x < 0 or neighbor_y < 0:
                continue
    
            if neighbor_x >= len(map):
                continue
    
            if neighbor_y >= len(map[0]):
                continue
    
            actual_neighbors.append((neighbor_x, neighbor_y))
    
        return actual_neighbors
    
    main()
    
    8 votes
    1. PapaNachos
      Link Parent
      I hope it didn't cause you too much frustration and this doesn't seem mean spirited, but that bug in particular is really, really funny.

      I hope it didn't cause you too much frustration and this doesn't seem mean spirited, but that bug in particular is really, really funny.

      6 votes
  2. tjf
    (edited )
    Link
    Python golf, day 9. Tips/improvements always welcome. Part 1 m=[[*map(int,l[:-1])]for l in open(0)] print(sum((1+m[r][c]for r in range(len(m))for c in range(len(m[0]))if all(0>r+i or r+i>=len(m)or...

    Python golf, day 9. Tips/improvements always welcome.

    Part 1
    m=[[*map(int,l[:-1])]for l in open(0)]
    print(sum((1+m[r][c]for r in range(len(m))for c in range(len(m[0]))if all(0>r+i or r+i>=len(m)or 0>c+j or c+j>=len(m[0])or m[r+i][c+j]>m[r][c]for i,j in((0,-1),(0,1),(-1,0),(1,0))))))
    

    Recursion came in handy for part 2.

    Part 2
    import math
    m=[[*map(int,l[:-1])]for l in open(0)]
    def f(r,c):
     if m[r][c]==9:return 0
     m[r][c]=9;return sum((f(r+i,c+j)for(i,j)in((0,-1),(0,1),(-1,0),(1,0))if 0<=r+i<len(m)and 0<=c+j<len(m[0])))+1
    print(math.prod(sorted(f(r,c)for r in range(len(m))for c in range(len(m[0]))if m[r][c]-9)[-3:]))
    

    Edit: Today is day 9, not day 8 lol. I am tired.

    6 votes
  3. DataWraith
    (edited )
    Link
    Day 09 (Rust) Looks like Eric is giving us a simple one to relax after yesterday's tough nut. That said, I'm sure there is some advanced trickery that makes it possible to speed things up, but...

    Day 09 (Rust)

    Looks like Eric is giving us a simple one to relax after yesterday's tough nut.

    That said, I'm sure there is some advanced trickery that makes it possible to speed things up, but today I solved everything in the most-obvious way possible.

    Imports & Parsing
    use std::collections::{HashSet, VecDeque};
    
    use nom::{character::complete::multispace0, IResult};
    
    type Row = Vec<u8>;
    type Grid = Vec<Row>;
    
    fn parse_row(input: &str) -> IResult<&str, Row> {
        let (input, _) = multispace0(input)?;
        let (input, row) = nom::character::complete::digit1(input)?;
    
        let row = row.chars().map(|c| c as u8 - '0' as u8).collect();
    
        Ok((input, row))
    }
    
    fn parse_grid(input: &str) -> IResult<&str, Grid> {
        let (input, grid) = nom::multi::many1(parse_row)(input)?;
        let (input, _) = multispace0(input)?;
        let (input, _) = nom::combinator::eof(input)?;
    
        Ok((input, grid))
    }
    
    Helper functions
    // Returns a vector with the coordinates of all low points.
    // It finds them in the obvious way: by
    // iterating through the grid and checking the neighbors.
    fn low_points(grid: &Grid) -> Vec<(usize, usize)> {
        let height = grid.len();
        let width = grid[0].len();
        let mut result = Vec::new();
    
        for y in 0..height {
            for x in 0..width {
                let val = grid[y][x];
    
                // North
                if y > 0 && grid[y - 1][x] <= val {
                    continue;
                }
    
                // East
                if x + 1 < width && grid[y][x + 1] <= val {
                    continue;
                }
    
                // South
                if y + 1 < height && grid[y + 1][x] <= val {
                    continue;
                }
    
                // West
                if x > 0 && grid[y][x - 1] <= val {
                    continue;
                }
    
                result.push((x, y));
            }
        }
    
        result
    }
    
    // Starting from a low_point, finds the basin size using a simple floodfill.
    fn find_basin_size(grid: &Grid, start: (usize, usize)) -> usize {
        let height = grid.len();
        let width = grid[0].len();
    
        let mut q = VecDeque::new();
        let mut seen = HashSet::new();
        let mut size = 0;
    
        q.push_back(start);
    
        while let Some(coordinate) = q.pop_front() {
            if seen.contains(&coordinate) {
                continue;
            }
    
            seen.insert(coordinate);
    
            size += 1;
    
            let (x, y) = coordinate;
            let val = grid[y][x];
    
            if y > 0 && grid[y - 1][x] > val && grid[y - 1][x] != 9 {
                q.push_back((x, y - 1));
            }
    
            if y + 1 < height && grid[y + 1][x] > val && grid[y + 1][x] != 9 {
                q.push_back((x, y + 1));
            }
    
            if x > 0 && grid[y][x - 1] > val && grid[y][x - 1] != 9 {
                q.push_back((x - 1, y));
            }
    
            if x + 1 < width && grid[y][x + 1] > val && grid[y][x + 1] != 9 {
                q.push_back((x + 1, y));
            }
        }
    
        size
    }
    
    Solutions
    fn part1(grid: &Grid) -> usize {
        let mut sum = 0;
    
        for (x, y) in low_points(grid) {
            sum += 1;
            sum += grid[y][x] as usize;
        }
    
        sum
    }
    
    fn part2(grid: &Grid) -> usize {
        let mut basin_sizes: Vec<usize> = low_points(grid)
            .iter()
            .map(|&s| find_basin_size(grid, s))
            .collect();
    
        basin_sizes.sort_unstable();
        basin_sizes.iter().rev().take(3).product()
    }
    
    fn main() {
        let input = include_str!("../../input-09.txt");
        let grid = parse_grid(input).expect("Failed to parse puzzle").1;
    
        println!("Part I:  {}", part1(&grid));
        println!("Part II: {}", part2(&grid));
    }
    
    6 votes
  4. Crespyl
    Link
    Hey it's the first map puzzle! Since I'm using Ruby again, I went ahead and pulled in a utility class for working with two-dimensional grids that I made for previous years. The first part was...

    Hey it's the first map puzzle!

    Since I'm using Ruby again, I went ahead and pulled in a utility class for working with two-dimensional grids that I made for previous years.

    The first part was pretty straight forward, but I lost a bit of time in the second part just being a little rusty on exactly how I needed the algorithm to behave.

    Part 1 Ruby
    def compute_p1(input)
      sum = 0
      grid = Grid.new(input)
      grid.each_index do |x,y|
        value = grid.get(x,y).to_i
        neighbors = grid.neighbors(x,y).reject {_1.nil?}.map(&:to_i)
        sum += (1+value) if neighbors.all? { _1 > value }
      end
      return sum
    end
    
    class Grid
      attr_accessor :grid
      attr_reader :width
      attr_reader :height
      attr_reader :default
    
      def initialize(input, default: nil)
        @grid = input.lines
                  .map(&:strip)
                  .map(&:chars)
        @width = @grid[0].size
        @height = @grid.size
        @default = default
      end
    
      def get(x,y)
        if x < 0 || x >= @width || y < 0 || y >= @height
          @default
        else
          @grid[y][x]
        end
      end
    
      def set(x,y,val)
        if x < 0 || x >= @width || y < 0 || y >= @height
          raise "Tried to write out of bounds"
        else
          @grid[y][x] = val
        end
      end
    
      def each_index
        height.times do |y|
          width.times do |x|
            yield(x,y)
          end
        end
      end
    
      def ==(other)
        return false if other.class != Grid
        return other.grid == @grid
      end
    
      def neighbors(x,y)
        [
          [-1, -1], [0, -1], [+1, -1],
          [-1,  0],          [+1, 0],
          [-1, +1], [0, +1], [+1, +1]
        ].map { |dx, dy| get(x+dx, y+dy) }
      end
    
      def to_s
        s = ""
        height.times do |y|
          width.times do |x|
            s << get(x,y) || default.to_s
          end
          s << "\n"
        end
        return s
      end
    
      def count(value)
        @grid.flatten.count(value)
      end
    end
    
    Part 2 Ruby
    def compute_p2(input)
      grid = Grid.new(input)
    
      low_points = []
      grid.each_index do |x,y|
        value = grid.get(x,y).to_i
        neighbors = grid.neighbors(x,y).reject {_1.nil?}.map(&:to_i)
        low_points.push([x,y]) if neighbors.all? { _1 > value }
      end
    
      basin_sizes = Hash.new(0)
    
      low_points.each do |point|
        visited = []
        open_set = [point]
    
        until open_set.empty?
          cur_x, cur_y = open_set.shift
          visited.push([cur_x, cur_y])
    
          [
                     [0, -1],
            [-1,  0],        [+1, 0],
                     [0, +1]
          ].each do |dx, dy|
            nx, ny = cur_x + dx, cur_y + dy
            next if visited.include?([nx, ny]) || open_set.include?([nx,ny])
    
            cell = grid.get(nx, ny)
            next if cell.nil? || cell == '9'
    
            open_set.push([nx, ny])
          end
        end
    
        basin_sizes[point] += visited.uniq.size
      end
    
      basin_sizes.values.sort[-3..].inject(:*)
    end
    

    I ran into a minor hitch in that I initially wanted to use an actual Set object for the visited/open sets, but the default implementation in the stdlib doesn't really support easily removing an arbitrary value the way I can with Array#shift/Array#pop (since a set is unordered by definition, and shift/pop rely on the collection having an order). Arrays work fine, but I have to go a bit out of the way to make sure I don't end up with duplicate entries.

    5 votes
  5. PapaNachos
    Link
    With problems like these I swear that my biggest problem is always keeping X and Y straight. Day 9 - Part A - Python Part A wasn't too bad, just built a grid with defaultdicts and searched it to...

    With problems like these I swear that my biggest problem is always keeping X and Y straight.

    Day 9 - Part A - Python

    Part A wasn't too bad, just built a grid with defaultdicts and searched it to find the low points. Mostly just keeping track of the boundaries and what-not. At some point I managed to swap my x and y axes, but I just ran with it.

    Interestingly, using the default dict grid instead of my normal stacked lists meant that going out of bounds would automatically generate a new entry into the dictionary and then crash because the dictionary size changed mid-loop, instead of just crashing due to an array out of bounds exception

    from collections import defaultdict
    
    #data = test_data
    data = real_data
    data = data.split('\n')
    
    grid = defaultdict(lambda: defaultdict(int))
    
    for index, row in enumerate(data):
        for index2, val in enumerate(list(row)):
            grid[index2][index] = int(val)
    
    bounds_y = len(grid.keys())
    bounds_x = len(grid[0].keys())
    
    directions = [(1,0),(-1,0),(0,1),(0,-1)]
    
    threat = 0
    
    for y_pos in grid.keys():
        row = ""
        for x_pos in grid[y_pos].keys():
            lower_found = False
            for direction in directions:
                nearby_x = x_pos + direction[0]
                nearby_y = y_pos + direction[1]
                if nearby_x >= 0 and nearby_x < bounds_x and nearby_y >= 0 and nearby_y < bounds_y:
                    if grid[nearby_y][nearby_x] <= grid[y_pos][x_pos]: #Assumes there are no ties
                        lower_found = True
            if not lower_found:
                row = row + "X"
                #print(grid[y_pos][x_pos] + 1)
                threat = threat + grid[y_pos][x_pos] + 1
            else:
                row = row + str(grid[y_pos][x_pos])
        #print(row)
    print(threat)
    
    Day 9 - Part B - Python

    This was a bit more difficult. I used the low points I generated in part A to build a list of starting points. Then I recursively searched outward from those points, adding any nearby locations that weren't already part of the basin. When there was nothing left to find I just took the size of the basin and shoved it into a list.

    from collections import defaultdict
    
    #data = test_data
    data = real_data
    data = data.split('\n')
    
    grid = defaultdict(lambda: defaultdict(int))
    
    for index, row in enumerate(data):
        for index2, val in enumerate(list(row)):
            grid[index2][index] = int(val)
    
    bounds_y = len(grid.keys())
    bounds_x = len(grid[0].keys())
    
    directions = [(1,0),(-1,0),(0,1),(0,-1)]
    
    low_point_list = []
    for y_pos in grid.keys():
        row = ""
        for x_pos in grid[y_pos].keys():
            lower_found = False
            for direction in directions:
                nearby_x = x_pos + direction[0]
                nearby_y = y_pos + direction[1]
                if nearby_x >= 0 and nearby_x < bounds_x and nearby_y >= 0 and nearby_y < bounds_y:
                    if grid[nearby_y][nearby_x] <= grid[y_pos][x_pos]: #Assumes there are no ties
                        lower_found = True
            if not lower_found:
                row = row + "X"
                #print(grid[y_pos][x_pos] + 1)
                low_point_list.append((y_pos, x_pos))
                threat = threat + grid[y_pos][x_pos] + 1
            else:
                row = row + str(grid[y_pos][x_pos])
        #print(row)
    def search_nearby(location, basin):
        x_pos = location[1]
        y_pos = location[0]
        for direction in directions:
            nearby_x = x_pos + direction[0]
            nearby_y = y_pos + direction[1]
            if (nearby_y, nearby_x)  not in basin:
                if nearby_x >= 0 and nearby_x < bounds_x and nearby_y >= 0 and nearby_y < bounds_y:
                    if grid[nearby_y][nearby_x] < 9:
                        basin.append((nearby_y, nearby_x))
                        search_nearby((nearby_y, nearby_x), basin)
    
    basin_sizes = []                    
    for low_point in low_point_list:
        basin = [low_point]
        search_nearby(low_point, basin)
        basin_sizes.append(len(basin))
    basin_sizes.sort()
    print(basin_sizes)
    print(basin_sizes[-3] * basin_sizes[-2] * basin_sizes[-1])
    
    Tips
    • Make sure to try to keep X and Y straight. It's alright if you flip them from what the input is, but you need to be consistent about how YOU use them

    • There are only 4 directions neighboring each point, but sometimes those checks will send you out of bounds of your grid. Think about how to stop your program from crashing or giving strange results when that happens

    • If you're not careful about how you search for the basins, you might end up in an infinite loop of checking for locations that are already stored in your basin

    5 votes
  6. tomf
    Link
    Nothing fancy here. Part 1 Split the input with "=ARRAYFORMULA( IF(ISBLANK(A2:A),, SPLIT( REGEXREPLACE(A2:A,""(.)"",""$1|""), ""|"")))" then pop this in and drag it around for the 100x100 "=IF(...

    Nothing fancy here.

    Part 1

    Split the input with

    "=ARRAYFORMULA(
      IF(ISBLANK(A2:A),,
       SPLIT(
        REGEXREPLACE(A2:A,""(.)"",""$1|""),
        ""|"")))"
    

    then pop this in and drag it around for the 100x100

    "=IF(
      AND(
       F2<IF(ISBLANK(F1),9,F1),
       F2<IF(ISBLANK(G2),9,G2),
       F2<IF(ISBLANK(F3),9,F3),
       F2<IF(ISBLANK(E2),9,E2)),
      F2+1,)"
    

    I hate formulas like this

    I shit you not. I looked at part two and figured it was impossible with a spreadsheet. I figured I'd just eyeball it --- so I styled the 9s as black and looked for what I thought were the largest areas. I selected a bunch of cells, documented the count... after five of these I was over it. I took the top three, multiplied, and bam -- topaz liked it. Not as fulfilling as sorting out yesterday's, but I'll take it.

    5 votes
  7. pnutzh4x0r
    Link
    JavaScript Repo Link Part 1 This was pretty straightforward. The only trick I used was to pad the heatmap to avoid bounds checking. const MAX = 9; const DIRECTIONS = [ [ 0, -1], // Left [ 0, 1],...

    JavaScript

    Repo Link

    Part 1

    This was pretty straightforward. The only trick I used was to pad the heatmap to avoid bounds checking.

    const MAX = 9;
    const DIRECTIONS = [
        [ 0, -1], // Left
        [ 0,  1], // Right
        [-1,  0], // Up
        [ 1,  0]  // Down
    ];
    
    function padmap(map) {
        let padrow = new Array(map[0].length + 2).fill(MAX);
        return [padrow, ...map.map(row => [MAX, ...row, MAX]), padrow];
    }
    
    function find_low_points(heatmap) {
        let low_points = [];
    
        for (let row = 1; row < heatmap.length - 1; row++) {
            for (let col = 1; col < heatmap[0].length - 1; col++) {
                let is_lower = DIRECTIONS.filter(([dr, dc]) =>
                    heatmap[row][col] < heatmap[row + dr][col + dc]
                );
                if (is_lower.length == DIRECTIONS.length) {
                    low_points.push([heatmap[row][col], row, col]);
                }
            }
        }
    
        return low_points;
    }
    
    let heatmap    = padmap(IO.readlines(line => line.split('').map(n => parseInt(n, 10))));
    let low_points = find_low_points(heatmap);
    let part_a     = low_points.reduce((t, p) => t + p[0] + 1, 0);
    IO.print(`Part A: ${part_a}`);
    
    Part 2

    For this part, once you have the low points, all you need to do is perform a walk or traversal from each low point until you run out of locations (ie. no more 9s). For this, I used a traditional iterative frontier based approach.

    In terms of JavaScript there were still a few gotchas that I am still learning. For instance, adding an Array to a Set works, but it compares the reference/pointer instead of the contents, so you can never find the Array again. To work around this, I gave each location or node an integer identifier based on its row and column.

    function find_basins(heatmap, low_points) {
        return low_points.map(([_, row, col]) => walk_heatmap(heatmap, [row, col]));
    }
    
    function walk_heatmap(heatmap, start) {
        let frontier = [start];
        let visited  = new Set();
    
        while (frontier.length) {
            let [row, col] = frontier.shift();
            let node_id    = row*heatmap[0].length + col;
    
            if (visited.has(node_id)) {
                continue;
            }
    
            visited.add(node_id);
    
            DIRECTIONS.forEach(([dr, dc]) => {
                if (heatmap[row + dr][col + dc] < MAX) {
                    frontier.push([row + dr, col + dc]);
                }
            });
        }
    
        return visited.size;
    }
    
    let basins     = find_basins(heatmap, low_points);
    let part_b     = basins.sort((a, b) => b - a).slice(0, 3).reduce((s, n) => s * n, 1);
    IO.print(`Part B: ${part_b}`);
    
    5 votes
  8. Crestwave
    Link
    Classic flood fill. POSIX AWK doesn't have asort() so getting the highest 3 numbers was jankier than it had any right to be. Part 1 #!/usr/bin/awk -f BEGIN { FS = "" } { for (x = 1; x <= NF; ++x)...

    Classic flood fill.
    POSIX AWK doesn't have asort() so getting the highest 3 numbers was jankier than it had any right to be.

    Part 1
    #!/usr/bin/awk -f
    BEGIN { FS = "" }
    
    {
    	for (x = 1; x <= NF; ++x)
    		grid[x, NR] = $x
    }
    
    END {
    	for (i in grid) {
    		split(i, xy, SUBSEP)
    		x = xy[1]
    		y = xy[2]
    
    		flag = 1
    
    		for (ox = -1; ox <= 1; ox += 2) {
    			adj = grid[x + ox, y]
    			if (grid[x, y] >= adj && adj != "")
    				flag = 0
    
    			adj = grid[x, y + ox]
    			if (grid[x, y] >= adj && adj != "")
    				flag = 0
    		}
    
    		if (flag)
    			total += grid[x, y] + 1
    	}
    
    	print total
    }
    
    Part 2
    #!/usr/bin/awk -f
    function flood(x, y, z) {
    	if (grid[x, y] >= z && grid[x, y] != 9 && grid[x, y] != "") {
    		size += 1
    		z = grid[x, y]
    		grid[x, y] = 9
    
    		flood(x - 1, y, z)
    		flood(x + 1, y, z)
    		flood(x, y - 1, z)
    		flood(x, y + 1, z)
    	}
    }
    
    BEGIN { FS = "" }
    
    {
    	for (x = 1; x <= NF; ++x)
    		grid[x, NR] = $x
    }
    
    END {
    	for (i in grid) {
    		split(i, xy, SUBSEP)
    		x = xy[1]
    		y = xy[2]
    
    		flag = 1
    
    		for (ox = -1; ox <= 1; ox += 2) {
    			adj = grid[x + ox, y]
    			if (grid[x, y] >= adj && adj != "")
    				flag = 0
    
    			adj = grid[x, y + ox]
    			if (grid[x, y] >= adj && adj != "")
    				flag = 0
    		}
    
    		if (flag)
    			pt[x, y] = grid[x, y]
    	}
    
    	for (i in pt) {
    		split(i, xy, SUBSEP)
    		size = 0
    
    		flood(xy[1], xy[2], pt[i])
    		sizes[size] += 1
    
    		if (size > max || max == "")
    			max = size
    	}
    
    	k = 3
    
    	for (i = max; i >= 0 && k > 0; --i) {
    		for (j = sizes[i]; j > 0 && k > 0; --j) {
    			k -= 1
    
    			if (total == "")
    				total = i
    			else
    				total *= i
    		}
    	}
    
    	print total
    }
    
    4 votes
  9. wycy
    (edited )
    Link
    Rust Rust use std::env; use std::io::{self, prelude::*, BufReader}; use std::fs::File; use std::collections::{HashMap,HashSet}; use point2d::point2d::Point2D; fn neighbor_addresses(pt: &Point2D)...

    Rust

    Rust
    use std::env;
    use std::io::{self, prelude::*, BufReader};
    use std::fs::File;
    use std::collections::{HashMap,HashSet};
    
    use point2d::point2d::Point2D;
    
    fn neighbor_addresses(pt: &Point2D) -> Vec<Point2D> {
        vec![Point2D{ x: pt.x  , y: pt.y-1 },  // up
             Point2D{ x: pt.x+1, y: pt.y   },  // right
             Point2D{ x: pt.x  , y: pt.y+1 },  // down
             Point2D{ x: pt.x-1, y: pt.y   }]  // left
    }
    
    fn neighbor_values(pt: &Point2D, map: &HashMap<Point2D,i64>) -> Vec<i64> {
        let neighbors = neighbor_addresses(&pt);
        let mut values: Vec<i64> = Vec::new();
        for neighbor in neighbors {
            match map.get(&neighbor) {
                Some(value) => values.push(*value),
                None => {},
            }
        }
        values
    }
    
    fn is_low(pt: Point2D, map: &HashMap<Point2D,i64>) -> bool {
        let neighbor_values = neighbor_values(&pt, &map);
        map.get(&pt).unwrap() < neighbor_values.iter().min().unwrap()
    }
    
    fn uphill_neighbors(pt: Point2D, map: &HashMap<Point2D,i64>) -> Vec<Point2D> {
        let this_value = map.get(&pt).unwrap();
        let neighbors = neighbor_addresses(&pt);
        let mut uphill: Vec<Point2D> = Vec::new();
        for neighbor in neighbors {
            match map.get(&neighbor) {
                Some(value) => {
                    if value > this_value && value < &9 {
                        uphill.push(neighbor);
                        for n in uphill_neighbors(neighbor,&map) { uphill.push(n); }
                    }
                },
                None => {},
            }
        }
        uphill
    }
    
    fn risk_level(pt: Point2D, map: &HashMap<Point2D,i64>) -> i64 {
        map.get(&pt).unwrap() + 1
    }
    
    fn map_extents(map: &HashMap<Point2D,i64>) -> (i64,i64,i64,i64) {
        let xmin = &map.keys().map(|&pt| pt.x).min().unwrap();
        let ymin = &map.keys().map(|&pt| pt.y).min().unwrap();
        let xmax = &map.keys().map(|&pt| pt.x).max().unwrap();
        let ymax = &map.keys().map(|&pt| pt.y).max().unwrap();
        (*xmin,*ymin,*xmax,*ymax)
    }
    
    fn solve(input: &str) -> io::Result<()> {
        let file = File::open(input).expect("Input file not found.");
        let reader = BufReader::new(file);
    
        // Input
        let input: Vec<String> = match reader.lines().collect() {
            Err(err) => panic!("Unknown error reading input: {}", err),
            Ok(result) => result,
        };
    
        // Build smoke map
        let mut smoke_map: HashMap<Point2D,i64> = HashMap::new();
        for (y,line) in input.iter().enumerate() {
            for (x,ch) in line.chars().enumerate() {
                let (x,y,v) = (x as i64, y as i64, ch.to_digit(10).unwrap());
                smoke_map.insert(Point2D{x,y},v.into());
            }
        }
    
        // Part 1
        let mut part1: i64 = 0;
        let mut basins: Vec<Point2D> = Vec::new();
        let (xmin,ymin,xmax,ymax) = map_extents(&smoke_map);
        for y in ymin..=ymax {
            for x in xmin..=xmax {
                let pt = Point2D { x: x, y: y };
                if is_low(pt,&smoke_map) {
                    basins.push(pt);
                    part1 += risk_level(pt,&smoke_map);
                }
            }
        }
        println!("Part 1: {}", part1); // 575
    
        // Part 2
        let mut basin_sizes: Vec<usize> = Vec::new();
        for basin in basins {
            let basin = uphill_neighbors(basin,&smoke_map);
            let this_basin: HashSet<Point2D> = HashSet::from_iter(basin);
            let basin_size = this_basin.len() + 1;
            basin_sizes.push(basin_size);
        }
        basin_sizes.sort_by(|a,b| b.cmp(a)); // descending sort
        let part2 = basin_sizes[0] * basin_sizes[1] * basin_sizes[2];
        println!("Part 2: {}", part2); // 1019700
    
        Ok(())
    }
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        solve(&filename).unwrap();
    }
    
    Point2D
    pub mod point2d {
        use std::ops::{Add,AddAssign};
        use std::iter::Sum;
        extern crate num_complex;
        use num_complex::Complex;
    
        #[derive(Debug, Copy, Clone, Hash)]
        pub struct Point2D {
            pub x: i64,
            pub y: i64,
        }
    
        impl Point2D {
            pub fn new(x: i64, y: i64) -> Self {
                Self {
                    x: x, y: y,
                }
            }
            pub fn zeros() -> Self {
                Self {
                    x: 0, y: 0,
                }
            }
            pub fn from_complex(cmplx: Complex<i64>) -> Self {
                Self {
                    x: cmplx.re, y: cmplx.im,
                }
            }
            pub fn as_complex(self) -> Complex<i64> {
                Complex::new(self.x, self.y)
            }
            pub fn as_tuple(self) -> (i64,i64) {
                (self.x, self.y)
            }
        }
        impl Add for Point2D {
            type Output = Self;
            fn add(self, other: Self) -> Self {
                Self {
                    x: self.x + other.x,
                    y: self.y + other.y,
                }
            }
        }
        impl AddAssign for Point2D {
            fn add_assign(&mut self, other: Self) {
                *self = Self {
                    x: self.x + other.x,
                    y: self.y + other.y,
                };
            }
        }
        impl PartialEq for Point2D {
            fn eq(&self, other: &Self) -> bool {
                self.x == other.x && self.y == other.y
            }
        }
        impl Eq for Point2D {}
    
        impl Sum for Point2D {
            fn sum<I>(iter: I) -> Self
            where
                I: Iterator<Item = Self>,
            {
                iter.fold(Self { x: 0, y: 0 }, |a, b| Self {
                    x: a.x + b.x,
                    y: a.y + b.y,
                })
            }
        }
    }
    
    4 votes
  10. kari
    Link
    I didn't even bother posting my code for yesterday ( I didn't finish part 2 until this morning) since my code was so gross, but I enjoyed today's problems! Rust (both parts) I probably could've...

    I didn't even bother posting my code for yesterday ( I didn't finish part 2 until this morning) since my code was so gross, but I enjoyed today's problems!

    Rust (both parts)

    I probably could've used structs but I think my code is pretty readable and reasonably concise as it is :)

    use crate::lib::aoc;
    use std::collections::BinaryHeap;
    use std::collections::HashSet;
    use std::collections::VecDeque;
    
    // Assumes that (row, col) are valid coordinates
    fn get_neighbor_coords(
        heightmap: &[Vec<u32>],
        (row, col): (usize, usize),
    ) -> HashSet<(usize, usize)> {
        if row == 0 && col == 0 {
            // Top-left corner
            HashSet::from([(row + 1, col), (row, col + 1)])
        } else if row == heightmap.len() - 1 && col == 0 {
            // Bottom-left corner
            HashSet::from([(row - 1, col), (row, col + 1)])
        } else if row == 0 && col == heightmap[row].len() - 1 {
            // Top-right corner
            HashSet::from([(row + 1, col), (row, col - 1)])
        } else if row == heightmap.len() - 1 && col == heightmap[row].len() - 1 {
            // Bottom-right corner
            HashSet::from([(row - 1, col), (row, col - 1)])
        } else if row == 0 {
            // Top-row, not a corner
            HashSet::from([(row + 1, col), (row, col + 1), (row, col - 1)])
        } else if row == heightmap.len() - 1 {
            // Bottom-row, not a corner
            HashSet::from([(row - 1, col), (row, col + 1), (row, col - 1)])
        } else if col == 0 {
            // Left-column, not a corner
            HashSet::from([(row + 1, col), (row - 1, col), (row, col + 1)])
        } else if col == heightmap[row].len() - 1 {
            // Right-column, not a corner
            HashSet::from([(row + 1, col), (row - 1, col), (row, col - 1)])
        } else {
            // Not on any edge
            HashSet::from([
                (row + 1, col),
                (row - 1, col),
                (row, col + 1),
                (row, col - 1),
            ])
        }
    }
    
    fn get_basin_size(heightmap: &[Vec<u32>], (row, col): (usize, usize)) -> u32 {
        // Use a bfs???
        let mut queue: VecDeque<(usize, usize)> = VecDeque::from([(row, col)]);
        let mut have_been_explored: HashSet<(usize, usize)> = HashSet::from([(row, col)]);
        while !queue.is_empty() {
            let (row, col) = queue.pop_front().unwrap();
            // 9s are crests, i.e. they aren't in the basin
            if heightmap[row][col] == 9 {
                continue;
            }
            // Now check all edges
            for coord in get_neighbor_coords(heightmap, (row, col)) {
                if heightmap[coord.0][coord.1] != 9 && !have_been_explored.contains(&coord) {
                    have_been_explored.insert(coord);
                    queue.push_back(coord);
                }
            }
        }
    
        have_been_explored.len() as u32
    }
    
    pub fn run() {
        let heightmap: Vec<Vec<u32>> = aoc::get_lines("./inputs/day9.in")
            .iter()
            .map(|line| {
                line.chars().fold(Vec::new(), |mut accum, cur| {
                    accum.push(cur.to_digit(10).expect("Failed to convert a char to u32!"));
                    accum
                })
            })
            .collect();
    
        let mut low_points: HashSet<(usize, usize)> = HashSet::new();
        for row in 0..heightmap.len() {
            for col in 0..heightmap[row].len() {
                let neighbor_coords = get_neighbor_coords(&heightmap, (row, col));
                let is_low_point =
                    neighbor_coords
                        .iter()
                        .fold(true, |is_low_point, (neighbor_row, neighbor_col)| {
                            is_low_point
                                && heightmap[row][col] < heightmap[*neighbor_row][*neighbor_col]
                        });
                if is_low_point {
                    low_points.insert((row, col));
                }
            }
        }
    
        let low_point_risk_values: Vec<u32> = low_points
            .iter()
            .map(|(row, col)| heightmap[*row][*col] + 1)
            .collect();
    
        let low_point_risk_values_sum = low_point_risk_values.iter().sum();
    
        let mut basin_sizes: BinaryHeap<u32> = BinaryHeap::new();
        for low_point in low_points {
            basin_sizes.push(get_basin_size(&heightmap, low_point));
        }
    
        let multiplicand_largest_3_basins =
            basin_sizes.pop().unwrap() * basin_sizes.pop().unwrap() * basin_sizes.pop().unwrap();
    
        aoc::output(
            9,
            "sum",
            low_point_risk_values_sum,
            multiplicand_largest_3_basins,
        );
    }
    
    4 votes
  11. bhrgunatha
    Link
    Data Structure Today is the 2nd day I've had prior engagements which mean I was very late starting. I'm never going to compete for the top 100 but I'm not going to do well on any of the private...
    Data Structure

    Today is the 2nd day I've had prior engagements which mean I was very late starting. I'm never going to compete for the top 100 but I'm not going to do well on any of the private leaderboards I joined now. Oh well.

    I converting every digit of my input into a number and made a hash table to map point -> height. It's actually not a graph.

    (define input (graph (map-input digits)))
    
    (define (digits l)
      (map string->number (regexp-match* #rx"[0-9]" l)))
    
    (define (graph input)
      (for*/hash ([(row r) (in-indexed input)]
                  [(height c) (in-indexed row)])
        (values (list r c) height)))
    
    Part 1

    The strategy is to find points lower than all their neighbours. Don't be stupid like me and think there must be 4 regions in your input because the example had 4.
    I didn't bother with bounds checking generating neighbours because height is 9 or less, so I used a default height of 10 for missing neighbours.

    (define (part-01 input)
      (for*/sum ([(point height) (in-hash input)]
                 #:when (lowest? input point height))
        (add1 height)))
    
    (define (lowest? G p h)
      (for/and ([n (neighbours G p)])
        (< h (hash-ref G n 10))))
    
    (define (neighbours G p)
      (match-define (list x y) p)
      (list (list (add1 x) y)
            (list (sub1 x) y)
            (list x (add1 y))
            (list x (sub1 y))))
    
    Part 2

    I generated a list of all basin sizes starting from each of the lowest points. Basin size is the number of visited points using a basic DFS. I ignored any points over 8 (which excludes the default of 10 for missing points again.)

    (define (part-02 input)
      (define basins
        (for*/list ([(point height) (in-hash input)]
                    #:when (lowest? input point height))
          (basin-size input point)))
      (match-define (list* 1st 2nd 3rd _) (sort basins >))
      (* 1st 2nd 3rd))
    
    (define (basin-size G p)
      (define (expand p visited)
        (for/fold ([visited visited])
                  ([n (neighbours G p)]
                   #:unless (set-member? visited n)
                   #:unless (> (hash-ref G n 10) 8))
          (set-union (expand n (set-add visited n)))))
      (set-count (expand p (set p))))
    
    3 votes
  12. asterisk
    Link
    Python import math heightmap = [[int(i) for i in line] for line in open("input.txt").read().split()] risk = int(0) basins = list() def hm(c: list) -> int: r, s = c return heightmap[r][s] def...
    Python
    import math
    
    heightmap = [[int(i) for i in line] for line in open("input.txt").read().split()]
    risk = int(0)
    basins = list()
    
    
    def hm(c: list) -> int:
        r, s = c
        return heightmap[r][s]
    
    
    def sides(r: int, c: int) -> set:
        points = set()
        if r > 0:
            points.add((r - 1, c))
        if r < len(heightmap) - 1:
            points.add((r + 1, c))
        if c > 0:
            points.add((r, c - 1))
        if c < len(heightmap[0]) - 1:
            points.add((r, c + 1))
        return points
    
    
    def low(r: int, c: int) -> bool:
        return True if hm((r, c)) < min(map(hm, sides(r, c))) else False
    
    
    def basin(r: int, c: int) -> list:
        ins = {(r, c)}
        basin = {(r, c)}
    
        while ins:
            news = set()
            for i in ins:
                n = set()
                for s in sides(*i) - basin:
                    if hm(s) > hm(i) and hm(s) != 9:
                        n.add(s)
                        basin.add(s)
                news.update(n)
            ins = news
        return basin
    
    
    for r, row in enumerate(heightmap):
        for c, col in enumerate(row):
            if low(r, c):
                risk += col + 1
                basins.append(basin(r, c))
    
    coords = list()
    for b in basins:
        coords += b
    
    
    class bcolors:
        OKBLUE = "\033[94m"
        ENDC = "\033[0m"
    
    
    for r, row in enumerate(heightmap):
        line = str()
        for c, col in enumerate(row):
            if (r, c) in coords:
                line += bcolors.OKBLUE + str(hm((r, c))) + bcolors.ENDC
            else:
                line += str(hm((r, c)))
        print(line)
    
    print("Part One:", risk)
    print("Part Two:", math.prod(sorted(map(len, basins), reverse=True)[:3]))
    

    What problem I had.

    First. From yesterday second half-day to today firth half-day I was without enthernet because one clever man somewhere cut enthernet wire.

    Second. I somewhy thought that side cell must +1, not just bigger. I found out it only after vizualization which is included here.

    3 votes
  13. [2]
    Gyrfalcon
    Link
    Well, I think I know a way I can solve part 2, and I am going to have to resist looking up anything about flood filling now that I've scrolled through here because I'd like to do it the hard way...

    Well, I think I know a way I can solve part 2, and I am going to have to resist looking up anything about flood filling now that I've scrolled through here because I'd like to do it the hard way first. I did part 1 very naively, which worked okay, but I am going to have to come back to part 2 tomorrow evening and reply with it then. Very interested to see what the actually good solutions are when I can let myself read them!

    Part 1
    import std/[strformat, sequtils, strutils, sugar, math]
    
    proc parseFile(inputFile: string): seq[seq[int]] =
      var input: seq[string] = collect(newSeq):
        for line in inputFile.lines: line
    
      var output: seq[seq[int]]
    
      for line in input:
        output.add(mapIt(line, parseInt("" & it))) # gotta be a string
    
      return output
    
    # Convenience to see the grid
    proc printGrid(grid: seq[seq[int]]) =
      for row in grid:
        var line = ""
        for num in row[0 .. ^2]:
          line = line & &"{num}, "
        line = line & &"{row[^1]}"
        echo line
    
    proc findLowPoints(grid: seq[seq[int]]): seq[int] = 
    
      # Doing this the naive way, *gulp*
      var output: seq[int]
      
      # Start with corners, top left
      if grid[0][0] < grid[0][1] and grid[0][0] < grid[1][0]:
        output.add(grid[0][0])
      # bottom left
      if grid[^1][0] < grid[^1][1] and grid[^1][0] < grid[^2][0]:
        output.add(grid[^1][0])
      # bottom right
      if grid[^1][^1] < grid[^1][^2] and grid[^1][^1] < grid[^2][^1]:
        output.add(grid[^1][^1])
      # top right
      if grid[0][^1] < grid[0][^2] and grid[0][^1] < grid[1][^1]:
        output.add(grid[0][^1])
    
      # Next do top row
      for colIdx in 1 .. (grid[0].len - 2):
        if grid[0][colIdx] < grid[0][colIdx - 1] and
           grid[0][colIdx] < grid[0][colIdx + 1] and
           grid[0][colIdx] < grid[1][colIdx]:
          output.add(grid[0][colIdx])
    
      # bottom row
      for colIdx in 1 .. (grid[^1].len - 2):
        if grid[^1][colIdx] < grid[^1][colIdx - 1] and
           grid[^1][colIdx] < grid[^1][colIdx + 1] and
           grid[^1][colIdx] < grid[^2][colIdx]:
          output.add(grid[^1][colIdx])
    
      # left edge
      for rowIdx in 1 .. (grid.len - 2):
        if grid[rowIdx][0] < grid[rowIdx + 1][0] and
           grid[rowIdx][0] < grid[rowIdx - 1][0] and
           grid[rowIdx][0] < grid[rowIdx][1]:
          output.add(grid[rowIdx][0])
    
      # right edge
      for rowIdx in 1 .. (grid.len - 2):
        if grid[rowIdx][^1] < grid[rowIdx + 1][^1] and
           grid[rowIdx][^1] < grid[rowIdx - 1][^1] and
           grid[rowIdx][^1] < grid[rowIdx][^2]:
          output.add(grid[rowIdx][^1])
    
      # now the regular case
      for rowIdx in 1 .. (grid.len - 2):
        for colIdx in 1 .. (grid[rowIdx].len - 2):
          if grid[rowIdx][colIdx] < grid[rowIdx + 1][colIdx] and
             grid[rowIdx][colIdx] < grid[rowIdx - 1][colIdx] and
             grid[rowIdx][colIdx] < grid[rowIdx][colIdx + 1] and
             grid[rowIdx][colIdx] < grid[rowIdx][colIdx - 1]:
            output.add(grid[rowIdx][colIdx])
    
      return output
    
    func totalDanger(lowPoints: seq[int]): int =
      return sum(lowPoints) + lowPoints.len
    
    
    proc main(inputFile: string) =
      var grid = parseFile(inputFile)
      var lowPoints = findLowPoints(grid)
      
      echo &"The total danger is {totalDanger(lowPoints)}"
    
    when is_main_module:
      # main("test.txt")
      main("input.txt")
    
    3 votes
    1. Gyrfalcon
      Link Parent
      Okay I did it! I realized throughout that it could be better done and so could all the code I did for the first part but I did it! Part 2 diff @@ -1,4 +1,4 @@ -import std/[strformat, sequtils,...

      Okay I did it! I realized throughout that it could be better done and so could all the code I did for the first part but I did it!

      Part 2 diff
      @@ -1,4 +1,4 @@
      -import std/[strformat, sequtils, strutils, sugar, math]
      +import std/[strformat, sequtils, strutils, sugar, math, tables, algorithm]
       
       proc parseFile(inputFile: string): seq[seq[int]] =
         var input: seq[string] = collect(newSeq):
      @@ -11,6 +11,58 @@ proc parseFile(inputFile: string): seq[seq[int]] =
       
         return output
       
      +# Do this in a way I should have done everything before lol
      +func gridToTable(grid: seq[seq[int]]): Table[(int,int), int] =
      +  var mapping: Table[(int, int), int]
      +
      +  for rowIdx in 0 ..< grid.len:
      +    for colIdx in 0 ..< grid[rowIdx].len:
      +      mapping[(rowIdx, colIdx)] = grid[rowIdx][colIdx]
      +
      +  return mapping
      +
      +func sizeBasin(mapping: Table[(int, int), int], lowPoint: (int, int)): int =
      +
      +  var basin: seq[(int, int)]
      +  basin.add(lowPoint)
      +  var numAdded: int = 1 # How many points we have added this round
      +
      +  while numAdded != 0:
      +    numAdded = 0
      +    var newPoints: seq[(int, int)]
      +    for point in basin:
      +      if mapping.hasKey((point[0], point[1] + 1)) and
      +         mapping[(point[0], point[1] + 1)] < 9 and
      +         not ((point[0], point[1] + 1) in basin) and
      +         not ((point[0], point[1] + 1) in newPoints):
      +        inc numAdded
      +        newPoints.add((point[0], point[1] + 1))
      +      
      +      if mapping.hasKey((point[0], point[1] - 1)) and
      +         mapping[(point[0], point[1] - 1)] < 9 and
      +         not ((point[0], point[1] - 1) in basin) and
      +         not ((point[0], point[1] - 1) in newPoints):
      +        inc numAdded
      +        newPoints.add((point[0], point[1] - 1))
      +
      +      if mapping.hasKey((point[0] + 1, point[1])) and
      +         mapping[(point[0] + 1, point[1])] < 9 and
      +         not ((point[0] + 1, point[1]) in basin) and
      +         not ((point[0] + 1, point[1]) in newPoints):
      +        inc numAdded
      +        newPoints.add((point[0] + 1, point[1]))
      +      
      +      if mapping.hasKey((point[0] - 1, point[1])) and
      +         mapping[(point[0] - 1, point[1])] < 9 and
      +         not ((point[0] - 1, point[1]) in basin) and
      +         not ((point[0] - 1, point[1]) in newPoints):
      +        inc numAdded
      +        newPoints.add((point[0] - 1, point[1]))
      +
      +    basin = basin & newPoints
      +
      +  return basin.len
      +
       # Convenience to see the grid
       proc printGrid(grid: seq[seq[int]]) =
         for row in grid:
      @@ -20,51 +72,60 @@ proc printGrid(grid: seq[seq[int]]) =
           line = line & &"{row[^1]}"
           echo line
       
      -proc findLowPoints(grid: seq[seq[int]]): seq[int] = 
      +proc findLowPoints(grid: seq[seq[int]]): (seq[int], seq[(int,int)]) = 
       
         # Doing this the naive way, *gulp*
      -  var output: seq[int]
      +  var values: seq[int]
      +  var positions: seq[(int, int)]
         
         # Start with corners, top left
         if grid[0][0] < grid[0][1] and grid[0][0] < grid[1][0]:
      -    output.add(grid[0][0])
      +    values.add(grid[0][0])
      +    positions.add((0, 0))
         # bottom left
         if grid[^1][0] < grid[^1][1] and grid[^1][0] < grid[^2][0]:
      -    output.add(grid[^1][0])
      +    values.add(grid[^1][0])
      +    positions.add((grid.len - 1, 0))
         # bottom right
         if grid[^1][^1] < grid[^1][^2] and grid[^1][^1] < grid[^2][^1]:
      -    output.add(grid[^1][^1])
      +    values.add(grid[^1][^1])
      +    positions.add((grid.len - 1, grid[^1].len - 1))
         # top right
         if grid[0][^1] < grid[0][^2] and grid[0][^1] < grid[1][^1]:
      -    output.add(grid[0][^1])
      +    values.add(grid[0][^1])
      +    positions.add((0, grid[0].len - 1))
       
         # Next do top row
         for colIdx in 1 .. (grid[0].len - 2):
           if grid[0][colIdx] < grid[0][colIdx - 1] and
              grid[0][colIdx] < grid[0][colIdx + 1] and
              grid[0][colIdx] < grid[1][colIdx]:
      -      output.add(grid[0][colIdx])
      +      values.add(grid[0][colIdx])
      +      positions.add((0, colIdx))
       
         # bottom row
         for colIdx in 1 .. (grid[^1].len - 2):
           if grid[^1][colIdx] < grid[^1][colIdx - 1] and
              grid[^1][colIdx] < grid[^1][colIdx + 1] and
              grid[^1][colIdx] < grid[^2][colIdx]:
      -      output.add(grid[^1][colIdx])
      +      values.add(grid[^1][colIdx])
      +      positions.add((grid.len - 1, colIdx))
       
         # left edge
         for rowIdx in 1 .. (grid.len - 2):
           if grid[rowIdx][0] < grid[rowIdx + 1][0] and
              grid[rowIdx][0] < grid[rowIdx - 1][0] and
              grid[rowIdx][0] < grid[rowIdx][1]:
      -      output.add(grid[rowIdx][0])
      +      values.add(grid[rowIdx][0])
      +      positions.add((rowIdx, 0))
       
         # right edge
         for rowIdx in 1 .. (grid.len - 2):
           if grid[rowIdx][^1] < grid[rowIdx + 1][^1] and
              grid[rowIdx][^1] < grid[rowIdx - 1][^1] and
              grid[rowIdx][^1] < grid[rowIdx][^2]:
      -      output.add(grid[rowIdx][^1])
      +      values.add(grid[rowIdx][^1])
      +      positions.add((rowIdx, grid[rowIdx].len - 1))
       
         # now the regular case
         for rowIdx in 1 .. (grid.len - 2):
      @@ -73,9 +134,10 @@ proc findLowPoints(grid: seq[seq[int]]): seq[int] =
                grid[rowIdx][colIdx] < grid[rowIdx - 1][colIdx] and
                grid[rowIdx][colIdx] < grid[rowIdx][colIdx + 1] and
                grid[rowIdx][colIdx] < grid[rowIdx][colIdx - 1]:
      -        output.add(grid[rowIdx][colIdx])
      +        values.add(grid[rowIdx][colIdx])
      +        positions.add((rowIdx, colIdx))
       
      -  return output
      +  return (values, positions)
       
       func totalDanger(lowPoints: seq[int]): int =
         return sum(lowPoints) + lowPoints.len
      @@ -85,7 +147,17 @@ proc main(inputFile: string) =
         var grid = parseFile(inputFile)
         var lowPoints = findLowPoints(grid)
         
      -  echo &"The total danger is {totalDanger(lowPoints)}"
      +  echo &"The total danger is {totalDanger(lowPoints[0])}"
      +
      +  var mapping = gridToTable(grid)
      +
      +  var basins = mapIt(lowPoints[1], sizeBasin(mapping, it))
      +  sort(basins)
      +
      +  var danger = basins[^1] * basins[^2] * basins[^3]
      +
      +  echo &"The basin factor is {danger}"
      +  
       
       when is_main_module:
         # main("test.txt")
      
      5 votes
  14. 3d12
    Link
    This problem, in my opinion, has a simplistic elegance to it, in that it can be done in many different ways. Recursion, using a fill algorithm, or even just multi-pass scanning. All just a matter...

    This problem, in my opinion, has a simplistic elegance to it, in that it can be done in many different ways. Recursion, using a fill algorithm, or even just multi-pass scanning. All just a matter of optimization, making this problem accessible to all skill levels of programmer.

    Mine was probably not the best solution, but I had a pretty proud moment when I realized that this type of solution would have been pretty much completely out-of-reach for me two years ago.

    Part 1
    const fs = require('fs');
    const readline = require('readline');
    
    let inputArr = [];
    
    async function openFileForReading(file) {
    	const fileStream = fs.createReadStream(file);
    
    	const rl = readline.createInterface({
    		input: fileStream,
    		crlfDelay: Infinity
    	});
    
    	for await (const line of rl) {
    		try {
    			inputArr.push(line);
    		} catch(e) {
    			console.error(e);
    		}
    	}
    }
    
    function isLowPoint(x,y) {
    	let neighbors = [];
    	let rowBoundary = inputArr.length-1;
    	let colBoundary = inputArr[0].length-1;
    	if (x-1 >= 0) {
    		neighbors.push([x-1, y]);
    	}
    	if (x+1 <= colBoundary) {
    		neighbors.push([x+1, y]);
    	}
    	if (y-1 >= 0) {
    		neighbors.push([x, y-1]);
    	}
    	if (y+1 <= rowBoundary) {
    		neighbors.push([x, y+1]);
    	}
    	let myValue = inputArr[y][x];
    	let isLow = true;
    	for (const neighbor of neighbors) {
    		let theirValue = inputArr[neighbor[1]][neighbor[0]];
    		if (parseInt(theirValue) <= parseInt(myValue)) {
    			isLow = false;
    			break;
    		}
    	}
    	return isLow;
    }
    
    (async function mainExecution() {
    	await openFileForReading('input.txt');
    	let totalSum = 0;
    	for (let y = 0; y < inputArr.length; y++) {
    		let currentRow = inputArr[y];
    		for (let x = 0; x < currentRow.length; x++) {
    			if (isLowPoint(x,y)) {
    				console.log("DEBUG: Found a low point! " + x + "," + y + " -- value: " + currentRow[x]);
    				totalSum += (parseInt(currentRow[x]) + 1);
    			}
    		}
    	}
    	console.log("Answer found! " + totalSum);
    })();
    
    Part 2
    const fs = require('fs');
    const readline = require('readline');
    
    let inputArr = [];
    
    async function openFileForReading(file) {
    	const fileStream = fs.createReadStream(file);
    
    	const rl = readline.createInterface({
    		input: fileStream,
    		crlfDelay: Infinity
    	});
    
    	for await (const line of rl) {
    		try {
    			inputArr.push(line);
    		} catch(e) {
    			console.error(e);
    		}
    	}
    }
    
    function findNeighbors(x,y) {
    	let neighbors = [];
    	let rowBoundary = inputArr.length-1;
    	let colBoundary = inputArr[0].length-1;
    	if (x-1 >= 0) {
    		neighbors.push({ x: x-1, y: y });
    	}
    	if (x+1 <= colBoundary) {
    		neighbors.push({ x: x+1, y: y });
    	}
    	if (y-1 >= 0) {
    		neighbors.push({ x: x, y: y-1 });
    	}
    	if (y+1 <= rowBoundary) {
    		neighbors.push({ x: x, y: y+1 });
    	}
    	return neighbors;
    }
    
    function isLowPoint(x,y) {
    	let neighbors = findNeighbors(x,y);
    	let myValue = inputArr[y][x];
    	let isLow = true;
    	for (const neighbor of neighbors) {
    		let theirValue = inputArr[neighbor.y][neighbor.x];
    		if (parseInt(theirValue) <= parseInt(myValue)) {
    			isLow = false;
    			break;
    		}
    	}
    	return isLow;
    }
    
    function findBasin(startPoint){
    	let eligibleTiles = [ startPoint ];
    	let basin = [];
    	while (eligibleTiles.length > 0) {
    		let currentTile = eligibleTiles.pop();
    		basin.push(currentTile);
    		let currentNeighbors = findNeighbors(currentTile.x,currentTile.y);
    		for (const neighbor of currentNeighbors) {
    			let theirValue = parseInt(inputArr[neighbor.y][neighbor.x]);
    			if (basin.filter(e => (e.x === neighbor.x && e.y === neighbor.y)).length === 0
    					&& eligibleTiles.filter(e => (e.x === neighbor.x && e.y === neighbor.y)).length === 0
    					&& !(theirValue === 9)) {
    				eligibleTiles.push(neighbor);
    			}
    		}
    	}
    	console.log("DEBUG: returning basin = " + basin.map(e => e.x + "," + e.y).join(';'));
    	return basin;
    }
    
    (async function mainExecution() {
    	await openFileForReading('input.txt');
    	let lowPoints = [];
    	let basins = [];
    	for (let y = 0; y < inputArr.length; y++) {
    		let currentRow = inputArr[y];
    		for (let x = 0; x < currentRow.length; x++) {
    			if (isLowPoint(x,y)) {
    				console.log("DEBUG: Found a low point! " + x + "," + y + " -- value: " + currentRow[x]);
    				lowPoints.push({ x: x, y: y });
    			}
    		}
    	}
    	for (const lowPoint of lowPoints) {
    		basins.push(findBasin(lowPoint));
    	}
    	console.log(basins.map(e => e.length).sort((a,b) => { return b - a }));
    	console.log("Answer found! " + basins.map(e => e.length).sort((a,b) => { return b - a }).slice(0,3).reduce((a,b) => a * b));
    })();
    
    3 votes
  15. bliden
    (edited )
    Link
    I really enjoyed this one! The way part one dovetailed into part two was very satisfying. I think I ended up doing depth-first search, though I'm curious to try a breadth first approach on part 2....

    I really enjoyed this one! The way part one dovetailed into part two was very satisfying. I think I ended up doing depth-first search, though I'm curious to try a breadth first approach on part 2.

    https://github.com/bcliden/advent-of-code-2021/blob/main/day-09-smoke-basin/src/main.rs

    I've been learning Rust this year, and AoC seemed like a great way to practice. I wish I'd done it in past years!
    Would love any feedback from Rust gurus (if you're around ☺). It was very nice using Option<T> to get grid squares without obsessing over what is/isn't in bounds.

    3 votes
  16. jzimbel
    (edited )
    Link
    Elixir I'm using a project that I started last year, so I took advantage of a CharGrid module that I previously defined to parse and work with a grid of characters. It still took a while because...

    Elixir

    I'm using a project that I started last year, so I took advantage of a CharGrid module that I previously defined to parse and work with a grid of characters. It still took a while because 1) I was too stubborn to look up and implement a flood-fill algorithm, and 2) iteratively building a collection while simultaneously changing the collection you're pulling values from does not lend itself to a FP/immutable data structures approach. I had to write two custom recursive functions to accomplish this.

    The first time I ran it, partial basins were getting merged together when they shouldn't, and it took me a while to realize I was doing a set union where I should have been doing an intersection. It solved the puzzle successfully after that.

    Part 2 runs in about 1.3s. I wonder how much faster it would be if I'd used a real flood-fill algorithm...

    Both parts
    defmodule AdventOfCode.Solution.Year2021.Day09 do
      alias AdventOfCode.CharGrid
    
      def part1(input) do
        input
        |> CharGrid.from_input()
        |> local_minima_risk_levels()
        |> Enum.sum()
      end
    
      def part2(input) do
        input
        |> CharGrid.from_input()
        |> get_basins()
        |> Enum.map(&MapSet.size/1)
        |> Enum.sort(:desc)
        |> Enum.take(3)
        |> Enum.product()
      end
    
      defp local_minima_risk_levels(grid) do
        grid
        |> CharGrid.filter_cells(fn {coords, value} ->
          grid
          |> CharGrid.adjacent_values(coords)
          |> Enum.all?(&(value < &1))
        end)
        # codepoint for '0' (aka ?0 in Elixir syntax) == 48;
        # The problem calls for value + 1, so subtract 47 from codepoint
        |> Enum.map(fn {_coords, char} -> char - 47 end)
      end
    
      defp get_basins(grid) do
        grid
        |> CharGrid.to_list()
        |> non_nine_coords()
        |> Enum.map(fn coords ->
          adjacent_non_nines =
            grid
            |> CharGrid.adjacent_cells(coords, :cardinal)
            |> non_nine_coords()
    
          MapSet.new([coords | adjacent_non_nines])
        end)
        |> merge_basins()
      end
    
      defp non_nine_coords(cells) do
        Enum.flat_map(cells, fn
          {_coords, ?9} -> []
          {coords, _value} -> [coords]
        end)
      end
    
      # This is inefficient but I didn't feel like learning
      # how to implement a proper flood-fill algorithm
      defp merge_basins(seed_basins, merged_basins \\ [])
    
      defp merge_basins([], merged_basins), do: merged_basins
    
      defp merge_basins([seed_basin | seed_basins], merged_basins) do
        {remaining, merged_basin} = flood_basin(seed_basin, seed_basins)
    
        merge_basins(remaining, [merged_basin | merged_basins])
      end
    
      @doc """
      This function repeatedly goes through the list of seed basins, merging
      connected ones to the target basin until no more can be merged.
    
      It returns the fully filled basin (a set of coordinate pairs)
      as well as a list of the remaining seed basins that have yet
      to be connected to anything else.
      """
      defp flood_basin(basin, seed_basins) do
        import MapSet, only: [intersection: 2, size: 1, union: 2]
    
        {remaining_seed_basins, new_basin} =
          Enum.flat_map_reduce(seed_basins, basin, fn seed_basin, acc ->
            if size(intersection(acc, seed_basin)) > 0 do
              {[], union(acc, seed_basin)}
            else
              {[seed_basin], acc}
            end
          end)
    
        if size(basin) == size(new_basin) do
          {remaining_seed_basins, new_basin}
        else
          flood_basin(new_basin, remaining_seed_basins)
        end
      end
    end
    
    CharGrid module

    This is me LARPing as a core library contributor 🤓

    defmodule AdventOfCode.CharGrid do
      @moduledoc "Data structure representing a finite grid of characters by a map of {x, y} => char"
    
      alias __MODULE__, as: T
    
      @type t :: %T{
              grid: grid,
              width: non_neg_integer,
              height: non_neg_integer
            }
    
      @typep grid :: %{coordinates => char}
    
      @type coordinates :: {non_neg_integer, non_neg_integer}
    
      @type cell :: {coordinates, char}
    
      @enforce_keys ~w[grid width height]a
      defstruct @enforce_keys
    
      # List of coords that produce the 8 coordinates surrounding a given coord when added to it
      @all_adjacent_deltas for x <- -1..1, y <- -1..1, not (x == 0 and y == 0), do: {x, y}
    
      # List of coords that produce the 4 coordinates horizontally/vertically adjacent to a given coord when added to it
      @cardinal_adjacent_deltas for x <- -1..1, y <- -1..1, abs(x) + abs(y) == 1, do: {x, y}
    
      # List of coords that produce the 4 coordinates diagonally adjacent to a given coord when added to it
      @intercardinal_adjacent_deltas for x <- -1..1, y <- -1..1, abs(x) + abs(y) == 2, do: {x, y}
    
      @adjacency_deltas_by_type %{
        all: @all_adjacent_deltas,
        cardinal: @cardinal_adjacent_deltas,
        intercardinal: @intercardinal_adjacent_deltas
      }
    
      @spec from_input(String.t()) :: t()
      def from_input(input) do
        charlists =
          input
          |> String.split()
          |> Enum.map(&String.to_charlist/1)
    
        height = length(charlists)
        width = length(hd(charlists))
    
        grid =
          for {line, y} <- Enum.with_index(charlists),
              {char, x} <- Enum.with_index(line),
              into: %{} do
            {{x, y}, char}
          end
    
        %T{
          grid: grid,
          width: width,
          height: height
        }
      end
    
      @doc "Gets the value at the given coordinates."
      @spec at(t(), coordinates) :: char | nil
      def at(%T{} = t, coords) do
        t.grid[coords]
      end
    
      @doc "Applies `fun` to each cell to produce a new CharGrid. `fun` must return a char."
      @spec map(t(), (cell -> char)) :: t()
      def map(%T{} = t, fun) do
        %{t | grid: for({coords, _} = cell <- t.grid, into: %{}, do: {coords, fun.(cell)})}
      end
    
      @doc "Converts the grid to a list of cells."
      @spec to_list(t()) :: list(cell)
      def to_list(%T{} = t) do
        Map.to_list(t.grid)
      end
    
      @doc "Returns the number of cells for which `predicate` returns a truthy value."
      @spec count(t(), (cell -> as_boolean(any()))) :: non_neg_integer()
      def count(%T{} = t, predicate) do
        Enum.count(t.grid, predicate)
      end
    
      @doc "Returns the number of cells containing `char`."
      @spec count_chars(t(), char) :: non_neg_integer()
      def count_chars(%T{} = t, char) do
        count(t, fn {_, c} -> c == char end)
      end
    
      @doc "Returns a list of cells for which `predicate` returns a truthy value."
      @spec filter_cells(t(), (cell -> as_boolean(any()))) :: list(cell)
      def filter_cells(%T{} = t, predicate) do
        Enum.filter(t.grid, predicate)
      end
    
      @type adjacency_type :: :all | :cardinal | :intercardinal
    
      @doc """
      Returns a list of cells adjacent to the one at `coords`.
    
      The type of adjacency is determined by the third argument:
    
      - `:all` (default behavior):
        ```
        OOO
        O*O
        OOO
        ```
      - `:cardinal`:
        ```
        .O.
        O*O
        .O.
        ```
      - `:intercardinal`:
        ```
        O.O
        .*.
        O.O
        ```
      """
      @spec adjacent_cells(t(), coordinates, adjacency_type) :: list(cell)
      def adjacent_cells(%T{} = t, coords, adjacency_type \\ :all) do
        @adjacency_deltas_by_type[adjacency_type]
        |> Enum.map(&sum_coordinates(coords, &1))
        |> Enum.map(fn adjacent_coords -> {adjacent_coords, at(t, adjacent_coords)} end)
        |> Enum.reject(fn {_coords, value} -> is_nil(value) end)
      end
    
      @doc """
      Convenience function that behaves the same as `adjacent_cells/3`,
      but returns only the char value of each adjacent cell.
      """
      @spec adjacent_values(t(), coordinates, adjacency_type) :: list(char)
      def adjacent_values(%T{} = t, coords, adjacency_type \\ :all) do
        t
        |> adjacent_cells(coords, adjacency_type)
        |> Enum.map(&elem(&1, 1))
      end
    
      @doc """
      Returns a list of values from the up to 8 cells reachable by a chess queen's move from the
      cell at `coords`.
    
      The optional `empty_char` (default `?.`) dictates which cells are considered unoccupied.
      """
      @spec queen_move_values(t(), coordinates, char) :: list(char)
      def queen_move_values(%T{} = t, coords, empty_char \\ ?.) do
        @all_adjacent_deltas
        |> Enum.map(&find_nonempty_on_line(t, &1, sum_coordinates(coords, &1), empty_char))
        |> Enum.reject(&is_nil/1)
      end
    
      defp find_nonempty_on_line(t, step, coords, empty_char) do
        case at(t, coords) do
          ^empty_char -> find_nonempty_on_line(t, step, sum_coordinates(coords, step), empty_char)
          val -> val
        end
      end
    
      defp sum_coordinates({x1, y1}, {x2, y2}), do: {x1 + x2, y1 + y2}
    end
    
    2 votes