Tietorakenteet ja algoritmit

kevät 2024

16. Maksimivirtaus

Tämän luvun tavoitteena on muodostaa suunnatussa verkossa virtaus (flow) alkusolmusta (source) loppusolmuun (sink). Jokaisella verkon kaarella on kapasiteetti (capacity), joka ilmaisee, paljonko virtausta kaaren kautta voi kulkea enintään.

Virtaus tulee muodostaa niin, että alkusolmusta lähtevä virtaus on yhtä suuri kuin loppusolmuun tuleva virtaus, ja kaikissa muissa solmuissa solmuun tuleva virtaus ja solmusta lähtevä virtaus on yhtä suuri. Kaikissa kaarissa virtauksen tulee olla enintään yhtä suuri kuin kaaren kapasiteetin.

Verkon maksimivirtaus (maximum flow) on suurin mahdollinen virtaus. Seuraava kuva näyttää esimerkin maksimivirtauksesta solmusta 11 solmuun 55.

Tässä merkintä x/cx/c tarkoittaa, että kaaren kapasiteetti on cc ja siitä on käytetty xx. Esimerkiksi kaaren 121 \rightarrow 2 kapasiteetti on 44 ja koko kapasiteetti on käytetty. Vastaavasti kaaren 131 \rightarrow 3 kapasiteetti on 66 ja siitä on käytetty 33.

Tässä verkossa maksimivirtaus on 77, joka on solmusta 11 lähtevä virtaus ja solmuun 55 tuleva virtaus. Kaikissa muissa solmuissa tuleva ja lähtevä virtaus on sama. Esimerkiksi solmussa 22 tuleva virtaus ja lähtevä virtaus on 44.

Ford-Fulkerson-algoritmi

Tavallinen tapa muodostaa verkon maksimivirtaus on käyttää Ford-Fulkerson-algoritmia. Siinä verkko esitetään erityisessä muodossa, jossa jokaista kaarta kohden verkkoon lisätään toinen vastakkaiseen suuntaan kulkeva kaari, jonka kapasiteetti on alussa 00. Esimerkkiverkossa alkutilanne on seuraava:

Algoritmi lisää verkkoon virtausta etsimällä polkuja alkusolmusta loppusolmuun. Tällaista polkua kutsutaan nimellä täydennyspolku (augmenting path). Alussa virtaus on 00, ja virtaus kasvaa algoritmin aikana.

Algoritmi etsii joka vaiheessa täydennyspolun, jossa jokaisen kaaren kapasiteetti on positiivinen. Tällainen polku lisää virtausta xx:llä, missä xx on pienin kapasiteetti polulla. Tämän jälkeen algoritmi pienentää jokaisen polun kaaren kapasiteettia xx:llä ja kasvattaa jokaisen vastakkaisen kaaren kapasiteettia xx:llä.

Algoritmi muodostaa täydennyspolkuja ja lisää virtausta, kunnes polun muodostaminen ei ole enää mahdollista. Kun näin tapahtuu, algoritmi päättyy ja sen muodostama virtaus on verkon maksimivirtaus.

Esimerkkiverkossa maksimivirtaus voidaan muodostaa seuraavasti kahdessa vaiheessa:

Ensimmäisessä vaiheessa algoritmi muodostaa täydennyspolun 12351 \rightarrow 2 \rightarrow 3 \rightarrow 5. Pienin kapasiteetti polulla on 44, joten polku lisää virtausta 44:llä. Jokaisen polun kaaren kapasiteetti pienenee 44:llä ja jokaisen vastakkaisen kaaren kapasiteetti kasvaa 44:llä.

Toisessa vaiheessa algoritmi muodostaa täydennyspolun 132451 \rightarrow 3 \rightarrow 2 \rightarrow 4 \rightarrow 5. Pienin kapasiteetti tällä polulla on 33, joten polku lisää virtausta 33:lla. Kaarten kapasiteetit muuttuvat vastaavasti kuin ensimmäisessä vaiheessa.

