Algoritmo per restituire tutte le combinazioni di k elementi da n


571

Voglio scrivere una funzione che accetta una matrice di lettere come argomento e un numero di quelle lettere da selezionare.

Supponi di fornire una matrice di 8 lettere e che desideri selezionare 3 lettere da quella. Quindi dovresti ottenere:

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

Matrici (o parole) in cambio composte da 3 lettere ciascuna.


2
Qualche preferenza per il linguaggio di programmazione?
Jonathan Tran,

7
Come vuoi gestire le lettere duplicate?
wcm,

Nessuna preferenza per il linguaggio, lo codificherò in ruby ​​ma un'idea generale di quali algoritmi usare andrebbe bene. Potrebbero esistere due lettere dello stesso valore ma non esattamente la stessa lettera due volte.
Fredrik,


In php, il seguente dovrebbe fare il trucco: stackoverflow.com/questions/4279722/…
Kemal Dağ

Risposte:


413

Art of Computer Programming Volume 4: Fascicle 3 ha un sacco di questi che potrebbero adattarsi alla tua situazione particolare meglio di come descrivo.

Codici grigi

Un problema che incontrerai è ovviamente la memoria e abbastanza rapidamente, avrai problemi con 20 elementi nel tuo set - 20 C 3 = 1140. E se vuoi iterare sul set è meglio usare un grigio modificato algoritmo di codice in modo da non tenerli tutti in memoria. Questi generano la combinazione successiva dalla precedente ed evitano le ripetizioni. Ce ne sono molti per usi diversi. Vogliamo massimizzare le differenze tra combinazioni successive? minimizzare? eccetera.

Alcuni dei documenti originali che descrivono i codici grigi:

  1. Alcuni percorsi Hamilton e un algoritmo di cambiamento minimo
  2. Algoritmo di generazione della combinazione di interscambio adiacente

Ecco alcuni altri articoli che trattano l'argomento:

  1. Un'attuazione efficiente dell'algoritmo di generazione di combinazioni di interscambio adiacente Eades, Hickey, Read Adiacente (PDF, con codice in Pascal)
  2. Generatori di combinazioni
  3. Rilievo di codici grigi combinatori (PostScript)
  4. Un algoritmo per i codici grigi

Chase's Twiddle (algoritmo)

