Как мы можем посчитать r-комбинации мультимножества? - PullRequest
0 голосов
/ 13 ноября 2018

Задача

Определить split мультисета M как упорядоченную пару мультимножеств одинакового размера, объединение которых равно M.

Как мы можем подсчитать разбиения мультимножества?

Обсуждение

Этот вопрос возникает из этого .

Эта проблема идентична подсчету числа r -комбинаций мультимножества с 2 r элементами, решение для которых показано в разделе 6.2 Вводная комбинационная техника Ричарда А. Бруальди, второе издание, 1992, Prentice-Hall, Inc. Чтобы увидеть, что они идентичны, заметьте, что между 1 r -комбинацией X мультимножества M из 2 r элементов ирасщепление (X, Y), где Y - все элементы M, которых нет в X, то есть Y = M − X.

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

Несмотря натот факт, что это хорошо известно математикам, был получен лишь частичный ответ здесь, на Математическом стеке и здесь, в Quora .

Ответы [ 3 ]

0 голосов
/ 14 ноября 2018

Не думаю, что это правильно, Эрик.Исходный вопрос спрашивает: если мой исходный массив содержит A, A, B, B, то сколько раз он может быть уникально разделен между двумя получателями - ответ в этом случае будет три, потому что массив можно разделить следующим образом:

Child Array 1 | Child Array 2
-----------------------------
A A           | B B
A B           | A B
B B           | A A

Этот исходный код (функция C ++) работает, но он ужасно медленный и неэффективный.Грубая сила и невежество.

int countPermutations(vector<int> itemset) {

  vector<vector<int> > permutations;

  do {
    vector<int> match;

    for(int i = 0; i < itemset.size(); i+=2) {
         match.push_back(itemset.at(i));
    }
    sort(match.begin(), match.end());

    int j = 0;
    bool found = false;
    while (!found && (j < permutations.size())) {
        found = (match == permutations.at(j))?true:false;
        j++;
    }
    if (!found) {
        permutations.push_back(match);
    }

  } while (next_permutation(itemset.begin(), itemset.end()));

  return permutations.size();
}

Есть мысли по оптимизации?

0 голосов
/ 14 ноября 2018

Это, безусловно, тот случай, когда исходная задача изоморфна задаче подсчета r -комбинаций мультимножества размера 2 r .

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

Предположим, мы представляем мультимножество в виде отсортированного вектора V размера n (где n & равно; 2 r ). (Технически, его не нужно сортировать; достаточно, чтобы он был «сгруппирован», чтобы все идентичные элементы были последовательными. Однако самый простой способ сделать это - отсортировать вектор.) Мы хотим получить все уникальные k -комбинации этого вектора. Все такие комбинации имеют одну из двух форм:

  • «Выберите первый элемент». Комбинация начинается с V 1 и продолжается комбинацией ( k & minus; 1) ( V 2 *) 1044 *, V 3 , & hellip ;, V n ).

  • «Пропустить первый элемент». Комбинация представляет собой комбинацию k ( V i , V i + 1 , & hellip; V n ), где i - наименьший индекс, такой что V i & ne; * 1 087 * В 1 * ** тысячи девяносто-одна * тысячи девяносто-два. (Чтобы избежать дублирования, нам нужно пропустить всех элементов, идентичных первому элементу.)

Единственная разница здесь с биномиальной рекурсией - это использование индекса i во втором варианте; если в наборе нет повторяющихся элементов, это уменьшается до i & 2; это приводит к рекурсии C ( n , k ) = C ( n минус 1, k минус 1) + C ( n минус 1, k ).

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

Решение динамического программирования инвертирует рекурсию, вычисляя количество k -комбинаций для последовательных суффиксов вектора. Необходимо сохранить значения только для двух суффиксов: предыдущего суффикса и предыдущего суффикса с другим первым элементом, соответствующим количеству, необходимому для первого и второго параметра приведенной выше рекурсии. (Фактический код использует префиксы вместо суффиксов, но это не имеет абсолютно никакого значения.)

В качестве незначительной оптимизации мы вычисляем только значения k -комбинаций для значений k между 0 и & lceil; n / 2 & rceil ;. Как и в случае с биномиальными коэффициентами, подсчет симметричен: число k -комбинаций равно количеству ( n & minus; k ) - комбинаций, поскольку каждый k -комбинация соответствует уникальной ( n & minus; k ) - комбинации, состоящей из всех невыбранных элементов. Дополнительная оптимизация возможна на основе того факта, что нам нужен только один счет в конце, но дополнительное усложнение только затеняет основной алгоритм.

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

Вот базовая реализация на C (с использованием массива строк):

/* Given a *sorted* vector v of size n, compute the number of unique k-combinations
 * of the elements of the vector for values of k between 0 and (n/2)+1.
 * The counts are stored in the vector r, which must be large enough.
 * Counts for larger values of k can be trivially looked up in the returned
 * vector, using the identity r[k] == r[n - k].
 * If v is not sorted, the result will be incorrect. The function does not
 * check for overflow, but the results will be correct modulo (UINTMAX + 1)
 */
