Algoritme van Prim - martenserver.com

advertisement
H9: Minimaal overspannende bomen
Definitie:
Een overspannende boom v/e ongerichte graaf bevat dezelfde knopen en een deel van de
verbidingen als takken, juist genoeg om de graaf samenhangend te houden.
Een minimaal overspannende boom (MOB) heeft het kleinste gewicht (som van de gewichten van
zijn takken) van alle overspannende bomen. Deze is niet noodzakelijk uniek.
Eigenschappen:
Snede-eigenschap: Als we twee groepen graafknopen hebben, met tussen hen meerdere
graafverbindingen, moeten we kiezen welke van die verbindingen we gaan toevoegen als
boomverbinding bij onze MOB. Dit is zoiezo de lichtste (of een van de lichtste als er meerdere met
hetzelfde laagste gewicht zijn) van al die verbindingen. Degene die zo gekozen worden om deel uit te
maken van de MOB, is zoiezo ook de enige uit heel die groep.
Dit noemt men een uitwendige eigenschap, omdat ze een verband legt tussen 1 boomtak van de
MOB, en meerdere graafverbindingen die buiten de boom liggen (niet tot de MOB behoren).
Dit is makkelijker te bewijzen als we uitgaan van het tegenovergestelde : Stel dat er een verbinding
(i,j) gekozen wordt tot boomverbinding voor de MOB, maar dat deze niet de lichtste is. Dan is er nog
een andere verbinding (k,l) die lichter is. Wanneer we deze ook zouden toevoegen aan de boom,
krijgen we een lus (van groep A naar groep B met 2 verbindingen ertussen.. das een lus e). Maar als
we nu (i,j) laten wegvallen, hebben we ineens een lichtere boom dan onze oorspronkelijke MOB, die
dus helemaal geen MOB was.
Als elke tak van een overspannende boom B aan de cut-eigenschap voldoet, is het zoiezo een MOB.
Stel nu dat B’ een MOB is, net als B. Dan moeten ze minstens een verbinding hebben tussen 2
groepen knopen die verschillend is. B heeft bijv Verbinding (i,j), en B’ zijn verbinding noemen we (k,l).
Wanneer we (k,l) toevoegen aan B, ontstaat er weer een lus, want beide verbindingen bestaan
tussen dezelfde groepen van knopen. Aangezien B en B’ allebei MOB’s zijn, kan het niet anders dan
dat deze twee verbindingen hetzelfde gewicht hebben, dus kunnen we evengoed in B’ (i,j) schrappen
en vervangen door (k,l). Zo krijgen we nog een MOB, nu B’’, die nog wat meer lijkt op B. Zo kunnen
we verder B’ transformeren, tot deze identiek is aan B.
Lus-eigenschap: (ik gebruik hier de definitie van Wikipedia) Als we een lus L hebben in de
graafverbindingen, en een van die verbindingen V van L is zwaarder dan een andere, dan kan V
onmogelijk deel uitmaken van een MOB.
Dit is een inwendige eigenschap, omdat ze een verband legt tussen meerdere boomtakken en 1
graafverbinding buiten de boom.
Ook weer makkelijk te zien als we het omgekeerde veronderstellen: Als er namelijk zo’n
graafverbinding V lichter is dan een boomtak op de weg tussen haar eindknopen, dan krijgen we een
lichtere boom als we die tak vervangen door V, en was de eerste dus geen MOB.
Deze twee eigenschappen laten toe om stap voor stap een MOB op te bouwen. Bij elke stap doet
men een lokaal optimale keuze, en het resultaat blijkt op het einde ook optimaal te zijn. Algoritmen
die volgens dit principe werken noemt men greedy algoritmen.
Algoritme van Prim:
Bij het opbouwen van een MOB worden de knopen telkens in 2 groepen verdeeld: zij die reeds deel
uitmaken van de MOB, en de rest. Om de volgende boomtak te vinden maakt men gebruik van de
eerste eigenschap: van alle verbindingen tussen de 2 groepen, behoort (één van) de lichtste tot de
MOB. Voor elke knoop die nog niet tot de MOB behoort, houden we de lichtste verbinding die hem
met de MOB verbindt bij.
Telkens wordt de lichtste verbiding en de bijhorende nieuwe knoop in de MOB opgenomen. Buren
van de nieuwe knoop die nog niet met de MOB verbonden waren, krijgen hun eerste (en voorlopig
dus lichtste) verbinding met de MOB. Zij die al verbonden waren met de MOB, moeten nu kijken of
deze nieuwe verbinding lichter is dan degene die ze tot nu toe hadden. Zo ja, smijten ze de oude weg
en houden ze deze bij.
De knopen die reeds tot de MOB behoorde, hebben er niets meer mee te maken, want hun
verbinding met de nieuwe knoop was zeker niet lichter dan de nieuwe verbinding.
Heel dit proces wordt herhaalt tot de MOB alle knopen bevat.
Voor een uitgetekend voorbeeld, zie p. 102 en figuur p. 103.
Voor de knopen die zich buiten de MOB bevinden maar er al mee verbonden zijn op te slagen,
gebruikt men een prioriteitswachtrij, geïmplementeerd met een heap, gerangschikt volgends het
gewicht van hun lichtste verbinding met de MOB. Met moet echter wel het gewicht kunnen
aanpassen (als er een betere verbinding gevonden is), wat een heap standaard niet ondersteunt.
Hiervoor zijn 2 mogelijkheden:
1. De heap serieel doorzoeken is O(n) dus daar beginnen we niet aan. In plaats daarvan houden
we de posities van elke knoop bij in bijv een aparte tabel, geïndexeerd met het
knoopnummer. Een positie vinden is dan O(1), en wijzigen (verminderen) van de prioriteit is
dan O(lg n) (ik veronderstel lg n omdat vervangen v/e element in een heap O(lg n) is).
2. We laten de knoop met de oude prioriteit er gewoon inzitten, en voegen nogmaals een
knoop toe met zijn nieuwe (lagere) prioriteit. Dit is netter en eenvoudiger te implementeren,
maar heeft als nadeel dat de grootte van de wachtrij O(m) wordt. (m=aantal verbindingen
denk ik, n = aantal knopen). Voor de afzonderlijke heapoperaties maakt dat niet zoveel uit (m
= O(n²) dus lg m = O(lg n)), maar er moeten nu O(m) elementen verwijdert worden ipv O(n).
En natuurlijk pakt dit ook meer geheugen in.
Efficiëntie van dit algoritme:
- Initialisatie van de tabellen met de informatie over de knopen O(n)
- Elke knoop komt één keer door de wachtrij, met toevoegen en verwijderen O(lg n), dus in
totaal O(n lg n)
- Allen buren van elke knoop moeten overlopen worden, en in het slechtste geval is dat
evenveel keer als dat hij buur is (zijn graad), wat een bovengrens van O(m lg n) geeft.
 In totaal dus een performantie van O((n+m)lg n), met n=O(m) dus tenslotte O(m lg n)
