Projeto de Algoritmos

Enumere as estrelas de nossa galáxia.

Quantas vezes a palavra "algoritmo" aparece neste sítio?

"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

Algoritmos de enumeração

Para resolver certos problemas é necessário enumerar — ou seja, fazer uma lista de — todos os objetos de um determinado tipo. O número de objetos é tipicamente muito grande, e portanto os algoritmos enumerativos consomem uma enorme quantidade de tempo.

Os algoritmos enumerativos não são complexos, mas têm suas sutilezas. As versões recursivas são particularmente úteis e interessantes.

Problemas de enumeração estão relacionado com palavras-chave como busca exaustiva, força bruta, backtracking, branch-and-bound.

Enumeração de subsequências

Vamos adotar a abreviatura 1..n para 1, 2, ... , n e a abreviatura s[1..k] para s[1], s[2], ... , s[k].  Uma subsequência de 1..n é qualquer sequência  s[1..k]  tal que

1   ≤   s[1]   <   s[2]   <   . . .   <   s[k]   ≤   n .

Em outras palavras, uma subsequência é o que se obtém quando alguns termos da sequência são apagados. Exemplo:  2,3,5 é uma subsequência de 1..5. Nosso problema:

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

Há uma correspondência biunívoca óbvia entre subsequências de 1..n e  subconjuntos  do conjunto {1, 2, . . . , n}. Portanto, o número de subsequências de 1..n é

2n .

O número de subsequências aumenta explosivamente com n:  ele dobra toda vez que n aumenta de uma unidade.

Subsequências em ordem lexicográfica

A ordem em que as subsequências de 1..n são enumeradas não é muito importante, mas certas ordens são mais naturais que outras. Uma das ordens mais naturais é a lexicográfica (esta é a ordem em que palavras aparecem em um dicionário). Uma sequência r[1..j] é lexicograficamente menor que outra s[1..k] se

  1.   existe i tal que  r[1..i-1] = s[1..i-1]  e  r[i] < s[i]  ou
  2.   j < k  e  r[1..j] = s[1..j] .

Exemplo: Eis a lista, em ordem lexicográfica, de todas as subsequências não vazias de 1..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

A lista do exemplo poderia ter sido gerada pela seguinte função.

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

void subseqLex( int n)
{ 
   int *s, k;    
   s = mallocc( (n+1) * sizeof (int));
   s[0] = 0;
   k = 0;

   while (1) {
      if (s[k] < n) {
         s[k+1] = s[k] + 1;
         k += 1;
      } else {
         s[k-1] += 1;
         k -= 1;
      }
      if (k == 0) break;
      imprima( s, k);
   }
   free( s);
}
0 1       k   n
0 2 4 5 7 8 xxx xxx

Cada iteração começa com uma subsequência s[1..k] de 1..n. A primeira iteração começa com a subsequência vazia. Cada iteração gera a sucessora de s[1..k] na ordem lexicográfica. Se s[1..k] não tiver sucessora, o processo termina.

Observações:

Subsequências em ordem lexicográfica: versão recursiva

A versão recursiva de subseqLex é muito interessante. A interface com o usuário fica a cargo da seguinte função-"embalagem":

// A função subseqLex2 recebe n e imprime,
// em ordem lexicográfica,
// todas as subsequências não vazias de 1..n.

void subseqLex2( int n)
{
   int *s;    
   s = mallocc( (n+1) * sizeof (int));
   recurs( s, 0, 1, n);
   free( s);
}

O serviço pesado é todo executado pela função recursiva recurs. Para cada valor de m, ela imprime todas as subsequências que incluem m e depois todas as que não incluem m.

void recurs( int s[], int k, int m, int n)
{
   if (m <= n) {
      s[k+1] = m;
      imprima( s, k+1);
      recurs( s, k+1, m+1, n); // inclui m
      recurs( s, k,   m+1, n); // não inclui m
   }
}

Que coisa faz, exatamente, a função recurs? Como explicar o funcionamento da função? Eis a resposta:

   A função recurs recebe s[1..k] e m e imprime,
   em ordem lexicográfica, cada uma das subsequências
   não vazias de m..n precedida do prefixo s[1..k].
   Em outras palavras, imprime todas as sequências que
   têm a forma s[1..k]t[k+1...], sendo t[k+1...] uma
   subsequência não vazia de m..n.

