Algoritmos para Grafos  |  Índice

Emparelhamentos em grafos bipartidos

Este capítulo apresenta o problema do emparelhamento máximo em grafos não-dirigidos.  Em seguida, resolve o problema em grafos não-dirigidos bipartidos.  (Em grafos não-dirigidos arbitrários, o problema é mais difícil.)

Sumário:

figs/grafos-exercicios/3-2-bipartite-thick-matching.png

Emparelhamentos

figs/grafos-exercicios/cube-thick-matching.png

Um emparelhamento (= matching) num grafo não-dirigido é um conjunto de arestas sem pontas em comum.  Em outras palavras, um emparelhamento é um conjunto M de arestas com a seguinte propriedade: o leque de cada vértice tem no máximo uma aresta de M.

É fácil encontrar emparelhamentos pequenos: o conjunto vazio, por exemplo, é um emparelhamento. É mais difícil encontrar emparelhamentos grandes. É especialmente difícil encontrar um emparelhamento máximo.

Problema:  Encontrar um emparelhamento máximo em um grafo não-dirigido.

Um emparelhamento M é máximo se não existe outro maior, ou seja, se não existe um emparelhamento M' tal que |M'| > |M|.

Exemplo A.  Imagine um grande projeto a ser executado por muitos times de 2 pessoas. Suponha que um grafo não-dirigido representa as compatibilidades entre as pessoas dispostas a participar do projeto. Um emparelhamento é um conjunto de times que respeita as compatibilidades.

Exercícios 1

  1. Qual o maior emparelhamento que um grafo não-dirigido com V vértices pode ter?
  2. Máximo versus maximal.  Um emparelhamento M é maximal se não estiver contido em outro maior, ou seja, se não existe emparelhamento M' tal que M'M.  Dê um exemplo de emparelhamento maximal que não é máximo.
  3. Circuito.  Qual o tamanho de um emparelhamento máximo num grafo não-dirigido que consiste em circuito e nada mais?
  4. Árvore.  Qual o tamanho de um emparelhamento máximo numa árvore?
  5.   figs/grafos-exercicios/3-2-bipartite-thick.png
    Encontre um emparelhamento máximo no grafo da figura.
  6. Grade.  Qual o tamanho de um emparelhamento máximo numa grade não-dirigida m-por-n?
  7. o---o---o     
    |   |   |     
    o---o---o---o 
    |   |   |   | 
    o---o---o---o 
        |   |   | 
        o---o---o 
    
    Encontre um emparelhamento máximo no grafo da figura.
  8. Cubo.  Encontre um emparelhamento máximo no cubo de dimensão n.
  9. Cavalo.  Encontre um emparelhamento máximo no grafo do cavalo num tabuleiro de xadrez t-por-t.
  10. Suponha que H0 e H1 são as duas componentes conexas de um grafo não dirigido G. Seja Mi um emparelhamento máximo em Hi para i = 0, 1.  É verdade que M0M1 é um emparelhamento máximo em G?  Repita o exercício depois de trocar componentes conexas por componentes aresta-biconexas.

Caminhos alternantes

Dado um emparelhamento num grafo não-dirigido, como obter um emparelhamento maior?  A resposta passa pelo conceito de caminho alternante. Antes de introduzir esse conceito, precisamos de um pouco de terminologia.

Suponha dado um emparelhamento M.  Se uma aresta v-w pertence a M, dizemos que v e w estão emparelhados ou casados.  Um vértice v é solteiro se o leque de v não contém arestas de M.

Como estamos tratando de grafos não-dirigidos, será necessário restringir um pouco a definição de caminho.  Caminhos como  a-b-a-c-a-d  e  a-b-c-d-e-c-b-d,  por exemplo, serão excluídos.  Mais precisamente, usaremos apenas caminhos que têm a seguinte propriedade: se um arco v-w pertence ao caminho então o arco antiparalelo w-v não pertence ao caminho. (Vale lembrar que, por definição, caminhos não têm arcos repetidos.)  Uma aresta v-w pertence ao caminho se v-w ou w-v é arco do caminho.

