Algoritmos de enumeração

Enumere as estrelas de nossa galáxia.
— um antigo problema

Often it appears that
there is no better way to solve a problem
than to try all possible solutions.
This approach, called exhaustive search,
is almost always slow,
but sometimes it is better than nothing.

— Ian Parberry, Problems on Algorithms

Para resolver certos problemas computacionais é necessário enumerar — ou seja, fazer uma lista de — todos os objetos de um determinado tipo (por exemplo, todas as sequências crescentes de números inteiros entre 1 e 999, ou todas as árvores binárias de busca com chaves entre 1 e 999).  O número de objetos é tipicamente muito grande, e portanto sua enumeração consome muito tempo.

Os algoritmos de enumeração não são complexos, mas têm suas sutilezas. As versões recursivas são particularmente úteis e interessantes.  Algoritmos de enumeração têm relação com conceitos como backtracking, busca exaustiva, força bruta, e branch-and-bound.

Sumário:

Enumeração de sequências

Os objetos principais deste capítulo são sequências de números inteiros, como  9 51 61 4 8 3 18 51 13 22 1 3  por exemplo.  Usaremos ⚠ a abreviatura especial  1 . . n  para a sequência  1 2 3 . . . n .  De modo mais geral, usaremos a abreviatura m . . n  para a sequência  m m+1 m+2 . . .  n .  Se m > n, a sequência é vazia.

Nosso problema: Enumerar as subsequências de  1 . . n ,  ou seja,  fazer uma lista em que cada subsequência de  1 . . n  apareça uma e uma só vez.

É claro que uma subsequência de 1 . . n é o mesmo que uma sequência estritamente crescente de alguns elementos do conjunto {1, 2, … , n}.  É claro também que há uma correspondência biunívoca entre as subsequências de 1 . . n e os subconjuntos de {1, 2, … , n}.

A ordem em que as subsequências de 1 . . n são enumeradas não é muito importante. Veja, por exemplo, as seguintes enumerações das subsequências não vazias de 1 . . 4:  a primeira está em ordem militar, enquanto a segunda está em ordem lexicográfica.

  • 1
  • 2
  • 3
  • 4
  • 1 2
  • 1 3
  • 1 4
  • 2 3
  • 2 4
  • 3 4
  • 1 2 3
  • 1 2 4
  • 1 3 4
  • 2 3 4
  • 1 2 3 4
  • 1
  • 1 2
  • 1 2 3
  • 1 2 3 4
  • 1 2 4
  • 1 3
  • 1 3 4
  • 1 4
  • 2
  • 2 3
  • 2 3 4
  • 2 4
  • 3
  • 3 4
  • 4

O número de subsequências de 1 . . n cresce explosivamente com n pois dobra toda vez que n aumenta em 1.

Exercícios 1

  1. Mostre que há uma correspondência biunívoca entre as subsequências de 1 . . n e os subconjuntos do conjunto {1, 2, … , n}.
  2. Mostre que o número de subsequências de 1 . . n dobra toda vez que somamos 1 ao valor de n.  Deduza daí que o número de subsequências de 1 . . n é  2n .
  3. Antes de ler a próxima seção, pense em como você resolveria o problema. Tente fazer um esboço do seu algoritmo.

Subsequências em ordem lexicográfica

Nossa solução do problema vai listar as subsequências de 1 . . n em ordem lexicográfica.  Dada uma subsequência ss, nosso algoritmo gerará sua sucessora lexicográfica, ou seja, a menor subsequência que é maior que ss, sendo menor e maior tomados no sentido lexicográfico.  Por exemplo, no conjunto de todas as subsequências de 1 . . 9 , a sucessora lexicográfica de  2 4 5 7 8  é  2 4 5 7 8 9  e a sucessora lexicográfica de  2 4 5 7 9  é  2 4 5 8.

Sequências serão representadas por vetores. Assim, uma sequência como  s1 s2 s3 . . . sk  será denotada simplesmente por  s[1..k].  Por conveniência, as sequências e vetores serão indexados a partir de 1 e não a partir de 0, como é mais comum em C.

Para transformar uma subsequência ss[1..k] de 1..n na sua sucessora lexicográfica, basta fazer o seguinte:

if (ss[k] < n) {
   ss[k+1] = ss[k] + 1; 
   ++k;
} else { 
   ss[k-1] += 1;
   --k;
}

(A operação dentro do bloco else {} é conhecida como backtrack ou volta de ré.)  O código só não está correto em dois casos: (1) se o valor original de k é 0 e (2) se k vale 1 e o valor original de ss[1] é n. No primeiro caso, a sucessora lexicográfica é definida por ss[k=1] = 1. No segundo, não existe sucessora.

Essa discussão leva à seguinte solução do problema de enumeração:

// Recebe n >= 1 e imprime todas as
// subsequências não vazias de 1..n,
// em ordem lexicográfica.

void 
ssLex (int n)
{ 
   int* ss, k;    
   ss = malloc ((n+1) * sizeof (int));
   ss[k=0] = 0;

   while (true) {
      if (ss[k] < n) {
         ss[k+1] = ss[k] + 1;
         ++k;
      } else {
         ss[k-1] += 1;
         --k;
      }
      if (k == 0) break;
      imprime (ss, k);
   }
   free (ss);
}

Cada iteração começa com uma subsequência ss[1..k] de 1..n.  A primeira iteração começa com a subsequência vazia. Cada iteração gera a sucessora lexicográfica de ss[1..k]. Se ss[1..k] não tiver sucessora, o processo termina.  A instrução imprime (ss, k) não faz mais que imprimir ss[1..k].

A sentinela ss[0] cuida da primeira iteração (que começa com k igual a 0) e da última iteração (que começa com ss[k] igual a n e k igual a 1).

O vetor ss[1..k] comporta-se como uma pilha, com topo ss[k].  É curioso como o código de ssLex se assemelha à versão iterativa da varredura e-r-d de uma árvore binária.

Exercícios 2

  1. Escreva uma função booleana que decida se uma sequência s[1 . . k] é lexicograficamente menor que uma sequência t[1 . . l].
  2. Que acontece se a função ssLex for executada com parâmetro n igual a 0?  Verifique, cuidadosamente, a correção do código da função nos casos em que n vale 1 e 2.
  3. No código de ssLex, a variável k pode crescer mas nunca é comparada com n.  Isso não é um erro?
  4. Analise a seguinte versão alternativa de ssLex:
    ss[0] = 0; ss[k=1] = 1; 
    while (k >= 1) {
       imprime (ss, k);
       if (ss[k] < n) {
          ss[k+1] = ss[k] + 1; ++k; } 
       else {
          ss[k-1] += 1; --k; } }
    
  5. Analise a seguinte versão alternativa de ssLex:
    ss[k=1] = 1;
    imprime (ss, 1);
    while (ss[1] < n) {
       if (ss[k] < n) {
          ss[k+1] = ss[k] + 1; ++k; } 
       else {
          ss[k-1] += 1; --k; }
       imprime (ss, k); }
    
  6. ★ Escreva uma versão da função ssLex que trate o vetor ss[0..k] explicitamente como uma pilha. Use as funções criapilha, empilha, desempilha, pilhavazia e liberapilha para manipular a pilha. Suponha que existe uma função tamanhodapilha que devolve o número de elementos da pilha e uma função imprimepilha que imprime o conteúdo da pilha a partir do seu segundo elemento.
  7. Modifique a função ssLex para que ela produza todas as subsequências não vazias de 0..n-1.
  8. Subset Sum.  Suponha que você emitiu cheques nos valores v[1], . . . , v[n] ao longo de um mês. No fim do mês, o banco informa que um total t foi debitado de sua conta. Quais dos cheques foram descontados? Por exemplo, se v é  61 62 63 64  e  t = 125  então só há duas possibilidades: ou foram debitados os cheques 1 e 4 ou foram debitados os cheques 2 e 3.  Essa é uma instância do seguinte problema da soma de subconjunto, mais conhecido como subset sum:  Dado um número t e um vetor v[1 . . n] de números (não necessariamente todos positivos), encontrar todas as subsequências s[1 . . k] de 1 . . n tais que  v[s[1]] + . . . + v[s[k]] = t .  Escreva uma função que resolva o problema.
  9. Two Sum.  Escreva um algoritmo rápido para o seguinte problema:  dados um vetor v[1 . . n] de números inteiros e um número inteiro t, decidir se existem dois índices distintos i e j tais que  v[i] + v[j] = t.  (O problema é superficialmente semelhante ao anterior, mas admite um algoritmo muito eficiente.)  Parte 2: Suponha que os elementos do vetor v[1 . . n] são diferentes entre si e calcule todos os pares i j de índices distintos tais que v[i] + v[j] = t.

