Генерация случайных сочетаний. Генерация сочетания по его порядковому номеру

Автор: Герасимов Василий Александрович
Источник: RSDN Magazine #3-2010
Опубликовано: 04.07.2011
Версия текста: 1.0
Базовые определения и обозначения
Сочетание
Число сочетаний
Перестановка
Вычисление биномиального коэффициента
Алгоритм, использующий дополнительную память
Алгоритм, не использующий дополнительную память
Генерация случайного сочетания методом случайной перестановки
Алгоритм
Оценка временной сложности
Реализация
Генерация сочетания по его номеру
Алгоритм
Оценка временной сложности
Реализация
Использование реализованных функций
Сравнение двух алгоритмов
Потребление памяти
Производительность
Некоторые полезные свойства алгоритма генерации сочетания по его номеру
Компактное хранение сочетаний
Заключение
Источники

Исходный текст библиотеки - Combinatorics.h

Демонстрационный пример - Demo.cpp

В многочисленной литературе по комбинаторике, дискретной математике и алгоритмам достаточно часто приводятся алгоритмы, с помощью которых можно перечислить все сочетания из n по k. Но вполне возможна и такая ситуация, когда необходимо выбрать всего одно сочетание. Например, случайным образом. Данная статья описывает два алгоритма генерации таких сочетаний. Также, благодаря специфике одного из алгоритмов, возникает возможность достаточно компактного хранения сочетания (при условии, что известно множество значений элементов, из которых выбирается сочетание). Приведена реализация алгоритмов в виде библиотеки на языке C++.

Базовые определения и обозначения

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

Сочетание

Имеем конечное множество G, состоящее из n элементов (|G| = n). Сочетанием из n элементов по k назовём любое подмножество H множества G, состоящее из k элементов (|H| = k).

ПРИМЕЧАНИЕ

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

В программе часто множество представляется как список из n элементов. Сочетание в таком случае иногда удобно представлять как список из n значений 0 или 1, где единица на i-й позиции означает, что i-й элемент множества принадлежит сочетанию. Это удобно, когда элементы множества представляют собой структуры, копирование которых нежелательно или занимает много времени. Мы в дальнейшем будем использовать оба формата представления – список из элементов типа bool или обычная последовательность элементов произвольного типа. Назовём для краткости первый формат «список из bool», а второй – «подмножество».

Число сочетаний

Формула для количества всех возможных сочетаний из n по k общеизвестна:


Число, стоящее слева от знака равенства, называется числом сочетаний из n по k (читается «цэ из эн по ка») или биномиальным коэффициентом.

Свойства числа сочетаний

В дальнейшем нам понадобятся некоторые свойства числа сочетаний.

1. Свойство симметричности:


2. Рекуррентная формула для числа сочетаний:


ПРИМЕЧАНИЕ

Интуитивно свойство 2 означает следующее. Если в множестве G, из которого составляется сочетание H, зафиксировать какой-либо элемент x, то все возможные сочетания делятся на две категории: сочетания H, которые содержат x, и сочетания H, не содержащие его. Количество сочетаний первого типа равно числу сочетаний из n-1 (так как элемент x фиксирован) по k-1, второго типа – из n-1 по k.

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

Пусть G = {g1, g2, …, gn} – множество, состоящее из n элементов. Тогда перестановкой Pn назовём упорядоченный набор всех n элементов из G, в котором элементы не повторяются:





Вычисление биномиального коэффициента

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

Алгоритм, использующий дополнительную память

Этот алгоритм основывается на рекуррентной формуле (свойство 2 биномиального коэффициента):


Зная значения коэффициента из n-1 по k и k-1, мы легко можем вычислить его для n и k, которые были заранее сохранены в таблице.

Условия окончания рекурсии будут следующие:


Также, чтобы сократить количество используемой памяти, будем использовать формулу


для всех k, больших n-k.

Реализация

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

Реализация алгоритма вычисления биномиального коэффициента, использующего дополнительную память
          #include
          <vector>
          namespace Combinatorics
{
  namespace ImplDetails
  {
    template<typename Integer, class Allocator1, class Allocator2>
    Integer GetBinomialCoefficientImpl(Integer n, Integer k,
      std::vector<std::vector<Integer, Allocator1>, Allocator2>& Cache)
    {
      // Значение k, которое мы действительно будем использоватьif (n - k < k)
        k = n - k;

      // Значения k == 0 или 1 позволяют не хранить в памяти все// коэффициенты с n <= 3:// Небольшая оптимизация, позволяющая выполнять// одну проверку вместо двух при k>1.// Равенства n == k и 1 == n - k проверять// не нужно, так как k = min(k, n - k)if (k <= 1)
        return (0 == k ? 1 : n);

      // Приводим n в этот тип, чтобы не было warning'овtypedef std::vector<std::vector<Integer, Allocator1>,
        Allocator2>::size_type NVectorSizeType;
      // Приводим k в этот тип, чтобы не было warning'овtypedef std::vector<Integer, Allocator1>::size_type
        KVectorSizeType;
      if (Cache.size() < NVectorSizeType(n - 3))
        Cache.resize(NVectorSizeType(n - 3));
      if (Cache[NVectorSizeType(n - 4)].size() < KVectorSizeType(k - 1))
        Cache[NVectorSizeType(n - 4)].resize(KVectorSizeType(k - 1));

      // Для целых чисел конструктор по умолчанию// (Integer()) будет давать 0,// поэтому невычисленные коэффициенты в кэше,// которые были получены// при выполнении операции resize(), равны нулю.// [n - 4] - так как для n == 0, 1, 2, 3 результат тривиален// (k == 0 или 1) и не сохраняетсяif (0 == Cache[NVectorSizeType(n - 4)][KVectorSizeType(k - 2)])
        Cache[NVectorSizeType(n - 4)][KVectorSizeType(k - 2)] =
        GetBinomialCoefficientImpl(n - 1, k - 1, Cache) +
        GetBinomialCoefficientImpl(n - 1, k, Cache);
      return Cache[NVectorSizeType(n - 4)][KVectorSizeType(k - 2)];
    }
  } // namespace ImplDetailstemplate<typename Integer, class Allocator1, class Allocator2>
  Integer GetBinomialCoefficient(Integer n, Integer k,
    std::vector<std::vector<Integer, Allocator1>, Allocator2>& Cache)
  {
    // Если тип Integer беззнаковый, то оптимизирующий компилятор// уберёт проверку k < 0if (n <= 0 || k < 0 || k > n)
      return 0;
    return ImplDetails::GetBinomialCoefficientImpl(n, k, Cache);
  }