Um caminho é alternante (= alternating) em relação a um emparelhamento M se for simples e se suas arestas estiverem alternadamente em M e fora de M.  Um caminho alternante é aumentador (= augmenting) se começa e termina num vértice solteiro e tem comprimento maior que 0.

Dado um emparelhamento M e um caminho aumentador P, considere o conjunto de arestas (MEP) ∪ (EPM), onde EP o conjunto de arestas de P.  Esse conjunto é conhecido como diferença simétrica, ou OU exclusivo, de MEP.  Esse conjunto será indicado por  MP  a pode ser descrito assim: tire de M todas arestas de P que estão em M e acrescente a M todas as arestas de P que não estão em M.  É fácil verificar que

  1. MP  é um emparelhamento
  2. MP é maior que M.

Segue daí a ideia de um algoritmo iterativo para o problema do emparelhamento máximo. [!] Cada iteração do algoritmo começa com um emparelhamento M. Cada iteração encontra um caminho aumentador P. A iteração seguinte começa com MP no papel de M.  Se não houver caminho aumentador, o processo para.  Quando isso acontece, o emparelhamento M é máximo!

figs/grafos-exercicios/cube-thick.png

Exemplo B.  Considere o grafo que tem arestas  0-1 1-2 2-3 3-0 4-5 5-6 6-7 7-4 0-4 1-5 2-6 3-7.  Comece com um emprelhamento vazio. Veja uma sequência de caminhos aumentadores que leva a um emparelhamento máximo:

0-1
2-6
4-0-1-5
3-0-4-7

Exercícios 2

  1. ★ Sejam M e N dois emparelhamentos.  Mostre que MN consiste em caminhos alternantes e ciclos alternantes. Supondo que N > M, mostre que MN contém um caminho que é aumentador em relação a M.
  2. ★ Como começa cada iteração do algoritmo do emparelhamento máximo sugerido acima?  (Cuidado! Não quero uma descrição das ações que ocorrem no início de uma iteração! Quero saber, isto sim, quais as informações disponíveis no início de uma iteração genérica, antes que a execução da iteração comece.)
  3. ★ Por que a definição de caminho aumentador exige que o caminho comece e termine num vértice solteiro?  Por que a definição exige que o caminho tenha comprimento maior que 0? Por que a definição exige que o caminho seja simples?
  4. Em cada um dos grafos abaixo, encontre um caminho aumentador para o emparelhamento vermelho. Encontre dois caminhos alternantes que não sejam aumentadores.
    figs/grafos-exercicios/3-2-bipartite-thick-matching-3.png     figs/grafos-exercicios/cube-thick-matching.png
  5. Slither.  Considere seguinte jogo em que dois jogadores, A e B, se alternam escolhendo vértices num grafo G. O jogador A escolhe o primeiro vértice, digamos v0.  A partir daí, no começo de cada jogada, os vértices já escolhidos formam um caminho simples v0v1v2–…–vk.  Se k é par, B escolhe um vértice vk+1 distinto dos demais mas adjacente a vk. Se k é ímpar, A faz um movimento análogo. O último jogador que puder fazer um movimento vence o jogo.  Prove que B tem uma estratégia vencedora se G tem um emparelhamento com V/2 arestas. Prove que A tem uma estratégia vencedora em caso contrário.

Representação de emparelhamentos

Antes de pensar em implementar algoritmos, é preciso inventar uma boa maneira de representar um emparelhamento.  Uma simples lista das arestas do emparelhamento não basta. Precisamos de uma representação que amarre cada vértice com a aresta do emparelhamento que incide naquele vértice.

Usaremos um vetor  match[]  de vértices indexado por vértices para representar um emparelhamento.  Se o emparelhamento casa v com w, teremos  match[v] ≡ w  e  match[w] ≡ v.  É como se match[] cuidasse de cada arco da aresta em separado.  Se v é solteiro, adotaremos a convenção match[v] ≡ -1.  É claro que vale a seguinte propriedade: se match[v] ≠ -1 então  match[match[v]] ≡ v.

figs/grafos-exercicios/3-2-bipartite-thick-matching.png