Ipv een gewone heap kan men ook een fibonacci heap gebruiken (speciaal voor dit soort
toepassingen ontworpen), met een geamortiseerde performantie van O(1) voor verminderen van
prioriteiten en toevoegen, en O(lg n) voor verwijderen. De totale performantie zou dan dalen tot
O(m + n lg n), maar de ingewikkelde implementatie van deze soort heap zorgt voor te grote
verborgen constanten om praktisch bruikbaar te zijn.
H10: Kortste afstanden
Met ‘kortst’ bedoelt men meestal ‘minste gewicht’, maar soms is wel degelijk enkel het aantal
verbindingen van belang.
Men kan zoeken naar de kortste afstand van alle knopen naar één knoop, of de kortste afstand
tussen elk paar knopen, of vanuit één knoop naar alle andere knopen. Elk van die problemen kan
meestal teruggeleidt worden naar dit laatste probleem.
10.1 Kortste afstanden vanuit één knoop
(we zullen hier onderstellen dat de gewichten positief zijn, tenzij anders vermeld)
Niet gewogen grafen
Bij grafen zonder gewichten (of waar die gewichten genegeerd worden), wordt afstand gedefinieerd
als het aantal verbindingen. Om systematisch vanuit een knoop de kortste afstanden te vinden,
volstaat het om alle knopen te vinden die via 1 verbinding bereikbaar zijn, dan via 2, enz. Dit is
analoog aan breedte-eerst zoeken. Telkens wanneer een knoop ontdekt wordt, is zijn afstand vanuit
de wortel gwn +1. Deze afstanden kunnen dan in een tabel bijgehouden worden.
Algoritme van Dijkstra
(vereist dat de gewichten positief zijn en ’t is een greedy algoritme)
De kortste wegen vanuit het vertrekpunt vormen ook hier een (overspannende) boom van de graaf.
Het algoritme bouwt deze boom stap voor stap op, door ook hier 3 groepen te onderscheiden. Zwart
= knopen die reeds deel uitmaken van de boom, grijs = randknopen die rechtstreeks buur zijn van
een zwarte knoop, en wit = de rest.
Voor elke randknoop wordt de (voorlopig) kortste afstand vanuit het vertrekpunt vanuit het
vertrekpunt bijgehouden. Telkens wordt de randknoop met de kleinste voorlopige afstand van alle
randknopen geselecteerd en in de boom opgenomen. Deze knoop krijgt dan zijn definitieve afstand
toegewezen, en zo nodig worden nu de rest van de randknopen aangepast. Zo tot alle knopen in de
boom zitten.
Waarom werkt dit? Wanneer de randknoop met de kleinste afstand geselecteerd wordt, is dat
meteen zijn definitieve afstand. Een kortere weg zou namelijk enkel mogelijk zijn via knopen die nog
niet in de boom zitten. Maar dat is onmogelijk, want de afstand tot de andere randknopen zijn zoiezo
groter of gelijk (anders zou onze knoop niet gekozen zijn), en de overige knopen zijn oftewel buren
van die randknopen, of liggen zelfs nog verder dan dat. Die weg is dus zoiezo niet korter, en
bovendien komt er dan nog een verbinding bij. (hier gaat dit dus niet op als we met negatieve
gewichten zouden werken)
Opnieuw worden de randen met de afstanden begehouden, en wel zo dat de kleinste er effectief
uitgeplukt kan worden. Net zoals bij Prim is dit met een prioriteitswachtrij, en met dezelfde
voorzieningen (tabel) om de prioriteit van de knopen in de wachtrij efficiënt te kunnen aanpassen.
De performantie is dus ook dezelfde als bij Prim, en wordt op exact dezelfde manier beschreven in de
cursus (zie Prim voor het te leren) Hier is het dus ook O((n+m)lg n), maar ze vereenvoudigen het hier
precies niet verder. Mss is hier niet n = O(m)? Geen idee...
10.2 Kortste afstanden tussen alle knopenparen
In principe zouden we dit kunnen oplossen met Dijkstra, door het toe te passen op alle knopen. Voor
ijle grafen is de performantie dan O(n(n+m)lg n), voor dichte grafen O(n³). Voor dichte grafen is het
algoritme van Floyd-Warshall, hoewel O(n³), in de praktijk sneller, en laat het bovendien ook
negatieve gewichten toe.
Het algoritme van Floyd-Warshall
Als we Dijkstra herhaaldelijk zouden toepassen hiervoor, zou die regelmatig dubbel werk verrichten
aangezien men regelmatig afstanden tegenkomt die ook nuttig zijn voor de volgende knoop(knopen).
Zo gebeurt er veel nutteloos werk. Floyd Warshall gaat dit proberen tegengaan door alle voorlopige
afstanden bij te houden. Dit is een voorbeeld van dynamisch programmeren.
Dynamisch programmeren is een belangrijke algoritmische methode om optimale oplossingen te
vinden voor combinatorische problemen. Net zoals bij verdeel-en-heers methodes vereist ze dat het
probleem een optimale deelstructuur heeft (een optimale oplossing moet bestaan uit optimale
oplossingen voor deelproblemen, die onafhankelijk moeten zijn). In sommige gevallen kan de
verdeel-en-heersmethode meermaals dezelfde deelproblemen oplossen, deze zijn dan overlappend,
omdat ze bijdragen aan de oplossing van verscheidene andere deelproblemen.
Dynamisch programmeren vermijdt dit overbodig werk door de oplossingen van alle mogelijke
deelproblemen op te slaan in tabelvorm, zodat ze later snel kunnen teruggevonden worden.
Een probleem komt dus in aanmerking voor dynamisch programmeren als het een optimale
deelstructuur heeft en de deelproblemen overlappend zijn. Dat is hier zoiezo het geval: een kortste
weg bestaat uit kortste wegen, die ook deelwegen kunnen zijn van andere kortste wegen.
We hebben 3 matrices waarmee we werken
-
-
De N x N burenmatrix met de gewichten tss alle knopen (0 bij zichzelf en oneindig als er geen
verbinding bestaat)
De N x N afstandenmatrix waarin de kortste afstanden tussen alle knopen staan (element
a(i,j) is de afstand tss knoop i en j). Nu hebben we de afstand van de kortste wegen maar niet
de wegen zelf, dus:
De voorlopermatrix waarin element v(i,j) de voorloper is van knoop j op een kortste weg
vanuit knoop i. Mbv deze en de vorige matrix kunnen we nu altijd de kortste weg
construeren.
Dit algoritme laat negatieve gewichten toe, maar geen negatieve lussen.
Het maakt gebruik van de eigenschap dat deelwegen van een kortste weg zelf ook kortste wegen zijn
(tussen hun eigen eindknopen).
Centraal in dit algoritme staan de intermediaire knopen op een weg tussen de eindknopen i en j (de
knopen die verschillen van i en j). Bij elke iteratie mag een nieuwe knoop als intermediaire knoop
gebruikt worden, totdat alle knopen als intermediaire knopen toegestaan zijn.
Als we nu kortste wegen hebben voor alle knopenparen, maar die enkel gebruik mochten maken van
de intermediaire knopen 1, 2, ... k-1. Als we nu een extra intermediaire knoop k toevoegen, hoe
leiden we dan de nieuwe kortste afstand af uit de vorige?
(p.109 heeft 2 puntjes die trachten de werking uit te leggen, maar Wikipedia deed het iets duidelijker.
Ik hoop dat dit ook juist is als ik het zo uitleg maar ik denk het wel)
Onderstel dat w(i,j) de kortste weg tussen i en j is, die nu ook knoop k mag gebruiken.
Oftewel is het echte kortste pad hetgene dat enkel de intermediaire knopen 1 tot k-1 gebruikte en
kennen we het al. Of er is inderdaad een nieuwere kortste weg nu dat we k erbij hebben gekregen. In
dat geval bestaat die weg uit de wegen w(i,k) en w(k,j). De lengte van deze twee zijn reeds bekend (ik
veronderstel omdat k een intermediaire knoop is, zijn eigenschappen en afstanden zijn dus al
onderzocht), dus als dat pad daadwerkelijk kleiner is, vervangen we het oude pad gewoon door dat
pad.
Zo gaat Floyd-Warshall dus beginnen met het kortste pad te bepalen voor (i,j) met maar 1
intermediaire knoop, dan voor 2, en zo tot n knopen. Op het einde hebben we dan het kortste pad
voor alle paren, gebruik makend van alle intermediaire knopen.
Om de oplossingsmatrix te berekenen wordt een reeks afstandsmatrixen berekend, met telkens
meer intermediaire knopen die toegelaten zijn. De laatste laat alle knopen toe en is dus de gezochte
oplossingsmatrix. Elke matrix wordt afgeleid uit zijn vorige (buiten A(1) want A(0) laat nix toe dus is
eigenlijk de burenmatrix). Niet al die matrices moeten opgeslaan worden, 2 opeenvolgende is
voldoende. Eigenlijk maar 1 aagezien de berekening ter plaatse kan gebeuren (geen idee wrm)
De performantie is duidelijk O(n³) (gn idee wrm, mss iets van O(n) voor alle matrices te initten, en n
keer alle matrices initten is n², ma van waar die ³ ... ). De verborgen constante is echter klein zodat
het zelfs voor middelmatige grote n goed bruikbaar is.
H11: Gerichte grafen zonder lussen
(= directed acyclic graph = DAG)
Zien er vanuit elke knoop eigenlijk uit zoals een boom, dus eigenlijk half graaf half boom. Efficiënt
testen of een graaf lusvrij is kan bijv via diepte-eerst zoeken (dan zijn er geen terugverbindingen)
11.1 Topologisch rangschikken
Topologisch rangschikken plaatst alle graafknopen zo op een horizontale as, dat alle verbindingen
naar rechts wijzen. Bijv als de graaf een verbinding (i,j) heeft, zal de knoop j steeds rechts staan van i,
wat natuurlijk onmogelijk is bij lussen. Het kan ook gebruikt worden om te testen of er lussen
voorkomen. Een topologische ordening is wel niet noodzakelijk uniek, omdat er knopen zonder
onderlinge relatie kunnen voorkomen. Hoe gaan we zo rangschikken? Daar zijn 2 manieren voor.
Via diepte eerst zoeken
Wanneer via diepte-eerst zoeken een knoop is afgewerkt, zijn alle verbindingen vanuit die knoop
behandeld, en die leiden allemaal naar reeds afgewerkte knopen (want boomtakken en
heenverbindingen wijzen naar beneden, in de deelboom waarvan de knoop wortel is, en de
dwarsverbindingen wijzen naar links). Maw als als een afgewerkte knoop op de as geplaatst wordt,
moeten alle vroeger afgewerkte knopen rechts daarvan liggen.
De postordernummering geeft de volgorde van afwerken. Alle knopen rechts van een knoop moeten
dus een lager postordernummer hebben.
Omdat postordernummeren niet veel meer is dan diepte-eerst zoeken, is de performantie O(n+m)
voor ijle grafen.
Via ingraden
Dit algoritme begint met het berekenen van alle ingraden. Een knoop zonder binnenkomende
verbinding (ingraad 0) mag direct op de as geplaatst worden. Eender welke knoop uit de rest die
ingraad nul heeft, mag er nu rechts geplaatst worden, aangezien enkel de daarvoor verwijderde
knoop een verbinding ernaar kon hebben. Moest dat niet zo zijn dan mocht die knoop niet op de as
gezet worden, aangezien er dan daarna nog een knoop komt wiens ‘pijl’ dan naar links zou moeten
wijzen. Maar dit is dus niet zo. Zo ook dan voor de volgende knoop met ingraad nul, want enkel de 2
vorige knopen konden nar hem wijzen. Zo gaan we verder tot ze er allemaal op de as staan.
Maw moeten we bij het verwijderen van elke knoop, de ingraden van al zijn buren met 1
verminderen (al de knopen waar hij naar wees). Elke knoop waarvan de ingraad dan nul werd of al
nul was, kan dan geplaatst worden. Deze kunnen opgeslagen worden in een stack of queue, en dan
zo afgehandeld worden. Elke keer als we een ingraad verminderen bij een knoop, moeten we dus
testen of die nul is geworden, om te zien of ie moet toegevoegd worden in de stack/queue. Naar hoe
ik het versta uit de cursus, doet de volgorde van het plaatsen van de nulknopen er niet toe. Kdenk
niet dat de verbindingen maar 1 naar rechts mogen wijzen, mag ook over verschillende knopen gaan,
zolang het maar naar rechts is. Dus de volgorde zal idd niet uitmaken.
Wat de performantie betreft: elke knoop wordt eenmaal aan de stack/queue toegevoegd en
verwijdert, dus dat is O(n). Van elke knoop moeten de buren behandeld worden, wat O(m) is (m =
aantal verbindingen). Het berekenen van de ingraden zelf is O(n+m). Waarom juist wetek niet, ik
denk O(n) voor alle ingraden in het begin te berekenen, en O(m) om voor elke verbinding een ingraad
af te trekken? Ma kzou denke da da dezelfde O(m) als derboven is.. Kweet ni juist, ma in ieder geval
O(n) + O(m) + O(n+m) maakt dat het in totaal O(n+m) is.
Wat gebeurt er als we dit algoritme gebruiken op een graaf met een lus? Ik denk (het antwoord staat
niet in de cursus) dat het algoritme vastloopt na een tijdje, omdat er geen knopen meer zijn met
ingraad=0. (teken maar is uit voor 3 knopen een een cirkel)
11.2 Kortste afstanden vanuit één knoop
Hoe berekenen?
Bij DAG’s is de kortste afstand steeds gedefinieerd, ook als er negatieve gewichten zijn. Er zijn
immers geen lussen, dus ook geen negatieve lussen.
Dijkstra: vond de kortste afstand tot de knopen in stijgende volgorde. Verder gelegen knopen konden
de kortste afstand tot een knoop niet verbeteren, aangezien alle gewichten postief waren.
Hier is het nog eenvoudiger: knopen die rechts liggen van een knoop in topologische volgorde,
kunnen niets beters meer opleveren voor de afstand, aangezien alle verbindingen naar rechts
wijzen. Het volstaat dus om de graaf topologisch te rangschikken, en dan de knopen in volgorde te
behandelen beginnende van de startknoop (niet noodzakelijk de meest linkse knoop!)
We houden opnieuw voorlopige afstanden en voorlopers bij voor alle nog niet behandelde knopen.
Als we een knoop behandelen kennen we zijn definitieve afstand en voorloper. Deze nieuwe kortste
afstand kan de voorlopige afstanden van zijn buren nog verbeteren, want die liggen rechts van hem
op de as. Deze moeten dan, samen met hun voorlopers, aangepast worden.
Qua performantie: topologisch rangschikken is zoiezo O(n+m), initten van de afstanden en
voorlopers O(n), en alle buren van elke knoop komen eenmaal aan bod, dus O(m). Voor ijle grafen is
het totaal dus O(n+m)
Toepassing: Projectplanning
Stel dat we een erg complex project hebben, dat uit meerdere deeltaken bestaat, en waarvan
sommige taken voor andere taken moeten afgewerkt zijn, en dat de uitvoeringstijd van elke taak
gekend is. Dan kunnen we met DAG gaan bekijken welke taken hoeveel vertraging mogen oplopen
zonder het hele project te vertragen, hoelang het duurt om alles af te werken,etc.
Hiervoor stellen we een graaf op waarbij de knopen de taken voorstellen, en de verbindingen de
taakduur. Knopen zonder opvolgers worden allen verbonden naar een algemene eindknoop, zodat
ook hun taakduur kan weergegeven worden, en als er meerdere knopen met ingraad = 0 zijn, komt
er een nieuwe startknoop bij met alle verbindingen naar die knopen als gewicht 0. (zie p.114 voor
een figuur als voorbeeld, de onderste weliswaar)
Met deze DAG kunnen we nu een aantal dingen bepalen:
1. Minimale tijd om alles te voltooien: elke weg tss begin- en eindknoop stelt een bepaalde
volgorde van taken voor, met als tijdsduur de lengte van die weg (som v gewichten).
Aangezien we hier alles moeten voltooien, moeten we alle knopen hebben gehad, dus maw
de langste weg van al die wegen.
Men kan ze bepalen door de kortste afstanden te bepalen, maar dan met het gewicht
omgekeerd. Of door het algoritme van de kortste weg aan te passen door anders te initten
en de vergelijkingen om te keren. Performantie is natuurlijk ook gewoon O(n+m).
2. Vroegste aanvangstijd v/e taak: de langste weg tussen de beginknoop en die knoop (ook al
berekend bij het vorige)
3. Uiterste aanvangstijd van elke taak, zonder de minimumtijd voor het hele project te
veranderen. Dus voor de eindknoop is uiterste aanvangstijd = de vroegste. Kan berekend
worden als die van zijn buren gekend is, want deze tijd is immers het minimum van de
uiterste aanvangtijden van zijn buren min de taakduur van zichzelf. We moeten dus in
omgekeerde topologische volgorde werken. Deze werd reeds bepaald bij de vorige
eigenschappen, dus enkel nog initten O(n) en behandelen van de buren O(m), dus in totaal
O(n+m)
4. Vertraging die elke taak mag oplopen, zonder invloed op het hele project = het verschil van
de uiterste aanvangstijd met de vroegste aanvangstijd. Taken die geen vertraging mogen
oplopen zijn kritiek, en er bestaat minstens één weg waarop alle taken kritiek zijn (critical
path)
H12: Complexiteit – P en NP
Alle tot hiertoe behandelde problemen hadden een efficiënte oplossing. Meer bepaald, hun
uitvoeringstijd werd begrensd door een veelterm in het aantal gegevens n, zoals O(n³), of met de het
aantal verbindingen in een graaf erbij (O(n+m) bijv). Er bestaan echter ook problemen waar geen
efficiënte oplossingen voor zijn, of waar men ze nog niet van gevonden heeft.
We verdelen de problemen op in complexiteitsklassen, naargelang hun uitvoeringstijd of
geheugenvereisten (hier bekijken we enkel het eerste). We beperken ons tot beslissingsproblemen,
die ‘ja’ of ‘nee’ als resultaat hebben.
De klasse P bevat alle problemen waarvan de uitvoeringstijd begrensd wordt door een veelterm in
de grootte van het probeem (‘P’ van Polynoom), bij uitvoering op een realistisch computermodel.
Met ‘grootte’ bedoelt men het aantal bits om de invoer voor te stellen. Al de problemen in P worden
als efficiënt oplosbaar beschouwd.
Waarom gebruikt men veeltermen voor het onderscheid te maken met niet-efficiënt oplosbare
problemen? Daar zijn 3 redenen voor:
1. Als de uitvoeringstijd niet kan begrensd worden door een veelterm, dan is het probleem
zeker niet efficiënt oplosbaar.
2. Veeltermen zijn de kleinste klasse functies die kunnen gecombineerd worden, en opnieuw
veeltermen opleveren. Hun klasse is gesloten onder de operaties optelling, som, en
samenstelling (veelterm van een veelterm), en zal dus altijd een veelterm blijven opleveren.
Efficiënte algoritmen voor eenvoudigere problemen kunnen aldus gecombineerd worden tot
een efficiënt algoritme voor een meer complex probleem.
3. Als de tijd voor een bepaald computermodel begrensd wordt door een veelterm, zal dat ook
bij alle andere modellen zo zijn (zolang ze ‘realistisch’ blijven)
Problemen die (voorlopig) niet tot de klasse P behoren, kan met trachter op te lossen met een
(hypotetische) niet deterministische machine, die in staat is om tegelijk elke mogelijke oplossing in
polynomiale tijd (deterministisch) te testen. De klasse van problemen die niet op die manier kunnen
opgelost worden, noemt men NP (Niet-Deterministisch Polynomiaal). Men kan NP ook definiëren als
de klasse van alle problemen waarvan de oplossing efficiënt kan getest worden. Elk probleem uit P
behoort dus ook zeker tot NP.
We hebben ook een paar speciale problemen in NP:
-
NP-compleet: Elk van die problemen zijn minstens even ‘zwaar’ dan elk ander probleem uit
NP. Als er ook maar één NP-compleet probleem efficiënt oplosbaar zou zijn, zouden alle
problemen uit NP efficiënt oplosbaar zijn, zodat NP gelijk wordt aan P.
Men toont aan dat een probleem NP-compleet is door polynomiale reductie, waar men
tracht aan te tonen dat elk probleem in NP efficiënt kan getransformeerd worden tot het
nieuwe probleem. Zo ja, dan is dat nieuw probleem NP-compleet. Dit is een gigantische taak,
maar als er al een ander NP-compleet probleem gekend is, dan volstaat het om aan te tonen
dat men dat efficiënt kan reduceren tot ons nieuw probleem.
-
NP-hard: Problemen die minstens even ‘zwaar’ zijn als alle problemen in NP, maar zelf niet
tot NP behoren. Alle problemen in NP kunnen dus ook efficiënt gereduceerd worden tot elk
NP-hard probleem (geen idee wa ze daarmee bedoelen.. moeilijker maken?)
Als men ontdekt dat een bepaald probleem NP-compleet is, weet men dat men best niet naar de
oplossing gaat zoeken. Maar er zijn nog een aantal mogelijkheden:
1. Als de afmetingen van het probleem klein zijn(klein aantal invoergegevens) kan men toch alle
mogelijkheden onderzoeken, en daarbij trachten het werk zoveel mogelijk te beperken.
2. Zoeken naar efficiënte algoritmen om speciale gevallen van het probleem om te lossen.
3. De begrenzing van de uitvoeringstijd is voor het slechtste geval, misschien valt het
gemiddelde beter mee?
4. Regelmatig zijn er benaderende algoritmen die de vereisten voor de oplossing wat relaxeren,
al zijn die benaderingen soms zelf NP-compleet.
5. Tenslotte kan men efficiënte heuristische methoden gebruiken die meestal een gode, maar
niet noodzakelijk optimale, oplossing vinden.
Download