Стандартная библиотека python, начиная с версии 2.2, предоставляет множество средств для генерирования комбинаторных объектов, но в интернете мне не удалось найти ни одной статьи, которая подробно рассказывала бы о работе с ними. Поэтому я решил исправить это упущение.


Начну с того, что расскажу о комбинаторике и ее основных формулах. Если же вы уже знакомы с этим разделом математики — можете пропустить эти абзацы.


Допустим, у нас есть строка, состоящая из n разных букв и мы хотим вычислить все способы переставить эти буквы местами так, чтобы получить новую строку. На первую позицию в строке мы можем выбрать одну из n букв, имеющихся у нас, на вторую позицию одну из n-1-ой буквы и так далее. В итоге получаем произведение n (n-1)… *1 = n! количество перестановок из n элементов без повторений.


Теперь представим, что количество букв в строке ограничено. У нас есть n доступных букв и мы хотим вычислить количество способов составить из них строку длины k, где k < n, каждую букву мы можем использовать лишь единожды. Тогда на первую позицию в строке мы можем поставить одну из n букв, на вторую позицию одну из n-1 буквы и на k-ую позицию одну из n-k+1 буквы. Общее количество строк будет равно n (n — 1) (n — 2) (n — k + 2) (n — k + 1) = n!/(n-k)! количество размещений из n по k. Если же уникальность букв не требуется, то мы получим формулу n...nn = n^k количество размещений из n по k с повторениями.


До этого мы перебирали последовательности с учетом порядка элементов, а что если порядок для нас не имеет значения. Например, у нас есть есть n разных конфет и мы хотим выбрать k из них, чтобы подарить другу, при чем k < n. Сколько существует способов выбрать k конфет из n без учета порядка? Ответ прост, в начале найдем размещение из n по k без повторений, но тогда одинаковые наборы конфет, имеющие разный порядок их следования будут повторяться. Сколько существует способов переставить k конфет? Правильно, перестановка из k элементов без повторений. Итоговый ответ: размещения из n по k делим на перестановки из k без повторений. Формула: $n!/(n-k)!/k!$ количество сочетаний из n по k.


Рассмотрим случай посложнее, у нас есть n коробок каждая из которых содержит множество конфет одного вкуса, но в разных коробках вкусы разные. Сколько существует способов составить подарок другу из k конфет, при чем один и тот же вкус может встречаться любое количество раз? Так как порядок для нас значения не имеет, давайте разложим подарочные сладости следующим образом: в начале будут лежать последовательно конфеты первого вкуса, затем второго и так далее, а между конфетами разных вкусов положим спички, если конфеты какого-то вкуса отсутствуют в нашем подарке — спички, которые должны были окаймлять этот вкус слева и справа будут стоять рядом. Того у нас получится последовательность, состоящая из k конфет и n-1 спички, ибо вкусов всего n, а спички разделяют их. Теперь заметим, что по расположению спичек, мы можем восстановить исходное множество. Тогда ответом будет количество способов разместить n-1 спичку в n+k-1 ячейку без учета порядка, что равно количеству сочетаний из n+k-1 по n-1, формула: $(n+k-1)!/k!/(n-1)!$ количество сочетаний из n по k с повторениями.


Теперь рассмотрим несколько задач на комбинаторику, чтобы закрепить материал.


Задача 1


Есть 20 человек, сколько существует способов разбить их на пары
Решение: возьмем первого человека, сколько существует способов выбрать ему пару: $20-1 = 19$, возьмем второго человека, сколько существует способов выбрать ему пару: $20-2-1 = 17$. Ответ: 19!!! = 654729075


Задача 2


Есть 10 мужчин и 10 девушек, сколько существует способов разбить их на компании, состоящие из одинакового количества и мужчин и девушек, пустая компания не считается
Решение:
Cпособ 1: количество способов собрать компанию из одного мужчины и одной девушки равно произведению количества способов выбрать одну девушку и количества способов выбрать одного мужчину. Количество способов выбрать одну девушку из 10 равно сочетанию из 10 по 1 без повторений, с мужчинами аналогично, поэтому возведем в квадрат. Далее аналогично вычислим сочетания из 10 по 2, из 10 по 3 и так далее до сочетания из 10 по 10. Итоговая формула: $(10!/(10-1)!/1!)^2 + (10!/(10-2)!/2!)^2 + ... +(10!/(10-10)!/10!)^2$.
Способ 2: рассмотрим множество мужчин, входящих в компанию и множество девушек, не входящих в нее. По этому множеству можно однозначно восстановить компанию, а количество людей в нем всегда равно 10, так как $k + (10 - k) = 10$, k — количество мужчин в компании, $10 - k$ — количество девушек, не вошедших в нее. Количество таких множеств равно количеству сочетаний из 20 по 10, в конечном ответе мы также вычтем единицу, чтобы не учитывать пустую компанию, когда в нашем множестве 10 девушек. Итоговая формула: $20!/(20-10)!/10! - 1 = 184755$.


