Подготовка к алгоритмическим задачам

Модуль itertools в Python: перебор без лишней памяти
Коротко
| Функция | Что делает |
|---|---|
product | декартово произведение (вложенные циклы) |
permutations | все перестановки |
combinations | все сочетания |
accumulate | накопленные суммы |
groupby | группировка подряд идущих |
chain | склейка последовательностей |
islice | срез для итератора |
Главное свойство всех функций модуля: они возвращают итераторы, а не списки. Элементы вычисляются по одному, память не расходуется на весь результат.
Комбинаторные функции
product: замена вложенным циклам
from itertools import product # было for i in range(3): for j in range(3): for k in range(3): ... # стало for i, j, k in product(range(3), repeat=3): ...
Особенно удобно, когда количество вложенных циклов заранее неизвестно:
# все двоичные строки длины 4 for bits in product('01', repeat=4): print(''.join(bits)) # 0000, 0001, 0010, ... # перебор комбинаций из разных наборов sizes = ['S', 'M', 'L'] colors = ['красный', 'синий'] for size, color in product(sizes, colors): print(size, color)
permutations: перестановки
from itertools import permutations list(permutations([1, 2, 3])) # [(1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), (3,2,1)] list(permutations([1, 2, 3], 2)) # размещения по 2 # [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]
Помните про рост: permutations от 10 элементов даёт 3.6 миллиона кортежей, от 12 — почти 480 миллионов. Перебор перестановок реалистичен максимум до 10–11 элементов.
combinations: сочетания
Порядок не важен, элементы идут в исходном порядке:
from itertools import combinations, combinations_with_replacement list(combinations([1, 2, 3], 2))
Решай алгоритмические задачи как профи