  template<typename Integer>
  Integer GetBinomialCoefficient(Integer n, Integer k)
  {
    static std::vector<std::vector<Integer> > _Cache;
    return GetBinomialCoefficient(n, k, _Cache);
  }
} // namespace Combinatorics
ПРИМЕЧАНИЕ

Несмотря на то, что в параметрах шаблонов тип чисел n и k был назван Integer, функции при желании можно использовать и с типом double или float.

Вычислительная сложность

Этот алгоритм хорош, когда необходимо вычислить сразу много биномиальных коэффициентов, но он использует дополнительную память. Затраты памяти на хранение биномиальных коэффициентов при этом будут не больше (n-3)*(min(k,n-k)-2)*sizeof(Integer) байт (при n>3, n-k и k>2), т.е. O(nk). Вычислительная сложность также равна O(nk) (количество операций сложения – по одной на каждый элемент таблицы), если коэффициент вычисляется впервые (т.е. полностью заполняется вся таблица) и O(1), если он был вычислен ранее и берётся из таблицы (кэша).

Алгоритм, не использующий дополнительную память

Алгоритм, не использующий дополнительную память, основывается на следующей рекуррентной формуле:


Реализация

Реализация алгоритма вычисления биномиального коэффициента, не использующего дополнительную память
          namespace Combinatorics
{
  namespace ImplDetails
  {
    template<typename Integer>
    Integer GetBinomialCoefficient2Impl(Integer n, Integer k)
    {
      if (0 == k) // случай n == k рассматривать не надоreturn 1;
      return (n * GetBinomialCoefficient2Impl(n - 1, k - 1)) / k;
    }
  } // namespace ImplDetailstemplate<typename Integer>
  Integer GetBinomialCoefficient2(Integer n, Integer k)
  {
    // Если тип Integer беззнаковый, то оптимизирующий// компилятор уберёт проверку k < 0if (n <= 0 || k < 0 || k > n)
      return 0;
    if (n - k < k)
      k = n - k;
    return ImplDetails::GetBinomialCoefficient2Impl(n, k);
  }
} // namespace Combinatorics

Вычислительная сложность

Эта реализация требует k операций умножения и деления. То есть его вычислительная сложность равна O(k).

Алгоритм работает быстрее, но у него есть определённые недостатки. Даже при небольших n он вызывает переполнение целых чисел, так как умножает n на биномиальный коэффициент, который представляет собой достаточно большое число. Так, если Integer – беззнаковое целое число, состоящее из четырёх байт, то переполнение возникает уже при n=50 и k=8.

Генерация случайного сочетания методом случайной перестановки

Первый алгоритм, который мы опишем, является достаточно простым и очевидным.

Алгоритм

Идея алгоритма состоит в следующем. Возьмём список объектов из n элементов G=(g1, g2, … gn). Это будет наше множество, из которого мы будем строить сочетание. Сочетанием H из n по k назовём строку из n битов, в которой ровно k единиц. Единица на i-м месте означает, что в данное сочетание входит элемент gi. Например, H может быть равно H'=(1, … 1, 0, … 0), где первые k элементов единицы, остальные – нули. Тогда случайное сочетание H – это случайная перестановка строки H'.

В STL есть функция random_shuffle(), которая генерирует случайную перестановку.

        namespace std
{
  template<class RandomAccessIterator>
  void random_shuffle(RandomAccessIterator First,
    RandomAccessIterator Last);

  template<class RandomAccessIterator,
  class RandomNumberGenerator>
    void random_shuffle(RandomAccessIterator First,
    RandomAccessIterator Last, RandomNumberGenerator& Rand);
} // namespace std

Первая функция использует генератор случайных чисел по умолчанию, вторая – в качестве параметра. RandomNumberGenerator – это функциональный объект, который в качестве параметра принимает число n типа, соответствующего результату операции Last-First, и возвращает случайное число r: 0≤r<n. RandomAccessIterator – итератор произвольного доступа.

Оценка временной сложности

Временная сложность данного алгоритма – это временная сложность алгоритма std::random_shuffle(). Если посмотреть исходники STL (например, в Microsoft Visual Studio 2008), то можно увидеть, что каждый элемент, начиная со второго, меняется местами со случайно выбранным предыдущим. Следовательно, нужно n-1 раз выбрать случайное число и n-1 раз поменять местами элементы, что равно O(n).

Реализация

Реализация алгоритма весьма проста, так как она основывается на генерации случайной перестановки – алгоритма, у которого уже есть готовая реализация.

Описание функций

Данный алгоритм реализуют несколько функций. Вот они.

Описание функций, реализующих алгоритм генерации сочетания методом случайной перестановки
          namespace Combinatorics
{
  // Функция, генерирующая сочетание размера k в формате "список из bool"// методом случайной перестановкиtemplate<typename Integer, class RandomAccessIterator,
    class RandomNumberGenerator>
  voidRandomCombinationBool(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    RandomNumberGenerator& RandomFunc);

  template<typename Integer, class RandomAccessIterator>
  voidRandomCombinationBool(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last);

  // Функция, генерирующая сочетание размера k в формате "подмножество"// методом случайной перестановкиtemplate<typename Integer, class RandomAccessIterator,
    class OutputIterator, class RandomNumberGenerator>
  voidRandomCombination(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest, RandomNumberGenerator& RandomFunc);

  template<typename Integer, class RandomAccessIterator,
    class OutputIterator>
  voidRandomCombination(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest);

  // Функция, генерирующая сочетание из n по k в формате "подмножество"// методом случайной перестановки.// В отличие от предыдущей функции, на итератор не накладывается// требование быть итератором произвольного доступаtemplate<typename Integer, class InputIterator, class OutputIterator,
    class RandomNumberGenerator>
  voidRandomCombination(Integer n, Integer k,
    InputIterator Src, OutputIterator Dest,
    RandomNumberGenerator& RandomFunc);

  template<typename Integer, class InputIterator, class OutputIterator>
  voidRandomCombination(Integer n, Integer k,
    InputIterator Src, OutputIterator Dest);
} // namespace Combinatorics

RandomFunc – функциональный объект, который при вызове RandomFunc(Max) возвращает случайное число x из диапазона 0≤x<Max. Там, где этот аргумент опущен, используется функция по умолчанию Combinatorics::DefaultRandomNumberGenerator(Integer) из библиотеки.

