Алгоритм повернення всіх комбінацій k елементів з n


571

Я хочу написати функцію, яка приймає масив букв як аргумент, і кількість цих літер для вибору.

Скажіть, ви надаєте масив з 8 літер і хочете вибрати з цього 3 букви. Тоді ви повинні отримати:

8! / ((8 - 3)! * 3!) = 56

Масиви (або слова) натомість складаються з 3 букв кожен.


2
Будь-які переваги мови програмування?
Джонатан Тран

7
Як ви хочете поводитися з дублікатами букв?
wcm

Немає переваги мови, я буду кодувати її в рубіні, але загальне уявлення про те, які алгоритми використовувати було б чудово. Дві букви з однаковим значенням могли існувати, але не та сама та сама буква двічі.
Фредрік


У програмі php слід зробити трюк: stackoverflow.com/questions/4279722/…
Кемаль Даğ

Відповіді:


412

Мистецтво комп’ютерного програмування Том 4: Чарівник 3 містить багато таких, які могли б відповідати вашій конкретній ситуації краще, ніж я описав.

Сірі коди

Проблема, до якої ви зіткнетеся, - це звичайно пам'ять і досить швидко, у вас будуть проблеми з 20 елементами у вашому наборі - 20 C 3 = 1140. І якщо ви хочете перебрати набір, найкраще використовувати модифікований сірий алгоритм коду, тому ви не зберігаєте їх у пам'яті. Вони генерують наступну комбінацію з попередньої та уникають повторів. Їх багато для різних цілей. Чи хочемо ми максимізувати відмінності між послідовними комбінаціями? мінімізувати? et cetera.

Деякі з оригінальних статей, що описують сірі коди:

  1. Деякі шляхи Гамільтона та алгоритм мінімальних змін
  2. Алгоритм генерації суміжних міжмісних комбінацій

Ось деякі інші статті, що висвітлюють цю тему:

  1. Ефективна реалізація алгоритму покоління комбінації Eades, Hickey файлів (PDF, з кодом на Паскалі)
  2. Комбіновані генератори
  3. Огляд комбінаторних сірих кодів (PostScript)
  4. Алгоритм сірих кодів

Чізл Чейза (алгоритм)