Subsequências: versão recursiva

A versão recursiva de ssLex é muito interessante. A interface com o usuário fica a cargo da seguinte função-embalagem (= wrapper-function):

// Recebe n >= 1 e imprime todas as
// subsequências não vazias de 1..n,
// em ordem lexicográfica.

void 
ssLexR (int n)
{
   int* ss;    
   ss = malloc ((n+1) * sizeof (int));
   ssLexComPrefixo (ss, 0, 1, n);
   free (ss);
}

O serviço pesado é todo executado pela função recursiva ssLexComPrefixo, cujo terceiro parâmetro é um índice m entre 1 e n+1:

static void 
ssLexComPrefixo (int ss[], int k, int m, int n)
{
   if (m <= n) {
      // caso 1: inclui m
      ss[k+1] = m;
      imprime (ss, k+1);
      ssLexComPrefixo (ss, k+1, m+1, n);
      // caso 2: não inclui m
      ssLexComPrefixo (ss, k,   m+1, n);
   }
}

(É tentador começar o código da função com  if (m > n) imprime (ss,k);  mas isso não produziria ordem lexicográfica. Veja a ordem lexicográfica especial abaixo.)  O que faz, exatamente, a função ssLexComPrefixo? Como explicar o funcionamento de ssLexComPrefixo?  Eis a resposta:

// A função ssLexComPrefixo recebe m <= n+1
// e uma subsequência ss[1..k] de 1..m-1
// e imprime, em ordem lexicográfica,
// todas as subsequências não vazias de m..n
// cada uma precedida do prefixo ss[1..k].

Em outras palavras, imprime todas as sequências da forma x[1..k..l] tais que x[1..k] = ss[1..k] e x[k+1..l] é uma subsequência não vazia de m..n.

Suponha, por exemplo, que ss[1] = 2, ss[2] = 4, e n = 9. Então a chamada ssLexComPrefixo (ss, 2, 7, n) imprime a lista

2 4 7
2 4 7 8
2 4 7 8 9
2 4 7 9
2 4 8
2 4 8 9
2 4 9

A primeira linha é produzida por imprime (ss,3);  as três linhas seguintes são produzidas por ssLexComPrefixo (ss, 3, 8, n);  e as demais, por ssLexComPrefixo (ss, 2, 8, n).

Portanto, a chamada ssLexComPrefixo (ss, 0, 1, n) no código de ssLexR faz exatamente o que queremos: imprime todas as subsequências não vazias de 1..n (com prefixo vazio).

A função ssLexR tem uma séria desvantagem em relação à sua versão iterativa:  a pilha de execução da versão recursiva consome bem mais espaço na memória que a versão iterativa (que só precisa de espaço para armazenar ss[0..n+1]).

