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.