Функции с названием RandomCombinationBool() генерируют случайное сочетание методом случайной перестановки в формате «список из bool». Параметр k – это размер генерируемого сочетания. RandomAccessIterator – итератор произвольного доступа, адресуемые элементы которого должны быть совместимы с типом bool (должны позволять присваивать им значение типа bool). Параметрами First и Last задаётся входная последовательность элементов, совместимых с bool.

Функции с названием RandomCombination() генерируют случайное сочетание методом случайной перестановки в формате «подмножество». Параметр n (где он указан) – это размер исходного множества, k – размер генерируемого сочетания. RandomAccessIterator – итератор произвольного доступа, с помощью параметров такого типа задаётся входная последовательность. OutputIterator – итератор вывода, в который записывается результат. InputIterator – итератор ввода.

Первые две такие функции получают входную последовательность с помощью итераторов произвольного доступа. Её длину они вычисляют, применяя выражение std::distance(First,Last). Собственно, на этом и заканчивается их использование как итераторов произвольного доступа. Если предоставляемый итератор не является итератором произвольного доступа, то можно использовать функции, которые принимают параметр n – длину входной последовательности. В остальном все эти функции одинаковы.

ПРИМЕЧАНИЕ

Очень важно передавать функциональный объект RandomFunc по ссылке, а не по значению, так как большинство генераторов случайных чисел используют своё внутреннее состояние. Таким образом, если исходный генератор, передаваемый в функцию, скопировать, а не передать по ссылке, два вызова RandomCombination() подряд могут привести к одинаковому результату.

Исходный код

Ниже приведена реализация описанных функций.

Реализация метода случайной перестановки
          #include
          <vector>
          #include
          <algorithm>
          namespace Combinatorics
{
  template<typename Integer, class RandomAccessIterator,
    class RandomNumberGenerator>
  void RandomCombinationBool(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    RandomNumberGenerator& RandomFunc)
  {
    RandomAccessIterator Iter = First;
    // Заполняем сочетание значениями true и false:for (Integer Index = 0; Iter != Last; ++Iter, ++Index)
      *Iter = (Index < k); // k единиц и n - k нулей// Случайно перемешиваем сочетание
    std::random_shuffle(First, Last, RandomFunc);
  }

  template<typename Integer, class RandomAccessIterator>
  void RandomCombinationBool(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last)
  {
    RandomCombinationBool(k, First, Last,
      DefaultRandomNumberGenerator<Integer>);
  }

  namespace ImplDetails
  {
    template<typename Integer,
      class InputIterator, class OutputIterator,
      class RandomNumberGenerator>
    void RandomCombinationImpl(Integer n, Integer k,
      InputIterator Src, OutputIterator Dest,
      RandomNumberGenerator& RandomFunc)
    {
      typedef std::vector<bool> BoolCombType;
      // Сочетание "bool-формата" размера n
      BoolCombType BoolCombination((BoolCombType::size_type)n);
      BoolCombType::iterator Iter = BoolCombination.begin();
      BoolCombType::iterator Last = BoolCombination.end();
      RandomCombinationBool(k, Iter, Last, RandomFunc);
      // Заполняем сочетание элементамиfor (; Iter != Last; ++Iter, ++Src)
        if (*Iter) // Данный элемент входит в сочетание
          *(Dest++) = *Src; // Копируем
    }
  } // namespace ImplDetailstemplate<typename Integer, class RandomAccessIterator,
    class OutputIterator, class RandomNumberGenerator>
  void RandomCombination(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest, RandomNumberGenerator& RandomFunc)
  {
    ImplDetails::RandomCombinationImpl(
      (Integer)(std::distance(First, Last)),
      k, First, Dest, RandomFunc);
  }

  template<typename Integer, class RandomAccessIterator,
    class OutputIterator>
  void RandomCombination(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest)
  {
    RandomCombination(k, First, Last, Dest,
      DefaultRandomNumberGenerator<Integer>);
  }

  template<typename Integer,
    class InputIterator, class OutputIterator,
    class RandomNumberGenerator>
  void RandomCombination(Integer n, Integer k,
    InputIterator Src, OutputIterator Dest,
    RandomNumberGenerator& RandomFunc)
  {
    ImplDetails::RandomCombinationImpl(n, k, Src, Dest,
      RandomFunc);
  }

  template<typename Integer,
    class InputIterator, class OutputIterator>
  void RandomCombination(Integer n, Integer k,
    InputIterator Src, OutputIterator Dest)
  {
    RandomCombination(n, k, Src, Dest,
      DefaultRandomNumberGenerator<Integer>);
  }
} // namespace Combinatorics

Генерация сочетания по его номеру

Алгоритм

Идея алгоритма состоит в том, чтобы каким-нибудь образом пронумеровать все сочетания из n по k, то есть поставить в соответствие каждому числу i из диапазона 0≤i<C(n,k) (где C(n,k) – это число сочетаний из n по k) заранее определённое сочетание.

Итак, сначала определим отношение порядка на множестве сочетаний из n по k. Для наглядности будем записывать сочетания в формате «список из bool». Наше отношение порядка будет лексикографическим.

Имеем два сочетания, a=(a1, a2, … , an-1, an) и b=(b1, b2, … , bn-1, bn), ai и bi (1≤i≤n) принимают значения 0 или 1, причём суммы a1, a2, … , an и b1, b2, … , bn равны k. Сочетание a предшествует сочетанию b (a<b), если a1=b1, a2=b2, … am-1=bm-1, am<bm для некоторого m: 1≤m≤n.

Для наглядности расположим, например, все сочетания из 7 по 3 в указанном порядке.

(0, 0, 0, 0, 1, 1, 1)
(0, 0, 0, 1, 0, 1, 1)
(0, 0, 0, 1, 1, 0, 1)
(0, 0, 0, 1, 1, 1, 0)
(0, 0, 1, 0, 0, 1, 1)
(0, 0, 1, 0, 1, 0, 1)
(0, 0, 1, 0, 1, 1, 0)
(0, 0, 1, 1, 0, 0, 1)
(0, 0, 1, 1, 0, 1, 0)
(0, 0, 1, 1, 1, 0, 0)
(0, 1, 0, 0, 0, 1, 1)
(0, 1, 0, 0, 1, 0, 1)
(0, 1, 0, 0, 1, 1, 0)
(0, 1, 0, 1, 0, 0, 1)
(0, 1, 0, 1, 0, 1, 0)
(0, 1, 0, 1, 1, 0, 0)
(0, 1, 1, 0, 0, 0, 1)
(0, 1, 1, 0, 0, 1, 0)
(0, 1, 1, 0, 1, 0, 0)
(0, 1, 1, 1, 0, 0, 0)
(1, 0, 0, 0, 0, 1, 1)
(1, 0, 0, 0, 1, 0, 1)
(1, 0, 0, 0, 1, 1, 0)
(1, 0, 0, 1, 0, 0, 1)
(1, 0, 0, 1, 0, 1, 0)
(1, 0, 0, 1, 1, 0, 0)
(1, 0, 1, 0, 0, 0, 1)
(1, 0, 1, 0, 0, 1, 0)
(1, 0, 1, 0, 1, 0, 0)
(1, 0, 1, 1, 0, 0, 0)
(1, 1, 0, 0, 0, 0, 1)
(1, 1, 0, 0, 0, 1, 0)
(1, 1, 0, 0, 1, 0, 0)
(1, 1, 0, 1, 0, 0, 0)
(1, 1, 1, 0, 0, 0, 0)