Exercícios 3

  1. Importante.  Verifique, cuidadosamente, a correção do código de ssLexR nos casos em que n vale 0, 1 e 2.
  2. Cadeias de caracteres.  Escreva uma função que imprima todas as subsequências não vazias de uma cadeia de caracteres ASCII dada.  Por exemplo, as subsequências não vazias de ABC são
    A  B  C  AB  AC  BC  ABC
    
  3. O vetor característico de uma subsequência ss[1..k] de 1..n é o vetor  b[1..n]  de bits que indica quais dos elementos 1..n estão em ss[1..k]  (ou seja, b[i] vale 1 se i está na subsequência e vale 0 em caso contrário).  Para n = 5, por exemplo, o vetor característico de  2 3 5  é  0 1 1 0 1 .  Escreva uma função que calcule o vetor característico de uma subsequência ss[1..k] de 1..n.  Escreva uma função que imprima todas as subsequências de 1..n da seguinte maneira: gera todas as sequências de bits b[1..n] em ordem lexicográfica e, para cada sequência de bits, imprime a correspondente subsequência ss[1..k] de 1..n.  (Sugestão: Imagine que um vetor de bits representa um número em notação binária e gere os vetores de bits em ordem numérica crescente.)  Sua função imprime as subsequências em ordem lexicográfica?
  4. ★ Ordem lexicográfica especial.  A ordem lexicográfica especial dá preferência às subsequências mais longas:  uma sequência s é menor que outra t na ordem lexicográfica especial se  (1) s[i] < t[i] para o menor i tal que s[i] ≠ t[i]  ou  (2) j > k e não existe i tal que s[i] ≠ t[i].  Veja, por exemplo, a enumeração de 1..3 em ordem lexicográfica especial:
    1 2 3
    1 2
    1 3
    1
    2 3
    2
    3
    

    Escreva uma função que enumere as subsequências de 1 . . n em ordem lexicográfica especial. Faça uma versão iterativa e outra recursiva.  Os algoritmos são um pouco mais simples que os da ordem lexicográfica usual.

  5. Ordem militar.  As subsequências de 1 . . n podem ser colocadas em ordem militar, ou ordem shortlex.  A figura ilustra a ordem no caso em que n vale 4.  Analise essa ordem. Escreva funções iterativa e recursiva que gerem todas as subsequências de 1 . . n em ordem militar.
    1
    2
    3
    4
    1 2
    1 3
    1 4
    2 3
    2 4
    3 4
    1 2 3
    1 2 4
    1 3 4
    2 3 4
    1 2 3 4
    
  6. Problema da bipartição equilibrada.  Dois irmãos receberam uma herança que consiste em n objetos, cada objeto i tendo valor v[i].  É possível dividir a herança v[1 . . n] em duas partes de mesmo valor?  Essa é uma instância do problema da bipartição equilibrada: Dado um vetor v[1 . . n] de números inteiros (não necessariamente todos positivos), encontrar uma subsequência s[1 . . k] de 1 . . n tal que  v[s[1]] + … + v[s[k]]  =  v[t[1]] + … + v[t[n−k]] ,  sendo t[1 . . n−k] o complemento de s[1 . . k] em 1 . . n.  Escreva uma função que resolva o problema.
  7. Combinações.  Escreva uma função que imprima todas as subsequências de  1 . . n  que têm exatamente  k  elementos.  (Isso corresponde aos subconjuntos de {1, 2, . . . , n} que têm exatamente k elementos.)

Enumeração de permutações

Uma permutação da sequência  1 . . n  é qualquer rearranjo dos termos dessa sequência.  Em outras palavras, uma permutação de 1 . . n é qualquer sequência p[1 . . n] em que cada elemento de 1 . . n aparece uma e uma só vez.  É fácil verificar que há exatamente n! permutações de 1 . . n.

Nosso problema: Enumerar as permutações de 1 . . n,  ou seja,  produzir uma lista em que cada permutação de 1 . . n apareça uma e uma só vez.

Na prática, a enumeração das permutações de 1 . . n só é razoável para valores muito modestos de n, uma vez que o número de permutações cresce explosivamente à medida que n aumenta.

A ordem em que as permutações são enumeradas não é muito importante. Na seguinte figura temos duas enumerações das permutações de 1 2 3 4: a primeira está em ordem recursiva clássica, enquanto a segunda está em ordem lexicográfica.

  • 1 2 3 4
  • 1 2 4 3
  • 1 3 2 4
  • 1 3 4 2
  • 1 4 3 2
  • 1 4 2 3
  • 2 1 3 4
  • 2 1 4 3
  • 2 3 1 4
  • 2 3 4 1
  • 2 4 3 1
  • 2 4 1 3
  • 3 2 1 4
  • 3 2 4 1
  • 3 1 2 4
  • 3 1 4 2
  • 3 4 1 2
  • 3 4 2 1
  • 4 2 3 1
  • 4 2 1 3
  • 4 3 2 1
  • 4 3 1 2
  • 4 1 3 2
  • 4 1 2 3
  • 1 2 3 4
  • 1 2 4 3
  • 1 3 2 4
  • 1 3 4 2
  • 1 4 2 3
  • 1 4 3 2
  • 2 1 3 4
  • 2 1 4 3
  • 2 3 1 4
  • 2 3 4 1
  • 2 4 1 3
  • 2 4 3 1
  • 3 1 2 4
  • 3 1 4 2
  • 3 2 1 4
  • 3 2 4 1
  • 3 4 1 2
  • 3 4 2 1
  • 4 1 2 3
  • 4 1 3 2
  • 4 2 1 3
  • 4 2 3 1
  • 4 3 1 2
  • 4 3 2 1

