Day 2: Red-Nosed Reports
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://blocks.programming.dev 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/22323136
- 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
Uiua
Took me a bit longer to get this one but still quite simple overall.
Spent quite some time on getting to know the try
and assert
operators better.
Run with example input here
# Get the indices matching the ascending/
# descending criteria
CheckAsc ← ≡°□⍚(⍣(⊸⍤.≍⍆.)⍣(⊸⍤.≍⇌⍆.)0)
# Get the indices matching the distance criteria
CheckDist ← ≡°□⍚(⍣(⊸⍤.≠1∈:0)0×⊓≥≤1,3⌵⧈-)
Split ← ⊙(▽≠1)▽,,
PartOne ← (
&rs ∞ &fo "input-2.txt"
⊜(□⊜⋕≠@ .)≠@\n.
CheckAsc.
▽
CheckDist
⧻⊚
)
PartTwo ← (
&rs ∞ &fo "input-2.txt"
⊜(□⊜⋕≠@ .)≠@\n.
CheckAsc.
Split
CheckDist.
Split
⊙(⊂)
⧻
:
⍚(≡(▽:°⊟)⍜¤⊞⊟:≠1⊞=.⇡⧻.)
≡(⧻⊚CheckDist▽CheckAsc.°□)
+⧻◴⊚
)
&p "Day 2:"
&pf "Part 1: "
&p PartOne
&pf "Part 2: "
&p PartTwo
Elixir
defmodule AdventOfCode.Solution.Year2024.Day02 do
use AdventOfCode.Solution.SharedParse
@impl true
def parse(input) do
for line <- String.split(input, "\n", trim: true),
do: String.split(line) |> Enum.map(&String.to_integer/1)
end
def part1(input) do
Enum.count(input, &is_safe(&1, false))
end
def part2(input) do
Enum.count(input, &(is_safe(&1, true) or is_safe(tl(&1), false)))
end
def is_safe([a, b, c | rest], can_fix) do
cond do
(b - a) * (c - b) > 0 and abs(b - a) in 1..3 and abs(c - b) in 1..3 ->
is_safe([b, c | rest], can_fix)
can_fix ->
is_safe([a, c | rest], false) or is_safe([a, b | rest], false)
true ->
false
end
end
def is_safe(_, _), do: true
end
Smalltalk
Discovered a number of frustrations with this supposedly small and elegant language
- Smalltalk’s block based iteration has NO control flow
- blocks are very dissimilar to functions
- You cannot early return from blocks (leading to a lot of horrible nested ifs or boolean operations)
- Smalltalk’s messages (~functions) cannot take multiple arguments, instead it has these sort of combined messages, so instead of a function with three arguments, you would send 3 combined messages with one argument. This is fine until you try to chain messages with arguments, as smalltalk will interpret them as a combined message and fail, forcing you to either break into lots of temp variables, or do lisp-like parenthesis nesting, both of which I hate
- Smalltalk’s order of operations, while nice and simple, is also quite frustrating at times, similar to #4, forcing you to break into lots of temp variables, or do lisp-like parenthesis nesting. For instance
(nums at: i) - (nums at: i+1)
which would benums[i] - nums[i+1]
in most languages
Part 1
day2p1: input
^ (input lines
collect: [ :l | l substrings collect: [ :s | s asInteger ]])
count: [ :nums |
(nums = nums sorted or: nums = nums sorted reverse)
and: [
(1 to: nums size-1) allSatisfy: [ :i |
((nums at: i) - (nums at: i+1)) abs between: 1 and: 3
] ] ]
Part 2
day2p2: input
| temp |
^ (input lines
collect: [ :l | (l substrings collect: [ :s | s asInteger ]) asOrderedCollection ])
count: [ :nums |
(self day2p2helper: nums)
or: [
((1 to: nums size) anySatisfy: [ :i |
temp := nums copy.
temp removeAt: i.
self day2p2helper: temp
])
or: [(self day2p2helper: nums reversed)
or: [
(1 to: nums size) anySatisfy: [ :i |
temp := nums reversed.
temp removeAt: i.
self day2p2helper: temp
]
]]] .
]
day2p2helper: nums
^ (1 to: nums size - 1) allSatisfy: [ :i |
((nums at: i+1) - (nums at: i)) between: 1 and: 3
].
#Zig
const std = @import("std");
const List = std.ArrayList;
const splitScalar = std.mem.splitScalar;
const parseInt = std.fmt.parseInt;
const print = std.debug.print;
const concat = std.mem.concat;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
const Answer = struct {
safe: u32,
tolerated: u32,
};
pub fn isSafe(levels: []i32) bool {
if (levels.len == 0) {
return false;
}
// slide window in pairs, advancing by one
var it = std.mem.window(i32, levels, 2, 1);
const first = it.first();
const decreasing = first[0] - first[1] > 0;
it.reset(); // rewind the iterator
while (it.next()) |slice| {
const lhs: i32 = slice[0];
const rhs: i32 = slice[1];
if (decreasing) {
if (lhs <= rhs) return false;
if (lhs - rhs < 1 or lhs - rhs > 3) return false;
} else {
if (rhs <= lhs) return false;
if (rhs - lhs < 1 or rhs - lhs > 3) return false;
}
}
return true;
}
pub fn solve(input: []const u8) !Answer {
var rows = splitScalar(u8, input, '\n');
// PART 1
// determine how many reports are safe
var safe_reports: u32 = 0;
var tolerated_reports: u32 = 0;
var unsafe_reports = List([]i32).init(alloc);
defer unsafe_reports.deinit();
while (rows.next()) |row| {
var levels = splitScalar(u8, row, ' ');
var report = List(i32).init(alloc);
defer report.deinit();
while (levels.next()) |level| {
const value = parseInt(i32, level, 10) catch continue;
report.append(value) catch continue;
}
if (isSafe(report.items)) {
safe_reports += 1;
} else {
try unsafe_reports.append(try alloc.dupe(i32, report.items));
}
}
// PART 2
// determine how many unsafe reports can be tolerated
for (unsafe_reports.items) |report| {
var index: usize = 0;
while (index < report.len) : (index += 1) {
// mutate report by removing one level
const mutated_report = concat(
alloc,
i32,
&[_][]const i32{ report[0..index], report[index + 1 ..] },
) catch report;
defer alloc.free(mutated_report);
if (isSafe(mutated_report)) {
tolerated_reports += 1;
break;
}
}
}
return Answer{ .safe = safe_reports, .tolerated = safe_reports + tolerated_reports };
}
pub fn main() !void {
const answer = try solve(@embedFile("input.txt"));
print("Part 1: {d}\n", .{answer.safe});
print("Part 2: {d}\n", .{answer.tolerated});
}
test "test input" {
const answer = try solve(@embedFile("test.txt"));
try std.testing.expectEqual(2, answer.safe);
try std.testing.expectEqual(4, answer.tolerated);
Kotlin:
import kotlin.math.abs
import kotlin.math.sign
data class Report(val levels: List<Int>) {
fun isSafe(withProblemDampener: Boolean): Boolean {
var orderSign = 0.0f // - 1 is descending; +1 is ascending
levels.zipWithNext().forEachIndexed { index, level ->
val difference = (level.second - level.first).toFloat()
if (orderSign == 0.0f) orderSign = sign(difference)
if (sign(difference) != orderSign || abs(difference) !in 1.0..3.0) {
// With problem dampener: Drop either element in the pair or the first element from the original list and check if the result is now safe.
return if (withProblemDampener) {
Report(levels.drop(1)).isSafe(false) || Report(levels.withoutElementAt(index)).isSafe(false) || Report(levels.withoutElementAt(index + 1)).isSafe(false)
} else false
}
}
return true
}
}
fun main() {
fun part1(input: List<String>): Int = input.map { Report(it.split(" ").map { it.toInt() }).isSafe(false) }.count { it }
fun part2(input: List<String>): Int = input.map { Report(it.split(" ").map { it.toInt() }).isSafe(true) }.count { it }
// Or read a large test input from the `src/Day01_test.txt` file:
val testInput = readInput("Day02_test")
check(part1(testInput) == 2)
check(part2(testInput) == 4)
// Read the input from the `src/Day01.txt` file.
val input = readInput("Day02")
part1(input).println()
part2(input).println()
}
The Report#isSafe method essentially solves both parts.
I’ve had a bit of a trip up in part 2:
I initially only checked, if the report was safe, if either elements in the pair were to be removed. But in the edge case, that the first pair has different monotonic behaviour than the rest, the issue would only be detected by the second pair with indices (2, 3), whilst removing the first element in the list would yield a safe report.