Если внимательно посмотреть на эти сочетания, то в их порядке легко проследить рекуррентную зависимость.

Рассмотрим первый элемент сочетаний. Сначала идут все сочетания, в которых он равен 0, потом – все, в которых он равен 1. Причём порядок следования сочетаний в обеих этих подгруппах повторяется – в каждой подгруппе сначала идут сочетания, в которых второй элемент – 0, а потом – в которых он равен 1 и т.д.

Количество сочетаний, в которых первый элемент равен нулю, равно числу сочетаний из n-1 по k, а количество сочетаний, в которых первый элемент равен единице, равно числу сочетаний из n-1 по k-1 (ответ на вопрос, почему так происходит, описан в примечании ко второму свойству биномиального коэффициента). Итак, если номер сочетания меньше числа сочетаний из n-1 по k, то первый элемент множества не входит в данное сочетание, иначе – входит.

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

Входные данные: CombNumber – номер сочетания, n, k – размер входного множества и размер сочетания, Src – входное множество (последовательность), из которого выбирается сочетание, Dest – выходное множество, в которое в результате работы алгоритма будет записано сочетание.

Выходные данные: сочетание, записанное в Dest.

  1. Если k=0 или k=n, то это тривиальный случай. При k=n включаем все оставшиеся элементы из Src в Dest. При k=0 всё уже сделано.
  2. Если CombNumber≥C(n,k), то *Dest ← *Src (записываем элемент в выходную последовательность), Dest ← Dest+1 (переходим к следующему элементу выходной последовательности), k ← k-1, CombNumber ← CombNumber-C(n,k).
  3. Src ← Src+1, n ← n-1.
  4. Перейти к шагу 1.

Оценка временной сложности

Данный алгоритм в значительной степени использует вычисление биномиального коэффициента, поэтому его эффективность зависит от эффективности вычисления биномиального коэффициента. Максимальное количество итераций равно n (минимальное - k). Кроме того, используется k операций присваивания. Есть также незначительные операции типа инкремента и вычитания целых чисел. Их также не более n.

Предположим, что в качестве алгоритма вычисления биномиального коэффициента используется первый описанный в статье алгоритм (использующий дополнительную память). Его временная сложность равна O(nk), если требуемое значение C(n,k) не вычислялось до момента вызова, и O(1), если оно уже находится в кэше.

Тогда при вычислении C(n,k), исходя из алгоритма, все остальные требуемые значения биномиального коэффициента также вычисляются. То есть, по сути, биномиальный коэффициент необходимо вычислить всего один раз, во все последующие вызовы значения извлекаются из кэша.

Таким образом, временная сложность всего алгоритма выбора случайного сочетания составляет O(nk), если кэш не заполнен, и O(n), если он заполнен нужными нам биномиальными коэффициентами.

Забегая вперёд, скажем, что при наличии заполненного кэша описываемый алгоритм более эффективен, чем алгоритм генерации сочетания методом случайной перестановки. То есть, если он вызывается многократно (например, хотя бы 5-10 раз), можно получить выигрыш в производительности (заполнение кэша окупается). Если же необходимо сгенерировать всего одно сочетание, то, конечно, лучше применить алгоритм генерации случайного сочетания методом случайной перестановки.

Реализация

Описание функций

Функций, относящихся к данному алгоритму, достаточно много, но не следует пугаться – все они однотипные.