Exemplo C.  No grafo não-dirigido da figura, suponha que os vértices da primeira linha sejam  0 1 2 3 4  e os da segunda linha sejam  5 6 7 8 9, sempre da esquerda para a direita.  Então o emparelhamento vermelho é representado pelo vetor

      v   0 1 2 3 4 5 6 7 8 9
match[v]  - 5 6 9 8 1 2 - 4 3

com - indicando -1.

Exercícios 3

  1. Verificação.  Escreva uma função que receba um grafo não-dirigido G (bipartido ou não) e um vetor match[] de vértices indexado por vértices e verifique se o vetor representa um emparelhamento.  Em caso afirmativo, a função deve devolver o tamanho (número de arestas) do emparelhamento e imprimir a lista de arestas do emparelhamento.
  2. Emparelhamento maximal.  Escreva uma função que calcule um emparelhamento maximal num grafo arbitrário. O emparelhamento deve ser representado por um vetor match[]. A função deve devolver o tamanho do emparelhamento.

Cálculo da diferença simétrica

A representação de emparelhamentos que acabamos de adotar permite calcular MP de maneira muito rápida.  Dado um emparelhamento M, suponha que P é um caminho aumentador que começa num vértice s e termina num vértice t (é claro que s e t são colteiros).  Suponha que o caminho é representado por um vetor pa[]: para cada vértice w do caminho, pa[w] é o vértice anterior a w no caminho. Suponha também que pa[s] ≡ s.  Então, se M é representado por um vetor match[], a seguinte função calcula MP modificando match[] à medida que percorre P do fim para o começo.

static vertex pa[1000];
/* Esta função 
   executa a operação M⊕P sobre um emparelhamento M e 
   um caminho aumentador P. 
   O emparelhamento é representado pelo vetor match[0..V-1]
   e o caminho é representado por um vetor pa[0..V-1].
   O término de P é t.
   A origem de P não é dada explicitamente
   mas caracterizada pela propriedade
   pa[s] == s. */
static void newMatching( vertex *match, vertex t) 
{ 
   vertex x;
   do {
      x = pa[t];
      match[t] = x;
      match[x] = t;
      t = pa[x]; 
   } while (t != x);
}

Em cada iteração, a aresta t-x é acrescentada ao emparelhamento e a aresta x-pa[x] é retirada do emparelhamento:

      .   .   x           .   x   .
       \ / \ / \           \ / \ / \
        .   .   t           .   t   .

Algoritmo húngaro

Todos os conceitos, definições e observações feitos até aqui aplicam-se a qualquer grafo não-dirigido.  A partir deste ponto, entretanto, vamos nos limitar a grafos bipartidos, pois encontrar caminhos aumentadores em grafos não-dirigidos arbitrários é bem mais difícil.

A seguinte função implementa o algoritmo do emparelhamento máximo esboçado acima. O coração (ou será cérebro?) da implementação é a função auxiliar augmentMatching().

#define UGraph Graph
/* Esta função 
   calcula um emparelhamento máximo M no grafo não-dirigido 
   bipartido G.
   A bipartição de G é dada pelo vetor color[0..V-1],
   que tem valores 0 e 1.
   A função devolve o tamanho de M
   e armazena uma representação de M no vetor match[0..V-1]: 
   para cada vértice v, 
   match[v] é o vértice que M casa com v 
   (ou -1 se v é solteiro).
   (Essa função não está no livro de Sedgewick.) */
int UGRAPHbipMatching( UGraph G, int *color, vertex *match) 
{ 
   for (vertex v = 0; v < G->V; ++v) match[v] = -1;
   int size = 0;
   while (augmentMatching( G, color, match))
      size++;
   return size;
}

A missão da função booleana auxiliar augmentMatching() é encontrar um caminho aumentador para o emparelhamento representado por match[].  Para procurar por um caminho aumentador, a função faz uma busca em largura modificada. (Poderia também fazer uma busca em profundidade.)  A busca começa, simultaneamente, em todos os vértices solteiros de cor 0 e anda dois passos de cada vez:

A busca constrói uma floresta BFS alternante cujas raízes são os vértices solteiros de cor 0. A floresta é alternante porque todos os caminhos na floresta são alternantes. Se essa floresta atingir um vértice solteiro de cor 1 teremos encontrado um caminho aumentador.