Exemplo: Suponha que s[1] vale 2, que s[2] vale 4 e que n vale 9. Então o comando recurs( s,2,7,n) gera 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 é gerada por imprima( s,3); as três linhas seguintes são geradas por recurs( s,3,8,n); as demais por recurs( s,2,8,n).

Portanto, recurs( s,0,1,n) faz exatamente o que queremos: imprime todas as subsequências de 1..n que têm a forma t[1...] com t[1] ≥ 1, ou seja, imprime todas as subsequências não vazias de 1..n.

A versão recursiva da função tem uma importante desvantagem em relação à versão iterativa vista acima:  ela usa uma quantidade exponencial de memória. (Essa memória — proporcional a 2n — é usada pela pilha de recursão.)  A versão iterativa, ao contrário, só precisa da memória "de trabalho" usada para armazenar o vetor s[0..n].

Exercícios

  1. Analise a seguinte versão alternativa de subseqLex.
       int *s, k;    
       s = mallocc( (n+1) * sizeof (int));
       s[0] = 0; s[1] = 1; 
       k = 1;
       while (k >= 1) {
          imprima( s, k);
          if (s[k] < n) {
             s[k+1] = s[k] + 1; k += 1;
          } else {
             s[k-1] += 1; k -= 1;
          }
       }
    
  2. Analise a seguinte versão alternativa de subseqLex:
       int *s, k;    
       s = mallocc( (n+1) * sizeof (int));
       s[1] = 1;
       imprima( s, 1);
       k = 1;
       while (s[1] < n) {
          if (s[k] < n) {
             s[k+1] = s[k] + 1; k += 1;
          } else {
             s[k-1] += 1; k -= 1;
          }
          imprima( s, k);
       }
    
  3. Escreva uma função que receba uma subsequência s[1..k] de 1..n e gere, no espaço alocado para s[], a próxima subsequência na ordem lexicográfica. A função deve devolver o número de termos (k+1 ou k-1) desta nova subsequência.
  4. Verifique que a ordem lexicográfica sobre o conjunto de todas as subsequências de 1..n é uma ordem total, ou seja, para quaisquer subsequências r e s tem-se
    1.   rs   ou   sr ,
    2.   se  rs   e   sr  então  r = s ,
    3.   se  rs   e   st  então  rt ,

    onde  "≤"  denota a relação "lexicograficamente menor que ou igual a".

  5. O vetor característico de uma subsequência s[1..k] de 1..n é a sequência  b[1..n]  de bits que indica quais dos elementos 1..n estão s[1..k]  (portanto b[i] vale 1 se i está na subsequência e vale 0 em c aso contrário).  Para n=5, por exemplo, o vetor característico de  2 3 5  é  01101 .  Escreva código que calcule o vetor característico de uma subsequência s[1..k] de 1..n.  Escreva uma função que imprima todas as subsequências de 1..n da seguinte maneira: gera todas sequências de bits b[1..n] em ordem lexicográfica e para cada sequência de bits gerada imprime a correspondente subsequência s[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?

Subsequências em ordem lexicográfica especial

A ordem lexicográfica "especial" dá preferência às subsequências mais longas. Eis a definição formal: uma sequência r[1..j] precede outra s[1..k] na ordem lexicográfica especial se

  1.  existe i tal que r[1..i-1] é igual a s[1..i-1] e r[i] < s[i]  ou
  2.  j > k  e  r[1..k] é igual a s[1..k] .

Exemplo: Eis a lista, em ordem lexicográfica especial, de todas as subsequências de 1..4:
1 2 3 4
1 2 3
1 2 4
1 2
1 3 4
1 3
1 4
1
2 3 4
2 3
2 4
2
3 4
3
4

A lista do exemplo poderia ter sido gerado pela seguinte função iterativa:

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

void subseqLexEsp( int n) { 
   int *s, k;    
   s = mallocc( (n+1) * sizeof (int));
   s[1] = 0;
   k = 1;
   while (1) {
      if (s[k] == n) {
         k -= 1; 
         if (k == 0) break;
      } else {
         s[k] += 1; 
         while (s[k] < n) {
            s[k+1] = s[k] + 1;
            k += 1;
         }
      }
      imprima( s, k);
   }
   free( s);
}

Eis a versão recursiva da função:

// Recebe n e imprime todas as subsequências 
// de 1..n (inclusive a subsequência vazia).

void subseqLexEsp2( int n) {
   int *s;    
   s = mallocc( (n+1) * sizeof (int));
   recursEsp( s, 0, 1, n);
   free( s);
}

// Esta função auxiliar recebe s[1..k] e imprime, 
// em ordem lexicográfica especial,
// todas as subsequências da forma s[1..k]t[k+1..]
// onde t[k+1..] é uma subsequência de m..n.
// Em seguida, imprime a sequência s[1..k].

void recursEsp( int s[], int k, int m, int n) {
   if (m > n) imprima( s, k);
   else {
      s[k+1] = m;
      recursEsp( s, k+1, m+1, n); // inclui m
      recursEsp( s, k,   m+1, n); // não inclui m
   }
}

Exemplo: Suponha que s[1] vale 2, que s[2] vale 4 e que n vale 9. Então o comando recursEsp( s,2,7,n) imprime a lista

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

Portanto, recurs( s,0,1,n) imprime todas subsequências de 1..n, inclusive a vazia.

A versão recursiva da função usa uma quantidade de espaço proporcional a 2n, enquanto a versão iterativa só precisa de espaço proporcional a n.

Exercícios

  1. As subsequências de 1..n podem ser colocadas "em ordem militar".  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
    
  2. Subset Sum.  Suponha que você emitiu cheques nos valores de p[1], ... , p[n] ao longo de um mês. No fim do mês, o banco informa que um total t foi descontado de sua conta. Quais dos cheques foram descontados? Por exemplo, se p = 61,62,63,64 e t = 125 então só há duas possibilidades: ou foram descontados os cheques 1 e 4 ou foram descontados os cheques 2 e 3. Este é o problema da "soma de subconjunto", mais conhecido como subset sum:
    Dado um número t e um vetor p[1..n], encontrar todas as subsequências s[1..k] de 1..n tais que   p[s[1]] + . . . + p[s[k]] = t .

    Escreva uma função que resolva o problema. 

Mais exercícios

  1. Combinações.  Escreva uma função que imprima todas as subsequências de  1..n  que têm exatamente  k  termos.  (Isso corresponde, exatamente, aos subconjuntos de {1,..,n} que têm exatamente k elementos.) 
  2. Permutações.  Uma permutação da sequência  1..n  é qualquer rearranjo dessa sequência. Por exemplo, as 6 permutações de 123 são 123, 132, 213, 231, 312, 321.  Escreva uma função que imprima, exatamente uma vez, cada uma das  n!  permutações de 1..n
  3. 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] é diferente de i para todo i.  Por exemplo, os 9 desarranjos de 1234 são 2143, 2341, 2413, 3142, 3412, 3421, 4123, 4312, 4321.  Escreva uma função que imprima, exatamente uma vez, cada desarranjo de 1..n
  4. 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 p[1..n] com valores em [1..m] dotado da seguinte propriedade: para cada i em [1..m] existe j tal que p[j] = i.
  5. Problema das Rainhas.  É possível colocar 8 rainhas do jogo de xadrez sobre o tabuleiro de modo que nenhuma das rainhas possa atacar outra?  
  6. Passeio do Cavalo.  Suponha dado um tabuleiro de xadrez n-por-n . Determine se é possível que um cavalo (= knight) do jogo de xadrez parta da posição (1,1) do tabuleiro e complete um passeio por todas as n×n posições do tabuleiro em n×n-1 passos válidos. 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 do cavalo.

  7. Familiarize-se com o Combinatorial Object Server de Frank Ruskey.

 


URL of this site: www.ime.usp.br/~pf/algoritmos/
Last modified: Tue Apr 23 07:54:02 BRT 2013
Paulo Feofiloff
IME-USP

Valid HTML 4.01 Transitional     Valid CSS!