Gerar combinações em Java

Gerar combinações em Java

1. Introdução

Neste tutorial,we’ll discuss the solution of the k-combinations problem in Java.

Primeiro, vamos discutir e implementar algoritmos recursivos e iterativos para gerar todas as combinações de um determinado tamanho. Em seguida, revisaremos as soluções usando bibliotecas Java comuns.

2. Visão geral das combinações

Simplificando,a combination is a subset of elements from a given set.

Ao contrário das permutações, a ordem em que escolhemos os elementos individuais não importa. Em vez disso, apenas nos importamos se um elemento específico está na seleção.

Por exemplo, em um jogo de cartas, temos que distribuir 5 cartas do baralho que consiste em 52 cartas. Não temos interesse na ordem em que as cinco cartas foram selecionadas. Em vez disso, apenas nos importamos com as cartas presentes na mão.

Alguns problemas exigem que avaliemos todas as combinações possíveis. Para fazer isso, enumeramos as várias combinações.

O número de maneiras distintas de escolher elementos “r” do conjunto de elementos “n” pode ser expresso matematicamente com a seguinte fórmula:

image

Portanto, o número de maneiras de escolher elementos pode crescer exponencialmente no pior dos casos. Portanto, para grandes populações, pode não ser possível enumerar as diferentes seleções.

Nesses casos, podemos selecionar aleatoriamente algumas seleções representativas. O processo é denominadosampling.

A seguir, revisaremos os vários algoritmos para listar combinações.

3. Algoritmos recursivos para gerar combinações

Recursive algorithms geralmente funciona particionando um problema em problemas menores semelhantes. Esse processo continua até chegarmos à condição de término, que também é o caso base. Então resolvemos o caso base diretamente.

We’ll discuss two ways to subdivide the task of choosing elements from a set. A primeira abordagem divide o problema em termos dos elementos do conjunto. A segunda abordagem divide o problema rastreando apenas os elementos selecionados.

3.1. Particionando por elementos no conjunto inteiro

Vamos dividir a tarefa de selecionar “r” elementos de“n” itens inspecionando os itens um por um. Para cada item do conjunto, podemos incluí-lo na seleção ou excluí-lo.

If we include the first item, then we need to choose “r – 1″ elements from the remaining “n – 1″ items. Por outro lado,if we discard the first item, then we need to select “r” elements out of the remaining “n – 1″ items.

Isso pode ser matematicamente expresso como:

imageimage

Agora, vamos dar uma olhada na implementação recursiva desta abordagem:

private void helper(List combinations, int data[], int start, int end, int index) {
    if (index == data.length) {
        int[] combination = data.clone();
        combinations.add(combination);
    } else if (start <= end) {
        data[index] = start;
        helper(combinations, data, start + 1, end, index + 1);
        helper(combinations, data, start + 1, end, index);
    }
}

O métodohelper faz duas chamadas recursivas para si mesmo. A primeira chamada inclui o elemento atual. A segunda chamada descarta o elemento atual.

A seguir, vamos escrever o gerador de combinação usando este métodohelper:

public List generate(int n, int r) {
    List combinations = new ArrayList<>();
    helper(combinations, new int[r], 0, n-1, 0);
    return combinations;
}

No código acima, o métodogenerate configura a primeira chamada para o métodohelpere passa os parâmetros apropriados.

A seguir, vamos chamar este método para gerar combinações:

List combinations = generate(N, R);
for (int[] combination : combinations) {
    System.out.println(Arrays.toString(combination));
}
System.out.printf("generated %d combinations of %d items from %d ", combinations.size(), R, N);

Ao executar o programa, obtemos a seguinte saída:

[0, 1]
[0, 2]
[0, 3]
[0, 4]
[1, 2]
[1, 3]
[1, 4]
[2, 3]
[2, 4]
[3, 4]
generated 10 combinations of 2 items from 5

Finalmente, vamos escrever o caso de teste:

@Test
public void givenSetAndSelectionSize_whenCalculatedUsingSetRecursiveAlgorithm_thenExpectedCount() {
    SetRecursiveCombinationGenerator generator = new SetRecursiveCombinationGenerator();
    List selection = generator.generate(N, R);
    assertEquals(nCr, selection.size());
}

É fácil observar que o tamanho da pilha necessário é o número de elementos no conjunto. When the number of elements in the set is large, say, greater than the maximum call stack depth, we’ll overflow the stack and get a StackOverflowError.

Portanto, esta abordagem não funciona se o conjunto de entrada for grande.

3.2. Particionando por elementos na combinação

Em vez de rastrear os elementos no conjunto de entrada,we’ll divide the task by tracking the items in the selection.

Primeiro, vamos ordenar os itens no conjunto de entrada usando os índices de "1" a "n”. Agora, podemos escolher o primeiro item dos primeiros “n-r+1″ itens.

Vamos supor que escolhemos o sitekth . Em seguida, precisamos escolher “r – 1″ itens dos“n – k” itens restantes indexados “k + 1″ a“n”.

Expressamos esse processo matematicamente como:

image

A seguir,let’s write the recursive method to implement this approach:

private void helper(List combinations, int data[], int start, int end, int index) {
    if (index == data.length) {
        int[] combination = data.clone();
        combinations.add(combination);
    } else {
        int max = Math.min(end, end + 1 - data.length + index);
        for (int i = start; i <= max; i++) {
            data[index] = i;
            helper(combinations, data, i + 1, end, index + 1);
        }
    }
}

