Avancerade grafalgoritmer

Det här får nog tas mer som kuriosa än som användbart för tävlingsuppgifter. Åtminstone har jag inte använt det själv. Men eftersom det hänger ihop med annat som har tagits upp tidigare, tyckte jag det kunde vara kul.

1. Bellman-Ford's algoritm för kortaste vägen

Detta är i och för sig ingen avancerad algoritm, tvärtom är det enkelheten som är trevlig, det kan vara bra att veta att en ganska intuitiv lösning verkligen ger kortaste vägen. Algoritmen är så här (w(e) är vikten på bågen e och start är den nod man vill veta kortaste vägen från)

BELLMAN-FORD
dist[v] := INF för varje nod v
// INF = t.ex. 1000000000
dist[start] := 0
repeat
    changed := FALSE

    for varje båge e = (u, v) do
        if dist[u] + w(e) < dist[v] then
            dist[v] := dist[u] + w(e)
            changed := TRUE
until not changed

Om man skriver på detta sätt förutsätter man att grafen inte innehåller några cykler med negativ vikt, då skulle den ju fortsätta i all evighet. Däremot får bågarnas vikter vara negativa. Vill man explicit kolla efter negativ-vikt-cykler kan man använda sig av det faktum att om det inte finns några sådana så körs repeat-loopen högst V-1 gånger, där V är antalet noder. Vill man explicit veta den kortaste vägen kan man precis som i Dijkstra's ha ett "föräldraträd" genom att när man uppdaterar dist också skriva
parent[v] := u

För att få Bellman-Ford i sitt sammanhang kan man skriva upp kortaste-väg-algoritmerna i en tabell.

Algoritm
Komplexitet
Klarar negativa vikter
Dijkstra's utan heap
O( V2 )
NEJ
Dijkstra's med heap
O( E log V )
NEJ
Bellman-Ford's
O( EV )
JA
Floyd-Warshall's (all pairs shortest paths)
O( V3 )
JA

Så vilken som är absolut bäst beror lite på hur tät grafen är. Om E = O(V^2), d.v.s. grafen är nära komplett, så är det t.ex. bättre att köra Dijkstra's utan heap än med heap (detsamma gäller förresten Prim's algoritm för MST, tack Erik). Och Floyd-Warshall's är då i stort sett lika snabb som Bellman-Ford's, även ifall man bara behöver en enstaka kortaste väg, så då kan man ju välja algoritm efter hur man har grafen sparad, eftersom Floyd-Warshall's kräver matrisform. Om grafen är gles så är ju Bellman-Ford's bra när man har negativa vikter, och kanske annars också om man råkar ha grafen sparad som en lista med bågar.

2. Viktad bipartit matchning

Detta är en variation på maximala matchningsproblemet . Vi har en viktad bipartit graf och vill hitta den matchning som maximerar summan av de valda bågarnas vikter. Problemet uppkommer exempelvis om n personer söker n jobb och varje person har en "lämplighetsfaktor" för varje jobb. Hur ska man fördela jobben så att den sammanlagda lämpligheten blir så stor som möjligt? Att testa alla n! kombinationer känns ofta lite jobbigt, så det är tur att det finns en bättre lösning.

De effektivaste algoritmerna bygger på dual variables som är en sorts viktstilldelningar för noderna. Om ni är intresserade kan ni ju läsa här (välj "PS.gz" och byt namn på filen till .ps), jag blir inte klok på det själv. Vill ni in på riktigt svåra grejer så titta på det generella fallet (välj "A paper describing...") då grafen inte är bipartit.

Den algoritm som återges nedan känns enklast tycker jag, och komplexiteten O(V^4) är för täta grafer inte sämre än den enklaste dual-algoritmen (Hungarian search). Här förutsätts n personer och n jobb och en "komplett graf" (så långt det går i en bipartit graf), d.v.s. varje person söker alla jobb.

WEIGHTED_MATCHING
M := {} //kommer att fyllas på med bågar som ingår i matchningen

Upprepa n gånger

    Bilda en ny komplett riktad graf (2n noder, bågarnas vikt nw) med
        utgångspunkt från den givna grafen (vikt w) genom att

        för varje båge e = (u, v) sätta
        nw(e) := 0     om u = v
                 INF   om både u och v är personer

                 INF   om både u och v är jobb
                 INF   om e ingår i den aktuella matchningen M
                 -w(e) om u är en person och v är ett jobb och e inte ingår i M
                 w(e)  om u är ett jobb och v är en person och e inte ingår i M
    Kör FLOYD-WARSHALL på nya grafen
    Sök igenom matrisen och välj den kortaste vägen från en omatchad person
        till ett omatchat jobb. Denna väg är p.g.a. viktningen ovan en s.k.
        augmenting path.
    Gå längs den funna vägen och förskjut matchningsbågarna i M på följande sätt:
        Före: a b-c d-e f Efter: a-b c-d e-f
    Detta utökar M med en båge precis som i den oviktade matchningsalgoritmen

Skillnaden mellan den vanliga matchningsalgoritmen och denna algoritm är alltså att i den förstnämnda kan man välja vilken augmenting path som helst, slutmatchningen blir alltid optimal ändå, men i ovanstående algoritm måste den kortaste vägen (i det avseende som definieras av nw) väljas i varje steg för att garantera att slutmatchningen får maximal vikt. Beviset för att det verkligen blir så har jag faktiskt inte sett, är det någon som kan ge det?