Huomaa, että toisessa vaiheessa polku kulkee kaarta 323 \rightarrow 2, joka on verkkoon lisätty vastakkainen kaari. Tätä kaarta on mahdollista kulkea, koska kaareen tuli kapasiteettia ensimmäisessä vaiheessa. Kulkemalla vastakkaisia kaaria algoritmi pystyy tällä tavalla peruuttamaan aiemmin muodostettua virtausta.

Näiden vaiheiden jälkeen verkossa ei ole mitään täydennyspolkua, jossa jokainen kapasiteetti olisi positiivinen. Tällöin algoritmi pysähtyy ja maksimivirtaus on löytynyt. Tässä tapauksessa maksimivirtaus on 4+3=74+3=7.

Algoritmin toteutus

Ford-Fulkerson-algoritmi voidaan toteuttaa seuraavasti:

class MaximumFlow:
    def __init__(self, nodes):
        self.nodes = nodes
        self.graph = {}
        for i in self.nodes:
            for j in self.nodes:
                self.graph[(i, j)] = 0

    def add_edge(self, node_a, node_b, capacity):
        self.graph[(node_a, node_b)] += capacity

    def add_flow(self, node, sink, flow):
        if node in self.seen:
            return 0
        self.seen.add(node)
        if node == sink:
            return flow
        for next_node in self.nodes:
            if self.flow[(node, next_node)] > 0:
                new_flow = min(flow, self.flow[(node, next_node)])
                inc = self.add_flow(next_node, sink, new_flow)
                if inc > 0:
                    self.flow[(node, next_node)] -= inc
                    self.flow[(next_node, node)] += inc
                    return inc
        return 0

    def construct(self, source, sink):
        self.flow = self.graph.copy()
        total = 0
        while True:
            self.seen = set()
            add = self.add_flow(source, sink, float("inf"))
            if add == 0:
                break
            total += add
        return total

Tässä toteutuksessa kaarten kapasiteetit tallennetaan matriisiin, jossa on paikka jokaiselle mahdolliselle kaarelle verkossa. Tämän ansiosta verkkoon ei tarvitse lisätä erikseen vastakkaisia kaaria.

Metodi construct muodostaa maksimivirtauksen ja palauttaa sen suuruuden. Metodi add_flow suorittaa syvyyshaun, joka lisää virtausta. Metodi etsii täydennyspolun, jossa jokainen kapasiteetti on positiivinen. Polun löytymisen jälkeen metodi muuttaa kaarten kapasiteetteja. Metodin palautusarvo ilmaisee, paljonko virtaus kasvaa.

Algoritmia voidaan käyttää näin esimerkkiverkossa:

m = MaximumFlow([1, 2, 3, 4, 5])

m.add_edge(1, 2, 4)
m.add_edge(1, 3, 6)
m.add_edge(2, 3, 8)
m.add_edge(2, 4, 3)
m.add_edge(3, 5, 4)
m.add_edge(4, 5, 5)

print(m.construct(1, 5)) # 7

Minimileikkaus

Ford-Fulkerson-algoritmi on ahne algoritmi, koska se muodostaa täydennyspolkuja, kunnes polkua ei voi enää muodostaa. Miten voidaan tietää, että algoritmin tuloksena on maksimivirtaus?

Algoritmin toiminnan ymmärtämistä helpottaa tarkastella asiaa toisen ongelman kautta. Verkon leikkaus (cut) tarkoittaa joukkoa kaaria, joiden poistaminen estää kulkemisen alkusolmusta loppusolmuun. Verkon minimileikkaus (minimum cut) on puolestaan yhteispainoltaan pienin leikkaus.

Tässä on esimerkkiverkon minimileikkaus, joka estää kulkemisen alkusolmusta 11 loppusolmuun 55:

Tässä tapauksessa minimileikkaus sisältää kaaret 242 \rightarrow 4 ja 353 \rightarrow 5, joiden yhteispaino on 3+4=73+4=7. Kun nämä kaaret poistetaan verkosta, siinä ei ole enää polkua alkusolmusta 11 loppusolmuun 55.

Esimerkkiverkon maksimivirtaus ja minimileikkaus ovat molemmat 77, eikä tämä ole sattumaa: maksimivirtaus ja minimileikkaus ovat aina yhtä suuret. Ongelmat siis liittyvät toisiinsa, ja tämän avulla voidaan perustella, miksi Ford-Fulkerson-algoritmi toimii oikein.

