Как (дешево) рассчитать все возможные длины-r комбинаций из n возможных элементов

Каков самый быстрый способ рассчитать все возможные длины-r комбинаций n возможных элементов, не прибегая к методам грубой силы или чему-либо, что требует STL?

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

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

Ответ 1

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

void r_nCr(const unsigned int &startNum, const unsigned int &bitVal, const unsigned int &testNum) // Should be called with arguments (2^r)-1, 2^(r-1), 2^(n-1)
{
    unsigned int n = (startNum - bitVal) << 1;
    n += bitVal ? 1 : 0;

    for (unsigned int i = log2(testNum) + 1; i > 0; i--) // Prints combination as a series of 1s and 0s
        cout << (n >> (i - 1) & 1);
    cout << endl;

    if (!(n & testNum) && n != startNum)
        r_nCr(n, bitVal, testNum);

    if (bitVal && bitVal < testNum)
        r_nCr(startNum, bitVal >> 1, testNum);
}

Как это работает:

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

Например, результаты 3C2 (все комбинации длины-2 из набора из 3 возможных элементов) могут быть выражены как 011, 110 и 101. Если множество всех возможных элементов {A, B, C }, то результаты могут быть выражены относительно этого множества как {B, C}, {A, B} и {A, C}.

Для этого объяснения я буду вычислять 5C3 (все комбинации длиной 3, состоящие из 5 возможных элементов).

Эта функция принимает 3 аргумента, все из которых являются целыми без знака:

  • Первый параметр - наименьшее возможное целое число, двоичное представление которого имеет число 1s, равное длине создаваемых комбинаций. Это исходное значение для генерирования комбинаций. Для 5C3 это будет 00111b, или 7 в десятичной форме.

  • Второй параметр - это значение наивысшего бита, которое в стартовом номере установлено равным 1. Это первый бит, который будет вычитаться при создании комбинаций. Для 5C3 это третий бит справа, который имеет значение 4.

  • Третий параметр - это значение n-го бита справа, где n - количество возможных элементов, которые мы объединяем. Это число будет побитовым с комбинациями, которые мы создаем, чтобы проверить, является ли самый последний бит комбинации 1 или 0. Для 5C3 мы будем использовать 5-й бит справа, который равен 10000b или 16 в десятичное.

Ниже приведены действия, выполняемые функцией:

  • Вычислить startNum - битVal, бит сдвинуть одно пространство влево и добавить 1, если битVal не 0.

Для первой итерации результат должен быть таким же, как startNum. Это значит, что мы можем распечатать первую комбинацию (которая равна startNum) внутри функции, поэтому нам не нужно делать это вручную раньше времени. Математика для этой операции происходит следующим образом:

00111 - 00100 = 00011    
00011 << 1 = 00110   
00110 + 1 = 00111
  1. Результат предыдущего расчета - это новая комбинация. Сделайте что-нибудь с этими данными.

Мы собираемся распечатать результат на консоли. Это делается с использованием цикла for, чья переменная начинается с числа бит, с которым мы работаем (вычисляется путем принятия log2 testNum и добавления 1; log2 (16) + 1 = 4 + 1 = 5) и заканчивается на 0. На каждой итерации мы смещаем бит справа на i-1 и печатаем самый правый бит и получаем результат с 1. Вот математика:

i=5:
00111 >> 4 = 00000
00000 & 00001 = 0

i=4:
00111 >> 3 = 00000
00000 & 00001 = 0

i=3:
00111 >> 2 = 00001
00001 & 00001 = 1

i=2:
00111 >> 1 = 00011
00011 & 00001 = 1

i=1:
00111 >> 0 = 00111
00111 & 00001 = 1

output: 00111
  1. Если самый левый бит n (результат вычисления на шаге 1) равен 0, а n не равно startNum, мы возвращаем с n в качестве нового startNum.

Очевидно, это будет пропущено на первой итерации, так как мы уже показали, что n равно startNum. Это становится важным в последующих итерациях, которые мы увидим позже.

  1. Если битVal больше 0 и меньше, чем testNum, рекурсия с текущим исходным исходным исходным кодом startNum в качестве первого аргумента. Второй аргумент - битVal, сдвинутый вправо на 1 (то же самое, что и целочисленное деление на 2).

Теперь мы перезаписываем с новым битомVal, установленным в значение следующего бита справа от текущего битваля. Следующий бит - это то, что будет вычтено на следующей итерации.

  1. Продолжать рекурсию, пока битVal не станет равным нулю.

Поскольку битваль смещается по битам справа на один во втором рекурсивном вызове, мы в конечном итоге достигнем точки, когда битVal равен 0. Этот алгоритм расширяется как дерево, а когда битVal равен нулю, а самый левый бит равен 1, мы возвращаемся к одному слою с нашей текущей позиции. В конце концов, это каскадирует весь путь назад.

В этом примере дерево имеет 3 поддеревья и 6 листовых узлов. Теперь я пройду через первое поддерево, состоящее из 1 корня node и 3 листовых узлов.

Мы начнем с последней строки первой итерации, которая

if (bitVal)
        r_nCr(startNum, bitVal >> 1, testNum);

Итак, теперь мы вводим вторую итерацию с помощью startNum = 00111 (7), bitVal = 00010 (2) и testNum = 10000 (16) (это число никогда не изменяется).