Итак, мы разобрались с теорией, теперь научимся генерировать комбинаторные объекты с помощью стандартной библиотеки python.
Работать мы будем с библиотекой itertools


from itertools import *

С помощью функции permutations можно сгенерировать все перестановки для итерируемого объекта.


Пример 1


for i in permutations('abc'):
    print(i, end=' ') # abc acb bac bca cab cba
print()
for i in permutations('abb'):
    print(i, end=' ') # abb abb bab bba bab bba 

Исходя из второго вызова заметим, что одинаковые элементы, стоящие на разных позициях, считаются разными.


Пример 2


for i in permutations('abc', 2):
    print(i, end=' ') # ab ac ba bc ca cb 

Размещение отличается от перестановки ограничением на количество доступных ячеек


Пример 3


for i in product('abc', repeat=2):
    print(i, end=' ') # aa ab ac ba bb bc ca cb cc

C помощью размещений с повторениями можно легко перебрать все строки фиксированной длины, состоящие из заданных символов


Пример 4


for i in combinations('abcd', 2):
    print(i, end=' ') # ab ac ad bc bd cd 

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


Пример 5


for i in combinations_with_replacement('abcd', 2):
    print(i, end=' ') # aa ab ac ad bb bc bd cc cd dd  

Результат аналогичен вызову combinations, но в результат также добавлены множества с одинаковыми элементами.


Материалы:
Н.В. Горбачев "Сборник олимпиадных задач по математике"
Документация по python на русском

Комментарии (12)


  1. tmnhy
    11.12.2019 21:10
    +1

    Язык развивается, что ж вы пишете, как в прошлом веке?

    Для первого примера:

    from itertools import permutations
    
    print(' '.join(map(lambda x: ''.join(x), permutations('abc'))))
     # abc acb bac bca cab cba
    


    Не нравится функциональный подход, пожалуйста:
    [''.join(x) for x in permutations('abc')]
    # ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']


    Раз акцент на язык программирования, то пишите на питоне, как на питоне, а не как на языке, который вы знаете.


    1. tmnhy
      11.12.2019 21:42

      Не успел отредактировать, только не запускайте примеры на коллекциях больше 9 элементов. )


    1. longclaps
      12.12.2019 05:04

      print(*map(''.join, permutations('abc')))

      Хорошо ругаетесь, а вот над функциональным подходом стоит поработать.


      1. AlexBin
        12.12.2019 12:12

        Хорошо ругаетесь, а вот над функциональным подходом стоит поработать.

        Не стоит. Я думаю, вы не уловили суть комментария автора. Кажется он хотел напомнить, что Питон рекомендует по возможности выбирать списковые включения вместо мапов (если я правильно понял посыл)


        1. longclaps
          12.12.2019 13:16

          Определённо не уловил: он же сам их (мапы) и использует.
          Использует хуже, чем мог бы, что я и отметил.
          Так что уж впредь запаситесь аргументами получше, как надумаете минусовать.
          На PEP сошлитесь, где «Питон рекомендует», а то подумают, что вы на заборе прочитали.


          1. AlexBin
            12.12.2019 14:31

            Определённо не уловил: он же сам их (мапы) и использует.

            Может я не так понял. Я подумал, что вместо первого примера он предлагает второй.

            На PEP сошлитесь, где «Питон рекомендует», а то подумают, что вы на заборе прочитали.

            Это не пеп, это Гвидо на заборе написал.

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

            Так что уж впредь запаситесь аргументами получше, как надумаете минусовать.

            Ого, сколько агрессии! Я не согласен с вашим комментарием, поставил ему минус и написал, почему я это сделал. Мне кажется, так и надо делать? Если вас это утешит, то за тот комментарий мне кто-то молча поставил минус в карму, не потрудившись аргументировать, ну бывают такие люди. Не принимайте близко к сердцу этот спор.


            1. AlexBin
              12.12.2019 14:43

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


              1. longclaps
                12.12.2019 16:30

                Никак.
                Редюс возвращает результат свёртки, а он может быть произвольного типа — того, который возвращает функция свёртки. Можно даже так пошалить:

                from functools import reduce
                
                l = [1, 2, 3]
                print(reduce(lambda x, y: (x, x.append(y * 2))[0], l, []))
                — только зачем?
                А вот списковое включение возвращает именно список, и с этим ничего не поделать.


    1. qasta
      13.12.2019 17:07

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

      Зачем везде лепить функциональный подход и «новые возможности языка»? «Старое» — не значит «плохое».


  1. tmnhy
    11.12.2019 21:26

    … предоставляет множество средств для генерирования комбинаторных объектов, но в интернете мне не удалось найти не одной статьи, которая подробно рассказывала бы о работе с ними

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


  1. Alexus819
    12.12.2019 10:48

    задача 1. Для второго человека почему 17 получилось?


    1. AgRuN Автор
      12.12.2019 16:16

      Первая пара 2 человека, их исключаем, осталось 18 человек. Берем первого из них и ищем ему пару из оставшихся 17. Исключаем вторую пару, осталось 16, берем первого из них и ищем пару из 15. Получается произведение нечетных чисел