Kun verkossa on mikä tahansa leikkaus, joka estää kulkemisen alkusolmusta loppusolmuun, tämä antaa ylärajan verkossa olevan virtauksen suuruudelle. Tämä johtuu siitä, että virtauksen täytyy edetä alkusolmun komponentista loppusolmun komponenttiin. Niinpä verkon virtaus on enintään yhtä suuri kuin verkon leikkaus.

Toisaalta voidaan näyttää, että maksimivirtaus vastaa verkossa olevaa leikkausta. Tämä voidaan havaita tarkastelemalla solmuja, joihin maksimivirtauksen muodostamisen jälkeen päästään alkusolmusta positiivisia kaaria. Esimerkkiverkossa nämä solmut ovat seuraavat:

Kun tarkastellaan näiden solmujen ulkopuolelle johtavia alkuperäisen verkon kaaria, näiden kaarten kapasiteettina täytyy olla 00, koska kaaria ei voi enää kulkea. Niinpä kaarten kapasiteetti on käytetty kokonaan ja niiden yhteiskapasiteetti on yhtä suuri kuin verkon maksimivirtaus. Toisaalta kaaret muodostavat myös leikkauksen, koska ne jakavat verkon kahteen osaan.

Koska verkon virtaus on enintään yhtä suuri kuin verkon leikkaus ja tässä tapauksessa on löydetty virtaus, joka on yhtä suuri kuin leikkaus, tämä tarkoittaa, että kyseinen virtaus on maksimivirtaus ja kyseinen leikkaus on minimileikkaus.

Esimerkki: Vankilapako

Tehtävä

Kaupungissa on risteyksiä ja teitä, jotka yhdistävät risteyksiä. Kaupungissa on vankila ja satama, jotka sijaitsevat tietyissä risteyksissä.

Kaaleppi on paennut vankilasta ja pyrkii satamaan. Poliisi haluaa estää pakenemisen sulkemalla teitä niin, että ei ole mitään reittiä vankilasta satamaan. Mikä on pienin määrä teitä, jotka poliisin riittää sulkea?

Tämä tehtävä voidaan ratkaista muodostamalla verkko, jonka solmut ovat risteyksiä ja kaaret ovat teitä. Valitaan alkusolmuksi vankilan risteys, loppusolmuksi sataman risteys ja jokaisen kaaren painoksi 11. Tämän verkon minimileikkaus ilmaisee, mitkä tiet poliisin tulee sulkea.

Tehtävää voisi myös laajentaa niin, että jokaisen tien sulkemisesta aiheutuu tietty haitta ja poliisi haluaa sulkea tiet niin, että yhteishaitta on mahdollisimman pieni. Tällöin haitan voisi esittää antamalla sen kaaren painoksi.

Täydennyspolkujen valinta

Ford-Fulkerson-algoritmin tehokkuus riippuu siitä, millä tavalla täydennyspolut valitaan. Jokainen polku kasvattaa virtausta ainakin 11:llä, minkä ansiosta algoritmi muodostaa enintään ff polkua, missä ff on verkon maksimivirtaus.

Yhden polun muodostaminen vie syvyyshaulla aikaa O(m)O(m), missä mm on verkon kaarten määrä, joten algoritmille saadaan aikavaativuus O(mf)O(mf).

Joissakin tapauksissa jokainen polku saattaa todella kasvattaa virtausta 11:llä, jolloin algoritmi joutuu muodostamaan suuren määrän polkuja. Tällainen tilanne on esimerkiksi seuraavassa verkossa:

Tässä kaaren kapasiteetti ZZ on jokin suuri luku. Algoritmi voi päätyä muodostamaan vuorotellen polkuja 12341 \rightarrow 2 \rightarrow 3 \rightarrow 4 ja 13241 \rightarrow 3 \rightarrow 2 \rightarrow 4, jotka kääntävät solmujen 22 ja 33 välisen kaaren suuntaa. Jokainen polku lisää virtausta 11:llä. Koska verkon maksimivirtaus on 2Z2Z, algoritmi muodostaa yhteensä 2Z2Z polkua, mikä on hidasta.