Na lista de permutações em ordem lexicográfica, a primeira permutação é crescente e a última é decrescente. Esse padrão se repete recursivamente da seguinte maneira: todas as permutações que têm um determinado prefixo, digamos pp[1..i], aparecem consecutivamente na lista e o correspondente sufixo, pp[i+1..n], é crescente na primeira permutação e decrescente na última.  Veja um exemplo com n = 4 e outro com n = 5:

  • 1 2 3 4
  • 1 2 4 3
  • 1 3 2 4
  • 1 3 4 2
  • 1 4 2 3
  • 1 4 3 2
  • 2 1 3 4
  • 2 1 4 3
  • 2 3 1 4
  • 2 3 4 1
  • 2 4 1 3
  • 2 4 3 1
  • 3 1 2 4
  • 3 1 4 2
  • 3 2 1 4
  • 3 2 4 1
  • 3 4 1 2
  • 3 4 2 1
  • 4 1 2 3
  • 4 1 3 2
  • 4 2 1 3
  • 4 2 3 1
  • 4 3 1 2
  • 4 3 2 1
  •       ⋮
  • 2 5 4 3 1
  • 3 1 2 4 5
  • 3 1 2 5 4
  • 3 1 4 2 5
  • 3 1 4 5 2
  • 3 1 5 2 4
  • 3 1 5 4 2
  • 3 2 1 4 5
  •       ⋮

Esse padrão é a base de um antigo algoritmo indiano para o problema.  O coração do algoritmo é uma função que recebe uma permutação qualquer pp[1..n] e constrói sua sucessora lexicográfica, ou seja, a menor permutação que é maior que pp[1..n], sendo menor e maior tomados no sentido lexicográfico.  Por exemplo, a partir da permutação  2 5 4 3 1  constrói a permutação  3 1 2 4 5.

A função constrói a sucessora lexicográfica em quatro passos: (1) encontra o menor i tal que pp[i+1..n] é decrescente, (2) encontra o único k em i+1..n tal que pp[k] > pp[i] > pp[k+1], (3) troca os valores de pp[i] e pp[k], (4) inverte pp[i+1..n], trocando pp[i+1] com pp[n], depois pp[i+2] com pp[n-1], etc.

// Recebe uma permutação pp[1..n] de 1..n. Se
// pp[1..n] não tem sucessora lexicográfica, 
// devolve 0. Senão, devolve 1 e armazena em
// pp[1..n] a sucessora lexicográfica da 
// permutação dada.

int
indiana (int* pp, int n) 
{
   int i, k, t;
   i = n-1; 
   while (i >= 1 && pp[i] > pp[i+1]) i--;
   if (i < 1) return 0;
   // pp[i] < pp[i+1] e
   // pp[i+1..n] é decrescente
   k = n;
   while (pp[i] > pp[k]) k--;
   // pp[k] > pp[i] > pp[k+1]
   t = pp[k]; pp[k] = pp[i]; pp[i] = t;
   // pp[i+1..n] é decrescente
   i += 1;
   k = n;
   while (i < k) {
      t = pp[i]; pp[i] = pp[k]; pp[k] = t;
      i++; 
      k--;
   }
   // pp[i+1..n] é crescente
   return 1;
}

