100 задач с LeetCode мы уже обозрели. Пора переходить к более тяжёлым наркотикам.
Дальше будем смотреть на задачи из последнего Advent of Code.
Задача первого дня — Historian Hysteria несложная и затравочная 😻.
📘 Условие части I
Даны два списка целых чисел одинаковой длины. Необходимо оценить, насколько они различаются.
Для этого:
- Сопоставим наименьшее число из левого списка с наименьшим числом из правого.
- Затем второе по величине — со вторым по величине, и так далее.
- Для каждой пары чисел вычисляется расстояние между ними — это модуль разности.
- Все такие расстояния суммируются, и результат отражает общее расхождение между списками.
Пример:
- Левый список:
3 4 2 1 3 3
- Правый список:
4 3 5 3 9 3
После сопоставления по возрастанию:
- Пары:
(1, 3), (2, 3), (3, 3), (3, 4), (3, 5), (4, 9)
- Расстояния:
2, 1, 0, 1, 2, 5
- Сумма расстояний:
2 + 1 + 0 + 1 + 2 + 5 = 11
💡 Идея
Нам нужно просто отсортировать оба списка и сопоставить соответствующие числа в них.
📘 Условие части II
Теперь требуется вычислить оценку схожести между двумя списками.
Для этого:
- Для каждого числа из левого списка определим, сколько раз оно встречается в правом списке.
- Затем умножим число на количество его вхождений.
- Все такие произведения складываются в итоговую сумму — это и есть оценка схожести.
Пример:
- Левый список:
3 4 2 1 3 3
- Правый список:
4 3 5 3 9 3
- Обработка значений:
3
встречается 3
раза → 3 × 3 = 9
4
встречается 1
раз → 4 × 1 = 4
2
встречается 0
раз → 2 × 0 = 0
1
встречается 0
раз → 1 × 0 = 0
3
→ 9
3
→ 9
- Итоговая сумма:
9 + 4 + 0 + 0 + 9 + 9 = 31
💡 Идея
Если списки отсортировать, то одинаковые элементы будут расположены рядом. Это позволяет эффективно посчитать количество вхождений каждого значения, не используя хеш-таблицы или сложные структуры. А сопоставить элементы из разных списков можно будет процедурой близкой по смыслу с сортировкой слиянием.
🛠 Детали реализации 🛠
🔸 Общий шаг
- Списки сортируются заранее — это необходимо и для минимизации расхождений, и для эффективной группировки значений.
🔹 solve_simple
- Используется метод
.zip()
для попарного объединения элементов двух отсортированных списков.
- Для каждой пары чисел вычисляется модуль разности.
- Итоговая сумма расхождений находится с помощью
.sum()
.
🔹 solve_hard
- Используются сгруппированные участки (
chunk_by
) каждого списка.
- Для каждого значения из левого списка ищется совпадающая группа в правом.
- Если значение совпадает, его вклад в итоговую сумму рассчитывается как
значение × число повторов слева × число повторов справа
.
⏱ Асимптотика
-
Временная сложность:
O(n·log n)
- Сортировка:
O(n·log n)
- Группировка и вычисления:
O(n)
-
Дополнительная память:
O(log n)
- Сортировка (in-place) входных данных:
O(log n)
для стэка рекурсивных вызовов
💻 Исходный код
use std::cmp::Ordering;
use super::TaskData;
/*
* pub struct TaskData {
* pub nums_left: Vec<i32>,
* pub nums_right: Vec<i32>,
* }
*/
/// Holds two sorted number lists and computes difference and similarity scores.
pub struct Solution {
nums_left: Vec<i32>,
nums_right: Vec<i32>,
}
impl Solution {
/// Constructs a new Solution, sorting both input lists.
pub fn new(mut data: TaskData) -> Self {
data.nums_left.sort_unstable();
data.nums_right.sort_unstable();
Self {
nums_left: data.nums_left,
nums_right: data.nums_right,
}
}
/// Part 1:
/// Pairs sorted values and sums their absolute differences.
pub fn solve_simple(&self) -> i32 {
self.nums_left.iter()
.zip(&self.nums_right)
.map(|(&l, &r)| (l - r).abs())
.sum()
}
/// Part 2:
/// For each number in the left list, multiplies it by the number of times
/// it appears in the right list, and sums the result.
pub fn solve_hard(&self) -> i32 {
// Group equal elements together in both lists
let mut left_chunks = self.nums_left.chunk_by(|a, b| a == b);
let mut right_chunks = self.nums_right.chunk_by(|a, b| a == b).peekable();
let mut sim_score = 0;
// Iterate over left groups
for left in &mut left_chunks {
let val_left = left[0];
let left_count = left.len() as i32;
// Advance right_chunks until val_right >= val_left
while let Some(&right) = right_chunks.peek() {
match val_left.cmp(&right[0]) {
Ordering::Less => break, // No more matches
Ordering::Equal => {
let right_count = right.len() as i32;
sim_score += val_left * left_count * right_count;
}
Ordering::Greater => {} // Skip unmatched right group
}
right_chunks.next();
}
}
sim_score
}
}
Также приведу и простой код для чтения входных данных
(чего не буду делать в обзорах следующих задач).
use std::error::Error;
use std::fs::File;
use std::io::{BufRead, BufReader};
/// Holds two lists of numbers read from the input file
pub struct TaskData {
pub nums_left: Vec<i32>,
pub nums_right: Vec<i32>,
}
/// Responsible for parsing the input file into TaskData
pub struct Reader {}
impl Reader {
/// Parses a file where each non-empty line contains a pair of integers
pub fn parse_input(path: &str) -> Result<TaskData, Box<dyn Error>> {
let reader = BufReader::new(File::open(path)?);
let mut nums_left = Vec::new();
let mut nums_right = Vec::new();
for line in reader.lines() {
let input = line?;
if !input.is_empty() {
// Parse line like "3 4" into (3, 4)
let (n0, n1) = parse_line!(input; i32, i32);
nums_left.push(n0);
nums_right.push(n1);
}
}
Ok(TaskData { nums_left, nums_right })
}
}
/// Parses a line into a tuple of typed values with item count validation
macro_rules! parse_line {
( $ln:expr; $( $x:ty ), *) => {{
let mut s = $ln.split_whitespace();
let ans = (
$(s.next()
.ok_or_else(|| trace_me!("too few items in line: {}", $ln))?
.parse::<$x>()?
), *
);
// Ensure there are no extra items
s.next().xor(Some("")).ok_or_else(|| trace_me!("too many items in line: {}", $ln))?;
ans
}};
}
/// Formats error messages with file and line context
macro_rules! trace_me {
( $msg_fmt: expr, $($arg: expr),* ) => {
trace_me!(format!($msg_fmt, $($arg), *))
};
( $msg_fmt: expr) => {
format!("Error '{}' at {}:{}", $msg_fmt, file!(), line!())
}
}
Tags: #rust #adventofcode #algorithms #sorting