static vertex pa[1000];
static bool visited[1000];
/* Esta função recebe um grafo não-dirigido bipartido G,
   com bipartição color[0..V-1],
   e um emparelhamento M representado
   por match[0..V-1]. 
   A função procura calcular um emparelhamento maior que M.
   Se tiver sucesso, devolve true 
   e modifica match[] de acordo.
   Se fracassar, devolve false sem alterar match[]. 
   (A função usa a estratégia de busca em largura.
   Essa função não está no livro de Sedgewick.) */
static bool augmentMatching( UGraph G, int *color, vertex *match) 
{ 
   for (vertex v = 0; v < G->V; ++v) visited[v] = false;
   QUEUEinit( G->V);
   for (vertex s = 0; s < G->V; ++s) {
      if (color[s] == 0 && match[s] == -1) {
         visited[s] = true; 
         pa[s] = s;
         QUEUEput( s); 
      }
   } 
   // a fila contém todos os vértices solteiros de cor 0

   while (!QUEUEempty( )) { 
      // color[v] == 0 para todo v na fila
      vertex v = QUEUEget( );
      for (link a = G->adj[v]; a != NULL; a = a->next) {
         vertex w = a->w; // color[w] == 1
         if (!visited[w]) { 
            visited[w] = true; 
            pa[w] = v; 
            if (match[w] == -1) { // caminho aumentador!
               newMatching( match, w);
               QUEUEfree( );
               return true;
            }
            vertex x = match[w]; // visited[x] == false
            visited[x] = true;
            pa[x] = w; // caminho ganhou segmento v-w-x
            QUEUEput( x); 
         }
      }
   }

   QUEUEfree( );
   return false;
}

Não é difícil verificar que a função augmentMatching() está correta.  É fácil deduzir daí que UGRAPHbipMatching() produz um emparelhamento.  Não é óbvio, entretanto, que o emparelhamento produzido pela função é máximo. Cuidaremos disso na próxima seção.

Desempenho.  O consumo de tempo da função augmentMatching() é essencialmente igual ao de uma busca BFS, ou seja, proporcional ao tamanho do grafo.  A função UGRAPHbipMatching() invoca augmentMatching() tantas vezes quanto é o tamanho de um emparelhamento máximo, portanto no máximo V/2 vezes.  Logo, o consumo de tempo de UGRAPHbipMatching() é proporcional a V (V + E) no pior caso.  (Como de hábito, V é o número de vértices e E é o número de arestas do grafo.)

Exercícios 4

  1. Para que serve o algoritmo húngaro?  Que problema resolve?
  2. ★ Como começa cada iteração do algoritmo do emparelhamento máximo?  (Cuidado! A pergunta não é sobre as ações que ocorrem no início de uma iteração! A pergunta é sobre as informações disponíveis no início de uma iteração genérica, antes que a execução da iteração comece.)
  3. Instâncias extremas.  A função UGRAPHbipMatching() produz resultado correto se G não tem arestas?  E se G tem apenas um vértice?  E se todos os vértices têm cor 0, ou seja, se color[v] ≡ 0 para todo v?  E se todos os vértices têm cor 1?
  4. Um caso extremo.  A função augmentMatching() produz resultado correto se G não tem vértices solteiros no emparelhamento match[]?  A função augmentMatching() produz resultado correto se o emparelhamento match[] é vazio?
  5. É verdade que imediatamente depois da linha x = match[w] de augmentMatching() tem-se color[x] == 0 e visited[x] == false?
  6. Rastreamento.  Acrescente código à função UGRAPHbipMatching() para que ela imprima, no fim de cada iteração, o caminho aumentador que acabou de ser calculado e o conjunto de arestas do novo emparelhamento.
  7. Busca em profundidade.  Reescreva a função augmentMatching() usando busca em profundidade. Será necessário escrever uma função recursiva dfsRaugment() como motor da busca em profundidade. Essa função absorve o código de newMatching() e assim dispensa o uso do vetor pa[].
  8. Grafos não bipartidos.  Que acontece se a função UGRAPHbipMatching() for aplicada a um grafo não bipartido? (Como não temos uma coloração dos vértices, a função augmentMatching() pode começar a busca em todos os vértices isolados ao mesmo tempo, mas será preciso reajustar de alguma maneira a definição do vetor visit[].) Dê uma resposta detalhada.
  9. Desafio.  Desenvolva um bom algoritmo para calcular um emparelhamento máximo num grafo não-dirigido arbitrário. A parte mais difícil é encontrar um caminho aumentador.

