Правила сложения и умножения в дискретных подсчетах
Правила сложения и умножения в дискретных подсчетах
Сколько времени потребуется серверу, чтобы методом полного перебора проверить все возможные пароли из восьми символов? Если пароль состоит только из строчных латинских букв, вариантов около двухсот миллиардов. Но если разрешить заглавные буквы, цифры и спецсимволы, пространство поиска мгновенно разрастается до сотен квадриллионов. Опытный разработчик не запускает перебор в цикле, чтобы узнать его объём: он вычисляет это число за несколько секунд с помощью двух базовых законов комбинаторики — правила сложения и правила умножения.
Вся алгоритмическая комбинаторика вырастает из ответов на два практических вопроса: «Сколько всего состояний может принять система?» и «Сколько итераций совершит алгоритм в худшем случае?». Чтобы находить точные ответы без перебора элементов в памяти, нужно научиться формализовать процесс выбора.
Дискретные множества и мощность
В дискретной математике объектом подсчёта выступает множество — набор различимых элементов. Множество может содержать типы HTTP-запросов, символы алфавита, узлы графа или состояния автомата.
Мощность множества — это количество элементов, входящих в данное множество. Если множество конечно, его мощность обозначается как .
Например, если множество статусов сетевого соединения определено как , то мощность этого множества равна четырем: .
Когда мы решаем алгоритмическую задачу, мы почти всегда конструируем сложные объекты из более простых: собираем строку из отдельных символов, маршрут — из промежуточных вершин графа, а конфигурацию запроса — из набора заголовков. То, как именно мы комбинируем эти элементы, определяет математическую операцию над их мощностями.
Правило сложения: логика взаимоисключающего выбора
Представьте, что микросервис может получить команду на перезагрузку из двух независимых очередей сообщений: через очередь брокера Kafka (где есть 3 типа сервисных событий) или через очередь RabbitMQ (где настроено 4 типа команд). Каждое сообщение обрабатывается одинаково — система берёт ровно одну команду за такт. Сколькими способами можно выбрать одну команду? Очевидно, способами.
Этот интуитивный принцип называется правилом сложения (или правилом суммы).
Правило сложения: Если объект можно выбрать способами, а объект можно выбрать способами, причём выбор объекта исключает одновременный выбор объекта (выборы несовместны), то выбор «либо , либо » можно осуществить способами.
На языке теории множеств правило описывает объединение непересекающихся множеств. Если множества и не имеют общих элементов (их пересечение пусто: ), то мощность их объединения равна сумме их мощностей:
Разберём формулу подробно:
- — объединение множеств и , то есть совокупность всех элементов, принадлежащих хотя бы одному из них;
- — итоговое количество доступных вариантов;
- и — количества элементов в исходных непересекающихся группах.
Пример: в меню консольной утилиты пользователь может выбрать один инструмент анализа: доступно 5 тестов производительности CPU и 3 теста памяти RAM. Тесты не пересекаются. Выбрать ровно один тест для запуска можно способами.
В коде правило сложения проявляется как конструкция ветвления if-elif-else: мы попадаем строго в одну из взаимоисключающих веток, и общее число сценариев выполнения складывается из возможностей каждой ветки.
Правило умножения: последовательные этапы и независимость
Что произойдет, если действия выполняются не вместо друг друга, а последовательно — одно за другим?
Допустим, веб-форма требует задать двухсимвольный идентификатор сессии: первый символ — заглавная латинская буква (из множества мощностью 3), а второй символ — десятичная цифра (из множества мощностью 5). Сколько различных идентификаторов может существовать?
Для буквы есть 5 продолжений: . Для буквы — те же 5 продолжений. Для буквы — снова 5 продолжений.
Каждый из 3 вариантов первого шага порождает ровно 5 вариантов второго шага. Общее число комбинаций равно .
Правило умножения: Если действие можно выполнить способами, и после каждого такого выбора действие можно выполнить способами, то составное действие «сначала , затем » можно выполнить способами.
В теоретико-множественных терминах правило умножения соответствует декартову произведению двух множеств:
Разберём составляющие формулы:
- — множество всех упорядоченных пар вида , где , а ;
- — общее число таких пар (комбинаций);
- — число выборов на первом этапе;
- — число выборов на втором этапе.
Практический пример: генерация тестовой матрицы. Если алгоритм нужно протестировать на 4 типах входных данных (целые, дробные, строки, null) и на 3 различных конфигурациях памяти (128 MB, 512 MB, 2 GB), то общее число тестовых прогонов матрицы составит .
В архитектуре программ правило умножения эквивалентно вложенным циклам for: внешний цикл совершает итераций, а внутренний на каждой итерации внешнего выполняется раз. Общее число операций тела внутреннего цикла равно .
Обобщение на этапов
Правило умножения легко расширяется на произвольное число последовательных решений. Если составной объект формируется за последовательных шагов, причём на первом шаге есть вариантов, на втором — , на третьем — , и так далее до -го шага с вариантами, то общее количество различных составных объектов вычисляется как произведение:
- — совокупное количество комбинаций;
- — глубина последовательности (длина цепочки выбора);
- — число допустимых вариантов на -м шаге.
Вернемся к вопросу из начала статьи. Если пароль состоит из 8 символов, и на каждой позиции может стоять любая из 26 строчных букв латинского алфавита, то число возможных паролей составляет:
Если же мы расширяем алфавит до 62 символов (26 строчных букв + 26 заглавных + 10 цифр), то при той же длине в 8 позиций получаем уже:
Пространство поиска увеличилось более чем в 1000 раз просто за счёт изменения мощности базового алфавита на каждом шаге.
Дерево решений: как увидеть умножение
Любой многоэтапный подсчёт наглядно разворачивается в дерево решений. Корень дерева — исходное состояние до начала выбора. Рёбра, выходящие из узла, соответствуют альтернативам на текущем этапе, а узлы следующего уровня — промежуточным состояниям. Листья дерева (конечные узлы) представляют собой все итоговые уникальные комбинации.
В дереве решений правило умножения работает только при одном ключевом условии: число ветвей, выходящих из любого узла уровня , должно быть строго одинаковым, какие бы конкретные альтернативы мы ни выбирали на предыдущих шагах. Сами варианты могут зависеть от контекста, но их количество обязано оставаться константой.
Сравним два фундаментальных правила в сводной таблице:
| Критерий | Правило сложения | Правило умножения |
|---|---|---|
| Логическая связка | «ИЛИ» (выбор альтернативы) | «И» (последовательность шагов) |
| Операция над множествами | Объединение при | Декартово произведение |
| Аналог в алгоритмах | Ветвление if-else, выбор маршрута |
Вложенные циклы, цепочка вызовов |
| Формула подсчёта | ||
| Результат выбора | Ровно один неделимый элемент | Составной кортеж из нескольких частей |
Главная ловушка: пересекающиеся множества
Типичная ошибка начинающих — применять правило сложения вслепую, забывая о требовании взаимного исключения.
Рассмотрим задачу: в бэкенд-сервисе зарегистрировано 100 пользователей. Из них 40 имеют роль EDITOR, а 25 — роль MODERATOR. Сколькими способами можно выбрать одного привилегированного пользователя?
Соблазн сложить приводит к ошибке, если один и тот же аккаунт может одновременно обладать обеими ролями. Допустим, 10 пользователей имеют обе роли сразу. Если мы сложим 40 и 25, эти 10 человек будут посчитаны дважды.
В таком случае истинное количество вариантов вычисляется с вычитанием дубликатов:
Для нашего примера: .
Правило сложения в чистом виде () строго требует, чтобы множества не пересекались (). Ситуации с пересекающимися множествами подчиняются формуле включений-исключений, которую мы детально изучим в одной из следующих глав курса.
Комбинирование правил: архитектура сложных структур
На практике реальные объекты редко описываются только одной операцией. Чаще всего сложные системы требуют многоуровневого анализа: задача разбивается на взаимоисключающие классы (правило сложения), а внутри каждого класса элементы строятся по цепочке шагов (правило умножения).
Задача о синтаксисе переменной
Разберём классический пример из компиляторостроения. Требуется определить, сколько различных допустимых идентификаторов длины от 1 до 3 символов можно составить в учебном языке программирования по следующим правилам:
- Идентификатор состоит из латинских букв (алфавит из 26 строчных букв) и цифр (от 0 до 9).
- Первым символом идентификатора обязана быть буква.
- Начиная со второй позиции, допускаются как буквы, так и цифры.
Шаг 1. Декомпозиция по взаимоисключающим классам (правило сложения). Идентификатор не может иметь длину 1 и длину 2 одновременно. Длины 1, 2 и 3 — это непересекающиеся сценарии:
- — общее число допустимых идентификаторов;
- — количество идентификаторов длины 1, 2 и 3 соответственно.
Шаг 2. Подсчёт вариантов для каждого класса (правило умножения).
- Длина 1: состоит только из первого символа, который обязан быть буквой.
- Длина 2: первый символ — буква (26 вариантов), второй — буква или цифра ( вариантов). По правилу умножения:
- Длина 3: первый символ — буква (26 вариантов), второй — буква или цифра (36 вариантов), третий — буква или цифра (36 вариантов). По правилу умножения:
Шаг 3. Финальный синтез. Складываем результаты всех сценариев:
Если бы компилятор проверял уникальность имени полным перебором массива в оперативной памяти, ему пришлось бы хранить и сверять почти 35 тысяч вариантов. Зная формулу, мы мгновенно оцениваем границы памяти для хэш-таблицы идентификаторов.
Связь с программным кодом: комбинаторика вместо перебора
Чтобы закрепить фундаментальную связь между правилами подсчёта и кодом, посмотрим на реализацию проверки пространства состояний на Python.
Представьте задачу: сервис авторизации генерирует проверочные коды двух видов:
- Короткий код: 1 латинская буква и 2 цифры (например,
A49). - Длинный код: 2 латинские буквы и 1 цифра (например,
AB7).
Мы можем сгенерировать все коды физически через itertools.product и посчитать длину списка. Но если диапазон параметров вырастет, этот код приведет к исчерпанию оперативной памяти (Memory Limit Exceeded). Формула же выполняется за процессорного времени и не требует памяти вовсе:
import string
letters = string.ascii_uppercase # 26 букв
digits = string.digits # 10 цифр
# Аналитический подсчет за O(1) памяти и времени:
# Вариант 1 (Буква-Цифра-Цифра): 26 * 10 * 10
# Вариант 2 (Буква-Буква-Цифра): 26 * 26 * 10
count_variant_1 = len(letters) * len(digits) * len(digits)
count_variant_2 = len(letters) * len(letters) * len(digits)
total_combinations = count_variant_1 + count_variant_2
print(f"Аналитический подсчет: {total_combinations}")
# Вывод: 2600 + 6760 = 9360
Умение декомпозировать сложную структуру на независимые альтернативы (сложение) и упорядоченные цепочки шагов (умножение) — базовый навык для построения алгоритмов. Именно эти два правила станут фундаментом для следующих тем курса: упорядоченного выбора без возвращения (размещений и перестановок) и неупорядоченного отбора (сочетаний).