No código acima, o loopfor escolhe o próximo item, Então,it calls the helper() method recursively to choose the remaining items. Paramos quando o número necessário de itens foi selecionado.

A seguir, vamos usar o métodohelper para gerar seleções:

public List generate(int n, int r) {
    List combinations = new ArrayList<>();
    helper(combinations, new int[r], 0, n - 1, 0);
    return combinations;
}

Finalmente, vamos escrever um caso de teste:

@Test
public void givenSetAndSelectionSize_whenCalculatedUsingSelectionRecursiveAlgorithm_thenExpectedCount() {
    SelectionRecursiveCombinationGenerator generator = new SelectionRecursiveCombinationGenerator();
    List selection = generator.generate(N, R);
    assertEquals(nCr, selection.size());
}

O tamanho da pilha de chamadas usado por essa abordagem é igual ao número de elementos na seleção. Portanto,this approach can work for large inputs so long as the number of elements to be selected is less than the maximum call stack depth.

Se o número de elementos a serem escolhidos também for grande, este método não funcionará.

4. Algoritmo Iterativo

Na abordagem iterativa, começamos com uma combinação inicial. Então,we keep generating the next combination from the current one until we have generated all combinations.

Vamos gerar as combinações em ordem lexicográfica. Começamos com a menor combinação lexicográfica.

Para obter a próxima combinação da atual, encontramos o local mais à direita na combinação atual que pode ser incrementado. Em seguida, incrementamos o local e geramos a menor combinação lexicográfica possível à direita desse local.

Vamos escrever o código que segue esta abordagem:

public List generate(int n, int r) {
    List combinations = new ArrayList<>();
    int[] combination = new int[r];

    // initialize with lowest lexicographic combination
    for (int i = 0; i < r; i++) {
        combination[i] = i;
    }

    while (combination[r - 1] < n) {
        combinations.add(combination.clone());

         // generate next combination in lexicographic order
        int t = r - 1;
        while (t != 0 && combination[t] == n - r + t) {
            t--;
        }
        combination[t]++;
        for (int i = t + 1; i < r; i++) {
            combination[i] = combination[i - 1] + 1;
        }
    }

    return combinations;
}

A seguir, vamos escrever o caso de teste:

@Test
public void givenSetAndSelectionSize_whenCalculatedUsingIterativeAlgorithm_thenExpectedCount() {
    IterativeCombinationGenerator generator = new IterativeCombinationGenerator();
    List selection = generator.generate(N, R);
    assertEquals(nCr, selection.size());
}

Agora, vamos usar algumas bibliotecas Java para resolver o problema.

5. Bibliotecas Java implementando combinações

Na medida do possível, devemos reutilizar as implementações de bibliotecas existentes em vez de lançar as nossas. Nesta seção, exploraremos as seguintes bibliotecas Java que implementam combinações:

  • Apache Commons

  • Goiaba

  • CombinatoricsLib

5.1. Apache Commons

A classeCombinatoricsUtils do Apache Commons fornece muitas funções de utilitário de combinação. Em particular, o métodocombinationsIterator retorna um iterador que irá gerar combinações em ordem lexicográfica.

Primeiro, vamos adicionar a dependência Mavencommons-math3 ao projeto:


    org.apache.commons
    commons-math3
    3.6.1

A seguir,let’s use the combinationsIterator method to print the combinations:

public static void generate(int n, int r) {
    Iterator iterator = CombinatoricsUtils.combinationsIterator(n, r);
    while (iterator.hasNext()) {
        final int[] combination = iterator.next();
        System.out.println(Arrays.toString(combination));
    }
}

5.2. Google Guava

A classeSets da biblioteca Guava fornece métodos utilitários para operações relacionadas a conjuntos. The combinations method returns all subsets of a given size.

Primeiro, vamos adicionar a dependência maven paraGuava library ao projeto:


    com.google.guava
    guava
    27.0.1-jre

A seguir,let’s use the combinations method to generate combinations:

Set> combinations = Sets.combinations(ImmutableSet.of(0, 1, 2, 3, 4, 5), 3);

Aqui, estamos usando o métodoImmutableSet.of para criar um conjunto a partir dos números fornecidos.

5.3. CombinatoricsLib

CombinatoricsLib é uma biblioteca Java pequena e simples para permutações, combinações, subconjuntos, partições inteiras e produto cartesiano.

Para usá-lo no projeto, vamos adicionar a dependênciacombinatoricslib3 do Maven:


    com.github.dpaukov
    combinatoricslib3
    3.3.0

A seguir,let’s use the library to print the combinations:

Generator.combination(0, 1, 2, 3, 4, 5)
  .simple(3)
  .stream()
  .forEach(System.out::println);

Isso produz a seguinte saída na execução:

[0, 1, 2]
[0, 1, 3]
[0, 1, 4]
[0, 1, 5]
[0, 2, 3]
[0, 2, 4]
[0, 2, 5]
[0, 3, 4]
[0, 3, 5]
[0, 4, 5]
[1, 2, 3]
[1, 2, 4]
[1, 2, 5]
[1, 3, 4]
[1, 3, 5]
[1, 4, 5]
[2, 3, 4]
[2, 3, 5]
[2, 4, 5]
[3, 4, 5]

Mais exemplos estão disponíveis emcombinatoricslib3-example.

6. Conclusão

Neste artigo, implementamos alguns algoritmos para gerar combinações.

Também revisamos algumas implementações de bibliotecas. Normalmente, usaríamos esses em vez de lançar o nosso próprio.

Como de costume, o código-fonte completo pode ser encontradoover on GitHub.