Exercícios 4

  1. Prove que há exatamente  n!  permutações de 1 . . n.
  2. Importante.  Verifique, cuidadosamente, a correção do código da função indiana nos casos em que n vale 1, 2, e 3.
  3. ★ Mostre que função indiana está correta, ou seja, produz a sucessora lexicográfica da permutação que recebeu. (Sugestão: Denote por pp' a permutação que a função produz. Mostre que qualquer permutação que seja lexicograficamente maior que pp é igual a pp' ou é lexicograficamente maior que pp'.)
  4. ★ Escreva uma função que imprima todas as permutações de 1..n invocando indiana repetidamente.
  5. Um algoritmo clássico.  Analise o seguinte algoritmo recursivo clássico de enumeração de permutações de 1..n. O que faz, exatamente, a função recursiva permsComPrefixo? As permutações são impressas em ordem lexicográfica?
    void permutacoes (int n) {
       int* pp;    
       pp = malloc ((n+1) * sizeof (int));
       for (int i = 1; i <= n; i++) pp[i] = i;
       permsComPrefixo (pp, 0, n);
       free (pp);
    }
    static void permsComPrefixo (int* pp, int i, int n) {
       if (i >= n-1)
          imprime (pp, n);
       else {
          for (int j = i+1; j <= n; j++) {
             troca (pp, i+1, j);
             permsComPrefixo (pp, i+1, n);
             troca (pp, i+1, j); } }
    }
    static void troca (int* pp, int k, int j) {
       int t = pp[k]; pp[k] = pp[j]; pp[j] = t; 
    }
    
  6. Permutação de cadeias de caracteres.  Escreva uma função que imprima todas as permutações de uma cadeia de caracteres ASCII dada.  Por exemplo, ao receber a cadeia ABC, a função deve imprimir
    ABC  ACB  BAC  BCA  CBA  CAB 
    

Exercícios 5: aplicações

  1. ★ Problema das rainhas.  É possível colocar 8 rainhas do jogo de xadrez sobre o tabuleiro 8-por-8 de modo que nenhuma delas possa atacar outra?  Escreva uma função que enumere todas as maneiras de dispor n rainhas num tabuleiro n-por-n .  Uma disposição das rainhas pode ser representada por um vetor pp[1 . . n] tal que a rainha da linha i fica na coluna pp[i].
  2. Passeio do cavalo.  Suponha dado um tabuleiro de xadrez n-por-n . Determine se é possível que um cavalo do jogo de xadrez parta da posição (1,1) do tabuleiro e passe exatamente uma vez por cada uma das demais posições do tabuleiro. Por exemplo, para um tabuleiro 5-por-5 uma solução do problema é
     1  6 15 10 21
    14  9 20  5 16
    19  2  7 22 11
     8 13 24 17  4
    25 18  3 12 23
    

    (Sugestão: Numere as casas do tabuleiro da maneira óbvia: a primeira linha da esquerda para a direita, depois a segunda linha, etc.  Agora examine todas as permutações de 1 2 . . . n2.  Para cada permutação, verifique se ela representa um passeio válido.)

  3. Permutação cíclica.  Uma permutação cíclica de uma sequência s1 s2 . . . sn é qualquer sequência da forma  sk sk+1 . . . sn s1 s2 . . . sk−1.  Escreva uma função que decida se uma sequência t1 t2 . . . tn é permutação cíclica de s1 s2 . . . sn.
  4. Permutação aleatória.  Escreva uma função que produza uma permutação aleatória de 1 . . n. A função deve produzir, com igual probabilidade, qualquer uma das n! permutações de 1 . . n.
  5. Distância tau de Kendall.  Como medir a distância entre duas permutações?  Veja exercício no capítulo do algoritmo Mergesort.
  6. Desarranjos.  Um desarranjo (= derangement) da sequência 1 . . n  é qualquer permutação dessa sequência que muda todos os termos de posição. Em outras palavras, um desarranjo de 1 . . n é qualquer permutação p[1 . . n]  de 1 . . n tal que p[i] ≠ i para todo i.  Por exemplo, os 9 desarranjos de 1 2 3 4 são  2 1 4 3,  2 3 4 1,  2 4 1 3,  3 1 4 2,  3 4 1 2,  3 4 2 1,  4 1 2 3,  4 3 1 2,  4 3 2 1.  Escreva uma função que imprima cada desarranjo de 1 . . n exatamente uma vez.
  7. Partições.  Escreva uma função que imprima uma lista de todas as partições do conjunto {1, 2, . . . , n} em m blocos não vazios. Você pode representar uma tal partição por um vetor w[1 . . n] com valores em 1 . . m dotado da seguinte propriedade: para cada i em 1 . . m existe j tal que w[ j] = i.