Перебор с возвратом
Идея: дерево решений
Перебор с возвратом (backtracking) — систематический обход всех вариантов, где решение строится шаг за шагом. На каждом шаге мы делаем выбор, уходим глубже, а вернувшись — отменяем выбор и пробуем следующий.
Шаблон почти всегда один и тот же:
def backtrack(состояние):
if решение_готово(состояние):
result.append(копия(состояние))
return
for выбор in варианты(состояние):
if не_подходит(выбор): # отсечение
continue
сделать(выбор)
backtrack(состояние)
отменить(выбор)
Подмножества
def subsets(nums):
result, path = [], []
def backtrack(start):
result.append(path[:]) # копия! path потом изменится
for i in range(start, len(nums)):
path.append(nums[i]) # выбрать
backtrack(i + 1) # продолжить
path.pop() # отменить
backtrack(0)
return result
print(subsets([1, 2, 3]))
Пройдите пример 👣 Пошагово: видно, как path растёт и укорачивается.
Самая частая ошибка — забыть копию
result.append(path) добавит ссылку на один и тот же список. После всех pop() он станет пустым, и в результате окажутся одни пустые списки. Нужно path[:] или list(path).
result, path = [], []
for x in [1, 2]:
path.append(x)
result.append(path) # ошибка: одна и та же ссылка
print(result)
path.clear()
print(result)
Перестановки
def permutations(nums):
result, path, used = [], [], [False] * len(nums)
def backtrack():
if len(path) == len(nums):
result.append(path[:])
return
for i, x in enumerate(nums):
if used[i]:
continue
used[i] = True
path.append(x)
backtrack()
path.pop()
used[i] = False
backtrack()
return result
print(permutations([1, 2, 3]))
Сочетания и отсечения
Выбрать k элементов из n: отличие от подмножеств — сохраняем только пути длины k. Отсечение: если элементов осталось меньше, чем нужно добрать, дальше идти бессмысленно.
calls = 0
def combine(n, k, prune):
global calls
result, path = [], []
def backtrack(start):
global calls
calls += 1
if len(path) == k:
result.append(path[:])
return
need = k - len(path)
stop = n - need + 2 if prune else n + 1 # не начинаем, если не хватит элементов
for i in range(start, stop):
path.append(i)
backtrack(i + 1)
path.pop()
backtrack(1)
return result
for prune in (False, True):
calls = 0
r = combine(12, 9, prune)
print(f"отсечение={prune}: сочетаний {len(r)}, вызовов {calls}")
Тот же результат, но рекурсивных вызовов в разы меньше. На собеседовании отсечения — главный способ показать, что вы думаете о производительности.
Ферзи
Классика: расставить n ферзей на доске n×n, чтобы они не били друг друга. Ставим по одному ферзю в строку и помним занятые столбцы и диагонали.
def solve_queens(n):
cols, diag1, diag2 = set(), set(), set()
placement, solutions = [], []
def backtrack(row):
if row == n:
solutions.append(placement[:])
return
for col in range(n):
if col in cols or row - col in diag1 or row + col in diag2:
continue
cols.add(col); diag1.add(row - col); diag2.add(row + col)
placement.append(col)
backtrack(row + 1)
placement.pop()
cols.remove(col); diag1.remove(row - col); diag2.remove(row + col)
backtrack(0)
return solutions
solutions = solve_queens(6)
print(f"решений для 6×6: {len(solutions)}")
for col in solutions[0]:
print(" ".join("Q" if c == col else "." for c in range(6)))
print([len(solve_queens(n)) for n in range(1, 9)])
Готовые генераторы itertools
Если нужно просто перечислить варианты, а не перебирать с отсечениями, используйте itertools — это быстрее и короче.
from itertools import permutations, combinations, combinations_with_replacement, product
print(list(permutations("abc", 2)))
print(list(combinations([1, 2, 3, 4], 2)))
print(list(combinations_with_replacement("ab", 2)))
print(["".join(p) for p in product("01", repeat=3)])
На собеседовании обычно просят написать перебор руками, а itertools упомянуть как «в продакшене я бы взял готовое».
Сложность и когда это не перебор
| Задача | Число вариантов |
|---|---|
| подмножества | 2ⁿ |
| перестановки | n! |
| сочетания из n по k | C(n, k) |
| скобки из n пар | число Каталана, ≈ 4ⁿ / n^1.5 |
Это экспоненты, так что перебор годится для n до 10–20. Если в задаче просят количество способов или оптимум (а не все варианты), а подзадачи повторяются — это сигнал к динамическому программированию из следующего урока.
Сигналы задачи на перебор
«Найдите все варианты/комбинации/перестановки», «расставьте», «можно ли составить», маленькие ограничения (n ≤ 15), головоломки (судоку, ферзи, слово в сетке).
Итог
- Шаблон: выбрать → рекурсивно продолжить → отменить выбор.
- Добавляйте в результат копию текущего пути.
- Отсечения отбрасывают заведомо бесполезные ветки и ускоряют перебор в разы.
- Сложность экспоненциальная; для «сколько способов» и «оптимум» думайте о ДП.
Практика решено 0 из 6
Все подмножества. Верните список всех подмножеств списка nums (элементы различны).
Порядок подмножеств неважен, внутри подмножества сохраняйте порядок исходного списка.
Решите перебором с возвратом, без itertools.
Какие тесты проверяют код (3)
assert sorted(subsets([1, 2, 3])) == [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]] assert subsets([]) == [[]] and len(subsets(list(range(10)))) == 1024 import inspect; assert "itertools" not in inspect.getsource(subsets), "Без itertools — перебором с возвратом"
def subsets(nums):
result, path = [], []
def backtrack(start):
result.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1)
path.pop()
backtrack(0)
return result
Сумма комбинаций. Даны различные положительные candidates и target. Верните все уникальные
комбинации чисел, дающие в сумме target; одно число можно брать сколько угодно раз.
Каждую комбинацию возвращайте отсортированной по возрастанию, порядок комбинаций неважен.
Какие тесты проверяют код (3)
assert sorted(combination_sum([2, 3, 6, 7], 7)) == [[2, 2, 3], [7]] assert sorted(combination_sum([2, 3, 5], 8)) == [[2, 2, 2, 2], [2, 3, 3], [3, 5]] assert combination_sum([2], 1) == [] and combination_sum([1], 2) == [[1, 1]]
def combination_sum(candidates, target):
candidates = sorted(candidates)
result, path = [], []
def backtrack(start, remaining):
if remaining == 0:
result.append(path[:])
return
for i in range(start, len(candidates)):
c = candidates[i]
if c > remaining:
break # дальше только больше — отсечение
path.append(c)
backtrack(i, remaining - c) # i, а не i + 1: число можно повторять
path.pop()
backtrack(0, target)
return result
Правильные скобочные последовательности. Верните все правильные скобочные последовательности
из n пар скобок в лексикографическом порядке ("(" < ")").
Стройте строку посимвольно: открывающую можно поставить, пока их меньше n,
закрывающую — пока закрытых меньше, чем открытых.
Какие тесты проверяют код (3)
assert generate_parentheses(1) == ["()"] and generate_parentheses(2) == ["(())", "()()"] assert generate_parentheses(3) == ["((()))", "(()())", "(())()", "()(())", "()()()"] assert len(generate_parentheses(8)) == 1430
def generate_parentheses(n):
result = []
def backtrack(s, opened, closed):
if len(s) == 2 * n:
result.append(s)
return
if opened < n:
backtrack(s + "(", opened + 1, closed)
if closed < opened:
backtrack(s + ")", opened, closed + 1)
backtrack("", 0, 0)
return result
Поиск слова в сетке. Есть ли в сетке букв board слово word? Слово составляется
из соседних по горизонтали или вертикали клеток, каждую клетку можно использовать один раз.
Какие тесты проверяют код (2)
b = [list("ABCE"), list("SFCS"), list("ADEE")]
assert exist(b, "ABCCED") and exist(b, "SEE") and not exist(b, "ABCB")
assert b == [list("ABCE"), list("SFCS"), list("ADEE")], "Сетку нужно вернуть в исходное состояние"
assert exist([list("AB"), list("CD")], "ACDB") and not exist([["A"]], "AA")
def exist(board, word):
rows, cols = len(board), len(board[0])
def dfs(r, c, i):
if i == len(word):
return True
if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != word[i]:
return False
saved, board[r][c] = board[r][c], "#" # помечаем клетку как занятую
found = any(dfs(r + dr, c + dc, i + 1) for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)))
board[r][c] = saved # отменяем выбор
return found
return any(dfs(r, c, 0) for r in range(rows) for c in range(cols))
Сколько вариантов перебирает алгоритм, генерирующий все перестановки n различных элементов?
Что генерирует каждая функция itertools для списка из n элементов?
| 2ⁿ вариантов | n! вариантов | C(n, k) вариантов | |
|---|---|---|---|
permutations(xs) |
|||
combinations(xs, k) |
|||
product([0, 1], repeat=n) |
Войдите, чтобы отмечать пройденные уроки.