Вторая итерация

Шаг 1:

n = 00111 - 00010 = 00101 // Subtract bitVal
n = 00101 << 1 = 01010 // Shift left
n = 01010 + 1 = 01011 // bitVal is not 0, so add 1

Шаг 2: результат печати. ​​

Шаг 3: Самый левый бит равен 0, а n не равно startNum, поэтому мы рекурсируем с n в качестве нового startNum. Теперь мы вводим третью итерацию с помощью startNum = 01011 (11), битVal = 00010 (2) и testNum = 10000 (16).

Третья итерация

Шаг 1:

n = 01011 - 00010 = 01001 // Subtract bitVal
n = 01001 << 1 = 10010 // Shift left
n = 10010 + 1 = 10011 // bitVal is not 0, so add 1

Шаг 2: результат печати. ​​

Шаг 3: Самый левый бит равен 1, поэтому не рекурсивно.

Шаг 4: битVal не равен 0, поэтому recurse с bitVal сдвигается вправо на 1. Теперь мы вводим четвертую итерацию с помощью startNum = 01011 (11), bitVal = 00001 (1) и testNum = 10000 (16).

Четвертая итерация

Шаг 1:

n = 01011 - 00001 = 01010 // Subtract bitVal
n = 01010 << 1 = 10100 // Shift left
n = 10100 + 1 = 10101 // bitVal is not 0, so add 1

Шаг 2: результат печати. ​​

Шаг 3: Самый левый бит равен 1, поэтому не рекурсивно.

Шаг 4: битVal не равен 0, поэтому recurse с bitVal сдвигается вправо на 1. Теперь мы вводим пятую итерацию с помощью startNum = 01011 (11), bitVal = 00000 (0) и testNum = 10000 (16).

Пятая итерация

Шаг 1:

n = 01011 - 00000 = 01011 // Subtract bitVal
n = 01011 << 1 = 10110 // Shift left
n = 10110 + 0 = 10110 // bitVal is 0, so add 0
// Because bitVal = 0, nothing is subtracted or added; this step becomes just a straight bit-shift left by 1.

Шаг 2: результат печати. ​​

Шаг 3: Самый левый бит равен 1, поэтому не рекурсивно.

Шаг 4: битVal равен 0, поэтому не повторяйте.

Возврат к второй итерации

Шаг 4: битVal не равен 0, поэтому recurse с bitVal сдвигается вправо на 1.

Это будет продолжаться до тех пор, пока битVal = 0 для первого уровня дерева и мы не вернемся к первой итерации, после чего мы полностью вернемся из функции.

Вот простая диаграмма, показывающая древовидное расширение функции: Diagram showing recursive expansion

И вот более сложная диаграмма, показывающая поток выполнения функции: Diagrom showing thread of execution

Вот альтернативная версия, использующая побитовое или вместо добавления и побитового-xor вместо вычитания:

void r_nCr(const unsigned int &startNum, const unsigned int &bitVal, const unsigned int &testNum) // Should be called with arguments (2^r)-1, 2^(r-1), 2^(n-1)
{
    unsigned int n = (startNum ^ bitVal) << 1;
    n |= (bitVal != 0);

    for (unsigned int i = log2(testNum) + 1; i > 0; i--) // Prints combination as a series of 1s and 0s
        cout << (n >> (i - 1) & 1);
    cout << endl;

    if (!(n & testNum) && n != startNum)
        r_nCr(n, bitVal, testNum);

    if (bitVal && bitVal < testNum)
        r_nCr(startNum, bitVal >> 1, testNum);
}
void r_nCr(const unsigned int &startNum, const unsigned int &bitVal, const unsigned int &testNum) // Should be called with arguments (2^r)-1, 2^(r-1), 2^(n-1)
{
    unsigned int n = (startNum ^ bitVal) << 1;
    n |= (bitVal != 0);

    for (unsigned int i = log2(testNum) + 1; i > 0; i--) // Prints combination as a series of 1s and 0s
        cout << (n >> (i - 1) & 1);
    cout << endl;

    if (!(n & testNum) && n != startNum)
        r_nCr(n, bitVal, testNum);

    if (bitVal && bitVal < testNum)
        r_nCr(startNum, bitVal >> 1, testNum);
}

Ответ 2

Как насчет этого?

#include <stdio.h>

#define SETSIZE 3
#define NELEMS  7

#define BYTETOBINARYPATTERN "%d%d%d%d%d%d%d%d"
#define BYTETOBINARY(byte)  \
    (byte & 0x80 ? 1 : 0), \
            (byte & 0x40 ? 1 : 0), \
            (byte & 0x20 ? 1 : 0), \
            (byte & 0x10 ? 1 : 0), \
            (byte & 0x08 ? 1 : 0), \
            (byte & 0x04 ? 1 : 0), \
            (byte & 0x02 ? 1 : 0), \
            (byte & 0x01 ? 1 : 0)

int main()
{
    unsigned long long x = (1 << SETSIZE) -1;
    unsigned long long N = (1 << NELEMS) -1;

    while(x < N)
    {
            printf ("x: "BYTETOBINARYPATTERN"\n", BYTETOBINARY(x));
            unsigned long long a = x & -x;
            unsigned long long y = x + a;
            x = ((y & -y) / a >> 1) + y - 1;
    }
};

Он должен печатать 7C3.