Задача 4-го дня Advent of Code — Ceres Search.
📌 Часть I. Условие задачи
Нам дано поле из символов. Нужно найти все вхождения слова "XMAS"
.
Слово может быть записано:
- по горизонтали,
- по вертикали,
- по диагонали,
- в обратном порядке,
- поверх других слов (т.е. вхождения могут перекрываться).
💡 Идея
Для поиска всех таких слов:
- Перебираем каждую ячейку как потенциальную стартовую.
- Из неё идём во все
8
направлений (вверх
, вниз
, вправо
, влево
, и 4 диагонали
).
- Читаем
4
символа в этом направлении.
- Сравниваем полученное слово с
"XMAS"
.
📌 Часть II. Условие задачи
Теперь требуется найти не само слово XMAS, а X
-образное расположение двух слов "MAS"
вокруг символа "A"
.
Пример расположения:
M . S
. A .
M . S
💡 Идея
- Перебираем внутренние клетки (без границ).
- Ищем такие клетки, где по центру стоит
"A"
.
- Проверяем две диагонали:
- Главную: верх-лево и низ-право,
- Побочную: верх-право и низ-лево.
- В каждой диагонали должна быть
"M"
и "S"
(в любом порядке).
🛠 Подробности реализации
- Все 8 направлений закодированы как массив смещений
DIRECTIONS
.
- Функция
read_in_direction
позволяет прочитать последовательность символов из поля, начиная с любой точки и идущую в указанном направлении.
- Функция
matches
сравнивает итератор с ожидаемым словом. Она останавливается при первом несовпадении.
- Решение второй части (
solve_hard
) просто перебирает центр "A"
, и проверяет обе диагонали.
⏱ Асимптотика
Пусть n × m
— размеры поля.
-
Временная сложность:
solve_simple
: O(n * m)
- Перебор всех клеток:
O(n * m)
- В каждой клетке — перебор 8 направлений, в каждом — чтение до 4 символов и сравнение ⇒
O(1)
solve_hard
: _O(n * m)
- Перебор внутренних клеток:
O(n * m)
- В каждой —
2
сравнения по 2
символа ⇒ O(1)
-
Пространственная сложность:
O(1)
- Используются только счётчики, временные переменные и короткие массивы длины
2
.
- Все итераторы — ленивые, без аллокации.
📄 Исходный код
use super::TaskData;
pub struct Solution;
impl Solution {
// All 8 directions: diagonals, verticals, and horizontals
const DIRECTIONS: [(i32, i32); 8] = [
(-1, 1), (-1, 0), (-1, -1),
(0, -1), (1, -1), (1, 0),
(1, 1), (0, 1),
];
/// Part I — count all "XMAS" occurrences in any direction
pub fn solve_simple(data: &TaskData) -> i32 {
let maze = &data.maze;
let rows = maze.len();
let cols = maze[0].len();
let mut count = 0;
for r in 0..rows {
for c in 0..cols {
for (dr, dc) in Self::DIRECTIONS {
let iter = Self::read_in_direction(maze, r, c, 4, dr, dc);
if Self::matches(iter, b"XMAS") {
count += 1;
}
}
}
}
count
}
/// Part II — count "X-MAS" patterns forming an X shape centered at 'A'
pub fn solve_hard(data: &TaskData) -> i32 {
let maze = &data.maze;
let rows = maze.len();
let cols = maze[0].len();
let mut count = 0;
for r in 1..rows - 1 {
for c in 1..cols - 1 {
if maze[r][c] != b'A' {
continue;
}
// Check diagonals around center 'A'
let mut diag1 = [maze[r - 1][c - 1], maze[r + 1][c + 1]];
let mut diag2 = [maze[r - 1][c + 1], maze[r + 1][c - 1]];
diag1.sort();
diag2.sort();
if Self::matches(diag1.into_iter(), b"MS") && Self::matches(diag2.into_iter(), b"MS") {
count += 1;
}
}
}
count
}
/// Reads a sequence of `len` characters from (r0, c0) in direction (dr, dc)
fn read_in_direction(maze: &[Vec<u8>], r0: usize, c0: usize, len: usize, dr: i32, dc: i32) -> impl Iterator<Item = u8> + '_ {
let rows = maze.len() as i32;
let cols = maze[0].len() as i32;
std::iter::once(maze[r0][c0]).chain(
(1..len).scan((r0, c0), move |(r, c), _| {
let nr = *r as i32 + dr;
let nc = *c as i32 + dc;
((0..rows).contains(&nr) && (0..cols).contains(&nc)).then(|| {
*r = nr as usize;
*c = nc as usize;
maze[*r][*c]
})
}),
)
}
/// Returns true if the iterator matches the expected byte string exactly
fn matches<I>(mut iter: I, expected: &[u8]) -> bool
where
I: Iterator<Item = u8>,
{
expected.iter().all(|&e| iter.next() == Some(e))
}
}
Tags: #rust #algorithms #adventofcode #strings