Філіп Дж Чейз, ` Алгоритм 382: Комбінації M із N об'єктів '(1970)

Алгоритм в С ...

Індекс комбінацій у лексикографічному порядку (Алгоритм пряжок 515)

Ви також можете посилатися на комбінацію за її індексом (у лексикографічному порядку). Зрозумівши, що індекс повинен бути деякою кількістю змін справа наліво на основі індексу, ми можемо побудувати щось, що має відновити комбінацію.

Отже, у нас є набір {1,2,3,4,5,6} ... і ми хочемо три елементи. Скажімо, {1,2,3} можна сказати, що різниця між елементами одна і по порядку, і мінімальна. {1,2,4} має одну зміну і є лексикографічно числом 2. Отже, кількість змін на останньому місці припадає на одну зміну лексикографічного впорядкування. На другому місці з однією зміною {1,3,4} є одна зміна, але припадає більше змін, оскільки вона знаходиться на другому місці (пропорційно кількості елементів у вихідному наборі).

Метод, який я описав, - це деконструкція, як здається, від встановленого до індексу, нам потрібно зробити зворотний - що набагато складніше. Ось так справляється вирішення проблеми. Я написав деякий C, щоб обчислити їх , з незначними змінами - я використовував індекс наборів, а не діапазон чисел, щоб представити множину, тому ми завжди працюємо від 0 ... n. Примітка:

  1. Оскільки комбінації є не упорядкованими, {1,3,2} = {1,2,3} - ми наказуємо їм бути лексикографічними.
  2. У цього методу є неявне 0 для початку набору для першої різниці.

Індекс комбінацій у лексикографічному порядку (Маккафрі)

Є й інший спосіб : його концепцію легше зрозуміти та запрограмувати, але це без оптимізацій Пряжки. На щастя, він також не створює повторюваних комбінацій:

Набір, x_k ... x_1 в Nякий максимально збільшує i = C (x_1, k) + C (x_2, k-1) + ... + C (x_k, 1), деC (n, r) = {n вибрати r} .

Як приклад: 27 = C(6,4) + C(5,3) + C(2,2) + C(1,1). Отже, 27-е лексикографічне поєднання чотирьох речей: {1,2,5,6}, це покажчики будь-якого набору, який ви хочете подивитися. Приклад нижче (OCaml), вимагає chooseфункції, залишеної читачеві:

(* this will find the [x] combination of a [set] list when taking [k] elements *)
let combination_maccaffery set k x =
    (* maximize function -- maximize a that is aCb              *)
    (* return largest c where c < i and choose(c,i) <= z        *)
    let rec maximize a b x =
        if (choose a b ) <= x then a else maximize (a-1) b x
    in
    let rec iterate n x i = match i with
        | 0 -> []
        | i ->
            let max = maximize n i x in
            max :: iterate n (x - (choose max i)) (i-1)
    in
    if x < 0 then failwith "errors" else
    let idxs =  iterate (List.length set) x k in
    List.map (List.nth set) (List.sort (-) idxs)

Невеликий і простий ітератор комбінацій

Наступні два алгоритми передбачені для дидактичних цілей. Вони реалізують ітератор і загальну комбінацію папок. Вони максимально швидкі, мають складність O ( n C k ). Споживання пам'яті пов'язане з k.

Почнемо з ітератора, який викликатиме функцію, надану користувачем для кожної комбінації

let iter_combs n k f =
  let rec iter v s j =
    if j = k then f v
    else for i = s to n - 1 do iter (i::v) (i+1) (j+1) done in
  iter [] 0 0

Більш загальна версія буде викликати надану користувачем функцію разом із змінною стану, починаючи з початкового стану. Оскільки нам потрібно передати стан між різними станами, ми не будемо використовувати цикл for, а натомість використовувати рекурсію,

let fold_combs n k f x =
  let rec loop i s c x =
    if i < n then
      loop (i+1) s c @@
      let c = i::c and s = s + 1 and i = i + 1 in
      if s < k then loop i s c x else f c x
    else x in
  loop 0 0 [] x

1
Чи створить це повторювані комбінації у випадку, коли набір містить рівні елементи?
Томас Ейл

2
Так, Томас. Це агностично для даних у масиві. Ви завжди можете відфільтрувати дублікати спочатку, якщо це бажаний ефект, або вибрати інший алгоритм.
nlucaroni

19
Дивовижна відповідь. Чи можете ви надати звіт про час виконання та аналіз пам'яті для кожного з алгоритмів?
uncaught_exceptions

2
Досить гарна відповідь. 20C3 - 1140, знак оклику тут заплутаний, оскільки він виглядає як факторний, а факториали дійсно вводять формулу пошуку комбінацій. Тому я відредагую знак оклику.
CashCow

3
Це гасно, що багато цитат стоять за платною стіною. Чи є можливість включати або посилання, що не належать до оплати, або включати фрагменти, що містяться в джерелах з джерел?
Терранс

195

В C #:

public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<T> elements, int k)
{
  return k == 0 ? new[] { new T[0] } :
    elements.SelectMany((e, i) =>
      elements.Skip(i + 1).Combinations(k - 1).Select(c => (new[] {e}).Concat(c)));
}

Використання:

var result = Combinations(new[] { 1, 2, 3, 4, 5 }, 3);

Результат:

123
124
125
134
135
145
234
235
245
345

2
Це рішення добре працює для "невеликих" наборів, але для більших наборів використовує трохи пам'яті.
Артур Карвальо

1
не пов'язаний безпосередньо, але код дуже цікавий / читабельний, і мені цікаво, у якій версії c # є такі конструкції / методи? (Я використовував тільки c # v1.0 і не дуже).
LBarret

Однозначно елегантний, але IEnumerable буде перераховано багато разів. Якщо це підкріплене якоюсь значною операцією ...
Дрю Ноакс

2
оскільки це метод розширення, ваша лінія використання може читати:var result = new[] { 1, 2, 3, 4, 5 }.Combinations(3);
Дейв Кузіно,

1
Чи можете ви надати точну не linq версію цього запиту, використовуючи рекурсивні петлі
irfandar

81

Коротке рішення Java:

import java.util.Arrays;

public class Combination {
    public static void main(String[] args){
        String[] arr = {"A","B","C","D","E","F"};
        combinations2(arr, 3, 0, new String[3]);
    }

    static void combinations2(String[] arr, int len, int startPosition, String[] result){
        if (len == 0){
            System.out.println(Arrays.toString(result));
            return;
        }       
        for (int i = startPosition; i <= arr.length-len; i++){
            result[result.length - len] = arr[i];
            combinations2(arr, len-1, i+1, result);
        }
    }       
}

Результат буде

[A, B, C]
[A, B, D]
[A, B, E]
[A, B, F]
[A, C, D]
[A, C, E]
[A, C, F]
[A, D, E]
[A, D, F]
[A, E, F]
[B, C, D]
[B, C, E]
[B, C, F]
[B, D, E]
[B, D, F]
[B, E, F]
[C, D, E]
[C, D, F]
[C, E, F]
[D, E, F]

це здається O (n ^ 3) правда? Цікаво, чи існує більш швидкий алгоритм для цього.
ЛЖ

Я працюю з 20 вибираю 10, і це здається мені досить швидким (менше 1 секунди)
demongolem

4
@NanoHead ви помиляєтесь. Це поєднання без повторення. І ваша справа з повторенням.
Jack The Ripper

Цей фрагмент коду слід простіше знайти в Інтернеті ... саме це я шукав!
Мануель С.

Я щойно перевірив це та ще 7 реалізацій Java - ця була на сьогоднішній день найшвидшою. 2-й швидкий був на порядок повільніше.
Стюарт

77

Чи можу я представити своє рекурсивне рішення Python для цієї проблеми?

def choose_iter(elements, length):
    for i in xrange(len(elements)):
        if length == 1:
            yield (elements[i],)
        else:
            for next in choose_iter(elements[i+1:len(elements)], length-1):
                yield (elements[i],) + next
def choose(l, k):
    return list(choose_iter(l, k))

Приклад використання:

>>> len(list(choose_iter("abcdefgh",3)))
56

Мені це подобається своєю простотою.


16
len(tuple(itertools.combinations('abcdefgh',3)))досягнемо того ж в Python з меншим кодом.
hgus1294

59
@ hgus1294 Щоправда, але це було б обманом. Op попросив алгоритм, а не "магічний" метод, прив'язаний до певної мови програмування.
MestreLion

1
Чи не повинен строго говорити діапазон першого циклу for i in xrange(len(elements) - length + 1):? Немає значення в python, як виходити з індексу зрізів обробляються витончено, але це правильний алгоритм.
Стефан Доллберг

62

Скажімо, ваш масив листів виглядає приблизно так: "ABCDEFGH". У вас є три індекси (i, j, k), які вказують, які літери ви будете використовувати для поточного слова. Почніть з:

ABCDEFGH
^ ^ ^
ijk

Спочатку ви змінюєте k, тож наступний крок виглядає так:

ABCDEFGH
^ ^ ^
ijk

Якщо ви досягли кінця, продовжуйте і змінюйте j, а потім k знову.

ABCDEFGH
^ ^ ^
ijk

ABCDEFGH
^ ^ ^
ijk

Після того, як ви досягли G, ви почнете також змінювати i.

ABCDEFGH
  ^ ^ ^
  ijk

ABCDEFGH
  ^ ^ ^
  ijk
...

Написане в коді це виглядає приблизно так

void print_combinations(const char *string)
{
    int i, j, k;
    int len = strlen(string);

    for (i = 0; i < len - 2; i++)
    {
        for (j = i + 1; j < len - 1; j++)
        {
            for (k = j + 1; k < len; k++)
                printf("%c%c%c\n", string[i], string[j], string[k]);
        }
    }
}

115
Проблема такого підходу полягає в тому, що він жорстко вводить параметр 3 в код. (Що робити, якщо бажано 4 символи?) Як я зрозумів питання, буде надано обидва масиви символів І кількість символів для вибору. Звичайно, один із способів вирішити цю проблему - замінити явно вкладені петлі на рекурсію.
joel.neely

10
@ Dr.PersonPersonII І чому трикутники мають відношення до ОП?
MestreLion

7
Ви завжди можете перетворити це рішення в рекурсивне з довільним параметром.
Rok Kralj

5
@RokKralj, як ми "перетворимо це рішення на рекурсивне з довільним параметром"? Мені це здається неможливим.
Аарон Мак-Дейд

3
Приємне інтуїтивне пояснення, як це зробити
Йонатан Сімсон

53

Наступний рекурсивний алгоритм вибирає всі комбінації k-елементів із впорядкованого набору:

  • виберіть перший елемент iвашої комбінації
  • комбінувати iз кожною з комбінацій k-1елементів, обраних рекурсивно із набору елементів, більших за i.

Повторіть вищезазначене для кожного iв наборі.

Важливо, щоб ви вибрали решту елементів як розмір більше i, щоб уникнути повторення. Таким чином [3,5] буде вибрано лише один раз, як [3] у поєднанні з [5], а не двічі (умова виключається [5] + [3]). Без цієї умови ви отримуєте варіанти замість комбінацій.


12
Дуже вдалий опис англійською мовою алгоритму, який використовується багатьма відповідями
MestreLion

друге вище; зокрема, це допомогло мені зрозуміти рішення, поставлене користувачем935714. обидва відмінні.
jacoblambert

25

У C ++ наступний порядок створює всі комбінації відстані довжини (перше, k) між діапазоном [перший, останній]:

#include <algorithm>

template <typename Iterator>
bool next_combination(const Iterator first, Iterator k, const Iterator last)
{
   /* Credits: Mark Nelson http://marknelson.us */
   if ((first == last) || (first == k) || (last == k))
      return false;
   Iterator i1 = first;
   Iterator i2 = last;
   ++i1;
   if (last == i1)
      return false;
   i1 = last;
   --i1;
   i1 = k;
   --i2;
   while (first != i1)
   {
      if (*--i1 < *i2)
      {
         Iterator j = k;
         while (!(*i1 < *j)) ++j;
         std::iter_swap(i1,j);
         ++i1;
         ++j;
         i2 = k;
         std::rotate(i1,j,last);
         while (last != j)
         {
            ++j;
            ++i2;
         }
         std::rotate(k,i2,last);
         return true;
      }
   }
   std::rotate(first,k,last);
   return false;
}

Його можна використовувати так:

#include <string>
#include <iostream>

int main()
{
    std::string s = "12345";
    std::size_t comb_size = 3;
    do
    {
        std::cout << std::string(s.begin(), s.begin() + comb_size) << std::endl;
    } while (next_combination(s.begin(), s.begin() + comb_size, s.end()));

    return 0;
}

Це надрукує наступне:

123
124
125
134
135
145
234
235
245
345

1
Що починається, чим закінчується в цьому випадку? Як воно насправді може щось повернути, якщо всі змінні, передані цій функції, передані за значенням?
Сергій Андреєв

6
@Sergej Andrejev: замінити beingі beginна s.begin(), і endна s.end(). Код уважно відповідає next_permutationалгоритму STL , описаному тут більш докладно.
Ентоні Лабарре

5
що відбувається? i1 = останній; --i1; i1 = k;
Маной Р

24

Я вважаю цю тему корисною і думав, що я додам рішення Javascript, яке можна перейти до Firebug. Залежно від вашого двигуна JS, якщо величина старту велика, це може зайняти небагато часу.

function string_recurse(active, rest) {
    if (rest.length == 0) {
        console.log(active);
    } else {
        string_recurse(active + rest.charAt(0), rest.substring(1, rest.length));
        string_recurse(active, rest.substring(1, rest.length));
    }
}
string_recurse("", "abc");

Вихід повинен бути таким:

abc
ab
ac
a
bc
b
c

4
@NanoHead Це не так. Вихідні дані вже показують "ac" - і "ca" - це те саме поєднання, що і "ac". Ви говорите про перестановки (з математики), де "ac" не було б таким же, як "ca".
Якоб Дженков

1
Це не n вибрати k.
shinzou

20
static IEnumerable<string> Combinations(List<string> characters, int length)
{
    for (int i = 0; i < characters.Count; i++)
    {
        // only want 1 character, just return this one
        if (length == 1)
            yield return characters[i];

        // want more than one character, return this one plus all combinations one shorter
        // only use characters after the current one for the rest of the combinations
        else
            foreach (string next in Combinations(characters.GetRange(i + 1, characters.Count - (i + 1)), length - 1))
                yield return characters[i] + next;
    }
}

Приємне рішення. Я посилається це у відповідь на це питання в останній час: stackoverflow.com/questions/4472036 / ...
wageoghe

Єдина проблема цієї функції - рекурсивність. Хоча це звичайно добре для програмного забезпечення, що працює на ПК, якщо ви працюєте з більш обмеженою ресурсом платформою (вбудованою, наприклад), вам не пощастило
Padu Merloti

Він також виділить багато списків і зробить багато роботи, щоб дублювати елементи масиву в кожен новий. Мені здається, що ці списки не підлягають колекціонуванню, поки не буде завершено весь перелік.
Niall Connaughton

Це гладко. Ви знайшли алгоритм чи це з нуля?
папараццо

20

Короткий приклад в Python:

def comb(sofar, rest, n):
    if n == 0:
        print sofar
    else:
        for i in range(len(rest)):
            comb(sofar + rest[i], rest[i+1:], n-1)

>>> comb("", "abcde", 3)
abc
abd
abe
acd
ace
ade
bcd
bce
bde
cde

Для пояснення рекурсивний метод описаний із наступним прикладом:

Приклад: ABCDE
Усі комбінації з 3:

  • A з усіма комбінаціями 2 з решти (BCDE)
  • B з усіма комбінаціями 2 з решти (CDE)
  • C з усіма комбінаціями 2 від решти (DE)

17

Простий рекурсивний алгоритм в Haskell

import Data.List

combinations 0 lst = [[]]
combinations n lst = do
    (x:xs) <- tails lst
    rest   <- combinations (n-1) xs
    return $ x : rest

Спочатку визначаємо особливий випадок, тобто вибираємо нульові елементи. Він створює єдиний результат, який є порожнім списком (тобто список, що містить порожній список).

При n> 0 xпроходить кожен елемент списку і xsє кожним елементом після x.

restвибирає n - 1елементи xsза допомогою рекурсивного дзвінка до combinations. Кінцевим результатом функції є список, де кожен елемент є x : rest(тобто список, який має xголову і restяк хвіст) для кожного різного значення xі rest.

> combinations 3 "abcde"
["abc","abd","abe","acd","ace","ade","bcd","bce","bde","cde"]

І звичайно, оскільки Haskell лінивий, список поступово формується у міру необхідності, так що ви можете частково оцінити експоненціально великі комбінації.

> let c = combinations 8 "abcdefghijklmnopqrstuvwxyz"
> take 10 c
["abcdefgh","abcdefgi","abcdefgj","abcdefgk","abcdefgl","abcdefgm","abcdefgn",
 "abcdefgo","abcdefgp","abcdefgq"]

13

І ось приходить дідусь COBOL, дуже злісна мова.

Припустимо масив із 34 елементів по 8 байт кожен (суто довільний вибір.) Ідея полягає в перерахуванні всіх можливих 4-елементних комбінацій і завантаженні їх у масив.

Ми використовуємо 4 індекси, по одному для кожної позиції в групі 4

Масив обробляється так:

    idx1 = 1
    idx2 = 2
    idx3 = 3
    idx4 = 4

Ми змінюємо idx4 від 4 до кінця. Для кожного idx4 ми отримуємо унікальне поєднання груп із чотирьох. Коли idx4 доходить до кінця масиву, ми збільшуємо idx3 на 1 і встановлюємо idx4 на idx3 + 1. Потім знову запускаємо idx4 до кінця. Ми продовжуємо таким чином, збільшуючи idx3, idx2 та idx1 відповідно, поки позиція idx1 не буде меншою за 4 від кінця масиву. Це закінчує алгоритм.

1          --- pos.1
2          --- pos 2
3          --- pos 3
4          --- pos 4
5
6
7
etc.

Перші ітерації:

1234
1235
1236
1237
1245
1246
1247
1256
1257
1267
etc.

Приклад COBOL:

01  DATA_ARAY.
    05  FILLER     PIC X(8)    VALUE  "VALUE_01".
    05  FILLER     PIC X(8)    VALUE  "VALUE_02".
  etc.
01  ARAY_DATA    OCCURS 34.
    05  ARAY_ITEM       PIC X(8).

01  OUTPUT_ARAY   OCCURS  50000   PIC X(32).

01   MAX_NUM   PIC 99 COMP VALUE 34.

01  INDEXXES  COMP.
    05  IDX1            PIC 99.
    05  IDX2            PIC 99.
    05  IDX3            PIC 99.
    05  IDX4            PIC 99.
    05  OUT_IDX   PIC 9(9).

01  WHERE_TO_STOP_SEARCH          PIC 99  COMP.

* Stop the search when IDX1 is on the third last array element:

COMPUTE WHERE_TO_STOP_SEARCH = MAX_VALUE - 3     

MOVE 1 TO IDX1

PERFORM UNTIL IDX1 > WHERE_TO_STOP_SEARCH
   COMPUTE IDX2 = IDX1 + 1
   PERFORM UNTIL IDX2 > MAX_NUM
      COMPUTE IDX3 = IDX2 + 1
      PERFORM UNTIL IDX3 > MAX_NUM
         COMPUTE IDX4 = IDX3 + 1
         PERFORM UNTIL IDX4 > MAX_NUM
            ADD 1 TO OUT_IDX
            STRING  ARAY_ITEM(IDX1)
                    ARAY_ITEM(IDX2)
                    ARAY_ITEM(IDX3)
                    ARAY_ITEM(IDX4)
                    INTO OUTPUT_ARAY(OUT_IDX)
            ADD 1 TO IDX4
         END-PERFORM
         ADD 1 TO IDX3
      END-PERFORM
      ADD 1 TO IDX2
   END_PERFORM
   ADD 1 TO IDX1
END-PERFORM.

але чому {} {} {} {}
shinzou

9

Ось вишукана загальна реалізація в Scala, як описано в 99 проблемах Scala .

object P26 {
  def flatMapSublists[A,B](ls: List[A])(f: (List[A]) => List[B]): List[B] = 
    ls match {
      case Nil => Nil
      case sublist@(_ :: tail) => f(sublist) ::: flatMapSublists(tail)(f)
    }

  def combinations[A](n: Int, ls: List[A]): List[List[A]] =
    if (n == 0) List(Nil)
    else flatMapSublists(ls) { sl =>
      combinations(n - 1, sl.tail) map {sl.head :: _}
    }
}

9

Якщо ви можете використовувати синтаксис SQL - скажімо, якщо ви використовуєте LINQ для доступу до полів структури чи масиву, або безпосередньо для доступу до бази даних, що має таблицю під назвою "Алфавіт", лише з одним полем "Літер", ви можете адаптувати наступне код:

SELECT A.Letter, B.Letter, C.Letter
FROM Alphabet AS A, Alphabet AS B, Alphabet AS C
WHERE A.Letter<>B.Letter AND A.Letter<>C.Letter AND B.Letter<>C.Letter
AND A.Letter<B.Letter AND B.Letter<C.Letter

Це поверне всі комбінації з 3 літер, незважаючи на те, скільки букв у таблиці "Алфавіт" (це може бути 3, 8, 10, 27 і т.д.).

Якщо ви хочете, це всі перестановки, а не комбінації (тобто ви хочете, щоб "ACB" і "ABC" вважати різними, а не з'являтися лише один раз), просто видаліть останній рядок (І) і це зроблено.

Після редагування: Після перечитання питання я розумію, що потрібен загальний алгоритм, а не лише конкретний для випадку вибору 3 елементів. Відповідь Адама Х'юса є повною, на жаль, я не можу її проголосувати (поки що). Ця відповідь проста, але працює лише тоді, коли потрібно точно 3 пункти.


7

Ще одна версія C # із лінивим генеруванням комбінованих індексів. Ця версія підтримує єдиний масив індексів для визначення відображення між списком усіх значень та значеннями для поточної комбінації, тобто постійно використовує додатковий простір O (k) протягом усього часу виконання. Код генерує окремі комбінації, включаючи першу, за O (k) час.

public static IEnumerable<T[]> Combinations<T>(this T[] values, int k)
{
    if (k < 0 || values.Length < k)
        yield break; // invalid parameters, no combinations possible

    // generate the initial combination indices
    var combIndices = new int[k];
    for (var i = 0; i < k; i++)
    {
        combIndices[i] = i;
    }

    while (true)
    {
        // return next combination
        var combination = new T[k];
        for (var i = 0; i < k; i++)
        {
            combination[i] = values[combIndices[i]];
        }
        yield return combination;

        // find first index to update
        var indexToUpdate = k - 1;
        while (indexToUpdate >= 0 && combIndices[indexToUpdate] >= values.Length - k + indexToUpdate)
        {
            indexToUpdate--;
        }

        if (indexToUpdate < 0)
            yield break; // done

        // update combination indices
        for (var combIndex = combIndices[indexToUpdate] + 1; indexToUpdate < k; indexToUpdate++, combIndex++)
        {
            combIndices[indexToUpdate] = combIndex;
        }
    }
}

Код тесту:

foreach (var combination in new[] {'a', 'b', 'c', 'd', 'e'}.Combinations(3))
{
    System.Console.WriteLine(String.Join(" ", combination));
}

Вихід:

a b c
a b d
a b e
a c d
a c e
a d e
b c d
b c e
b d e
c d e

Це зберігає впорядкованість. Я очікую, що набір результатів також містить те, c b aчого він не має.
Дмитро Нестерук

Завдання - генерувати всі комбінації, які задовольняють n понад k. Біноміальні коефіцієнти відповідають на питання про те, скільки способів вибору не упорядкованого підмножини k елементів із фіксованого набору n елементів. Тому запропонований алгоритм робить все, що повинен.
Крістоф

6

https://gist.github.com/3118596

Існує реалізація для JavaScript. Він має функції для отримання k-комбінацій та всіх комбінацій масиву будь-яких об'єктів. Приклади:

k_combinations([1,2,3], 2)
-> [[1,2], [1,3], [2,3]]

combinations([1,2,3])
-> [[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]]

6

Тут ви маєте ліниву оцінену версію алгоритму, кодовану в C #:

    static bool nextCombination(int[] num, int n, int k)
    {
        bool finished, changed;

        changed = finished = false;

        if (k > 0)
        {
            for (int i = k - 1; !finished && !changed; i--)
            {
                if (num[i] < (n - 1) - (k - 1) + i)
                {
                    num[i]++;
                    if (i < k - 1)
                    {
                        for (int j = i + 1; j < k; j++)
                        {
                            num[j] = num[j - 1] + 1;
                        }
                    }
                    changed = true;
                }
                finished = (i == 0);
            }
        }

        return changed;
    }

    static IEnumerable Combinations<T>(IEnumerable<T> elements, int k)
    {
        T[] elem = elements.ToArray();
        int size = elem.Length;

        if (k <= size)
        {
            int[] numbers = new int[k];
            for (int i = 0; i < k; i++)
            {
                numbers[i] = i;
            }

            do
            {
                yield return numbers.Select(n => elem[n]);
            }
            while (nextCombination(numbers, size, k));
        }
    }

І тестова частина:

    static void Main(string[] args)
    {
        int k = 3;
        var t = new[] { "dog", "cat", "mouse", "zebra"};

        foreach (IEnumerable<string> i in Combinations(t, k))
        {
            Console.WriteLine(string.Join(",", i));
        }
    }

Сподіваюся, це допоможе тобі!


6

У мене був алгоритм перестановки, який я використовував для euler проекту, у python:

def missing(miss,src):
    "Returns the list of items in src not present in miss"
    return [i for i in src if i not in miss]


def permutation_gen(n,l):
    "Generates all the permutations of n items of the l list"
    for i in l:
        if n<=1: yield [i]
        r = [i]
        for j in permutation_gen(n-1,missing([i],l)):  yield r+j

Якщо

n<len(l) 

ви повинні мати всю необхідну вам комбінацію без повторень, чи потрібна вона?

Це генератор, тому ви використовуєте його в чомусь подібному:

for comb in permutation_gen(3,list("ABCDEFGH")):
    print comb 

5
Array.prototype.combs = function(num) {

    var str = this,
        length = str.length,
        of = Math.pow(2, length) - 1,
        out, combinations = [];

    while(of) {

        out = [];

        for(var i = 0, y; i < length; i++) {

            y = (1 << i);

            if(y & of && (y !== of))
                out.push(str[i]);

        }

        if (out.length >= num) {
           combinations.push(out);
        }

        of--;
    }

    return combinations;
}

5

Версія Clojure:

(defn comb [k l]
  (if (= 1 k) (map vector l)
      (apply concat
             (map-indexed
              #(map (fn [x] (conj x %2))
                    (comb (dec k) (drop (inc %1) l)))
              l))))

5

Скажімо, ваш масив листів виглядає приблизно так: "ABCDEFGH". У вас є три індекси (i, j, k), які вказують, які літери ви будете використовувати для поточного слова. Почніть з:

ABCDEFGH
^ ^ ^
ijk

Спочатку ви змінюєте k, тож наступний крок виглядає так:

ABCDEFGH
^ ^ ^
ijk

Якщо ви досягли кінця, продовжуйте і змінюйте j, а потім k знову.

ABCDEFGH
^ ^ ^
ijk

ABCDEFGH
^ ^ ^
ijk

Після того, як ви досягли G, ви почнете також змінювати i.

ABCDEFGH
  ^ ^ ^
  ijk

ABCDEFGH
  ^ ^ ^
  ijk
...
function initializePointers($cnt) {
    $pointers = [];

    for($i=0; $i<$cnt; $i++) {
        $pointers[] = $i;
    }

    return $pointers;     
}

function incrementPointers(&$pointers, &$arrLength) {
    for($i=0; $i<count($pointers); $i++) {
        $currentPointerIndex = count($pointers) - $i - 1;
        $currentPointer = $pointers[$currentPointerIndex];

        if($currentPointer < $arrLength - $i - 1) {
           ++$pointers[$currentPointerIndex];

           for($j=1; ($currentPointerIndex+$j)<count($pointers); $j++) {
                $pointers[$currentPointerIndex+$j] = $pointers[$currentPointerIndex]+$j;
           }

           return true;
        }
    }

    return false;
}

function getDataByPointers(&$arr, &$pointers) {
    $data = [];

    for($i=0; $i<count($pointers); $i++) {
        $data[] = $arr[$pointers[$i]];
    }

    return $data;
}

function getCombinations($arr, $cnt)
{
    $len = count($arr);
    $result = [];
    $pointers = initializePointers($cnt);

    do {
        $result[] = getDataByPointers($arr, $pointers);
    } while(incrementPointers($pointers, count($arr)));

    return $result;
}

$result = getCombinations([0, 1, 2, 3, 4, 5], 3);
print_r($result);

На основі https://stackoverflow.com/a/127898/2628125 , але більш абстрактно, для будь-якого розміру покажчиків.


Що це за жахлива мова? Баш?
shinzou

1
php, але мова тут не має значення, алгоритм
Олександр Книга

Я такий щасливий, що відмовляюся вивчати цю мову. Мова, де її перекладачу / упоряднику потрібна допомога з розпізнаванням змінних, не повинно існувати у 2018 році.
shinzou

4

Все сказане і зроблене тут приходить код O'caml для цього. Алгоритм видно з коду ..

let combi n lst =
    let rec comb l c =
        if( List.length c = n) then [c] else
        match l with
        [] -> []
        | (h::t) -> (combi t (h::c))@(combi t c)
    in
        combi lst []
;;

4

Ось метод, який дає всі комбінації заданого розміру з рядка випадкової довжини. Схожий на рішення Quinmars, але працює для різного введення та k.

Код можна змінити, щоб обернути його, тобто 'dab' від введення 'abcd' wk = 3.

public void run(String data, int howMany){
    choose(data, howMany, new StringBuffer(), 0);
}


//n choose k
private void choose(String data, int k, StringBuffer result, int startIndex){
    if (result.length()==k){
        System.out.println(result.toString());
        return;
    }

    for (int i=startIndex; i<data.length(); i++){
        result.append(data.charAt(i));
        choose(data,k,result, i+1);
        result.setLength(result.length()-1);
    }
}

Вихід для "abcde":

abc abd abe acd ace ade bcd bce bde cde



3

Ось моя пропозиція на C ++

Я спробував накласти якомога менше обмежень на тип ітератора, тому це рішення передбачає просто переслати ітератор, і це може бути const_iterator. Це має працювати з будь-яким стандартним контейнером. У випадках, коли аргументи не мають сенсу, він викидає std :: invalid_argumnent

#include <vector>
#include <stdexcept>

template <typename Fci> // Fci - forward const iterator
std::vector<std::vector<Fci> >
enumerate_combinations(Fci begin, Fci end, unsigned int combination_size)
{
    if(begin == end && combination_size > 0u)
        throw std::invalid_argument("empty set and positive combination size!");
    std::vector<std::vector<Fci> > result; // empty set of combinations
    if(combination_size == 0u) return result; // there is exactly one combination of
                                              // size 0 - emty set
    std::vector<Fci> current_combination;
    current_combination.reserve(combination_size + 1u); // I reserve one aditional slot
                                                        // in my vector to store
                                                        // the end sentinel there.
                                                        // The code is cleaner thanks to that
    for(unsigned int i = 0u; i < combination_size && begin != end; ++i, ++begin)
    {
        current_combination.push_back(begin); // Construction of the first combination
    }
    // Since I assume the itarators support only incrementing, I have to iterate over
    // the set to get its size, which is expensive. Here I had to itrate anyway to  
    // produce the first cobination, so I use the loop to also check the size.
    if(current_combination.size() < combination_size)
        throw std::invalid_argument("combination size > set size!");
    result.push_back(current_combination); // Store the first combination in the results set
    current_combination.push_back(end); // Here I add mentioned earlier sentinel to
                                        // simplyfy rest of the code. If I did it 
                                        // earlier, previous statement would get ugly.
    while(true)
    {
        unsigned int i = combination_size;
        Fci tmp;                            // Thanks to the sentinel I can find first
        do                                  // iterator to change, simply by scaning
        {                                   // from right to left and looking for the
            tmp = current_combination[--i]; // first "bubble". The fact, that it's 
            ++tmp;                          // a forward iterator makes it ugly but I
        }                                   // can't help it.
        while(i > 0u && tmp == current_combination[i + 1u]);

        // Here is probably my most obfuscated expression.
        // Loop above looks for a "bubble". If there is no "bubble", that means, that
        // current_combination is the last combination, Expression in the if statement
        // below evaluates to true and the function exits returning result.
        // If the "bubble" is found however, the ststement below has a sideeffect of 
        // incrementing the first iterator to the left of the "bubble".
        if(++current_combination[i] == current_combination[i + 1u])
            return result;
        // Rest of the code sets posiotons of the rest of the iterstors
        // (if there are any), that are to the right of the incremented one,
        // to form next combination

        while(++i < combination_size)
        {
            current_combination[i] = current_combination[i - 1u];
            ++current_combination[i];
        }
        // Below is the ugly side of using the sentinel. Well it had to haave some 
        // disadvantage. Try without it.
        result.push_back(std::vector<Fci>(current_combination.begin(),
                                          current_combination.end() - 1));
    }
}

3

Ось код, який я нещодавно написав на Java, який обчислює та повертає всі поєднання елементів "num" з елементів "outOf".

// author: Sourabh Bhat (heySourabh@gmail.com)

public class Testing
{
    public static void main(String[] args)
    {

// Test case num = 5, outOf = 8.

        int num = 5;
        int outOf = 8;
        int[][] combinations = getCombinations(num, outOf);
        for (int i = 0; i < combinations.length; i++)
        {
            for (int j = 0; j < combinations[i].length; j++)
            {
                System.out.print(combinations[i][j] + " ");
            }
            System.out.println();
        }
    }

    private static int[][] getCombinations(int num, int outOf)
    {
        int possibilities = get_nCr(outOf, num);
        int[][] combinations = new int[possibilities][num];
        int arrayPointer = 0;

        int[] counter = new int[num];

        for (int i = 0; i < num; i++)
        {
            counter[i] = i;
        }
        breakLoop: while (true)
        {
            // Initializing part
            for (int i = 1; i < num; i++)
            {
                if (counter[i] >= outOf - (num - 1 - i))
                    counter[i] = counter[i - 1] + 1;
            }

            // Testing part
            for (int i = 0; i < num; i++)
            {
                if (counter[i] < outOf)
                {
                    continue;
                } else
                {
                    break breakLoop;
                }
            }

            // Innermost part
            combinations[arrayPointer] = counter.clone();
            arrayPointer++;

            // Incrementing part
            counter[num - 1]++;
            for (int i = num - 1; i >= 1; i--)
            {
                if (counter[i] >= outOf - (num - 1 - i))
                    counter[i - 1]++;
            }
        }

        return combinations;
    }

    private static int get_nCr(int n, int r)
    {
        if(r > n)
        {
            throw new ArithmeticException("r is greater then n");
        }
        long numerator = 1;
        long denominator = 1;
        for (int i = n; i >= r + 1; i--)
        {
            numerator *= i;
        }
        for (int i = 2; i <= n - r; i++)
        {
            denominator *= i;
        }

        return (int) (numerator / denominator);
    }
}

3

Коротке рішення Javascript:

Array.prototype.combine=function combine(k){    
    var toCombine=this;
    var last;
    function combi(n,comb){             
        var combs=[];
        for ( var x=0,y=comb.length;x<y;x++){
            for ( var l=0,m=toCombine.length;l<m;l++){      
                combs.push(comb[x]+toCombine[l]);           
            }
        }
        if (n<k-1){
            n++;
            combi(n,combs);
        } else{last=combs;}
    }
    combi(1,toCombine);
    return last;
}
// Example:
// var toCombine=['a','b','c'];
// var results=toCombine.combine(4);

3

Алгоритм:

  • Полічити від 1 до 2 ^ н.
  • Перетворіть кожну цифру у її двійкове подання.
  • Перекладіть кожен "на" біт на елементи вашого набору, виходячи з положення.

В C #:

void Main()
{
    var set = new [] {"A", "B", "C", "D" }; //, "E", "F", "G", "H", "I", "J" };

    var kElement = 2;

    for(var i = 1; i < Math.Pow(2, set.Length); i++) {
        var result = Convert.ToString(i, 2).PadLeft(set.Length, '0');
        var cnt = Regex.Matches(Regex.Escape(result),  "1").Count; 
        if (cnt == kElement) {
            for(int j = 0; j < set.Length; j++)
                if ( Char.GetNumericValue(result[j]) == 1)
                    Console.Write(set[j]);
            Console.WriteLine();
        }
    }
}

Чому це працює?

Існує біекція між підмножинами набору n-елементів і n-бітовими послідовностями.

Це означає, що ми можемо з'ясувати, скільки підмножин існує, підраховуючи послідовності.

наприклад, чотири набору елементів нижче можна представити через {0,1} X {0, 1} X {0, 1} X {0, 1} (або 2 ^ 4) різних послідовностей.

Отже - все, що нам потрібно зробити, - це порахувати від 1 до 2 ^ n, щоб знайти всі комбінації. (Ми ігноруємо порожній набір.) Далі переведіть цифри у їх двійкове представлення. Потім замініть елементи вашого набору на біти "on".

Якщо ви хочете отримати лише результати k елементів, друкуйте лише тоді, коли k біт "увімкнено".

(Якщо ви хочете, щоб усі підмножини замість підмножини були довжиною k, видаліть частину cnt / kElement.)

(Для доказів див. Безкоштовний навчальний курс MIT Mathematics for Computer Science, Lehman et al, розділ 11.2.2. Https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-042j-mathematics- для інформатики-осінь-2010 / читання / )


3

короткий код пітона, що дає позиції індексу

def yield_combos(n,k):
    # n is set size, k is combo size

    i = 0
    a = [0]*k

    while i > -1:
        for j in range(i+1, k):
            a[j] = a[j-1]+1
        i=j
        yield a
        while a[i] == i + n - k:
            i -= 1
        a[i] += 1

2

Я написав клас для обробки загальних функцій для роботи з біноміальним коефіцієнтом, який тип проблеми підпадає під вашу проблему. Він виконує такі завдання:

  1. Виводить усі K-індекси у приємному форматі для будь-якого N, вибираючи K у файл. K-індекси можуть бути замінені більш описовими рядками або літерами. Цей метод робить рішення такого типу проблеми досить тривіальним.

  2. Перетворює K-індекси у відповідний індекс запису в упорядкованій таблиці біноміальних коефіцієнтів. Ця методика набагато швидша, ніж старі опубліковані методи, які покладаються на ітерацію. Це робиться, використовуючи математичну властивість, властиву трикутнику Паскаля. Мій документ розповідає про це. Я вважаю, що я перший відкрив та опублікував цю техніку, але можу помилитися.

  3. Перетворює індекс у впорядкованій таблиці біноміальних коефіцієнтів у відповідні К-індекси.

  4. Використовує метод Марка Домінуса для обчислення біноміального коефіцієнта, який набагато рідше переповнює і працює з більшими числами.

  5. Клас записаний у .NET C # і надає спосіб управління об’єктами, пов'язаними з проблемою (якщо такі є) за допомогою загального списку. Конструктор цього класу приймає значення bool під назвою InitTable, що, коли true, створить загальний список для зберігання об'єктів, якими керує. Якщо це значення помилкове, воно не створить таблицю. Таблицю не потрібно створювати для виконання 4 вищезазначених методів. Для доступу до таблиці надаються методи доступу.

  6. Існує пов'язаний тестовий клас, який показує, як використовувати клас та його методи. Він пройшов широку перевірку з 2 випадками, і невідомі помилки.

Щоб прочитати про цей клас та завантажити код, перегляньте статтю Таблиця біноміального коефіцієнта .

Перетворити цей клас у C ++ не повинно.


Назвати це "методом Марка Домінуса" насправді не правильно, оскільки, як я вже згадував, йому як мінімум 850 років, і не так складно придумати. Чому б не назвати це методом Лілавати ?
MJD
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.