Лекция №7. Комбинаторика

Хорошевич Павел Александрович

Определения

Комбинаторика - раздел математики, изучающий дискретные объекты, множества (сочетания, перестановки, размещения и перечисления элементов) и отношения на них.

Сколькими способами можно выбрать открытку и конверт для поздравления, если предлагается 3 различных конверта и 2 различных открытки?

Комбинаторное правило умножения

Вы хотите выучить два иностранных языка из десяти самых распространённых. Сколько разных комбинаций языков вы можете выучить?

Сначала мы берём один из 10 языков, а потом - один из оставшихся 9: \(10 \cdot 9 = 90\).

Если элемент A можно выбрать \(n\) способами, а элемент B - \(m\) способами, то упорядоченную пару A и B можно выбрать \(n \cdot m\) способами.

Умножение используется, когда элементы выбираются последовательно друг за другом из одного множества.

Комбинаторное правило сложения

Вы хотите выбрать фильм для просмотра. У вас есть выбор из 7 комедий и 8 мелодрам. Сколько всего фильмов у вас есть на выбор?

Множество комедий и мелодрам не пересекаются, значит нужно сложить количество элементов: \(7+8=15\) фильмов на выбор.

Если элемент A можно выбрать \(n\) способами, и элемент B - \(m\) способами, то A или B можно выбрать \(n + m\) способами.

Сложение используется, когда выбираем элементы из нескольких непересекающихся множеств.

Задачи на генерирование комбинаторных объектов

  1. Оптимизация и планирование: Генерация всех возможных маршрутов или расписаний для оптимизации процессов, например, в логистике или производстве.
  2. Комбинаторные игры: Анализ всех возможных ходов и стратегий в играх, таких как шахматы или шашки, для определения оптимальной стратегии.
  3. Криптография: Генерация ключей и паролей, где важна уникальность и порядок элементов.

Перестановки

Перестановки без повторений

Перестановками из \(n\) различных элементов называются наборы, каждый из которых содержит все эти \(n\) элементов, взятых в определенном порядке.

\[ P_n=n! \]

Порядок следования элементов в перестановке учитывается.

Список перестановок \(P_3\) для последовательности 🔴🟩🔷:

[[🔴,🟩,🔷],[🔴,🔷,🟩],[🟩,🔴,🔷],[🟩,🔷,🔴],[🔷,🔴,🟩],[🔷,🟩,🔴]]

Рисунок 1

Пример задачи

Вы приехали на экскурсию в новый для вас город. Туристический гид говорит, что есть как минимум 4 достопримечательности, которые нужно посетить обязательно, но не сказал в каком порядке. Выведите на экран все возможные комбинации маршрутов.

\[ P_4=4!=24 \]

import math
n = 4
P = math.factorial(n)
print(P)

Использование вложенных циклов

r = [1, 2, 3, 4]
n = len(r)
for a in range(n):
    for b in range(n):
        for c in range(n):
            for d in range(n):
                if a!=b and c!=a and c != b and d !=a and d!=b and d!=c:
                    print(r[a], r[b], r[c], r[d])

Вывод перестановок без повторений

  1. Просматриваем очередную перестановку справа налево. Ищем индекс первого элемента, который меньше элемента справа (j).
  2. Просматриваем перестановку справа налево и находим первый элемент который больше элемента с индексом j. Сохраняем индекс элемента (k).
  3. Меняем элементы с индексами j и k местами.
  4. Порядок элементов правее индекса j меняем на противоположный (реверсирование массива).
  5. Получаем новую перестановку.
p = [0, 1, 2, 3, 4]
n = len(p) - 1 # индекс последнего элемента

while p[0] == 0:
    j = n
    while p[j-1] > p[j]:
        j -= 1

    k = n
    while p[j-1] > p[k]:
        k -= 1

    # вывод перестановки на экран
    print(p[1:])

    # обмен найденных элементов местами
    p[j-1], p[k] = p[k], p[j-1]

    # реверсирование части списка
    p = p[:j] + list(reversed(p[j:]))

Циклический сдвиг элементов массива

def shift_left(a, k):
    return a[1:k+1] + [a[0]] + a[k+1:]

