Fluxos Máximos

Motivação

A EDP pretende construir uma nova central de distribuição de energia para a área Metropolitana de Lisboa na Serra de Montachique, visto que partindo de lá predispõe de uma rede de energia elétrica já construída. Temos, claro, que cada ligação tem uma capacidade limite de energia que pode transportar. Assim sendo, o engenheiro Paulo ficou encarregue de descobrir o fluxo máximo de energia que pode ser transportado num dado momento pela rede, distribuído pelas várias ligações da mesma.

O problema do fluxo máximo é utilizado para modelar soluções para vários problemas deste género, como redes de saneamento e água, transporte de partes por linhas de montagem, entre outras. É um problema bastante presente na indústria!

O problema do fluxo máximo consiste, então, em encontrar o melhor caminho - o caminho onde conseguimos levar mais material - de uma fonte a um destino/sumidouro sem violar a capacidade de nenhum arco da rede.

Redes de Fluxos

Uma rede de fluxos corresponde a um grafo G=(V,E)G = (V, E) dirigido, onde cada arco do mesmo tem uma capacidade associada não negativa. O grafo é necessariamente ligado. Consideramos sempre, claro, que há um caminho possível entre ss e tt que passa por qualquer outro vértice do grafo (caso contrário haveria vértices que não faria sentido aparecerem no grafo, já que não seriam úteis à rede).

Uma propriedade bastante importante destas redes é a conservação do fluxo:

vVf(u,v)=0,uV{s,t}\sum_{v \in V} f(u, v) = 0, \quad\forall_{u \in V - \{s, t\}}

Isto é, para todos os vértices da rede (exceto a fonte e o destino), a quantidade de material que sai tem de ser a mesma que entra. Observe-se o exemplo abaixo:

Podemos notar que para todo o vértice do grafo (exceto a fonte e o sumidouro) a quantidade de material que sai é igual à quantidade de material que entra. Por exemplo, no vértice CC temos 12+7=1912 + 7 = 19 material a entrar (via BB e EE) e 15+4=1915 + 4 = 19 a sair (via FF e DD).

Vale a pena realçar que o fluxo que sai da fonte é necessariamente igual ao que entra no sumidouro. Também na imagem acima podemos verificar que tal se verifica: 11+8=15+4=1911 + 8 = 15 + 4 = 19, verificando-se, portanto, que flow in equals flow out. Diz-se que este valor é, então, o valor do fluxo da rede.

Por fim, podemos olhar para redes de fluxo diferentes - redes que apresentam mais do que uma fonte e/ou sumidouro. Para estes casos específicos, definem-se:

  • uma super-fonte, que liga (com capacidade \infty) todas as fontes do grafo original;

  • um super-sumidouro, ao qual ligam (com capacidade \infty) todos os sumidouros do grafo original.

Tanto a super-fonte como o super-sumidouro limitam-se a fornecer tanto fluxo quanto pretendido pelas fontes e a receber tanto quanto enviado pelos sumidouros originais, pelo que todas as propriedades referidas acima acabam por manter-se. Abaixo podemos observar um exemplo desta transformação:

Método de Ford-Fulkerson

O método de Ford-Fulkerson transcende uma implementação de algoritmo específica - corresponde a uma combinação de ideias e implementações de outros algoritmos: redes residuais, caminhos de aumento e cortes. Será, contudo, abordada uma implementação para um algoritmo genérico de Ford-Fulkerson mais abaixo.

O método baseia-se na seguinte lógica:

FordFulkersonMethod(G, s, t)
  inicializar o fluxo f a 0
  enquanto existir um caminho de aumento p
    aumentar o fluxo f segundo p
  return f

Vamos, então, introduzir as três ideias fulcrais ao método.

Redes Residuais

Uma rede residual, GfG_f, de uma rede de fluxo GG com fluxo ff indica-nos como podemos mudar o fluxo nas arestas de GG. Temos que cada arco pode ter uma capacidade residual associada, cf(u,v)c_f(u, v), igual à diferença entre a capacidade do arco e o fluxo que o atravessa atualmente. GfG_f indicará, portanto, a capacidade residual de cada arco de GG tal que cf(u,v)=c(u,v)f(u,v)>0c_f(u, v) = c(u, v) - f(u, v) > 0 (os arcos já com capacidade máxima não nos interessam aqui). Formalizando, temos:

cf(u,v)={c(u,v)f(u,v)se (u,v)Ef(v,u)se (v,u)Ec_f(u, v) = \begin{cases} c(u, v) - f(u, v) & \text{se } (u, v) \in E\\ f(v, u) & \text{se } (v, u) \in E \end{cases}

O segundo ponto é particularmente relevante, e podemos observá-lo na imagem abaixo: para uma aresta no "sentido contrário", consideramos que a sua capacidade residual corresponde ao próprio fluxo que o arco transporta. A definição deverá ficar mais clara com a imagem-exemplo da secção abaixo.

Caminhos de Aumento

Dado um grafo com respetivas redes de fluxo ff e residual GfG_f, temos que um caminho de aumento pp corresponde a um caminho simples de ss para tt em GfG_f. Podemos, em pp, aumentar o fluxo em todo o arco de pp com uma quantidade até cfc_f, a capacidade residual de pp, tal que:

cf(p)=min{cf(u,v):(u,v)p} c_f(p) = \min\{c_f(u, v): (u, v) \in p\}

Significa, então, que podemos aumentar o fluxo em todo o arco de pp por uma quantidade igual à menor capacidade residual dos arcos que o compõem. Abaixo podemos ver um exemplo:

O caminho destacado à direita corresponde a um caminho de aumento pp para a rede em à esquerda. Temos que cf(p)=min{5,4,5}=4c_f(p) = \min\{5, 4, 5\} = 4, pelo que podemos aumentar o fluxo em todo o caminho de aumento pp por 44.

De realçar que podemos aqui observar a rede residual, e as noções de caminhos residuais devem então ficar mais claras: existem arcos nos dois sentidos do arco de GG, respeitando a definição proposta acima para cf(u,v)c_f(u, v). Os arcos que já transportam capacidade máxima de fluxo apenas apresentam arco numa direção.

Podemos, claro, visto que estamos a trabalhar com capacidades e fluxos não negativos, afirmar que:

f=f+fpf=f+fp>f f' = f + f_p \wedge |f'| = |f| + |f_p| > |f|

Ou seja, que o fluxo da rede após a aplicação do aumento à rede, ff', é estritamente maior que o fluxo da rede original, ff. Consideramos para este efeito que fp=cf(p)>0f_p = c_f(p) > 0.

Cortes em Redes de Fluxo

Os cortes em redes de fluxo correspondem a uma partição (S,T)(S, T) de VV, com T=VST = V-S, tal que sStTs \in S \wedge t \in T. Seja ff um fluxo, o fluxo líquido de um corte corresponde a:

f(S,T)=uSvTf(u,v)uSvTf(v,u) f(S, T) = \sum_{u \in S} \sum_{v \in T} f(u, v) - \sum_{u \in S} \sum_{v \in T} f(v, u)

A imagem abaixo poderá tornar o conceito mais claro:

Na rede da imagem, consideramos S={A,B,D}T={C,E,F}S = \{A, B, D\} \wedge T = \{C, E, F\}. Os arcos que cruzam o corte são (B,C),(C,D)(B, C), (C, D) e (D,E)(D, E). Como a definição acima indica, o sentido dos arcos é relevante para a determinação do fluxo líquido do corte: voltando a olhar para a imagem-exemplo dos caminhos de aumento, temos que:

f(B,C)=12,f(D,E)=11,f(C,D)=4    f(S,T)=12+114=19f(B, C) = 12, f(D, E) = 11, f(C, D) = 4 \implies f(S, T) = 12 + 11 - 4 = 19

Assim sendo, o fluxo líquido do corte associado à rede acima é 1919. Realça-se ainda que f(S,T)f(S, T) foi aumentado segundo os arcos que cruzavam o corte de SS para TT e diminuído segundo os que o cruzavam no sentido contrário, tal como a definição indica.

Por fim, a capacidade de um corte corresponde à soma das capacidades residuais dos arcos que cruzam o corte de SS para TT, isto é:

c(S,T)=uSvTc(u,v)c(S, T) = \sum_{u \in S} \sum_{v \in T} c(u, v)

O corte mínimo de uma rede será, então, o corte com a menor capacidade de entre todos os cortes da mesma.

Pegando ainda na imagem anterior, teríamos que a capacidade do corte referido seria igual a 12+14=2612 + 14 = 26.

Temos ainda algumas afirmações a fazer quanto aos cortes:

  • Dado um fluxo ff para uma rede GG, o fluxo líquido de qualquer corte da rede é o mesmo, sendo igual a f|f|.

  • O valor de qualquer fluxo ff numa rede GG é majorado pela capacidade de qualquer corte da rede.

Prova do primeiro ponto

Temos que V=TSV = T \cup S, e então f(S,V)=f(S,TS)=f(S,T)+f(S,S)=f(S,T)+0=f(S,T)f(S, V) = f(S, T \cup S) = f(S, T) + f(S, S) = f(S, T) + 0 = f(S, T), já que f(S,S)=0f(S, S) = 0.

Tendo f(S,V)=f(S,T)f(S, V) = f(S, T), e com f=f(S,T)=f(S,V)|f| = f(S, T) = f(S, V), podemos começar a construir a prova pretendida:

f=f(S,V)=f(s,V)+f(S{s},V)=vVf(s,v)vVf(v,s)+uS{s}(vVf(u,v)vVf(v,u))=vV(f(s,v)+uS{s}f(u,v))vV(f(v,s)+uS{s}f(v,u))=vVuSf(u,v)vVuSf(v,u)\begin{aligned}|f| &= f(S, V) = f(s, V) + f(S - \{s\}, V)\\ \\ &= \sum_{v \in V}f(s, v) - \sum_{v \in V}f(v, s) + \sum_{u \in S - \{s\}}(\sum_{v \in V}f(u, v) - \sum_{v \in V}f(v, u)) \\ \\ &= \sum_{v \in V}(f(s, v) + \sum_{u \in S - \{s\}}f(u, v)) - \sum_{v \in V}(f(v, s) + \sum_{u \in S - \{s\}}f(v, u)) \\ \\ &= \sum_{v \in V}\sum_{u \in S}f(u, v) - \sum_{v \in V}\sum_{u \in S}f(v, u) \end{aligned}

Ora, visto que V=STST=V = S \cup T \wedge S \cap T = \emptyset, podemos simplificar a soma acima mais ainda (em função de SS e TT em vez de SS e VV):

f=vSuSf(u,v)+vTuSf(u,v)vSusf(v,u)vTuSf(v,u)=vTuSf(u,v)vTuSf(v,u)+(vSuSf(u,v)vSuSf(v,u))\begin{aligned}|f| &= \sum_{v \in S} \sum_{u \in S}f(u, v) + \sum_{v \in T} \sum_{u \in S}f(u, v) - \sum_{v \in S} \sum_{u \in s}f(v, u) - \sum_{v \in T} \sum_{u \in S}f(v, u) \\ \\ &= \sum_{v \in T} \sum_{u \in S}f(u,v) - \sum_{v \in T} \sum_{u \in S}f(v, u) + (\sum_{v \in S}\sum_{u \in S}f(u, v) - \sum_{v \in S}\sum_{u \in S}f(v, u)) \end{aligned}

Esta última diferença entre parêntesis anula-se, pela conservação do fluxo, pelo que ficamos com:

f=vTuSf(u,v)vTuSf(v,u)=f(S,T)\begin{aligned}|f| &= \sum_{v \in T} \sum_{u \in S}f(u,v) - \sum_{v \in T} \sum_{u \in S}f(v, u) \\ \\ &= f(S, T) \end{aligned}

Seja (S,T)(S, T) um corte de GG e ff um qualquer fluxo da rede. Temos, pela afirmação provada imediatamente acima, que f=f(S,T)|f| = f(S, T). A partir daí, podemos facilmente deduzir:

f=f(S,T)=uSvTf(u,v)uSvTf(v,u)uSvTf(u,v)uSvTc(u,v)=c(S,T)\begin{aligned}|f|&=f(S, T)\\ &= \sum_{u \in S} \sum_{v \in T} f(u, v) - \sum_{u \in S} \sum_{v \in T} f(v, u)\\ &\leq \sum_{u \in S} \sum_{v \in T} f(u, v)\\ &\leq \sum_{u \in S} \sum_{v \in T} c(u, v)\\ &= c(S, T) \end{aligned}

Assim sendo, podemos extrair que o valor do fluxo mínimo é majorado pelo valor de qualquer corte da rede. Mais ainda, o valor do fluxo máximo de uma rede é majorado pela capacidade do corte mínimo da mesma, afirmação particularmente útil para o Teorema apresentado abaixo:

Teorema do Fluxo Máximo - Corte Mínimo

Seja ff o fluxo de uma rede GG com fonte ss e sumidouro tt. Para uma rede assim apresentada, as seguintes proposições são equivalentes:

  • ff é o fluxo máximo da rede.

  • A rede residual GfG_f não contém caminhos de aumento.

  • f=c(S,T)|f| = c(S, T) para algum corte (S,T)(S, T) na rede.

A equivalência das três afirmações pode ser provada através de uma implicação cíclica entre elas. Procuremos prová-las:

Prova do teorema

(1)    (2)(1) \implies (2) indica que se ff for o fluxo máximo da rede, então GfG_f não contém caminhos de aumento. Suponhamos o oposto: que GfG_f contém caminhos de aumento, mesmo considerando ff como um fluxo máximo da rede. Nesse caso, haveria pelo menos um caminho pp tal que f+fp>f|f| + |f_p| > |f|, uma contradição, já que ff já é máximo.

(2)    (3)(2) \implies (3) indica que se GfG_f for uma rede residual sem caminhos de aumento, então f=c(S,T)|f| = c(S, T) para algum corte (S,T)(S, T) na rede. Tenhamos S={vGf tal que haˊ um caminho de s para v}S = \{v | G_f \text{ tal que há um caminho de s para v} \}, T=VST = V - S. Por contradição, suponhamos que fc(S,T)|f| \neq c(S, T) para todo o corte da rede. Isso implicaria, claro, que f(S,T)<c(S,T)f(S, T) < c(S, T) para todo o corte, já que é impossível ter o contrário. Agora das duas uma:

  • Existe um arco (u,v)(u, v) tal que cf(u,v)>0c_f(u, v) > 0; caso tal acontecesse, esse arco estaria na rede residual, pelo que não pode ser verdade.

  • Existe um arco de refluxo; contudo, se tal acontecesse, seria possível chegar a uu a partir de tt, o que também não pode acontecer.

A proposição fica então provada.

Por fim, (3)    (1)(3) \implies (1). Temos, claro, que fc(S,T)|f| \leq c(S, T) para todo o corte da rede. Tendo f=c(S,T)|f| = c(S, T), temos obrigatoriamente que f|f| terá de ser o fluxo máximo da rede, visto que caso não fosse, f>c(S,T)|f| > c(S, T) para um qualquer corte na rede, o que não pode acontecer.

A implicação cíclica fica assim provada, pelo que as afirmações são necessariamente equivalentes.

Implementação Genérica de Ford-Fulkerson

Depois de explicada a teoria por detrás do método de Ford-Fulkerson, chegou então a altura de mostrar a sua implementação:

FordFulkerson(G, s, t)
  for each edge e in G.E
    e.f = 0 // fluxo pelo arco = 0 inicialmente
  while (existe um caminho p de s para t na rede residual)
    c_f(p) = min{c_f(u, v) | (u, v) in p} // capacidade residual de p
    for each edge e in p
      if e in E_f
        e.f += c_f(p) // incrementa fluxo pelo arco
      else
        e.f -= c_f(p) // decrementa fluxo pelo arco

Começamos por inicializar o fluxo de todos os arcos da rede a 00. De seguida, procuramos sucessivamente encontrar caminhos de aumento na rede residual (até estes deixarem de existir), e atualizamos os arcos que formam cada caminho de acordo com o fluxo a aumentar: se o arco pertence a EE, o fluxo é aumentado; caso não corresponda a um arco da rede original, o fluxo é subtraído pela quantidade indicada.

Encontra-se abaixo um exemplo da aplicação do algoritmo de Ford-Fulkerson:

Exemplo da aplicação do Algoritmo

Consideremos uma rede GG, inicialmente vazia, com capacidades tais que:

O primeiro caminho de aumento encontrado é o que se segue. Podemos notar que neste primeiro instante não se veem arestas com sentido contrário, já que todas têm fluxo 00 atualmente:

Foi encontrado mais um caminho de aumento, e agora já podemos notar as tais "arestas contrárias" a aparecer:

Os passos seguintes seguem todos a mesma lógica, até que não existam mais caminhos de aumento.

Chegámos, por fim, a uma rede sem caminhos de aumento restantes. O valor do corte mínimo corresponde, então, ao valor do fluxo máximo da rede: 2323.

Podemos dizer, aqui, que um corte mínimo seria tal que S={s,v1,v2,v4}T={v3,t}S = \{s, v_1, v_2, v_4\} \wedge T = \{v_3, t\}. Os arcos que cruzam o corte têm necessariamente de estar saturados (ou seja, o fluxo deles é igual à capacidade máxima do arco). Mais ainda, a soma do fluxo que atravessa o corte é igual ao valor do fluxo. Abaixo podemos observar o corte mínimo na rede:

Em relação à análise da complexidade temporal do algoritmo, podemos afirmar que:

  • o loop inicial leva Θ(E)\Theta(E) tempo;

  • calcular a capacidade residual mínima do caminho pode levar O(E)O(E) tempo;

  • atualizar o fluxo do arco ee em pp leva O(1)O(1) tempo; é realizado no máximo EE vezes por ciclo.

Resta, então, falar sobre a complexidade do ciclo while em si: encontrar um caminho leva O(E)O(E) tempo, recorrendo a uma DFS/BFS adequada. O ciclo em si, contudo, pode ser executado até f|f^*| vezes (onde ff^{*} é o fluxo máximo da rede), tornando a complexidade temporal do algoritmo O(fE)O(|f^{*}|E). A razão para ter de ser executado até f|f^{*}| vezes, da maneira que está construído atualmente, pode ficar mais aparente ao olhar para o exemplo seguinte:

Considerando uma rede como a que está acima, temos que f|f^{*}| = 20000002000000. Na pior das hipóteses, teremos de realizar igual quantidade de caminhos que passem pelo arco (u,v)(u, v), o que pode tornar a aplicação do algoritmo impraticável. Assim sendo, vamos estudar o algoritmo de Edmonds-Karp, que permite uma majoração da complexidade de O(VE2)O(VE^2), bastante melhor na vasta maioria dos casos.

Algoritmo de Edmonds-Karp

O algoritmo de Edmonds-Karp tem por base o método de Ford-Fulkerson, procurando uma majoração diferente para a complexidade temporal do mesmo.

O objetivo aqui passa por encontrar sempre o caminho de aumento mais curto, isto é, com menos arcos, não menos pesado, presente na rede residual GfG_f. Para tal, o algoritmo recorre a uma BFS com origem em ss e destino em tt - a procura para assim que encontrar um caminho de aumento que os una. O algoritmo termina quando não existem mais caminhos de aumento - isto é, quando a BFS não consegue encontrar mais caminhos de aumento que unam ss e tt.

Monotonia de Edmonds-Karp

É interessante definir distância de Edmonds-Karp: δf(s,t)\delta_f(s, t), a distância do caminho mais curto entre ss e tt em GfG_f.

Mais ainda, é relevante notar que, visto que a BFS vai sempre encontrando os caminhos de aumentado atualmente mais curtos (e que estes vão desaparecendo), podemos afirmar com segurança que, durante o decorrer de Edmonds-Karp, δf(s,t)\delta_f(s, t) terá uma tendência crescente (não estritamente, claro).

Abaixo segue um exemplo bastante simples do decorrer do algoritmo sobre uma rede:

Exemplo da aplicação do Algoritmo

Comecemos com uma rede que se encontra inicialmente da seguinte forma:

Podemos notar que inicialmente o fluxo que passa por todas as arestas é 0, e que portanto a respetiva capacidade residual é igual à capacidade máxima do arco.

Nas BFSs abaixo, os números acima de cada vértice correspondem à distância percorrida desde ss até ao vértice em questão. Podemos, nesta primeira procura, notar que há 2 caminhos de aumento de tamanho 33. Escolhemos um deles (por exemplo scdts \to c \to d \to t), e, ao escolhê-lo, verificamos que a capacidade residual mínima de entre as arestas do caminho é 44 - o fluxo em todos os arcos é incrementado em 44 unidades.

De seguida, vamos ao outro caminho de aumento com 3 unidades (descoberto anteriormente), sabts \to a \to b \to t. Aqui, a menor capacidade residual dos seus arcos é 1212, e o fluxo em todos os arcos é incrementado em 1212.

Explorados todos os caminhos de aumento de tamanho 33, a BFS seguinte encontra um caminho de tamanho 44. A menor capacidade residual de um dos seus arcos é 77, e o fluxo em todos os arcos é incrementado em 77.

A partir daqui não existe uma procura que encontre um caminho de aumento para a rede, pelo que o algoritmo termina.

Temos que o fluxo máximo encontrado é igual à soma dos fluxos que saem da fonte, que é igual à soma dos fluxos que chegam ao sumidouro e à soma dos fluxos que atravessam o corte mínimo da rede, 2323:

Podemos notar que todos os arcos que atravessam o corte no sentido "positivo", da partição de ss para a de tt estão saturados. Mais ainda, e apesar de não ser aqui percetível, é relevante verificar que o fluxo dos arcos que cruzam o corte no sentido "negativo" contribui negativamente para o fluxo máximo da rede: se tivéssemos um arco que cruzasse aqui o corte no sentido contrário com fluxo 44, o fluxo máximo da rede seria 234=1923 - 4 = 19.

Por fim, é interessante verificar que o fluxo máximo pode também ser dado pela quantidade de fluxo aumentada a cada BFS realizada: é óbvio, claro, a cada BFS o fluxo é aumentado, nunca decrescido. Aqui, verifica-se com 4+12+7=234 + 12 + 7 = 23.

Podemos notar que as distâncias de Edmonds-Karp podem variar entre 1,...,V11, ..., |V| - 1: um caminho de aumento pode ter no mínimo 1 aresta (se o grafo ligar diretamente ss a tt), e no máximo V1|V| - 1 arcos (tal como o exemplo de seguida demonstra):

Ora, temos ainda que podemos ter E|E| caminhos de aumento até δf(s,t)\delta_f(s, t) aumenta (podemos ter um caminho que contenha todas as arestas, onde uma aresta vai ficando saturada de cada vez), e assim podem existir O(VE)O(|V|\cdot|E|) caminhos de aumento numa rede.

Visto que cada caminho de aumento pode ser encontrado em O(V+E)=O(E)O(|V| + |E|) = O(|E|) tempo (via BFS), temos que a complexidade temporal de Edmonds-Karp é O(VE2)O(|V|\cdot|E|^2).

A complexidade pode enganar

Não nos podemos esquecer que o algoritmo de Edmonds-Karp não é mais que uma implementação do método de Ford-Fulkerson, partilhando assim a sua majoração temporal (O(fE)O(|f^*| |E|)). Assim sendo, dependendo da topologia do grafo, a realização do algoritmo pode até levar tempo inferior a VE2V E^2 a terminar!

Este tipo de perguntas pode sair em exame, e é bastante útil ter em mente: o algoritmo tem ambas as majorações temporais, consideramos a que for menor perante a topologia da rede apresentada. O algoritmo apresentado abaixo (para encontrar emparelhamentos bipartidos máximos) é um dos casos onde isto acontece.

Emparelhamento Bipartido Máximo

Motivação

Imaginemos que temos um conjunto de mm máquinas e outro de nn tarefas. Cada máquina pode ser atribuída a apenas uma tarefa, mas nem todas as máquinas podem realizar todas as tarefas. Na melhor das hipóteses, qual será o maior número de tarefas que o conjunto de máquinas pode realizar simultaneamente? Mais ainda, nesse mesmo melhor caso, que máquina realiza que tarefa?

Podemos formalizar o problema acima através de um conjunto JJ, um conjunto que mapeia todas as máquinas a todas as tarefas que podem executar, tal que:

J={(mi,tj),mitj}.J = \{(m_i, t_j), m_i \longmapsto t_j\}.

O nosso objetivo passará então por procurar o melhor emparelhamento possível tal que há um valor ótimo de "arestas ativas" - máquinas a realizar tarefas, portanto. JJ pode ser representado por um grafo, claro:

Como podemos notar, o grafo que corresponde a JJ diz-se um grafo bipartido - um grafo onde VV pode ser separado em V1V_1 e V2V_2, em que não existem arestas entre os vértices de V1V_1 nem entre os de V2V_2, apenas de V1V_1 para V2V_2 (e possivelmente vice-versa). O problema do emparelhamento bipartido máximo passará, então, por encontrar o emparelhamento com cardinalidade máxima num grafo bipartido: em relação a este exemplo, encontrar o número máximo de tarefas que podem ser executadas simultaneamente, considerando que cada máquina pode estar ligada a apenas uma tarefa ao mesmo tempo.

Em relação ao grafo acima, podíamos dizer que um possível emparelhamento bipartido seria (m2,t1)(m_2, t_1), (m4,t2)(m_4, t_2) e (m3,t3)(m_3, t_3). Este emparelhamento não é máximo - existem outros emparelhamentos com cardinalidade superior a este. Um emparelhamento bipartido máximo correspondente a este conjunto seria, por exemplo, (m1,t1)(m_1, t_1), (m2,t2)(m_2, t_2) e (m3,t4)(m_3, t_4) e (m4,t3)(m_4, t_3). Aqui, verificamos que a cardinalidade do emparelhamento é 44, não podendo sequer aumentá-la mais. Pode ser importante realçar ainda que um emparelhamento bipartido ser máximo não significa que este cobre necessariamente todas as "máquinas" do mesmo - significa apenas que não é possível encontrar um outro emparelhamento bipartido com cardinalidade superior.

Como encontrar o emparelhamento bipartido máximo?

Será interessante ter um algoritmo que nos permita encontrar o emparelhamento bipartido máximo de um grafo. A lógica utilizada para o mesmo é bastante simples: adicionamos uma fonte e um sumidouro ao grafo, com a fonte ligada à partição das "máquinas" e o sumidouro ligado à partição das "tarefas". Atribuímos então fluxo unitário a cada arco, de modo a que uma máquina possa apenas realizar uma tarefa ao mesmo tempo. Pegando no grafo acima, podemos verificar como ficaria depois destas alterações:

O fluxo máximo nunca poderá, claro, exceder min{fs,ft}\min{\{f_s, f_t\}}, ou seja nunca poderá haver uma máquina a realizar múltiplas tarefas, nem poderá haver tarefas a ser realizadas por mais que uma máquina. Podemos ainda notar que, formalmente, f(u,v)=1f(u, v) = 1 apenas se a máquina uu estiver a realizar a tarefa jj.

Este algoritmo corresponde a uma situação em que a complexidade temporal do método de Ford-Fulkerson prevalece sobre a de Edmonds-Karp. Temos necessariamente que:

fmV, onde m corresponde ao nuˊmero de maˊquinas|f^*| \leq m \leq |V|, \quad \text{ onde m corresponde ao número de máquinas}

já que no pior caso todas as máquinas podem realizar uma tarefa. Nesse caso, podemos afirmar que por Ford-Fulkerson a complexidade temporal do algoritmo é dada por O(fE)=O(VE)O(f^* E) = O(VE), que é melhor que a de Edmonds-Karp, dada por O(VE2)O(VE^2).

Algoritmos baseados em Pré-Fluxo

Até aqui observámos algoritmos para o fluxo máximo baseados em caminhos de aumento. Estes algoritmos têm, contudo, a particularidade menos agradável de possuírem operações pouco localizadas - as procuras por caminhos de aumento iniciam-se sempre em ss, mesmo que percorrer a rede desde aí se possa tornar pouco eficiente. Abaixo encontra-se um exemplo de uma situação onde os algoritmos baseados em caminhos de aumento se podem tornar mais morosos do que podiam/deviam ser idealmente:

Ao olhar para este exemplo, podemos até pensar no porquê de não levarmos só o fluxo kk todo até vkv_k e depois verificar os caminhos a partir daí, escusando de percorrer sempre a rede toda - pensamos nós e pensou Karzanov em 1974, uns bons anos depois do aparecimento do método de Ford-Fulkerson. O matemático russo expôs um dos algoritmos que vamos abordar mais à frente, Push-Relabel, para fazer frente a esta mesma situação. Tarjan e Goldberg, sensivelmente uma década depois, apresentaram o algoritmo Relabel-To-Front baseado neste último.

Pré-Fluxos

Os algoritmos baseados em pré-fluxos são, como referido acima, mais localizados que os baseados em caminhos de aumento. Trabalham sobre um vértice de cada vez, em vez de procurar encontrar caminhos por toda a rede, observando os vizinhos de cada vértice.

Durante o decorrer do algoritmo, a conservação do fluxo não se verifica.

Cada vértice contém um pré-fluxo, fluxo que nele incide, sendo que o objetivo será "expulsá-lo" completamente de si para os vizinhos (tendo como objetivo final que todo o fluxo que sai de ss atinja tt). O primeiro passo é sempre saturar todos os arcos que ligam ss aos seus vizinhos, expulsando o máximo de fluxo possível da fonte, sendo que futuramente poderão (e normalmente irão) ocorrer refluxos.

Antes de definir pré-fluxo, é importar referir uma das suas propriedades: a restrição de capacidade. ff só pode ser considerado um pré-fluxo caso:

(u,v)E,0f(u,v)c(u,v)\forall_{(u, v) \in E}, 0 \leq f(u, v) \leq c(u, v)

Posto isto, podemos ainda afirmar que o fluxo que entra num vértice uu deve ser maior (sim, maior) ou igual ao que dele sai para este poder ser considerado um pré-fluxo, sendo a respetiva quantidade de fluxo a mais considerada o seu excesso, ee:

e(u)=vVf(v,u)vVf(u,v).e(u) = \sum_{v \in V} f(v, u) - \sum_{v \in V} f(u, v).

A primeira coisa a fazer é, como referido acima, saturar todos os arcos que ligam ss aos seus vizinhos, expulsando o máximo de fluxo possível da fonte. Este passo é partilhado pelos dois algoritmos estudados abaixo, e é um dos pontos fulcrais dos mesmos.

De seguida, é importante ter em conta que cada vértice tem uma altura associada - podemos pensar nas várias alturas como as várias secções de uma encosta - o fluxo cairá pela encosta até ao sumidouro. O fluxo, claro, só poderá cair de um vértice uu para outro vv caso esteja "mais alto" na colina - para tal, é necessário ir atualizando as alturas dos vértices da rede com o decorrer do algoritmo.

Exemplo de alturas

Poderemos, no decorrer do algoritmo, encontrar situações em que queremos expulsar fluxo de um vértice, mas todos os arcos que dele saem estão ou saturados ou com as respetivas adjacências à mesma altura/acima dele. Quando isso acontece, efetuamos a operação relabel, explorada mais à frente, que atualiza a sua altura (permitindo então que o fluxo caia).

No final do algoritmo, nenhum dos vértices (exceto ss e tt) tem excesso de fluxo no seu reservatório.

Função de Alturas

Seja G=(V,E)G=(V, E) uma rede de fluxo com fonte ss e sumidouro tt. Mais ainda, seja ff um pré-fluxo da rede. Uma função h:VNh: V \to \N é uma função tal que:

  • h(s)=Vh(s) = |V|;

  • h(t)=0h(t) = 0;

  • h(u)h(v)+1h(u) \leq h(v) + 1

para todo o arco residual (u,v)Ef(u, v) \in E_f. Caso h(u)>h(v)+1h(u) > h(v) + 1, então o arco (u,v)(u, v) não pertence à rede residual.

Operações Básicas

O conjunto dos algoritmos baseados em pré-fluxo é composto por algumas operações básicas (que todos partilham) - push e relabel.

Push ocorre quando um dado vértice uu possui excesso. Recebe como argumento vv, para além de uu, onde vv corresponde ao vértice-destino de um arco que sai de pp. Caso o arco (u,v)(u, v) não esteja saturado e h(u)=h(v)+1h(u) = h(v) + 1, podemos expulsar fluxo de uu para vv, isto é, "atirar fluxo pela colina abaixo". O pseudocódigo da operação é bastante simples:

Push(u, v) // complexidade temporal: O(1)
  // excesso < capacidade residual -> não saturamos o arco
  let delta := min(e(u), c_f(u, v))
  if (u, v) in E // arco da rede
    (u, v).f = delta
  else // arco residual
    (u, v).f -= delta
  e(u) -= delta
  e(v) += delta

Existem dois tipos (bastante óbvios, pelo nomes) de pushes:

  • pushes saturantes, onde a operação push satura o arco pelo qual vai enviar o fluxo;

  • pushes não-saturantes, caso contrário. De notar que após um push deste tipo,

  • uu deixa de ter excesso (caso contrário poderia, e deveria, enviar mais fluxo pelo arco).

A utilidade desta definição será clara mais à frente.

Acima foi referido que push só pode ser realizado caso, entre outras condições, h(u)=h(v)+1h(u) = h(v) + 1 - isto é, caso o diferencial de altura entre os vértices seja igual a 1. Caso não seja o caso (e não houver nenhum vértice que atualmente nos permita expulsar fluxo em excesso do vértice), somos forçados a fazer relabel do mesmo: a alterar a sua altura, de modo a poder expulsá-lo.

Relabel(u)
  h(u) := 1 + min{h(v) | (u, v) in E_f}

Ou seja, a nova altura de uu corresponde à menor altura de um dos seus vizinhos, mais 1 (para ficar mais alto que ele).

Tal como Dijkstra e Bellman-Ford tinham InitializeSingleSource no seu início, temos aqui uma operação semelhante. Queremos:

  • Inicializar a altura de ss a V|V|;

  • Inicializar a altura de todos os outros vértices a 0, bem como o respetivo excesso;

  • Inicializar o fluxo de todos os arcos (e dos respetivos residuais) a 0;

  • Expulsar todo o fluxo possível da fonte, atirando para as suas adjacências. Os excessos de cada vértice vizinho são atualizados de acordo com o fluxo atirado.

O nome da operação não foge muito do apontado acima: InitializePreFlow, com pseudo-código como este:

InitializePreFlow(G, s)
  for each v in V
    u.h = 0
    u.e = 0
  for each (u, v) in E
    (u, v).f = 0
    (v, u).f = 0
  s.h = |V|
  for each u in s.adj
    f(s, u) = c(s, u) // saturar o arco
    f(u, s) = -f(s, u) // arco residual
    u.e = f(s, u)
    s.e -= f(s, u) // excesso em s incrementado conforme o fluxo atirado

Método Push-Relabel

O pseudocódigo genérico do método de push-relabel é:

PushRelabel(G)
  InitializePreFlow(G)
  while there is a vertex u with excess
    let v be a vertex where (u, v) is a residual edge and h(u) = h(v) + 1
    if v exists // se houver um vértice que respeite a condição acima
      push(u, v)
    else
      relabel(u)

Dois pontos interessantes a retirar do método (e de todos os algoritmos que o implementam, incluindo o algoritmo abaixo) é que:

  • os vértices ss e tt devem, no final, ter fluxos simétricos, e o seu respetivo módulo deve corresponder ao fluxo máximo da rede;

  • todos os outros vértices devem, no final, apresentar excesso 00.

Tal como o método de Ford-Fulkerson, push-relabel não é considerado um algoritmo - não nos dá, de forma determinística, uma opção a escolher. Pode, contudo, parecer algo "básico", pelo que será interessante provar a sua correção (e que de facto resolve o problema do fluxo máximo)*. A complexidade temporal do algoritmo é O(V2E)O(V^2 \cdot E), já melhor que a de Edmonds-Karp.

* As provas serão adicionadas assim que possível. Por agora apenas é mencionado o método em si, já que apenas é avaliado o algoritmo abaixo (e esse tem exemplo associado, incluindo uma abordagem mais aprofundada).

Algoritmo Relabel-To-Front

O método push-relabel, como referido acima, tem complexidade O(V2E)O(|V|^2 \cdot E) - uma melhoria em relação a Edmonds-Karp. Contudo, estudaremos de seguida um algoritmo (que implementa as operações básicas push e relabel), com uma terceira operação-base adicional que permite a alteração da complexidade temporal para O(V3)O(|V|^3), bastante melhor para redes muito densas, com muito mais arcos que vértices.

Um dos pilares do algoritmo é uma lista LL, que mantém todos os vértices V\{s,t}V \backslash \{s, t\}, inicialmente ordenados arbitrariamente. O algoritmo atravessa LL do início ao fim, aplicando Discharge a cada um dos vértices: sucessivos pushes e relabels até que o vértice já não possua excesso. Caso o vértice tenha sido relabeled, passa para o início de LL (daí o nome do algoritmo, relabel-to-front), e a passagem pela lista é recomeçada. O algoritmo termina caso consiga passar por todos os elementos da lista sem realizar qualquer descarga - estamos, então, na presença do fluxo máximo da rede.

Discharge(u)
  while u.e != 0
    v = u.current
    if v = nil
      Relabel(u)
      u.current = head(u.neighbors) // primeiro vizinho de u
    else if c_f(u, v) > 0 and h(u) = h(v) + 1
      Push(u, v)
      u.current = v.next // proximo vizinho
    else
      u.current = v.next

Aqui, consideramos que u.current corresponde ao vértice da lista de adjacências de uu que estamos atualmente a visitar.

  • Caso este seja nil, chegámos ao fim das adjacências do vértice. Assim sendo, e como ainda não expulsámos o fluxo todo de uu, precisamos de realizar um relabel (com vista a poder, agora, atirar mais fluxo pela colina), voltando a ler a lista desde o início.

  • Caso não seja nil e possamos atirar fluxo para u.current, atiramo-lo (via push) e passamos ao próximo vizinho.

  • Caso não seja nil mas também não possamos atirar fluxo, passamos só ao próximo vizinho.

O algoritmo corresponde, então, a uma sequência de descargas por LL até haver uma passagem completa pela mesma sem necessidade de descarregar fluxo. O pseudocódigo é o que se segue:

RelabelToFront(G, s, t)
  // consideramos L já inicializada
  InitializePreFlow(G, s)
  for each u in L
    u.current = head(u.neighbors)
  u = head(L)
  while u != nil
    let previous_height := h(u)
    Discharge(u)
    if h(u) > previous_height
      head(L) = u
    u = u.next // next -> proximo vertice de L

A aplicação do algoritmo é trivial, porém é bastante fácil enganarmo-nos num passo intermédio (costumam ser uns quantos, e é fácil haver detalhes a passar despercebidos). Nos slides encontram-se exemplos da execução do algoritmo.

Por fim, podemos afirmar que a complexidade temporal do algoritmo é O(V3)O(V^3). Podemos trivialmente provar que, por vértice, podemos realizar no máximo VV operações de relabel (começando da altura base, podemos encontrar todos os vértices em escada, subindo a altura de 11 em 11 até V). Havendo VV vértices, podemos afirmar que no máximo ocorrerão V2V^2 operações de relabel durante relabel-to-front. Por fim, temos que cada iteração pode ter até VV descargas (atirando fluxo para todos os vértices), e havendo no máximo V2V^2 relabels, podemos claramente majorar a complexidade temporal da execução do algoritmo em O(V3)O(V^3).