Описание функций, реализующих алгоритм генерации сочетания по его номеру
          namespace Combinatorics
{
  // Функция, генерирующая сочетание в формате "список из bool" по его// порядковому номеруtemplate<typename Integer, class RandomAccessIterator,
    class GetBinomialCoefficientFunc>
  voidCombinationFromNumberBool(Integer CombNumber, Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class RandomAccessIterator>
  voidCombinationFromNumberBool(Integer CombNumber, Integer k,
    RandomAccessIterator First, RandomAccessIterator Last);

  // Функция, генерирующая сочетание в формате "список из bool" по его// порядковому номеру// В отличие от предыдущей функции, на итератор не накладывается// требование быть итератором произвольного доступаtemplate<typename Integer, class OutputIterator,
    class GetBinomialCoefficientFunc>
  voidCombinationFromNumberBool(Integer CombNumber,
    Integer n, Integer k,
    OutputIterator Dest,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class OutputIterator>
  voidCombinationFromNumberBool(Integer CombNumber,
    Integer n, Integer k,
    OutputIterator Dest);

  // Функция, генерирующая сочетание в формате "подмножество" по его// порядковому номеруtemplate<typename Integer, class RandomAccessIterator,
    class OutputIterator, class GetBinomialCoefficientFunc>
  voidCombinationFromNumber(Integer CombNumber, Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class RandomAccessIterator,
    class OutputIterator>
  voidCombinationFromNumber(Integer CombNumber, Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest);

  // Функция, генерирующая сочетание в формате "подмножество" по его// порядковому номеру// В отличие от предыдущей функции, на итератор не накладывается// требование быть итератором произвольного доступаtemplate<typename Integer, class InputIterator, class OutputIterator,
    class GetBinomialCoefficientFunc>
  voidCombinationFromNumber(Integer CombNumber, Integer n, Integer k,
    InputIterator Src, OutputIterator Dest,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class InputIterator, class OutputIterator>
  voidCombinationFromNumber(Integer CombNumber, Integer n, Integer k,
    InputIterator Src, OutputIterator Dest);

  // Функция, генерирующая случайное сочетание в формате "список из bool"// по его порядковому номеруtemplate<typename Integer, class RandomAccessIterator,
    class GetBinomialCoefficientFunc, class RandomNumberGenerator>
  voidRandomCombinationFromNumberBool(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    RandomNumberGenerator& RandomFunc,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class RandomAccessIterator>
  voidRandomCombinationFromNumberBool(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last);

  // Функция, генерирующая случайное сочетание в формате "список из bool"// по его порядковому номеру// В отличие от предыдущей функции, на итератор не накладывается// требование быть итератором произвольного доступаtemplate<typename Integer, class OutputIterator,
    class GetBinomialCoefficientFunc, class RandomNumberGenerator>
  voidRandomCombinationFromNumberBool(Integer n, Integer k,
    OutputIterator Dest,
    RandomNumberGenerator& RandomFunc,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class OutputIterator>
  voidRandomCombinationFromNumberBool(Integer n, Integer k,
    OutputIterator Dest);

  // Функция, генерирующая случайное сочетание в формате "подмножество"// по его порядковому номеруtemplate<typename Integer, class RandomAccessIterator,
    class OutputIterator,
    class GetBinomialCoefficientFunc, class RandomNumberGenerator>
  voidRandomCombinationFromNumber(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest,
    RandomNumberGenerator& RandomFunc,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class RandomAccessIterator,
    class OutputIterator>
  voidRandomCombinationFromNumber(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest);

  // Функция, генерирующая случайное сочетание в формате "подмножество"// по его порядковому номеру// В отличие от предыдущей функции, на итератор не накладывается// требование быть итератором произвольного доступаtemplate<typename Integer, class InputIterator,
    class OutputIterator, class GetBinomialCoefficientFunc,
    class RandomNumberGenerator>
  voidRandomCombinationFromNumber(Integer n, Integer k,
    InputIterator Src, OutputIterator Dest,
    RandomNumberGenerator& RandomFunc,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class InputIterator, class OutputIterator>
  voidRandomCombinationFromNumber(Integer n, Integer k,
    InputIterator Src, OutputIterator Dest);
} // namespace Combinatorics

На параметры GetBinomialCoefficient, RandomFunc, n, k, Src, Dest, First, Last, типы RandomAccessIterator, InputIterator, OutputIterator накладываются те же ограничения и требования, что и в функциях, реализующих алгоритм генерации методом случайной перестановки.

Параметр CombNumber (там, где он присутствует) должен удовлетворять условию 0≤CombNumber≤C(n,k) – это порядковый номер сочетания.

Функции с названием CombinationFromNumberBool() генерируют сочетание по заданному номеру в формате «список из bool». На вход им подаётся номер сочетания, его размер, возможно, функциональный объект, вычисляющий число сочетаний (если его нет, то таковой выбирается по умолчанию). На выходе они должны записать в Dest последовательность элементов, синтаксически совместимых с bool (которым можно присвоить значение типа bool).

Функции с названием CombinationFromNumber() генерируют сочетание по заданному номеру в формате «подмножество». На вход они принимают то же, что и CombinationFromNumberBool(), а также итератор (итераторы), описывающий входную последовательность. На выходе они должны сформировать нужную подпоследовательность (другими словами, сочетание) и записать её в Dest.

Функции с названиями RandomCombinationFromNumberBool() и RandomCombinationFromNumber() генерируют случайные сочетания в форматах «список из bool» и «подмножество», соответственно. На вход имеют те же параметры, что и соответствующие им функции без префикса «Random», за исключением параметра RandomFunc, который передаётся вместо CombNumber. Вызывают соответствующие им функции (без префикса «Random») с параметром CombNumber = RandomFunc(GetBinomialCoefficient(n,k)).

Исходный код

Ниже приведена реализация описанных функций.

Реализация метода генерации сочетания по его номеру
          #include
          <algorithm>
          #include
          <assert.h>
          #include
          <limits>
          namespace Combinatorics
{
  namespace ImplDetails
  {
    // Шаблон для условного включения в последовательность// некоторого элемента в случае// сочетания в формате "подмножество"template<class InputIterator, class OutputIterator>
    void IncludeInSequence(InputIterator& Src,
      OutputIterator& Dest,
      bool bInclude)
    {
      if (bInclude)
        *(Dest++) = *(Src++); // Записываем в последовательность-приемникelse
        ++Src; // Не записываем в последовательность-приемник,// но переходим к рассмотрению следующего элемента в источнике
    }
    // Шаблон для условного включения в последовательность// некоторого элемента в случае// сочетания в формате "список из bool"template<class OutputIterator>
    void IncludeInSequence(void* /*Null*/,
      OutputIterator& Dest, bool bInclude)
    {
      *(Dest++) = bInclude; // Последовательность-приемник - это// последовательность из типа, совместимого с bool, поэтому// записываем в любом случае
    }

    // Вспомогательный шаблон, помогающий понять,// что мы должны сгенерировать сочетание// в формате "список из bool"template<class InputIterator>
    struct IsBoolSequence
    {
      enum { Result = false };
    };

    template<>
    struct IsBoolSequence<void*>
    {
      enum { Result = true };
    };

    // Вспомогательный шаблон, помогающий понять,// что итератор передаётся по ссылке.// В таком случае нам необходимо,// если мы не прибавляем элементы к сочетанию,// всё равно увеличить итератор,// так как клиент, возможно, будет использовать// его далее.template<class InputIterator>
    struct IsReference
    {
      enum { Result = false };
    };

    template<class InputIterator>
    struct IsReference<InputIterator&>
    {
      enum { Result = true };
    };

    template<class InputIterator, typename Integer>
    void Advance(InputIterator Iter, Integer Offset)
    {
      std::advance(Iter, Offset);
    }

    // эта специализация сделана для того, чтобы компилятор// не выдавал ошибки, вызываться она не будетtemplate<typename Integer>
    void Advance(void* Iter, Integer Offset)
    {
      assert(false);
    }

    // Если InputIterator == void*, то будем считать,// что это bool-вариант сочетанияtemplate<typename Integer, class InputIterator, class OutputIterator,
      class GetBinomialCoefficientFunc>
    void CombinationFromNumberImpl(Integer CombNumber,
      Integer n, Integer k, InputIterator Src, OutputIterator Dest,
      GetBinomialCoefficientFunc& GetBinomialCoefficient)
    {
      while (n >= k && n >= 1 && k >= 0)
      {
        assert(0 <= CombNumber);
        assert(CombNumber < GetBinomialCoefficient(n, k));
        // Тривиальные случаи// Ни один элемент не входит в сочетание или все входятif (0 == k || n == k)
        {
          // Небольшая оптимизация (оптимизирующий компилятор// уберёт ненужный код)if (!IsBoolSequence<InputIterator>::Result && 0 == k)
          {
            // Перед нами обычная последовательность// (не список из "bool")// и элементы более вставлять не нужно// Если итератор передан нам по ссылке, то его,// возможно, будут использовать в дальнейшемif (IsReference<InputIterator>::Result)
              Advance(Src, n);
            return;
          }
          while (n--)
            IncludeInSequence(Src, Dest, 0 != k);
          return;
        }
        --n;
        Integer FirstPart = (Integer)(GetBinomialCoefficient(n, k));
        // Не включаем в последовательность,// но увеличиваем значения итераторовif (CombNumber < FirstPart)
          IncludeInSequence(Src, Dest, false);
        else// Включаем в последовательность
          IncludeInSequence(Src, Dest, true),
            CombNumber -= FirstPart, --k;
      }
    }
  } // namespace ImplDetailstemplate<typename Integer, class OutputIterator,
    class GetBinomialCoefficientFunc>
  void CombinationFromNumberBool(Integer CombNumber,
    Integer n, Integer k, OutputIterator Dest,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    ImplDetails::CombinationFromNumberImpl(CombNumber, n, k,
      (void*)0, Dest, GetBinomialCoefficient);
  }

  template<typename Integer, class OutputIterator>
  void CombinationFromNumberBool(Integer CombNumber,
    Integer n, Integer k, OutputIterator Dest)
  {
    ImplDetails::CombinationFromNumberImpl(CombNumber, n, k,
      (void*)0, Dest, GetBinomialCoefficient<Integer>);
  }

  template<typename Integer, class RandomAccessIterator,
    class GetBinomialCoefficientFunc>
  void CombinationFromNumberBool(Integer CombNumber, Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    ImplDetails::CombinationFromNumberImpl(CombNumber,
      (Integer)std::distance(First, Last), k, (void*)0,
      First, GetBinomialCoefficient);
  }

  template<typename Integer, class RandomAccessIterator>
  void CombinationFromNumberBool(Integer CombNumber, Integer k,
    RandomAccessIterator First, RandomAccessIterator Last)
  {
    ImplDetails::CombinationFromNumberImpl(CombNumber,
      (Integer)std::distance(First, Last), k, (void*)0,
      First, GetBinomialCoefficient<Integer>);
  }

  template<typename Integer, class InputIterator, class OutputIterator,
    class GetBinomialCoefficientFunc>
  void CombinationFromNumber(Integer CombNumber, Integer n, Integer k,
    InputIterator Src, OutputIterator Dest,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    ImplDetails::CombinationFromNumberImpl(CombNumber, n, k,
      Src, Dest, GetBinomialCoefficient);
  }

  template<typename Integer, class InputIterator, class OutputIterator>
  void CombinationFromNumber(Integer CombNumber, Integer n, Integer k,
    InputIterator Src, OutputIterator Dest)
  {
    ImplDetails::CombinationFromNumberImpl(CombNumber, n, k,
      Src, Dest, GetBinomialCoefficient<Integer>);
  }

  template<typename Integer, class RandomAccessIterator,
    class OutputIterator, class GetBinomialCoefficientFunc>
  void CombinationFromNumber(Integer CombNumber, Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    ImplDetails::CombinationFromNumberImpl(CombNumber,
      (Integer)std::distance(First, Last), k, First, Dest,
      GetBinomialCoefficient);
  }

  template<typename Integer, class RandomAccessIterator,
    class OutputIterator>
  void CombinationFromNumber(Integer CombNumber, Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest)
  {
    ImplDetails::CombinationFromNumberImpl(CombNumber,
      (Integer)std::distance(First, Last), k, First, Dest,
      GetBinomialCoefficient<Integer>);
  }

  template<typename Integer, class OutputIterator,
    class GetBinomialCoefficientFunc, class RandomNumberGenerator>
  void RandomCombinationFromNumberBool(Integer n, Integer k,
    OutputIterator Dest, RandomNumberGenerator& RandomFunc,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    CombinationFromNumberBool(RandomFunc(GetBinomialCoefficient(n, k)),
      n, k, Dest, GetBinomialCoefficient);
  }
  template<typename Integer, class OutputIterator>

  void RandomCombinationFromNumberBool(Integer n, Integer k,
    OutputIterator Dest)
  {
    CombinationFromNumberBool(
      DefaultRandomNumberGenerator(GetBinomialCoefficient(n, k)),
      n, k, Dest);
  }

  template<typename Integer, class RandomAccessIterator,
    class GetBinomialCoefficientFunc, class RandomNumberGenerator>
  void RandomCombinationFromNumberBool(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    RandomNumberGenerator& RandomFunc,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    RandomCombinationFromNumberBool((Integer)std::distance(First, Last),
      k, First, RandomFunc, GetBinomialCoefficient);
  }

  template<typename Integer, class RandomAccessIterator>
  void RandomCombinationFromNumberBool(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last)
  {
    RandomCombinationFromNumberBool((Integer)std::distance(First, Last),
      k, First);
  }

  template<typename Integer, class InputIterator, class OutputIterator,
    class GetBinomialCoefficientFunc, class RandomNumberGenerator>
  void RandomCombinationFromNumber(Integer n, Integer k,
    InputIterator Src, OutputIterator Dest,
    RandomNumberGenerator& RandomFunc,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    CombinationFromNumber(RandomFunc(GetBinomialCoefficient(n, k)),
      n, k, Src, Dest, GetBinomialCoefficient);
  }

  template<typename Integer, class InputIterator, class OutputIterator>
  void RandomCombinationFromNumber(Integer n, Integer k,
    InputIterator Src, OutputIterator Dest)
  {
    CombinationFromNumber(
      DefaultRandomNumberGenerator(GetBinomialCoefficient(n, k)),
      n, k, Src, Dest);
  }

  template<typename Integer, class RandomAccessIterator,
    class OutputIterator, class GetBinomialCoefficientFunc,
    class RandomNumberGenerator>
  void RandomCombinationFromNumber(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest, RandomNumberGenerator& RandomFunc,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    RandomCombinationFromNumber((Integer)std::distance(First, Last),
      k, First, Dest, RandomFunc, GetBinomialCoefficient);
  }

  template<typename Integer, class RandomAccessIterator,
    class OutputIterator>
  void RandomCombinationFromNumber(Integer k,
    RandomAccessIterator First, RandomAccessIterator Last,
    OutputIterator Dest)
  {
    RandomCombinationFromNumber((Integer)std::distance(First, Last),
      k, First, Dest);
  }
} // namespace Combinatorics

Использование реализованных функций

Реализованные функции используют семантику итераторов STL и, следовательно, их можно использовать одинаково удобно как с обычными массивами, так и с контейнерами стандартной библиотеки шаблонов C++ и даже с пользовательскими типами данных. Лучше всего различные варианты вызова функций можно продемонстрировать с помощью следующей небольшой программы. Файл Demo.cpp с программой можно скачать по ссылке в конце статьи.

Использование функций библиотеки
      #include
      "Combinatorics.h"
      #include
      <set>
      #include
      <vector>
      #define ARSZ(arr) (sizeof(arr)/sizeof(arr[0]))

usingnamespace ::Combinatorics;

int main(int, char*)
{
  staticconstunsignedint _n = 100;
  staticconstunsignedint _k = 50;
  staticconstunsignedint _CombNumber = 2345;
  std::set<unsignedshort> Set;
  for (unsignedshort i=0; i<_n; ++i)
    Set.insert(i + 1); // Множество из _n элементов от 1 до _n
  std::vector<int> SubSet(_k);
  bool SubSetBool[_n];

  // Случайное сочетание в формате "список из bool",// указываем генератор случайных чисел.// Вариант с RandomAccessIterator
  RandomCombinationBool(_k, SubSetBool, SubSetBool + ARSZ(SubSetBool),
    DefaultRandomNumberGenerator<int>);

  // Случайное сочетание - указываем лишь итераторы// начала последовательностей.// Последовательности должны вмещать _n и _k элементов соответственно!// Вариант с InputIterator - OutputIterator
  RandomCombination(_n, _k, Set.begin(), SubSet.begin());

  // Сочетание в формате "список из bool" по его номеру - каждый раз будем// получать один и тот же результат.// Вариант с OutputIterator
  CombinationFromNumberBool(_CombNumber, _n, _k, SubSetBool);

  // Сочетание в формате "список из bool" по его номеру - каждый раз будем// получать один и тот же результат.// Вариант с InputIterator - OutputIterator
  CombinationFromNumber(_CombNumber, _n, _k, Set.begin(), SubSet.begin());

  // Случайное сочетание в формате "список из bool"// алгоритмом генерации сочетания по его порядковому номеру.// Не указываем _CombNumber, а генерируем его с помощью// генератора случайных чисел по умолчанию
  RandomCombinationFromNumberBool(_k, SubSetBool,
    SubSetBool + ARSZ(SubSetBool));

  // Случайное сочетание в формате "подмножество"// алгоритмом генерации сочетания по его порядковому номеру.// Вариант с RandomAccessIterator
  RandomCombinationFromNumber(_k, Set.begin(), Set.end(), SubSet.begin());
}

Сравнение двух алгоритмов

Потребление памяти

Алгоритм генерации случайного сочетания в формате «подмножество» методом случайной перестановки создаёт vector<bool> из n элементов, следовательно, он использует O(n) памяти. Для формата «список из bool» дополнительной памяти не требуется.

Алгоритм генерации сочетания по его номеру сам по себе дополнительной памяти не использует. Но он использует функцию GetBinomialCoefficient(), которая (в одном из вариантов алгоритма GetBinomialCoefficient) использует O(nk) байт памяти для кэширования значений коэффициента.

Производительность

Как и отмечалось ранее, производительность алгоритмов в разных условиях отличается. Чтобы показать сравнительную производительность реализованных алгоритмов, напишем простую программу.

Программа, иллюстрирующая производительность
        #include
        "Combinatorics.h"
        #include
        <iostream>
        using
        namespace ::Combinatorics;
usingnamespace ::std;
using ::std::endl;

class GetBinomCoeff
{
public:
  std::vector<std::vector<unsigned> > Cache;
  unsignedoperator()(unsigned n, unsigned k)
  {
    return GetBinomialCoefficient(n, k, Cache);
  }
};

int main(int, char*)
{
  staticconstunsigned _Cnt = 50000; // Сколько раз будем вызывать функцииstaticconstunsigned _n = 500;
  staticconstunsigned _k = 275;
  int Set[_n];
  int SubSet[_k];
  for (unsigned i = 0; i < _n; ++i) // Заполняем массив
    Set[i] = i + 1;
  CTimer Timer; // Измерение времениdouble SecondsPassed; // Измерение времени// 1
  Timer.Reset(); // Начинаем отмерять времяfor (unsigned i = 0; i < _Cnt; ++i)
    RandomCombination(_n, _k, Set, SubSet);
  SecondsPassed = Timer.GetTime();
  cout << "RandomCombination: " << SecondsPassed << " seconds" << endl;

  // 2
  Timer.Reset(); // Начинаем отмерять время// _Cnt / 100 - Сокращаем число повторений, чтобы не ждать слишком долго!for (unsigned i = 0; i < _Cnt / 100; ++i)
    RandomCombinationFromNumber(_n, _k, Set, SubSet,
      DefaultRandomNumberGenerator<unsigned>, GetBinomCoeff());
  SecondsPassed = Timer.GetTime();
  cout << "RandomCombinationFromNumber without cache: ~" <<
    (100.0 * SecondsPassed) << " seconds" << endl;

  // 3
  GetBinomCoeff GetBinomCoeffFunc;
  GetBinomCoeffFunc(_n, _k); // Заполняем кэш
  Timer.Reset(); // Начинаем отмерять времяfor (unsigned i = 0; i < _Cnt; ++i)
    RandomCombinationFromNumber(_n, _k, Set, SubSet,
      DefaultRandomNumberGenerator<unsigned>, GetBinomCoeffFunc);
  SecondsPassed = Timer.GetTime();
  cout << "RandomCombinationFromNumber with cache: " <<
    SecondsPassed << " seconds" << endl;

  // 4
  Timer.Reset(); // Начинаем отмерять время// Раз в сколько вызовов мы будем удалять кэшstaticconstunsigned _Cnt2 = 25;
  for (unsigned i = 0; i < _Cnt / _Cnt2; ++i) {
    GetBinomCoeff GetBinomCoeffFunc2;
    for (unsigned j = 0; j < _Cnt2; ++j)
      RandomCombinationFromNumber(_n, _k, Set, SubSet,
        DefaultRandomNumberGenerator<unsigned>, GetBinomCoeffFunc2);
  }
  SecondsPassed = Timer.GetTime();
  cout << "RandomCombinationFromNumber with periodical cache deletion: " <<
    SecondsPassed << " seconds" << endl;

  return 0;
}

Здесь CTimer – это класс, измеряющий время. Его код здесь для краткости не приводится.

1 – это измерение времени работы простого алгоритма RandomCombination().

2 – это измерение времени работы алгоритма RandomCombinationFromNumber() при условии, что кэш значений биномиального коэффициента не заполнен. Как впоследствии мы увидим, при таком условии время его работы неприемлемо.

3 – это измерение времени работы алгоритма RandomCombinationFromNumber() при условии, что кэш значений биномиального коэффициента заполнен.

4 сделано для сравнения времени работы двух алгоритмов в реальных условиях. Суммарно здесь RandomCombinationFromNumber() вызывается то же число раз, что и RandomCombination() в первом примере, но кэш значений биномиального коэффициента сбрасывается раз в 25 вызовов.

Вот вывод программы на процессоре Core 2 Duo E7500.

RandomCombination: 4.11091 seconds
RandomCombinationFromNumber without cache: ~90.7942 seconds
RandomCombinationFromNumber with cache: 0.317365 seconds
RandomCombinationFromNumber with periodical cache deletion: 3.78864 seconds

Из этого можно сделать следующие выводы.

Таким образом, если для n=500 и k=275 применять RandomCombinationFromNumber(), то он окупит себя в смысле времени выполнения за C вызовов, где C<25. Если n и k меньше, то C также уменьшается. Например, для n=250 и k=135 RandomCombinationFromNumber() окупается уже за C<12 вызовов. Эти данные, разумеется, справедливы лишь для целых 32-битных чисел. В общем случае мы имеем объекты некоторых классов, которые, возможно, копируются медленнее. Тогда C может уменьшиться ещё больше.

Некоторые полезные свойства алгоритма генерации сочетания по его номеру

Компактное хранение сочетаний

Положим, существует некоторая заранее известная и не изменяющаяся последовательность элементов длины n. Каким-либо образом, неважно каким, выбрана подпоследовательность этой последовательности, в которой элементы не повторяются. Её длину обозначим через k. Очевидно, указанная подпоследовательность является одним из сочетаний из n по k. Следовательно, её можно сгенерировать описанным ранее алгоритмом генерации сочетания по порядковому номеру сочетания. Так как алгоритм является детерминированным, подпоследовательность полностью определяется исходной последовательностью и номером сочетания.

Суммируя вышесказанное, можно легко реализовать алгоритм сохранения подпоследовательности. Для этого нам недостаёт ещё одной функции – NumberFromCombination().

Объявление функции NumberFromCombination()
        namespace Combinatorics
{
  // Функция, получающая порядковый номер по сочетанию// в формате "список из bool"template<typename Integer, class InputIterator,
    class GetBinomialCoefficientFunc>
  Integer NumberFromCombinationBool(Integer n, Integer k,
    InputIterator Comb,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer, class InputIterator>
  Integer NumberFromCombinationBool(Integer n, Integer k,
    InputIterator Comb);

  // Функция, получающая порядковый номер по сочетанию// в формате "подмножество"template<typename Integer, class InputIterator1,
    class InputIterator2, class GetBinomialCoefficientFunc>
  Integer NumberFromCombination(Integer n, Integer k,
    InputIterator1 SrcSet, InputIterator2 Comb,
    GetBinomialCoefficientFunc& GetBinomialCoefficient);

  template<typename Integer,
    class InputIterator1, class InputIterator2>
  Integer NumberFromCombination(Integer n, Integer k,
    InputIterator1 SrcSet, InputIterator2 Comb);
} // namespace Combinatorics

Чтобы сохранить сочетание, нам необходимо записать в файл лишь параметры k и номер сочетания. Таким образом, например, сохраняя список из k 32-битных чисел, мы экономим 4k-8 байт, записывая всего 8 байт вместо 4k (разумеется, при условии, что k и n достаточно малы, чтобы номер сочетания не превосходил 232-1).

Ниже приведена реализация объявленных функций.

Реализация функций NumberFromCombination()
        namespace Combinatorics
{
  namespace ImplDetails
  {
    template<class InputIterator1, class InputIterator2>
    bool IncludedInSequence(InputIterator1& SrcSet, InputIterator2& Comb)
    {
      if (*(SrcSet++) == *Comb)
        return ++Comb, true;
      elsereturnfalse;
    }

    template<class InputIterator>
    bool IncludedInSequence(void* /*Null*/, InputIterator& Comb)
    {
      return *(Comb++);
    }

    template<typename Integer,
      class InputIterator1, class InputIterator2,
      class GetBinomialCoefficientFunc>
    Integer NumberFromCombinationImpl(Integer n, Integer k,
      InputIterator1 SrcSet, InputIterator2 Comb,
      GetBinomialCoefficientFunc& GetBinomialCoefficient)
    {
      Integer CombNumber = 0;
      while (n >= k && n >= 1 && k >= 0)
      {
        if (0 == k || n == k)
          break;
        --n;
        if (IncludedInSequence(SrcSet, Comb))
          CombNumber += GetBinomialCoefficient(n, k--);
      }
      return CombNumber;
    }
  } // namespace ImplDetailstemplate<typename Integer, class InputIterator,
    class GetBinomialCoefficientFunc>
  Integer NumberFromCombinationBool(Integer n, Integer k,
    InputIterator Comb,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    return ImplDetails::NumberFromCombinationImpl(n, k, (void*)0, Comb,
      GetBinomialCoefficient);
  }

  template<typename Integer, class InputIterator>
  Integer NumberFromCombinationBool(Integer n, Integer k,
    InputIterator Comb)
  {
    return ImplDetails::NumberFromCombinationImpl(n, k, (void*)0, Comb,
      GetBinomialCoefficient<Integer>);
  }

  // Функция, генерирующая порядковый номер по сочетанию// в формате "подмножество"template<typename Integer,
    class InputIterator1, class InputIterator2,
    class GetBinomialCoefficientFunc>
  Integer NumberFromCombination(Integer n, Integer k,
    InputIterator1 SrcSet, InputIterator2 Comb,
    GetBinomialCoefficientFunc& GetBinomialCoefficient)
  {
    return ImplDetails::NumberFromCombinationImpl(n, k, SrcSet, Comb,
      GetBinomialCoefficient);
  }

  template<typename Integer,
    class InputIterator1, class InputIterator2>
  Integer NumberFromCombination(Integer n, Integer k,
    InputIterator1 SrcSet, InputIterator2 Comb)
  {
    return ImplDetails::NumberFromCombinationImpl(n, k, SrcSet, Comb,
      GetBinomialCoefficient<Integer>);
  }
} // namespace Combinatorics

Заключение

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

Источники

  1. В. Липский. Комбинаторика для программистов. М: Мир. 1988.
  2. http://www.cplusplus.com


Эта статья опубликована в журнале RSDN Magazine #3-2010. Информацию о журнале можно найти здесь