Генерация перестановок с помощью циклических сдвигов

  1. Создать последовательность размером \(n\). Заполнить ее значениями равными индексам элементов. Создать вспомогательную переменную (в примере \(k\)). Установить ее значение равное индексу последнего элемента. Перейти к пункту 2.
  1. Вывести на экран последовательность. Установить значение \(k\) равное индексу последнего элемента. Перейти к пункту 3.
  1. Выполнить циклический сдвиг влево элементов последовательности от первого до \(k\)-го. Если \(k\)- й элемент последовательности не равен \(k\) то перейти к пункту 2. В противном случае перейти к пункту 4.

  2. Указать новое значение \(k = k-1\). Если \(k\) равен индексу первого элемента последовательности то закончить алгоритм. В противном случае перейти к пункту 3.

Сдвиг

Сдвиг
def shift_left(a, k):
    return a[1:k+1] + [a[0]] + a[k+1:]

lst = [0, 1, 2, 3]
k = len(lst) - 1
n = k

print(lst) # первая перестановка
while k > 0:
    # сдвиг элементов списка влево до k-ого включительно
    lst = shift_left(lst, k)
    if lst[k] != k:
        print(lst) # очередная перестановка
        k = n
    else:
        k -= 1

Генерация перестановок - алгоритм Хипа

Для первого вызова функции используем последовательность из элементов которой хотим получить перестановки и значение \(k\), равное ее длине.

  1. Проверить значение \(k\). В случае если \(k = 1\), вернуть последовательность как текущую перестановку. Закончить вызов функции.
  1. Используя цикл от индекса первого элемента последовательности до \(k\) с шагом в 1 выполнить действия:
    • Рекурсивно вызвать функцию передав последовательность и значение \(k-1\)

    • Если \(k\) четное выполнить обмен \(i\) и \(k\) элемента местами, в противном случае выполнить обмен первого элемента последовательности и \(k\).

Хип

Хип
def hip(a, k):
    if k == 1:
        print(a)
    else:
        for i in range(k):
            hip(a, k - 1)
            if k % 2 == 0:
                a[i], a[k-1] = a[k-1], a[i]
            else:
                a[0], a[k-1] = a[k-1], a[0]

a = [0, 1, 2]
n = len(a)
hip(a, n)

Стандартный модуль itertools

Python-модуль itertools содержит вспомогательные функции для создания итерируемых объектов.

permutations(<последователность>)

Генерация перестановок:

from itertools import permutations
  
lst = ["Физика", "Математика", "Рус-яз", "Бел-яз", "Информатика"]
for p in permutations(lst):
    print(p)

Функция permutations() генерирует все перестановки для последовательности значений. Функция возвращает итератор. Пример знакомого итератора - функция range()

Размещения

Размещения без повторений

Размещениями из \(n\) различных элементов по \(m\) называются наборы, каждый из которых содержит \(m\) элементов из \(n\), взятых в определенном порядке.

\[ A^{m}_n=\frac{n!}{(n-m)!} \]

Порядок следования элементов в размещении учитывается.

Список размещений \(A^2_3\) для последовательности 🔴🟩🔷:

[[🔴,🟩],[🔴,🔷],[🟩,🔴],[🟩,🔷],[🔷,🔴],[🔷,🟩]]

Рисунок 2

Пример задачи

Студентам нужно сдать 5 экзаменов за 11 дней сессии. Сколько различных расписаний экзаменов можно составить?

\[ A^5_{11}=\frac{11!}{(11-5)!}=55440 \]

import math
n = 11
m = 5
A = math.factorial(n) // math.factorial(n - m)
print(A)

Генерация размещений

Используем модуль itertools. Функция permutations() может принимать вторым параметром количество элементов в размещении.

permutations(<последовательность>, <m>)

Пример:

from itertools import permutations

lst = ["Физика", "Математика", "Рус-яз", "Бел-яз", "Информатика"]

for p in permutations(lst, 3):
    print(p)

Сочетания

Сочетания без повторений

Сочетаниями из \(n\) различных элементов по \(m\) называются наборы, каждый из которых содержит \(m\) элементов из \(n\).

\[ C^m_n=\frac{n!}{(n-m)!\cdot m!} \]