Phillip J Chase, ` Algorithm 382: Combinations of M out of N Objects '(1970)

L'algoritmo in C ...

Indice delle combinazioni nell'ordine lessicografico (Buckles Algorithm 515)

Puoi anche fare riferimento a una combinazione in base al suo indice (in ordine lessicografico). Comprendendo che l'indice dovrebbe essere una certa quantità di cambiamento da destra a sinistra in base all'indice, possiamo costruire qualcosa che dovrebbe recuperare una combinazione.

Quindi, abbiamo un set {1,2,3,4,5,6} ... e vogliamo tre elementi. Diciamo {1,2,3} possiamo dire che la differenza tra gli elementi è una e in ordine e minima. {1,2,4} ha una modifica ed è lessicograficamente il numero 2. Quindi il numero di "modifiche" all'ultimo posto rappresenta una modifica nell'ordinamento lessicografico. Il secondo posto, con una modifica {1,3,4} ha una modifica, ma tiene conto di altre modifiche poiché è al secondo posto (proporzionale al numero di elementi nel set originale).

Il metodo che ho descritto è una decostruzione, a quanto pare, dall'insieme all'indice, dobbiamo fare il contrario - il che è molto più complicato. Ecco come Buckles risolve il problema. Ho scritto alcune C per calcolarle , con piccole modifiche - ho usato l'indice degli insiemi piuttosto che un intervallo di numeri per rappresentare l'insieme, quindi lavoriamo sempre da 0 ... n. Nota:

  1. Poiché le combinazioni non sono ordinate, {1,3,2} = {1,2,3} - ordiniamo che siano lessicografiche.
  2. Questo metodo ha uno 0 implicito per avviare il set per la prima differenza.

Indice delle combinazioni nell'ordine lessicografico (McCaffrey)

C'è un altro modo : il suo concetto è più facile da capire e programmare ma è senza le ottimizzazioni di Buckles. Fortunatamente, non produce anche combinazioni duplicate:

L'insieme x_k ... x_1 in Nche massimizza i = C (x_1, k) + C (x_2, k-1) + ... + C (x_k, 1), dove C (n, r) = {n scegli r}.

Per un esempio: 27 = C(6,4) + C(5,3) + C(2,2) + C(1,1). Quindi, la 27a combinazione lessicografica di quattro cose è: {1,2,5,6}, questi sono gli indici di qualunque set tu voglia guardare. Esempio sotto (OCaml), richiede choosefunzione, lasciato al lettore:

(* 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)

Un iteratore di combinazioni piccole e semplici

I seguenti due algoritmi sono forniti a scopo didattico. Implementano combinazioni iteratrici e (più generali) di cartelle. Sono il più veloci possibile, con la complessità O ( n C k ). Il consumo di memoria è limitato da k.

Inizieremo con l'iteratore, che chiamerà una funzione fornita dall'utente per ogni combinazione

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

Una versione più generale chiamerà la funzione fornita dall'utente insieme alla variabile di stato, a partire dallo stato iniziale. Dato che dobbiamo passare lo stato tra stati diversi, non useremo il ciclo for, ma utilizzeremo invece la ricorsione,

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
Questo produrrà combinazioni duplicate nel caso in cui il set contenga elementi uguali?
Thomas Ahle,

2
Sì, sarà Thomas. È indipendente dai dati nell'array. Puoi sempre filtrare prima i duplicati, se questo è l'effetto desiderato, o scegliere un altro algoritmo.
nlucaroni,

19
Risposta fantastica. Potete fornire un riepilogo del tempo di esecuzione e analisi della memoria per ciascuno degli algoritmi?
uncaught_exceptions,

2
Risposta abbastanza buona. 20C3 è 1140, il punto esclamativo qui confonde in quanto sembra un fattoriale, e i fattoriali inseriscono la formula per trovare le combinazioni. Modificherò quindi il punto esclamativo.
CashCow,

3
Fa schifo che molte delle citazioni siano dietro un paywall. Esiste la possibilità di includere collegamenti non paywall o includere frammenti quotabili da fonti?
Terrance

195

In 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)));
}

Uso:

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

Risultato:

123
124
125
134
135
145
234
235
245
345

2
Questa soluzione funziona bene per set "piccoli" ma per set più grandi usa un po 'di memoria.
Artur Carvalho,

1
non direttamente correlato ma, il codice è molto interessante / leggibile e mi chiedo quale versione di c # abbia questo costrutti / metodi? (Ho usato solo c # v1.0 e non molto).
LBarret,

Decisamente elegante, ma IEnumerable verrà elencato molte volte. Se questo è supportato da un'operazione significativa ...
Drew Noakes, il

2
dal momento che è un metodo di estensione che la tua riga di utilizzo può leggere:var result = new[] { 1, 2, 3, 4, 5 }.Combinations(3);
Dave Cousineau,

1
Puoi fornire la versione esatta non linq di questa query usando loop ricorsivi
irfandar,

81

Breve soluzione 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);
        }
    }       
}

Il risultato sarà

[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]

questo sembra essere O (n ^ 3) giusto? Mi chiedo che ci sia un algoritmo più veloce per farlo.
LZH,

Sto lavorando con 20 scegli 10 e questo sembra essere abbastanza veloce per me (meno di 1 secondo)
demongolem

4
@NanoHead ti sbagli. Questa è una combinazione senza ripetizione. E il tuo caso è con ripetizione.
Jack The Ripper,

Questo pezzo di codice dovrebbe essere più facile da trovare sul web ... questo è esattamente quello che stavo cercando!
Manuel S.

Ho appena testato questa e altre 7 implementazioni Java: questa è stata di gran lunga la più veloce. Il secondo più veloce è stato più lento di un ordine di grandezza.
stuart

77

Posso presentare la mia soluzione Python ricorsiva a questo problema?

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))

Esempio di utilizzo:

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

Mi piace per la sua semplicità.


16
len(tuple(itertools.combinations('abcdefgh',3)))otterrà la stessa cosa in Python con meno codice.
hgus1294,

59
@ hgus1294 Vero, ma sarebbe barare. Op ha richiesto un algoritmo, non un metodo "magico" legato a un particolare linguaggio di programmazione.
MestreLion,

1
A rigor di termini il primo intervallo di loop dovrebbe essere for i in xrange(len(elements) - length + 1):? Non importa in Python poiché uscire dall'indice delle sezioni è gestito con garbo ma è l'algoritmo corretto.
Stephan Dollberg,

62

Diciamo che il tuo array di lettere è simile al seguente: "ABCDEFGH". Hai tre indici (i, j, k) che indicano quali lettere utilizzerai per la parola corrente. Inizia con:

ABCDEFGH
^ ^ ^
IJK

Prima devi variare k, quindi il passaggio successivo è simile al seguente:

ABCDEFGH
^ ^ ^
IJK

Se hai raggiunto la fine, vai avanti e vari j e poi di nuovo k.

ABCDEFGH
^ ^ ^
IJK

ABCDEFGH
^ ^ ^
IJK

Una volta che hai raggiunto G, inizi anche a variare i.

ABCDEFGH
  ^ ^ ^
  IJK

ABCDEFGH
  ^ ^ ^
  IJK
...

Scritto in codice questo sembra qualcosa del genere

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
Il problema con questo approccio è che collega il parametro 3 al codice. (E se fossero desiderati 4 caratteri?) Come ho capito la domanda, sarebbero stati forniti sia l'array di caratteri che il numero di caratteri da selezionare. Ovviamente, un modo per aggirare quel problema è sostituire i loop esplicitamente nidificati con la ricorsione.
joel.neely

10
@ Dr.PersonPersonII E perché i triangoli hanno rilevanza per l'OP?
MestreLion,

7
Puoi sempre trasformare questa soluzione in ricorsiva con un parametro arbitrario.
Rok Kralj,

5
@RokKralj, come "trasformiamo questa soluzione in ricorsiva con un parametro arbitrario"? Mi sembra impossibile.
Aaron McDaid il

3
Una bella spiegazione intuitiva su come farlo
Yonatan Simson,

53

Il seguente algoritmo ricorsivo seleziona tutte le combinazioni di elementi k da un set ordinato:

  • scegli il primo elemento idella tua combinazione
  • combinare icon ciascuna delle combinazioni di k-1elementi scelti ricorsivamente dall'insieme di elementi più grande di i.

Iterate quanto sopra per ciascuno inel set.

È essenziale scegliere il resto degli elementi come più grande di i, per evitare la ripetizione. In questo modo [3,5] verrà scelto una sola volta, come [3] combinato con [5], anziché due volte (la condizione elimina [5] + [3]). Senza questa condizione si ottengono variazioni anziché combinazioni.


12
Ottima descrizione in inglese dell'algoritmo utilizzato da molte delle risposte
MestreLion,

secondo quanto sopra; in particolare, questo mi ha aiutato a capire la soluzione proposta da user935714. entrambi sono eccellenti.
jacoblambert,

25

In C ++ la seguente routine produrrà tutte le combinazioni di distanza (first, k) tra la gamma [first, last):

#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;
}

Può essere usato in questo modo:

#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;
}

Questo stamperà quanto segue:

123
124
125
134
135
145
234
235
245
345

1
Che cos'è l'inizio, che fine ha in questo caso? Come può effettivamente restituire qualcosa se tutte le variabili passate a questa funzione sono passate per valore?
Sergej Andrejev,

6
@Sergej Andrejev: sostituire beinge begincon s.begin()e endcon s.end(). Il codice segue da vicino l' next_permutationalgoritmo di STL , descritto qui in maggiori dettagli.
Anthony Labarre,

5
che cosa sta succedendo? i1 = ultimo; --i1; i1 = k;
Manoj R

24

Ho trovato utile questa discussione e ho pensato di aggiungere una soluzione Javascript che puoi inserire in Firebug. A seconda del tuo motore JS, la stringa iniziale potrebbe richiedere un po 'di tempo.

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");

L'output dovrebbe essere il seguente:

abc
ab
ac
a
bc
b
c

4
@NanoHead Questo non è sbagliato. L'output mostra già "ac" - e "ca" è la stessa combinazione di "ac". Stai parlando di permutazioni (in matematica) in cui "ac" non sarebbe lo stesso di "ca".
Jakob Jenkov,

1
Questo non è n scegliere 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;
    }
}

Bella soluzione. Ho fatto riferimento in risposta a questa recente interrogazione: stackoverflow.com/questions/4472036/...
wageoghe

L'unico problema con questa funzione è la ricorsività. Sebbene di solito vada bene per il software in esecuzione su PC, se stai lavorando con una piattaforma con risorse più limitate (ad esempio incorporato), sei sfortunato
Padu Merloti,

Allocherà inoltre un sacco di elenchi e farà molto lavoro per duplicare gli elementi dell'array in ognuno di essi. Mi sembra che queste liste non saranno collezionabili fino a quando l'intera enumerazione non sarà completa.
Niall Connaughton,

Questo è perfetto. Hai trovato un algoritmo o è da zero?
paparazzo,

20

Breve esempio in 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

Per spiegazione, il metodo ricorsivo è descritto con il seguente esempio:

Esempio: ABCDE
Tutte le combinazioni di 3 sarebbero:

  • A con tutte le combinazioni di 2 dal resto (BCDE)
  • B con tutte le combinazioni di 2 dal resto (CDE)
  • C con tutte le combinazioni di 2 dal resto (DE)

17

Semplice algoritmo ricorsivo in Haskell

import Data.List

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

Definiamo innanzitutto il caso speciale, ovvero selezionando zero elementi. Produce un singolo risultato, che è un elenco vuoto (ovvero un elenco che contiene un elenco vuoto).

Per n> 0, xpassa attraverso ogni elemento dell'elenco e segue xsogni elemento x.

restseleziona gli n - 1elementi xsdall'uso di una chiamata ricorsiva a combinations. Il risultato finale della funzione è un elenco in cui ogni elemento è x : rest(cioè un elenco che ha xcome testa e restcoda) per ogni diverso valore di xe rest.

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

E, naturalmente, poiché Haskell è pigro, l'elenco viene gradualmente generato in base alle esigenze, in modo da poter valutare parzialmente combinazioni esponenzialmente grandi.

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

13

E qui arriva il nonno COBOL, il linguaggio molto diffamato.

Supponiamo che un array di 34 elementi di 8 byte ciascuno (selezione puramente arbitraria). L'idea è quella di enumerare tutte le possibili combinazioni di 4 elementi e caricarle in un array.

Usiamo 4 indici, uno ciascuno per ogni posizione nel gruppo di 4

L'array viene elaborato in questo modo:

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

Variamo idx4 da 4 alla fine. Per ogni idx4 otteniamo una combinazione unica di gruppi di quattro. Quando idx4 arriva alla fine dell'array, incrementiamo idx3 di 1 e impostiamo idx4 su idx3 + 1. Quindi eseguiamo di nuovo idx4 fino alla fine. Procediamo in questo modo, aumentando rispettivamente idx3, idx2 e idx1 fino a quando la posizione di idx1 è inferiore a 4 dalla fine dell'array. Questo termina l'algoritmo.

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

Prime iterazioni:

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

Un esempio 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.

ma perché {} {} {} {}
shinzou,

9

Ecco un'implementazione generica ed elegante in Scala, come descritto in 99 Problemi di 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

Se è possibile utilizzare la sintassi SQL, ad esempio se si utilizza LINQ per accedere ai campi di una struttura o di un array o si accede direttamente a un database con una tabella denominata "Alfabeto" con un solo campo di caratteri "Lettera", è possibile adattare codice:

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

Questo restituirà tutte le combinazioni di 3 lettere, nonostante quante lettere hai nella tabella "Alfabeto" (può essere 3, 8, 10, 27, ecc.).

Se quello che vuoi sono tutte le permutazioni, piuttosto che le combinazioni (cioè vuoi che "ACB" e "ABC" contino come diversi, piuttosto che apparire solo una volta) basta eliminare l'ultima riga (quella AND) e il gioco è fatto.

Post-modifica: dopo aver riletto la domanda, mi rendo conto che è necessario l' algoritmo generale , non solo uno specifico per la selezione di 3 elementi. La risposta di Adam Hughes è completa, purtroppo non posso ancora votarla. Questa risposta è semplice ma funziona solo quando vuoi esattamente 3 articoli.


7

Un'altra versione C # con generazione pigra degli indici di combinazione. Questa versione mantiene un unico array di indici per definire una mappatura tra l'elenco di tutti i valori e i valori per la combinazione corrente, ovvero utilizza costantemente O (k) spazio aggiuntivo durante l'intero runtime. Il codice genera combinazioni individuali, inclusa la prima, nel tempo 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;
        }
    }
}

Codice di prova:

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

Produzione:

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

Questo preserva l'ordinamento. Mi aspetto che il set di risultati contenga anche c b aciò che non contiene .
Dmitri Nesteruk,

Il compito è generare tutte le combinazioni che soddisfano n su k. I coefficienti binomiali rispondono alla domanda su quanti modi ci sono scegliendo un sottoinsieme non ordinato di k elementi da un insieme fisso di n elementi. Pertanto l'algoritmo proposto fa quello che dovrebbe.
Christoph,

6

https://gist.github.com/3118596

Esiste un'implementazione per JavaScript. Ha funzioni per ottenere combinazioni k e tutte le combinazioni di una matrice di qualsiasi oggetto. Esempi:

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

Qui hai una versione lazy valutata di quell'algoritmo codificato in 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));
        }
    }

E parte di prova:

    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));
        }
    }

Spero che questo ti aiuti!


6

Avevo un algoritmo di permutazione che ho usato per il progetto Euler, in 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

Se

n<len(l) 

dovresti avere tutte le combinazioni di cui hai bisogno senza ripetizioni, ne hai bisogno?

È un generatore, quindi lo usi in qualcosa del genere:

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

Versione 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

Diciamo che il tuo array di lettere è simile al seguente: "ABCDEFGH". Hai tre indici (i, j, k) che indicano quali lettere utilizzerai per la parola corrente. Inizia con:

ABCDEFGH
^ ^ ^
IJK

Prima devi variare k, quindi il passaggio successivo è simile al seguente:

ABCDEFGH
^ ^ ^
IJK

Se hai raggiunto la fine, vai avanti e vari j e poi di nuovo k.

ABCDEFGH
^ ^ ^
IJK

ABCDEFGH
^ ^ ^
IJK

Una volta che hai raggiunto G, inizi anche a variare 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);

Basato su https://stackoverflow.com/a/127898/2628125 , ma più astratto, per qualsiasi dimensione di puntatori.


Cos'è questo linguaggio orribile? Bash?
shinzou,

1
php, ma la lingua non conta qui, l'algoritmo lo fa
Oleksandr Knyga

Sono così felice che mi rifiuto di imparare questa lingua. Una lingua in cui il suo interprete / compilatore ha bisogno di aiuto per riconoscere le variabili non dovrebbe esistere nel 2018.
Shinzou,

4

Detto questo, ecco il codice O'caml. L'algoritmo è evidente dal codice ..

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

Ecco un metodo che ti dà tutte le combinazioni di dimensioni specificate da una stringa di lunghezza casuale. Simile alla soluzione di Quinmars, ma funziona con input e k diversi.

Il codice può essere modificato per essere completato, ovvero "dab" dall'input "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);
    }
}

Uscita per "abcde":

abc abd abe acd ace ade bcd bce bde cde


4

codice pitone breve, che fornisce posizioni di indice

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

Questo è molto elegante / efficiente e funziona bene. L'ho appena tradotto in C ++.
Gattino accovacciato


3

Ecco la mia proposta in C ++

Ho cercato di imporre il minor numero possibile di iteratori sul tipo di iteratore, quindi questa soluzione presuppone un iteratore in avanti e può essere un const_iterator. Questo dovrebbe funzionare con qualsiasi contenitore standard. Nei casi in cui gli argomenti non hanno senso, genera 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

Ecco un codice che ho scritto di recente in Java, che calcola e restituisce tutta la combinazione di elementi "num" da elementi "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

Una soluzione Javascript concisa:

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

Algoritmo:

  • Contare da 1 a 2 ^ n.
  • Converti ogni cifra nella sua rappresentazione binaria.
  • Traduci ogni bit 'on' in elementi del tuo set, in base alla posizione.

In 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();
        }
    }
}

Perché funziona

C'è una biiezione tra i sottoinsiemi di un set di elementi n e sequenze n-bit.

Ciò significa che possiamo capire quanti sottoinsiemi esistono contando le sequenze.

ad esempio, i quattro elementi impostati di seguito possono essere rappresentati da {0,1} X {0, 1} X {0, 1} X {0, 1} (o 2 ^ 4) sequenze diverse.

Quindi - tutto ciò che dobbiamo fare è contare da 1 a 2 ^ n per trovare tutte le combinazioni. (Ignoriamo il set vuoto.) Successivamente, traduci le cifre nella loro rappresentazione binaria. Quindi sostituisci gli elementi del tuo set con bit "on".

Se si desidera solo i risultati dell'elemento k, stampare solo quando k bit sono "attivi".

(Se si desidera tutti i sottoinsiemi anziché i sottoinsiemi di lunghezza k, rimuovere la parte cnt / kElement.)

(Per la prova, vedere Matematica per informatica gratuita MIT, Lehman et al, sezione 11.2.2. Https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-042j-mathematics- per-informatica-scienza-autunno-2010 / letture / )


2

Ho scritto una classe per gestire le funzioni comuni per lavorare con il coefficiente binomiale, che è il tipo di problema che rientra nel problema. Esegue le seguenti attività:

  1. Stampa tutti gli indici K in un bel formato per qualsiasi N, scegli K in un file. Gli indici K possono essere sostituiti con stringhe o lettere più descrittive. Questo metodo rende piuttosto banale la risoluzione di questo tipo di problema.

  2. Converte gli indici K nell'indice corretto di una voce nella tabella dei coefficienti binomiali ordinati. Questa tecnica è molto più veloce delle vecchie tecniche pubblicate che si basano sull'iterazione. Lo fa usando una proprietà matematica inerente al Triangolo di Pascal. Il mio articolo parla di questo. Credo di essere il primo a scoprire e pubblicare questa tecnica, ma potrei sbagliarmi.

  3. Converte l'indice in una tabella dei coefficienti binomiali ordinati nei corrispondenti indici K.

  4. Utilizza il metodo Mark Dominus per calcolare il coefficiente binomiale, che ha molte meno probabilità di traboccare e funziona con numeri più grandi.

  5. La classe è scritta in .NET C # e fornisce un modo per gestire gli oggetti relativi al problema (se presenti) usando un elenco generico. Il costruttore di questa classe accetta un valore booleare chiamato InitTable che, quando true, creerà un elenco generico per contenere gli oggetti da gestire. Se questo valore è falso, non creerà la tabella. Non è necessario creare la tabella per eseguire i 4 metodi precedenti. Sono disponibili metodi di accesso per accedere alla tabella.

  6. Esiste una classe di test associata che mostra come utilizzare la classe e i suoi metodi. È stato ampiamente testato con 2 casi e non ci sono bug noti.

Per informazioni su questa classe e scaricare il codice, vedere Tablizing The Binomial Coeffieicent .

Non dovrebbe essere difficile convertire questa classe in C ++.


Non è proprio corretto chiamarlo "metodo Mark Dominus", perché come ho già detto ha almeno 850 anni e non è difficile da pensare. Perché non chiamarlo metodo Lilavati ?
MJD,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.