3. Kinesiska brevbärarproblemet

Detta problem beskrevs redan under Hamilton- och Eulerkretsar , men nu har vi verktyget för att lösa det, åtminstone i det fall då grafen är riktad. Det gäller alltså att hitta en rundtur som besöker alla bågar minst en gång men som ändå är så kort som möjligt (med avseende på bågarnas sammanlagda vikter), precis som när man delar ut post i ett villaområde. Vi förutsätter hädanefter att grafen är riktad och att den består av en enda starkt sammanhängande komponent (annars kommer brevbäraren aldrig tillbaka).

Det bästa vore naturligtvis om man kunde hitta en Eulerkrets, eftersom den alltid är optimal. Kriteriet för att det finns en Eulerkrets är enkelt: om varje nod har lika stor utgrad som ingrad så finns det alltid en sådan, och den kan konstrueras på samma cykelkopplingssätt som beskrevs för oriktade grafer i Hamilton- och Eulerkretsar . Annars skulle man vilja lägga till bågar mellan de "jobbiga noder" som inte har ingraden lika stor som utgraden. Men brevbäraren måste naturligtvis fortfarande följa de bågar som finns, så vikten av de nya bågarna blir helt enkelt den kortaste vägen mellan noderna i ursprungsgrafen. Om det finns många jobbiga noder gäller det att bestämma vilka par som ska förbindas för att summan av vikterna ska bli så liten som möjligt, och det är här som viktad bipartit matchning kommer in.

Alltså, rent praktiskt:
1. Låt Z(u) för varje nod u beteckna Ingrad( u) - Utgrad( u)
2. Använd t.ex. Floyd-Warshall's algoritm för att hitta kortaste vägarna mellan noderna med Z != 0.
3. Skapa en ny, bipartit, "komplett" graf där varje gammal nod u ger upphov till |Z| nya noder, vilka hamnar till vänster om Z >0 och till höger om Z<0. Om t.ex. Z(u) = -3 ger u upphov till tre noder på högersidan med identiska uppsättningar bågar. Bågarnas vikt sätts lika med kortaste vägen från steg 2.
4. Använd algoritmen ovan för att hitta en fullständig matchning med minimal vikt (byt tecken på nw för att få minimum).
5. Lägg i den ursprungliga grafen till de bågar som ingår i de kortaste vägar som väljs i matchningen och konstruera en Eulerkrets i den utökade grafen.

Uppgift 1: Visa att den bipartita grafen har lika många noder till höger och vänster.
Uppgift 2: Visa att problemet för en oriktad graf ger upphov till viktad matchning i en generell graf, vilket är mycket svårare.
Uppgift 3: Implementera ovanstående algoritm om du känner för det.

4. Hopcroft-Karp's algoritm för bipartit matchning

Till sist tänkte jag säga något om den beryktade Hopcroft-Karps matchningsalgoritm, som skulle ha behövts för att få full poäng på Knights (nu kom ju folk inte riktigt så långt, maxpoängen blev bara 40/100 på den uppgiften). Här snackar vi alltså oviktad matchning igen, och det är inget konstigt alls. Istället för att välja bara en augmenting path i taget, hittar man flera stycken på en enda sökning. Om dessa inte har någon nod gemensamt kan man ju sedan göra omflyttningarna i vilken ordning som helst. Så i varje utökningssteg gör man en BFS-sökning där man börjar med att i kön (med avstånd 0) lägga alla omatchade noder på vänstersidan och sedan fortsätta sökningen genom att följa icke-matchningsbågar åt höger och matchningsbågar åt vänster som figuren nedan visar, tills dess att en omatchad nod på högersidan hittas, vilket innebär en augmenting path. Då gör man färdigt alla noder på samma "avståndsnivå" så att man säkert har fått med alla augmenting paths med minimal längd (3 i exemplet nedan). Sedan gör man en ny sökning av typen DFS, där man begränsar sig till de noder som besöktes i den första sökningen. Man kan tänka sig att man söker i "BFS-trädet", men man behöver inte spara detta explicit. I exemplet kan man t.ex. börja från B och då hitta vägen B2A1 som då direkt kan processas och noderna markeras som tagna. När man sedan fortsätter DFS-sökningen från D hittar man D3C4 och när även denna har processats ser matchningen ut som till höger, den är naturligtvis redan maximal.


Nu kan man ju invända att vi hade tur som hittade B2A1-vägen först. Eftersom besöksordningen i DFS är helt godtycklig kunde vi lika gärna ha börjat med att hitta D2A1-vägen och när sedan sökningen fortsätter från B så tar det stopp direkt eftersom 2 redan är besökt. I detta fall hittas bara en augmenting path i detta steg och matchningen kommer inte ännu att vara maximal, utan en ny sökning måste göras för att hitta en ytterligare augmenting path, nämligen B2D3C4 . Emellertid lyckades Hopcroft och Karp bevisa att även om man inte hittar maximalt antal augmenting paths varje gång, utan endast garanterar att det inte finns någon ytterligare augmenting path i BFS-trädet när man har processat några godtyckliga vägar, så reduceras antalet utökningssteg från V till O(sqrt(V)) . Eftersom både BFS och DFS går på O(E) tid, så blir komplexiteten för hela algoritmen
O(E sqrt(V))

Uppgift 4: Försök lösa Knights från BOI 2001