Coberturas (de arestas por vértices)

Passamos a tratar agora de um problema que parece não ter relação alguma com emparelhamentos, mas vai levar à prova de que o emparelhamento calculado pelo algoritmo dos caminhos aumentadores é máximo.

Uma cobertura (= vertex cover) de um grafo não-dirigido G é um conjunto de vértices que contém pelo menos uma ponta de cada aresta de G.  (Essa definição vale em qualquer grafo não-dirigido, seja ele bipartido ou não.)

Coberturas grandes são fáceis de encontrar: o conjunto de todos os vértices do grafo, por exemplo, é uma cobertura.  É bem mais difícil encontrar coberturas pequenas.  É especialmente difícil encontrar uma cobertura mínima. (Uma cobertura K é mínima se não existe outra cobertura K' tal que |K'| < |K|.)

figs/grafos-exercicios/3-2-bipartite-thick.png

Exemplo D.  No grafo não-dirigido da figura, suponha que os vértices da primeira linha sejam  0 1 2 3 4  e os da segunda linha sejam  5 6 7 8 9,  sempre da esquerda para a direita.  O conjunto de vértices  0 1 2 3 4   é uma cobertura.  O conjunto   3 4 5 6   também é uma cobertura.  Mostre que essa cobertura é mínima!

Exemplo E.  As obras de arte de uma galeria ficam expostos ao longo de vários corredores retos. Nos cruzamentos de corredores há pequenas praças. Quero colocar guardas nas praças, no máximo um guarda em cada praça, de modo que todos os corredores sejam vigiados. Qual o menor número de guardas necessário?

Cobertura mínima.  Há uma relação simples entre coberturas e emparelhamentos: para qualquer cobertura K e qualquer emparelhamento M em qualquer grafo não-dirigido, tem-se

|K| ≥ |M|.

Portanto, se |K| = |M| então a cobertura K é mínima e o emparelhamento M é máximo.

Temos aí um meio de provar que um dado emparelhamento M é máximo:  basta exibir uma cobertura com apenas |M| vértices!  (Compare a relação entre emparelhamentos e coberturas com a De acordo com relação entre distâncias e potenciais viáveis e com a relação entre coloração de vértices e cliques. Essas desigualdades são manifestações do fenômeno da dualidade em programação linear.)

Em geral uma cobertura mínima é maior que um emparelhamento máximo.  No caso de grafos não-dirigidos bipartidos, entretanto, vale a igualdade, como passamos a demonstrar.  Suponha que a última iteração do algoritmo dos caminhos aumentadores começa com um emparelhamento M.  Seja X o conjunto dos vértices visitados na tentativa frustrada de encontrar um caminho aumentador. Em outras palavras, X é o conjunto dos términos de todos os caminhos alternantes que começam em vértices solteiros de C0, sendo C0 o conjunto dos vértices s tais que color[s] ≡ 0.  Então a diferença simétrica (XC0) ∪ (C0X), que denotaremos por

X ⊕ C0,

é uma cobertura. E essa cobertura tem o mesmo tamanho que o emparelhamento M. (Esses dois fatos não são óbvios, mas são fáceis de provar.)

Podemos dizer, então, que o algoritmo dos caminhos aumentadores prova o seguinte teorema de Kőnig-Egerváry: num grafo bipartido, um emparelhamento M é máximo se e somente se existe uma cobertura K tal que |M| ≡ |K|.

É fácil estender o código de UGRAPHbipMatching() para que a função produza não só um emparelhamento como também uma cobertura do mesmo tamanho que o emparelhamento. O vetor característico cover[] da cobertura pode ser calculado assim no fim de UGRAPHbipMatching():

   for (vertex v = 0; v < G->V; ++v) 
      if (color[v] == 1 && visited[v] || 
          color[v] == 0 && !visited[v]) 
         cover[v] = true;
      else cover[v] = false;

