Изучите алгоритм поиска с возвратом (backtracking) на примере укладки тетрамино. Практический код на Python и советы по оптимизации. Попробуйте прямо сейчас!
Когда я впервые услышал термин «поиск с возвратом» (backtracking), он показался мне сложным алгоритмом, доступным только учёным. Но после нескольких недель изучения и реализации в проекте Тетрис Оптимизатор я понял удивительную вещь: поиск с возвратом — это просто искусство принять решение, проверить, работает ли оно, и если нет — отменить его и попробовать что-то другое. Эта статья объясняет алгоритм на практическом примере, а не на абстрактных задачах.
Представьте, что у вас есть несколько фигурок тетриса (тетрамино), и ваша цель — разместить их все в наименьшем возможном квадрате. Вот пример фигур:
A A
A A
B B B
B
C C C C
D
D
D DЗадача — разместить каждую фигуру так, чтобы:
Это гораздо сложнее, чем кажется на первый взгляд.
Сначала я думал, что можно просто размещать фигуры одну за другой: поставить A, потом B, потом C, потом D — и готово. Но программирование не всегда так любезно. Иногда первая позиция, которую вы выбрали для фигуры A, делает невозможным размещение D позже. Ошибка была не в D — ошибка случилась гораздо раньше.
Алгоритм поиска с возвратом работает так:
По сути, это означает: «Если этот путь не работает, давай вернёмся и исследуем другой».
Предположим, у нас есть четыре тетрамино. Дерево решений выглядит так:
Начало
├── Поставить A в (0,0)
│ ├── Поставить B
│ │ ├── Поставить C
│ │ │ ├── D встаёт ✅
│ │ │ └── D не встаёт ❌
│ │ └── Попробовать другую позицию
│ └── Переместить A в другое место
└── Попробовать другую позицию для AКаждая ветка — это ещё одна возможность. Поиск с возвратом исследует эти ветки, пока не найдёт работающую.
Сердце алгоритма удивительно компактно. Вот пример на Python:
def solve(index):
# Базовый случай: все фигуры размещены
if index == len(pieces):
return True
# Перебираем все возможные позиции для текущей фигуры
for x in range(board_size):
for y in range(board_size):
# Проверяем, можно ли поставить фигуру
if can_place(pieces[index], x, y):
# Размещаем фигуру
place(pieces[index], x, y)
# Рекурсивно пытаемся разместить следующую
if solve(index + 1):
return True
# Если не получилось — убираем фигуру (возврат)
remove(pieces[index], x, y)
# Ни одна позиция не подошла
return FalseВсё вращается вокруг четырёх действий:
Последний шаг — удаление фигуры — и даёт алгоритму название «поиск с возвратом».
Представьте, что вы решаете лабиринт. Вы выбрали левый путь, прошли немного и упёрлись в тупик. Останетесь ли вы там навсегда? Нет. Вы вернётесь к последнему перекрёстку и попробуете другое направление. Ваш алгоритм делает то же самое.
Поставить фигуру
↓
Тупик
↓
Убрать фигуру
↓
Попробовать другое местоКод не был сложной частью. Сложно было понять, как мыслят рекурсивные вызовы. Как только я перестал представлять рекурсию как «функции, вызывающие функции», и начал представлять её как исследование дерева решений, всё стало гораздо яснее. Рисование деревьев рекурсии на бумаге помогло мне понять, почему алгоритм ведёт себя именно так.
Многие думают, что поиск с возвратом — это просто полный перебор. Но это не совсем так. Полный перебор исследует все возможности. Поиск с возвратом прекращает исследовать путь в тот момент, когда понимает, что этот путь не может привести к успеху.
Может ли фигура C встать?
Нет.
Не продолжай.
Вернись немедленно.Такое раннее отклонение экономит огромное количество работы.
Мой проект «Тетрис Оптимизатор» научил меня, что одного поиска с возвратом недостаточно. Несколько улучшений могут кардинально сократить время выполнения:
Маленькие оптимизации могут превратить решатель, работающий минутами, в тот, что справляется за секунды.
Изучение поиска с возвратом изменило то, как я думаю о программировании. Оно научило меня, что решение сложных задач не всегда требует немедленного знания ответа. Вместо этого нужно иметь смелость:
Этот урок применим далеко за пределами алгоритмов. Иногда самый быстрый путь к решению начинается с готовности сделать шаг назад.
Если вы сейчас изучаете рекурсию или поиск с возвратом, не расстраивайтесь, если сначала кажется запутанным. Почти все сталкиваются с этим, потому что сложность не в написании кода, а в изменении способа мышления о решении задач. Как только этот ментальный сдвиг произойдёт, поиск с возвратом станет одним из самых элегантных приёмов в вашем арсенале.
Что делать прямо сейчас: Возьмите любую задачу комбинаторной оптимизации (например, задачу о ферзях или судоку) и попробуйте реализовать её на Python с использованием поиска с возвратом. Начните с маленького размера, нарисуйте дерево решений и убедитесь, что каждый шаг понятен. Удачи в кодинге!
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →