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
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
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
}
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.
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...
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.
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.