Порядок следования элементов в сочетании не важен.

Список сочетаний \(C^2_3\) для последовательности 🔴🟩🔷:

[[🔴,🟩],[🔴,🔷],[🟩,🔷]]

Рисунок 3

Пример задачи

Учитель литературы дал вам список из 5 книг и сказал, что за лето нужно прочитать любые 3 из них. Выведите на экран все возможные наборы из трёх книг.

import math
n = 5
m = 3
C = math.factorial(n) // (math.factorial(n - m) * math.factorial(m))

Пример сокращения импорта функции:

from math import factorial as f
n = 5
m = 3
C = f(n) // (f(n - m) * f(m))

Генерация сочетаний

  1. Создаем последовательность (в дальнейшем a) размером \(k\) элементов. Индексация в последовательности начинается с нуля. Заполняем последовательность значениями от 1 до \(k\). Переходим к пункту 2.
  2. Используем последовательность как очередное сочетание. Переходим к пункту 3.
  1. Начиная с конца последовательности ищем такой элемент, что выполняется условие \(a[i] \le n-k+i\). Если такой элемент найден то переходим у пункту 4. В противном случае заканчиваем алгоритм.
  2. Увеличиваем найденный элемент на единицу. От найденного элемента и до конца последовательности устанавливаем значения на единицу больше предыдущего элемента. Переходим к пункту 2.

Сочетания

Сочетания
n = 3
m = 2

a = [i for i in range(1, m+1)]

while True:
    print(a)
    s = -1
    for i in range(m-1, -1, -1):
        if a[i] <= n - m + i:
            s = i
            a[i] += 1
            break
    if s == -1:
        break
    for i in range(s+1, m):
        a[i] = a[i-1] + 1

Генерация сочетаний модулем itertools

Функция combinations генерирует сочетания из элементов последовательности по \(m\) элементов.

combinations(<последовательность>, <m>)

Пример:

from itertools import combinations

lst = ["Поправка-22", "Братья Карамазовы", "Обломов", "Воскресение", "Мы"]

for сomb in combinations(lst, 3):
    print(сomb)

Алгоритм выбора комбинационной формулы

Для комбинаций без повторяющихся элементов:

Алгоритм

Алгоритм

Примеры решения задач

Задача № 1

На мастер-класс записалось 5 студентов, которых нужно рассадить за 5 пронумерованных столов. Посчитайте количество комбинаций рассадки и выведите их на экран.

Имена студентов вводятся с клавиатуры.

Задача на генерацию перестановок: в комбинации используются все элементы, важен их порядок.

from itertools import permutations
from math import factorial as f

stud_count = 5

names_list = []
for i in range(stud_count):
    name = input("Введите имя: ")
    names_list.append(name)

print(f"Количество перестановок: {f(stud_count)}")
print("Возможные рассадки студентов:")
for p in permutations(names_list):
    print(p)

Задача № 2

Напишите программу с помощью корой можно выбрать из 25 учеников класса шесть человек, которые будут дежурить по школе. Посчитайте количество комбинаций и выведите одну, выбранную случайным образом.

Задача на сочетания: в комбинации не все исходные элементы и не важен порядок их следования.

from itertools import combinations
from random import choice

n = int(input("Количество учеников: "))
m = int(input("Количество дежурных: "))

comb = list(combinations(range(1, n+1), m))
on_duty = choice(comb)
print(f"Сегодня дежурят: {on_duty}")

Задача № 3

Студенты одной из групп изучают 6 дисциплин по 3 пары ежедневно. Сколькими способами можно составить расписание на один день? Выведите 6 первых сгенерированных расписаний в следующем формате:

  1. Дисциплина 1
  2. Дисциплина 2
  3. Дисциплина 3

Задача на размещения: в комбинации не все исходные элементы, порядок элементов важен.

from itertools import permutations

subj_list = ["ПРЗИ", "ТВиМС", "КСиВК", "АСиТЧ", "СиИЭОР", "Физ-ра"]
m = 3 # количество пар в день

p = list(permutations(subj_list, m))

lst = p[:6] # первые 6 элементов списка p

print("Расписание:")
for day in lst:
    print("------")
    for i in range(m):
        print(f"{i+1}. {day[i]}")