Day 18: Lavaduct Lagoon
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)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Python
0.09 line-seconds (third simplest after days 6 and 2).
from .solver import Solver
class Day18(Solver):
def __init__(self):
super().__init__(18)
def presolve(self, input: str):
self.lines = input.splitlines()
def solve_first_star(self):
commands = []
for line in self.lines:
direction, distance, *_ = line.split(' ')
commands.append((direction, int(distance)))
return self._solve(commands)
def solve_second_star(self):
commands = []
for line in self.lines:
_, _, command = line.split(' ')
distance = int(command[2:-2], 16)
direction = ('R', 'D', 'L', 'U')[int(command[-2])]
commands.append((direction, distance))
return self._solve(commands)
def _solve(self, commands: list[tuple[str, int]]):
points: list[tuple[int, int]] = [(0, 0)]
perimeter_integer_points = 1
x, y = 0, 0
for direction, distance in commands:
dx, dy = {'R': (1, 0), 'L': (-1, 0), 'U': (0, -1), 'D': (0, 1)}[direction]
x, y = x + dx * distance, y + dy * distance
perimeter_integer_points += distance
points.append((x, y))
last_x, last_y = points[-1]
perimeter_integer_points += abs(last_x) + abs(last_y) - 1
area_x2 = sum((points[i][1] + points[(i+1) % len(points)][1]) *
(points[i][0] - points[(i+1) % len(points)][0])
for i in range(len(points)))
interior_integer_points = (area_x2 - perimeter_integer_points) // 2 + 1
return interior_integer_points + perimeter_integer_points
Rust
Code
use std::fs;
use std::path::PathBuf;
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// A file containing the input
input_file: PathBuf,
#[arg(short, long)]
part: Option,
}
fn main() {
// Parse CLI arguments
let cli = Cli::parse();
// Read file
let input_text = fs::read_to_string(&cli.input_file)
.expect(format!("File \"{}\" not found", cli.input_file.display()).as_str());
let (run_part_1, run_part_2) = if let Some(part) = cli.part {
match part {
1 => (true, false),
2 => (false, true),
_ => unimplemented!(),
}
} else {
(true, true)
};
let (it1, it2) = preprocess(&input_text);
if run_part_1 {
let solution = solve(it1);
println!("Part 1: {solution}");
}
if run_part_2 {
let solution = solve(it2);
println!("Part 2: {solution}");
}
}
/// Preprocessing for solving
/// Extracts important information from the input
/// `part` specifies which part to preprocess for.
/// Returns a vector for each part containing a direction and amount
fn preprocess(input: &str) -> (Vec<((isize, isize), isize)>, Vec<((isize, isize), isize)>) {
let it = input.lines().map(|l| {
let line: Vec<_> = l.split(' ').collect();
let direction: char = line[0].chars().nth(0).unwrap();
let amount: isize = line[1].parse().unwrap();
let color: &str = &line[2][2..8];
let direction = match direction {
'R' => (1, 0),
'L' => (-1, 0),
'U' => (0, 1),
'D' => (0, -1),
_ => unreachable!(),
};
((direction, amount), {
let amount: isize = isize::from_str_radix(&color[..5], 16).unwrap();
let direction = match &color[5..] {
"0" => (1, 0),
"1" => (0, -1),
"2" => (-1, 0),
"3" => (0, 1),
_ => unreachable!(),
};
(direction, amount)
})
});
let it1 = it.clone().map(|(i1, _i2)| i1).collect();
let it2 = it.map(|(_i1, i2)| i2).collect();
(it1, it2)
}
/// Finds the area using the shoelace formula
fn solve(it: Vec<((isize, isize), isize)>) -> isize {
// Get coordinates from it
let mut coords: Vec<(isize, isize)> = Vec::with_capacity(it.len() + 1);
// Start at (0, 0)
coords.push((0, 0)); // Needs to be at both begining and end
let (mut x, mut y) = (0, 0);
let mut perimeter_length = 0;
// Compute and add the coords
for (direction, amount) in it {
let translation = (direction.0 * amount, direction.1 * amount);
x += translation.0;
y += translation.1;
coords.push((x, y));
perimeter_length += amount;
}
// Should end where it started
assert_eq!(coords.last().unwrap(), &(0, 0));
// Shoelace formula
let a = coords
.iter()
.zip(coords.iter().skip(1))
.map(|((x1, y1), (x2, y2))| (x1 * y2) - (x2 * y1))
.sum::()
/ 2;
// Found by drawing, then trial and error
// Shoelace area missing 1/2 of perimeter
// Inside and outside corners cancel out except for one
a.abs() + perimeter_length / 2 + 1
}
Yay math!
Nim
I am not making good time on these anymore.
For part 1, I walked through the dig plan instructions, keeping track of the highest and lowest x and y values reached, and used those to create a character grid, with an extra 1 tile border around it. Walked the instructions again to plot out the trench with , flood-filled the exterior with
O
, and then counted the non-O
tiles. Sort of similar to the pipe maze problem.
This approach wouldn’t have been viable for part 2, due to the scale of the numbers involved. Instead I counted the number of left and right turns in the trench to determine whether it was being dug in a clockwise or counterclockwise direction, and assumed that there were no intersections. I then made a polygon that followed the outer edge of the trench. Wherever there was a run of 3 inward turns in a row, that meant there was a rectangular protrusion that could be chopped off of the main polygon. Repeatedly chopping these off eventually turns the polygon into a rectangle, so it’s just a matter of adding up the area of each. This worked great for the example input.
Unfortunately when I ran it on the actual input, I ran out of sets of inward turns early, leaving an “inside out” polygon. I thought this meant that the input must have intersections in it that I would have to untwist somehow. To keep this short, after a long debugging process I figured out that I was introducing intersections during the chopping process. The chopped regions can have additional trench inside of them, which results in those parts ending up outside of the reduced polygon. I solved this by chopping off the narrowest protrusions first.
Good job on persevering with this one. Your approach for part 2 sounds quite viable, it is very similar to the Ear clipping method for triangulating a polygon.
Yeah, I read up on ear clipping for a small game dev project a while back, though I don’t remember if I actually ended up using it. So my solution is inspired by what I remember of that.
C
Fun and interesting puzzle! In part 1 I fumbled a bit trying to implement even/odd outside/inside tracking before realizing that wouldn’t work for this shape and just did the flood fill.
For part 2 I correctly guessed that like the intersecting cuboids (2021 day 22) it would be about finding a better representation for the grid or avoiding representing it entirely. Long story shorter:
/*
* Conceptually: the raw map, which is too large to fit directly in
* memory for part 2, is made much smaller by collapsing (and counting)
* identical rows and columns. Another way to look it at is that a grid
* is fitted to make 'opaque' cells.
* | |#|##|#
* For example: -+---+-+--+-
* #|###|#| |#
* #### ### 1 -+---+-+--+-
* ##### # ### # 1 #| | | |#
* # # becomes # # 2 or: #| | | |#
* # # ##### 1 -+---+-+--+-
* ######## 13121 #|###|#|##|#
*
* To avoid a lot of complex work, instead of actually collapsing and
* splitting rows and columns, we first generate the wall rectangles and
* collect the unique X and Y coordinates. Those are locations of our
* virtual grid lines.
*/
Despite being quite happy with this solution, I couldn’t help but notice the brevity and simplicity of the other solutions here. Gonna have a look what’s happening there and see if I can try that approach too.
(Got bitten by a nasty overflow btw, the list of unique X coordinates was overwriting the list of unique Y coordinates. Oh well, such is the life of a C programmer.)
Oh, just like day 11! I hadn’t thought of that. I was initially about to try something similar by separating into rectangular regions, as in ear-clipping triangulation. But that would require a lot of iterating, and something about “polygon” and “walking the edges” went ping in my memory…
Nim
Decided to go for a polygon approach for part 1 using the Shoelace formula to calculate the area. This meant part 2 only resulted in larger values, no additional computation.
Code runs in <1ms for part 1 and 2 combined
Shoelace formula
This would have been really useful to know about. I’ve committed to a certain level of wheel-reinvention for this event unless I get really stuck, but I’m sure it’ll come up again in the future.
This was actually something I learned for my job, it was nice to be able to apply it here.
I like your commitment to wheel-reinvention, it can be a lot more fun than going for an existing or ‘intended’ approach.