Tehokkaampi algoritmi saadaan, kun algoritmi valitsee aina polun, jossa kaarten määrä on mahdollisimman pieni. Tämä voidaan toteuttaa etsimällä polku syvyyshaun sijasta leveyshaulla. Tuloksena oleva algoritmi tunnetaan nimellä Edmonds-Karp-algoritmi. Voidaan osoittaa, että tällöin algoritmi muodostaa enintään O(nm)O(nm) polkua ja algoritmin aikavaativuus on O(nm2)O(nm^2).

Yllä olevassa verkossa Edmonds-Karp-algoritmi muodostaa vain kaksi kahden kaaren täydennyspolkua: esimerkiksi ensin 1241 \rightarrow 2 \rightarrow 4 ja sitten 1341 \rightarrow 3 \rightarrow 4. Molemmat polut lisäävät virtausta ZZ:llä, minkä jälkeen algoritmi päättyy.

Maksimiparitus

Tarkastellaan suuntaamatonta verkkoa, joka on kaksijakoinen (bipartite), mikä tarkoittaa, että verkon solmut voidaan jakaa kahteen ryhmään niin, että verkossa on kaaria vain ryhmästä toiseen mutta ei ryhmien sisäisesti.

Verkon maksimiparitus (maximum matching) on suurin mahdollinen kaarten joukko, jossa pätee, että jokainen solmu kuuluu enintään yhteen kaareen. Seuraava kuva näyttää esimerkin kaksijakoisen verkon maksimiparituksesta:

Tässä solmut ovat jakautuneet kahteen ryhmään niin, että toinen ryhmä sisältää solmut {1,2,3,4}\{1,2,3,4\} ja toinen ryhmä sisältää solmut {5,6,7}\{5,6,7\}. Verkon maksimiparitus sisältää 33 kaarta, ja se voidaan muodostaa kaarista 161-6, 353-5 ja 474-7.

Kaksijakoisen verkon maksimiparitus voidaan etsiä maksimivirtauksen avulla rakentamalla verkko seuraavasti:

Ideana on muuttaa kaksijakoinen verkko suunnatuksi ja lisätä siihen alkusolmu ja loppusolmu. Alkusolmusta on kaari jokaiseen vasemman ryhmän solmuun ja jokaisesta oikean ryhmän solmusta on kaari loppusolmuun. Lisäksi vasemman ryhmän solmusta on kaari oikean ryhmän solmuun, jos näiden solmujen välillä on kaari alkuperäisessä verkossa. Jokaisen kaaren painona on 11.

Tämän verkon maksimivirtaus on yhtä suuri kuin alkuperäisen verkon maksimiparitus. Tämä johtuu siitä, että jokainen virtaukseen kuuluva polku kulkee jonkin alkuperäisen verkon kaaren kautta. Lisäksi kaarten painojen ansiosta jokainen solmu voi kuulua enintään yhteen polkuun.

Esimerkki: Tanssiaiset

Tehtävä

Tanssiaisiin osallistuu nn opiskelijaa Kumpulan kampukselta ja nn opiskelijaa Viikin kampukselta. Jokainen tanssipari muodostuu Kumpulan ja Viikin opiskelijasta.

Kukin opiskelija on toimittanut listan toisen kampuksen opiskelijoista, joiden kanssa hän suostuu tanssimaan. Tehtäväsi on etsiä mahdollisimman suuri määrä tanssipareja, joissa otetaan huomioon opiskelijoiden listat.

Tämä tehtävä voidaan ratkaista muodostamalla verkko, jossa jokaista opiskelijaa vastaa solmu ja kahden solmun välillä on kaari, jos opiskelijat ovat eri kampuksilta ja suostuvat tanssimaan toistensa kanssa. Tämä verkko on kaksijakoinen, koska kaksi saman kampuksen opiskelijaa ei voi muodostaa tanssiparia.

Koska verkko on kaksijakoinen, sen maksimiparitus eli suurin mahdollinen tanssiparien määrä voidaan löytää maksimivirtauksen avulla lisäämällä verkkoon alkusolmu ja loppusolmu sekä sopivat kaaret.