A cobertura definida por cover[] é um certificado de maximalidade do emparelhamento e o emparelhamento é um certificado de minimalidade da cobertura.  Essa versão modificada de UGRAPHbipMatching() é, portanto, autocertificada

Exercícios 5

  1. Considere as seguintes afirmações: (1) em qualquer grafo, nenhum emparelhamento é maior que uma cobertura e (2) todo grafo bipartido tem um emparelhamento e uma cobertura do mesmo tamanho. Qual dessas duas afirmações é óbvia? Qual não é óbvia?
  2. Critique o seguinte afirmação: Aqui temos um emparelhamento com 100 arestas e uma cobertura com 100 vértices. De acordo com o teorema de Kőnig-Egerváry, o emparelhamento é máximo.
  3. Verifica cobertura.  Escreva uma função que verifique se um dado conjunto de vértices de um grafo não-dirigido é uma cobertura.
  4. Coberturas versus emparelhamentos.  Seja K uma cobertura e M um emparelhamento em um grafo não-dirigido.  Prove que  |K| ≥ |M|.
  5. É verdade que em qualquer grafo não-dirigido (bipartido ou não) existe uma cobertura K e um emparelhamento M tais que |K| ≡ |M|?
  6. Seja M um emparelhamento e K uma cobertura num grafo qualquer (não necessariamente bipartido). Suponha que |M| ≡ |K|.  Mostre que todo toda aresta de M tem exatamente uma ponta em K e que todo vértice de K incide em uma aresta de M.
  7. Mostre que, em qualquer grafo não-dirigido (bipartido ou não), o tamanho de um emparelhamento maximal é pelo menos 50% do tamanho de um emparelhamento máximo.
  8. Encontre uma cobertura mínima em cada um dos grafos não-dirigidos da figura. Prove que a cobertura é de fato mínima.
    O---O---O                O        
    |   |   |              / | \      
    O---O---O---O         O  |  O     
    |   |   |   |            O   \    
    O---O---O---O           / \   O   
        |   |   |          O   O   \  
        O---O---O             / \   O 
                             O   O    
    
  9. Exiba uma cobertura mínima da grade não-dirigida 3-por-5. Prove que sua cobertura é mínima.
  10. Calcule um emparelhamento máximo no grafo não-dirigido da figura.  Prove que o seu emparelhamento é máximo.
  11. Prova.  Seja M um emparelhamento máximo num grafo não-dirigido bipartido G. Seja C0 o conjunto de vértices de cor 0 e C1 o conjunto C1 de vértices de cor 1 de G.  Aplique a função augmentMatching() aos argumentos G, C0, C1M. Seja X o conjunto dos vértices visitados na tentativa frustrada de encontrar um caminho aumentador. Mostre que (XC0) ∪ (C0X) é uma cobertura. Depois, mostre que essa cobertura tem |M| vértices.
  12. Suponha que o algoritmo húngaro é aplicado a um grafo bipartido conexo com muitos vértices e muitas arestas. No fim da última iteração do algoritmo, é possível que o conjunto X seja vazio?  É possível que X seja o conjunto de todos os vértices?  Discuta essas duas possibilidades. Em cada caso, descreva o estado do algoritmo no início da última iteração.
  13. Emparelhamento máximo e cobertura mínima.  Escreva a versão estendida da função UGRAPHbipMatching() sugerida no fim da acima. A função deve devolver não só um emparelhamento máximo, representado pelo vetor match[], como também uma cobertura mínima, representada por um vetor cover[].
  14. Desafio.  Invente um bom algoritmo para calcular uma cobertura mínima em qualquer grafo não-dirigido (bipartido ou não).

Conclusão

Este capítulo traz mais uma caracterização demonstrada por um algoritmo. As três caracterizações desse tipo vistas até aqui são:

A primeira caracterização foi demonstrada pela função GRAPHcycle0().  A segunda é demonstrada pela função UGRAPHtwoColor(). A terceira é demonstrada pela função UGRAPHbipMatching() somada ao fragmento de código que calcula uma cobertura mínima.


Perguntas e respostas