void multicomb(const char** v, size_t n, uintmax_t* r) {
  size_t lim = n / 2 + 1;
  // Prev retains the counts for the previous prefix ending with
  // a different element
  uintmax_t prev[lim];
  // If there are no elements, there is 1 0-combination and no other combinations.
  memset(r, 0, sizeof prev);
  r[0] = 1;
  // Iterate over the unique elements of v
  for (size_t k = 0; k < n; ) {
    // Save the counts for this prefix
    memcpy(prev, r, sizeof prev);
    // Iterate over the prefixes with the same ending value
    do {
      for (size_t i = lim - 1; i > 0; --i)
        r[i] = r[i - 1] + prev[i];
    } while (++k < n && strcmp(v[k - 1], v[k]) == 0);
  };
}

По сравнению с решением в самоответе ОП, эта версия:

  • переполняется позже, потому что это зависит только от сложения. (Тот факт, что нет деления, также значительно облегчает вычисление числа по модулю простого числа.)
  • занимает квадратичное время вместо экспоненциального.
0 голосов
/ 13 ноября 2018

Вот решение.Я включил объяснение в комментарии.

#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>


typedef uintmax_t UInt;
#define UIntFormat PRIuMAX

#define NumberOf(a) (sizeof (a) / sizeof *(a))


/*  In this code, a multiset is represented with:

        an integer N0 which is the number of types of elements in the set,

        an integer N1 which is the number of types of elements in the set that
        each appear finitely many times (N0-N1 types each appear infinitely
        many times), and

        an array M[] in which each M[i] is the number of times that an element
        of type i appears.

    Collectively, this is referred to as the multiset M.
*/


/*  Return the number of ways to choose r things from n things.  This is
    n! / (r! * (n-r)!).
*/
static UInt Choose(UInt r, UInt n)
{
    UInt result = 1;
    for (UInt i = 1; i <= r; ++i)
        result = result * n-- / i;
    return result;
}


//  Count the number of r-combinations of a multiset.
static UInt CountRCombinations(UInt r, size_t N0, size_t N1, UInt M[])
{
    /*  If we have only the unlimited types, there is a one-to-one
        correspondence between r objects with N0-1 dividers placed between
        them, each divider marking a transition from one type to another.  For
        example, consider four objects of three types.  Below "o" represents
        any object, and "|" is a divider.  For each arrangement of four o's and
        two |s, we show how it defines a selection of four objects of three
        types:

            oooo|| -> aaaa||
            ooo|o| -> aaa|b|
            ooo||o -> aaa||c
            oo|oo| -> aa|bb|
            oo|o|o -> aa|b|c
            oo||oo -> aa||cc
            o|ooo| -> a|bbb|
            o|oo|o -> a|bb|c
            o|o|oo -> a|b|cc
            o||ooo -> a||ccc
            |oooo| -> |bbbb|
            |ooo|o -> |bbb|c
            |oo|oo -> |bb|cc
            |o|ooo -> |b|ccc
            ||oooo -> ||cccc

        Therefore, the number of combinations equals the number of ways to
        arrange r indistinguishable objects of one type with N0-1
        indistinguishable objects of a different type.
    */
    if (N1 == 0)
        return Choose(r, r+N0-1);

    /*  Otherwise, we count the combinations:

            Select one of the limited types (we use the last one, N1-1, because
            it is easily removed from the array simply by reducing the size of
            the array).

            Count the number of combinations there would be if that type were
            unlimited.

            Count the number of combinations there would be if there were at
            least M[i]+1 instances of elements of that type.

            Subtract to get the number of combinations that have 0 to M[i]
            instances of elements of that type.
    */
    else
    {
        /*  Let M' be the multiset M with the last type changed to unlimited.

            So, where M is represented with N0, N1, M[], M' is represented with
            N0, N1-1, M[].
        */

        //  Change the last limited type to unlimited.
        N1 -= 1;

        //  Count the r-combinations drawn from M'.
        UInt C = CountRCombinations(r, N0, N1, M);

        /*  Now we count the combinations which have at least M[N1]+1 instances
            of the (formerly) last type.

            Consider that each such combination has M[N1]+1 instances of that
            type plus some combination of r - (M[N1]+1) elements drawn from M',
            including zero or more instances of the last type.  (Note that if r
            <= M[N1], there are no such combinations, since we would be asking
            for a negative number of elements.)

            So the number of combinations which have at least M[N1]+1 instances
            of the last type equals the number of combinations of that type
            plus some combination of r - (M[N1]+1) elements drawn from M'.
        */
        if (M[N1] < r)
            C -= CountRCombinations(r - (M[N1] + 1), N0, N1, M);

        return C;
    }
}


//  Count the number of splits of a multiset M that contains N types of things.
static UInt CountSplits(size_t N, UInt M[])
{
    //  Count the number of elements.
    UInt T = 0;
    for (size_t i = 0; i < N; ++i)
        T += M[i];

    //  Return the number of T/2-combinations of M.
    return T % 2 ? 0 : CountRCombinations(T/2, N, N, M);
}


int main(void)
{
    UInt M[] = { 3, 4, 5 };
    size_t N = NumberOf(M);

    printf("The number of splits of {");
    for (size_t i = 0; i < N; ++i) printf(" %" UIntFormat, M[i]);
    printf(" } is %" UIntFormat ".\n", CountSplits(N, M));
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...