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 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 e 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:
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 temos material a entrar (via e ) e a sair (via e ).
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: , 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 ) todas as fontes do grafo original;
-
um super-sumidouro, ao qual ligam (com capacidade ) 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, , de uma rede de fluxo com fluxo indica-nos como podemos mudar o fluxo nas arestas de . Temos que cada arco pode ter uma capacidade residual associada, , igual à diferença entre a capacidade do arco e o fluxo que o atravessa atualmente. indicará, portanto, a capacidade residual de cada arco de tal que (os arcos já com capacidade máxima não nos interessam aqui). Formalizando, temos:
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 e residual , temos que um caminho de aumento corresponde a um caminho simples de para em . Podemos, em , aumentar o fluxo em todo o arco de com uma quantidade até , a capacidade residual de , tal que:
Significa, então, que podemos aumentar o fluxo em todo o arco de 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 para a rede em à esquerda. Temos que , pelo que podemos aumentar o fluxo em todo o caminho de aumento por .
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 , respeitando a definição proposta acima para . 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:
Ou seja, que o fluxo da rede após a aplicação do aumento à rede, , é estritamente maior que o fluxo da rede original, . Consideramos para este efeito que .
Cortes em Redes de Fluxo
Os cortes em redes de fluxo correspondem a uma partição de , com , tal que . Seja um fluxo, o fluxo líquido de um corte corresponde a:
A imagem abaixo poderá tornar o conceito mais claro:
Na rede da imagem, consideramos . Os arcos que cruzam o corte são 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:
Assim sendo, o fluxo líquido do corte associado à rede acima é . Realça-se ainda que foi aumentado segundo os arcos que cruzavam o corte de para 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 para , isto é:
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 .
Temos ainda algumas afirmações a fazer quanto aos cortes:
-
Dado um fluxo para uma rede , o fluxo líquido de qualquer corte da rede é o mesmo, sendo igual a .
-
O valor de qualquer fluxo numa rede é majorado pela capacidade de qualquer corte da rede.
Prova do primeiro ponto
Temos que , e então , já que .
Tendo , e com , podemos começar a construir a prova pretendida:
Ora, visto que , podemos simplificar a soma acima mais ainda (em função de e em vez de e ):
Esta última diferença entre parêntesis anula-se, pela conservação do fluxo, pelo que ficamos com:
Seja um corte de e um qualquer fluxo da rede. Temos, pela afirmação provada imediatamente acima, que . A partir daí, podemos facilmente deduzir:
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 o fluxo de uma rede com fonte e sumidouro . Para uma rede assim apresentada, as seguintes proposições são equivalentes:
-
é o fluxo máximo da rede.
-
A rede residual não contém caminhos de aumento.
-
para algum corte 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
indica que se for o fluxo máximo da rede, então não contém caminhos de aumento. Suponhamos o oposto: que contém caminhos de aumento, mesmo considerando como um fluxo máximo da rede. Nesse caso, haveria pelo menos um caminho tal que , uma contradição, já que já é máximo.
indica que se for uma rede residual sem caminhos de aumento, então para algum corte na rede. Tenhamos , . Por contradição, suponhamos que para todo o corte da rede. Isso implicaria, claro, que para todo o corte, já que é impossível ter o contrário. Agora das duas uma:
-
Existe um arco tal que ; 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 a partir de , o que também não pode acontecer.
A proposição fica então provada.
Por fim, . Temos, claro, que para todo o corte da rede. Tendo , temos obrigatoriamente que terá de ser o fluxo máximo da rede, visto que caso não fosse, 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 . 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 , 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 , 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 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: .
Podemos dizer, aqui, que um corte mínimo seria tal que . 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 tempo;
-
calcular a capacidade residual mínima do caminho pode levar tempo;
-
atualizar o fluxo do arco em leva tempo; é realizado no máximo vezes por ciclo.
Resta, então, falar sobre a complexidade do ciclo while
em si: encontrar um caminho
leva tempo, recorrendo a uma DFS/BFS adequada. O ciclo em si, contudo, pode
ser executado até vezes (onde é o fluxo máximo da rede), tornando
a complexidade temporal do algoritmo . A razão para ter de ser executado
até 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 = . Na pior das hipóteses, teremos de realizar igual quantidade de caminhos que passem pelo arco , 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 , 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 . Para tal, o algoritmo recorre a uma BFS com origem em e destino em - 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 e .
Monotonia de Edmonds-Karp
É interessante definir distância de Edmonds-Karp: , a distância do caminho mais curto entre e em .
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, 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 até ao vértice em questão. Podemos, nesta primeira procura, notar que há 2 caminhos de aumento de tamanho . Escolhemos um deles (por exemplo ), e, ao escolhê-lo, verificamos que a capacidade residual mínima de entre as arestas do caminho é - o fluxo em todos os arcos é incrementado em unidades.
De seguida, vamos ao outro caminho de aumento com 3 unidades (descoberto anteriormente), . Aqui, a menor capacidade residual dos seus arcos é , e o fluxo em todos os arcos é incrementado em .
Explorados todos os caminhos de aumento de tamanho , a BFS seguinte encontra um caminho de tamanho . A menor capacidade residual de um dos seus arcos é , e o fluxo em todos os arcos é incrementado em .
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, :
Podemos notar que todos os arcos que atravessam o corte no sentido "positivo", da partição de para a de 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 , o fluxo máximo da rede seria .
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 .
Podemos notar que as distâncias de Edmonds-Karp podem variar entre : um caminho de aumento pode ter no mínimo 1 aresta (se o grafo ligar diretamente a ), e no máximo arcos (tal como o exemplo de seguida demonstra):
Ora, temos ainda que podemos ter caminhos de aumento até aumenta (podemos ter um caminho que contenha todas as arestas, onde uma aresta vai ficando saturada de cada vez), e assim podem existir caminhos de aumento numa rede.
Visto que cada caminho de aumento pode ser encontrado em tempo (via BFS), temos que a complexidade temporal de Edmonds-Karp é .
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 (). Assim sendo, dependendo da topologia do grafo, a realização do algoritmo pode até levar tempo inferior a 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 máquinas e outro de 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 , um conjunto que mapeia todas as máquinas a todas as tarefas que podem executar, tal que:
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. pode ser representado por um grafo, claro:
Como podemos notar, o grafo que corresponde a diz-se um grafo bipartido - um grafo onde pode ser separado em e , em que não existem arestas entre os vértices de nem entre os de , apenas de para (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 , e . 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, , e e . Aqui, verificamos que a cardinalidade do emparelhamento é , 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 , 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, apenas se a máquina estiver a realizar a tarefa .
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:
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 , que é melhor que a de Edmonds-Karp, dada por .
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 , 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 todo até 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 atinja ). O primeiro passo é sempre saturar todos os arcos que ligam 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. só pode ser considerado um pré-fluxo caso:
Posto isto, podemos ainda afirmar que o fluxo que entra num vértice 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, :
A primeira coisa a fazer é, como referido acima, saturar todos os arcos que ligam 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 para outro caso esteja "mais alto" na colina - para tal, é necessário ir atualizando as alturas dos vértices da rede com o decorrer do algoritmo.
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 e ) tem excesso de fluxo no seu reservatório.
Função de Alturas
Seja uma rede de fluxo com fonte e sumidouro . Mais ainda, seja um pré-fluxo da rede. Uma função é uma função tal que:
-
;
-
;
-
para todo o arco residual . Caso , então o arco 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 possui excesso. Recebe como argumento , para além de , onde corresponde ao vértice-destino de um arco que sai de . Caso o arco não esteja saturado e , podemos expulsar fluxo de para , 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,
-
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, - 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 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 a ;
-
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 e 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 .
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 é , 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 - 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 , bastante melhor para redes muito densas, com muito mais arcos que vértices.
Um dos pilares do algoritmo é uma lista , que mantém todos os vértices , inicialmente ordenados arbitrariamente. O algoritmo atravessa 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 (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 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 , 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 parau.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 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 é . Podemos trivialmente provar que, por vértice, podemos realizar no máximo operações de relabel (começando da altura base, podemos encontrar todos os vértices em escada, subindo a altura de em até V). Havendo vértices, podemos afirmar que no máximo ocorrerão operações de relabel durante relabel-to-front. Por fim, temos que cada iteração pode ter até descargas (atirando fluxo para todos os vértices), e havendo no máximo relabels, podemos claramente majorar a complexidade temporal da execução do algoritmo em .