[(1,2), (1,3), (2,3)]
list(combinations_with_replacement([1, 2, 3], 2))
[(1,1), (1,2), (1,3), (2,2), (2,3), (3,3)]
Практический пример — перебор всех пар:
```python
points = [(0, 0), (3, 4), (6, 8)]
for a, b in combinations(points, 2):
dist = ((a[0]-b[0])**2 + (a[1]-b[1])**2) ** 0.5
print(a, b, round(dist, 2))
Это чище и быстрее, чем двойной цикл с if i < j.
Перебор всех подмножеств
Классическая идиома, которую стоит запомнить:
from itertools import chain, combinations def powerset(items): return chain.from_iterable( combinations(items, r) for r in range(len(items) + 1) ) print(list(powerset([1, 2, 3]))) # [(), (1,), (2,), (3,), (1,2), (1,3), (2,3), (1,2,3)]
accumulate: накопленные значения
Префиксные суммы одной строкой:
from itertools import accumulate import operator list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10] list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24] — произведения list(accumulate([3, 1, 4, 1, 5], max)) # [3, 3, 4, 4, 5] — префиксный максимум list(accumulate([1, 2, 3], initial=0)) # [0, 1, 3, 6] — Python 3.8+
Параметр initial=0 особенно полезен для префиксных сумм: он даёт привычный массив длины n+1, где prefix[i] — сумма первых i элементов.
nums = [3, 1, 4, 1, 5] prefix = list(accumulate(nums, initial=0)) def range_sum(l: int, r: int) -> int: return prefix[r + 1] - prefix[l] print(range_sum(1, 3)) # 6
groupby: группировка подряд идущих
Самая недопонятая функция модуля. Она группирует только соседние одинаковые элементы, а не все одинаковые в последовательности.
from itertools import groupby data = [1, 1, 2, 2, 2, 1, 1] for key, group in groupby(data): print(key, list(group)) # 1 [1, 1] # 2 [2, 2, 2] # 1 [1, 1] ← единицы разбились на две группы!
Чтобы сгруппировать все одинаковые, данные нужно предварительно отсортировать по тому же ключу:
words = ['кот', 'дом', 'кит', 'дым'] for key, group in groupby(sorted(words, key=len), key=len): print(key, list(group)) # 3 ['дом', 'дым', 'кит', 'кот']
Зато поведение «только соседние» полезно само по себе — например, для сжатия последовательностей:
def rle_encode(s: str) -> str: """Кодирование длин серий: aaabbc → a3b2c1""" return ''.join(f'{ch}{len(list(g))}' for ch, g in groupby(s)) print(rle_encode('aaabbc')) # a3b2c1
Работа с последовательностями
chain: склейка
from itertools import chain list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5] # развернуть список списков nested = [[1, 2], [3, 4], [5, 6]] list(chain.from_iterable(nested)) # [1, 2, 3, 4, 5, 6]
chain.from_iterable — самый быстрый способ уплощить список списков, быстрее вложенного генератора.
islice: срез для итератора
Обычный срез [1:5] не работает с итераторами и генераторами. islice решает эту проблему:
from itertools import islice, count # бесконечный счётчик first_five = list(islice(count(10), 5)) # [10, 11, 12, 13, 14] # первые N строк файла без чтения всего файла with open('big.txt') as f: for line in islice(f, 10): print(line)
Бесконечные итераторы
from itertools import count, cycle, repeat count(10) # 10, 11, 12, ... count(0, 0.5) # 0, 0.5, 1.0, ... cycle('abc') # a, b, c, a, b, c, ... repeat('x', 3) # x, x, x
Обязательно ограничивайте их через islice, zip или break — иначе бесконечный цикл.
# нумерация с чередованием for i, color in zip(count(1), cycle(['красный', 'синий'])): if i > 4: break print(i, color)
pairwise: соседние пары
Появилась в Python 3.10, до этого её писали вручную:
from itertools import pairwise list(pairwise([1, 2, 3, 4])) # [(1,2), (2,3), (3,4)] # проверка возрастания nums = [1, 3, 5, 7] print(all(a < b for a, b in pairwise(nums))) # True
Фильтрация
from itertools import takewhile, dropwhile, compress, filterfalse nums = [1, 2, 3, 10, 4, 5] list(takewhile(lambda x: x < 5, nums)) # [1, 2, 3] — до первого несоответствия list(dropwhile(lambda x: x < 5, nums)) # [10, 4, 5] — после первого несоответствия list(compress('абвгд', [1, 0, 1, 0, 1])) # ['а', 'в', 'д'] — по маске list(filterfalse(lambda x: x % 2, nums)) # [2, 10, 4] — обратный filter
takewhile останавливается на первом несоответствии и не проверяет остальные — в отличие от filter, который проходит всю последовательность.
Практический пример: решение задачи перебором
Когда n маленькое, itertools заменяет ручной бэктрекинг:
from itertools import combinations def subset_sum(nums: list[int], target: int): """Найти подмножество с заданной суммой.""" for size in range(1, len(nums) + 1): for subset in combinations(nums, size): if sum(subset) == target: return list(subset) return None print(subset_sum([3, 34, 4, 12, 5, 2], 9)) # [4, 5]
Решение за O(2ⁿ), зато написано за минуту. Для n ≤ 20 этого часто достаточно.
Главное преимущество: экономия памяти
from itertools import permutations import sys # список всех перестановок 10 элементов — сотни мегабайт # perms = list(permutations(range(10))) # итератор — константная память for perm in permutations(range(10)): if perm[0] == 9: break # прервались, не построив весь список
Именно поэтому все функции возвращают итераторы. Оборачивать в list() стоит только тогда, когда результат действительно нужен целиком.
Частые ошибки
groupby без сортировки. Самая частая. Группируются только соседние элементы, и результат оказывается неожиданным.
Повторный проход по итератору. Итератор одноразовый:
perms = permutations([1, 2, 3]) print(len(list(perms))) # 6 print(len(list(perms))) # 0 — уже исчерпан!
Использование группы после перехода к следующей. В groupby объект группы становится недействительным, как только вы перешли к следующей. Сохраняйте через list(group) сразу.
Бесконечный итератор без ограничения. for x in count(): ... без break повесит программу.
list() на больших перестановках. list(permutations(range(12))) попытается выделить память под 479 миллионов кортежей.
Что запомнить
- Все функции возвращают итераторы — память расходуется по одному элементу.
productзаменяет вложенные циклы, в том числе с неизвестной заранее глубиной.combinationsдля пар и подмножеств,permutationsдля порядка.accumulate(nums, initial=0)даёт готовые префиксные суммы.groupbyгруппирует только соседние элементы — сортируйте заранее.- Итератор одноразовый: повторный проход даст пустоту.
