Universiteit Twente Semester 2005/1 Afdeling Informatica 2 e huiswerkserie 13 december 2005 Algoritmen, Datastructuren en Complexiteit (214020/5) De deadline voor het inleveren van deze huiswerkserie (bij voorkeur een hard copy) bij de student-assistent is tijdens het practicum in week 2, dus in de periode 11-13 januari 2006, afhankelijk van de verroostering van uw practicum. Heeft u in een eerder studiejaar het practicum voltooid (graag vooraf melden, zie TeleTOP), dan stuurt u de uitwerking per e-mail naar g.f.vanderhoeven@utwente.nl. Bij de opgaven waar om een algoritme wordt gevraagd dient u zowel een beknopte en duidelijke beschrijving te geven van het principe van het algoritme alsmede een pseudo-codebeschrijving. Plagiaat zal streng worden bestraft. Er zijn 4 opgaven. Er zijn 90 punten te behalen. Het cijfer voor deze serie is (het aantal behaalde punten gedeeld door 10) plus 1. Veel succes! Opgave 1 8 1 6 3 5 7 4 9 2 7 5 3 2 9 4 6 1 8 3 4 9 1 5 7 2 6 8 7 2 6 3 4 8 5 9 1 9 4 2 1 8 6 5 3 7 6 1 5 2 7 9 4 8 3 15 pt Hier ziet u zes 3 3 vierkanten gevuld met de getallen 1 t/m 9, die allen in elk vierkant precies eenmaal voorkomen. Het feit dat in een aantal vierkanten de sommen van rijen en kolommen steeds 15 zijn, doet voor deze opgave niet terzake. Een verzamelaar van dergelijke vierkanten heeft een collectie van ongeveer 500 exemplaren en wil eventuele dubbele exemplaren daarin opsporen. Hij is op zoek naar een hashfunctie h die elk vierkant een index in een tabel toewijst. Hij heeft daarvoor twee ideeën. 1. Vervang elk getal in het vierkant door zijn pariteit (d.w.z. 0 voor een even getal, 1 voor een oneven getal), interpreteer vervolgens elke kolom van boven naar beneden gelezen als een binaire representatie van een element uit de verzameling {0, 1, 2, 3, 4, 5, 6, 7} en beschouw het resulterende drietal (van links naar rechts gelezen) als de representatie van een waarde in het achttallig stelsel (d.w.z. x 0, x 1, x 2 representeert 64x 0 +8x 1 +x 2 ). Hieronder ziet u hoe de zo geconstrueerde h de waarde 186 vindt bij het eerste voorbeeldvierkant. 1
8 1 6 3 5 7 4 9 2 0 1 0 1 1 1 0 1 0 2 7 2 186 Wat zijn de indices in de tabel die deze hashfunctie voor de andere voorbeeldvierkanten vindt, uitgaand van open addressing en linear probing met h (k, i) = (h(k) + i) mod 511? 2. Vervang elke kolom in het vierkant door een permutatie van {1, 2, 3}, die wordt verkregen door de getallen in de kolom op volgorde te nummeren. Zet de permutatie van boven naar beneden gelezen om in een getal uit de verzameling {0, 1, 2, 3, 4, 5} en beschouw het resulterende drietal (van links naar rechts gelezen) als de representatie van een waarde in het zestallig stelsel (d.w.z. x 0, x 1, x 2 staat voor 36x 0 +6x 1 +x 2 ). De omzetting van een permutatie naar een getal is op basis van de volgende formule: x 0, x 1, x 2 wordt afgebeeld op 2(x 0 1) als x 1 < x 2 en op 2(x 0 1) + 1 anders. Hieronder ziet u hoe de zo geconstrueerde h de waarde 147 vindt bij het eerste voorbeeldvierkant. 8 1 6 3 5 7 4 9 2 3 1 2 1 2 3 2 3 1 4 0 3 147 Wat zijn de indices in de tabel die deze hashfunctie voor de andere voorbeeldvierkanten vindt, uitgaand van open addressing en linear probing met h (k, i) = (h(k) + i) mod 215? 3. Kunt u een oordeel formuleren over de kwaliteit van deze twee mogelijk hashfuncties? Betrek de omvang van de tabel, de methode van open addressing en de verdeling van vierkanten over indices in uw beschouwing. 2
Uitwerking opgave 1 1. De volgende tabel geeft de antwoorden op de onderdelen 1 en 2. vierkant 8-tallig index 6-tallig index A 272 186 403 147 B 474 316 430 162 C 626 406 404 148 D 711 457 403 147 148 149 E 711 457 458 430 162 163 F 067 55 403 147 148 149 150 Voor alle duidelijkheid: in de kolom 8-tallig staat de kolomsgewijze vertaling van het vierkant naar een getal in het 8-tallig stelsel, zoals bedoeld in onderdeel 1. In de kolom 6-tallig staat de overeenkomstige vertaling van onderdeel 2. De twee kolommen index geven de conversie van deze representatie naar indices in de tabel aan, met zonodig probing. Waar sprake is van probing zijn de opeenvolgende probes aangegeven, en het uiteindelijke resultaat is vetgedrukt. 2. De twee relevante verschillen zijn de volgende. Methode 2 gebruikt een surjectieve functie met een volstrekt evenwichtige verde-ling. Op iedere index worden vierkanten afgebeeld, het aantal vierkanten bij een index is voor iedere index hetzelfde. Dat is een goede eigenschap voor een hashfunctie. Methode 1 is in dat opzicht veel minder fraai. Veel 8-tallige codes corresponderen helemaal niet met vierkanten, omdat na de pariteitsvertaling ( er ) altijd 5 enen en 4 nullen in 9 het vierkant zullen staan. Er zijn dus maar indices die met een groep 5 vierkanten corresponderen. Een getal dat 8-tallig geschreven kan worden met alleen de cijfers 0, 1, 2 en 4 zal bijvoorbeeld nooit rechtstreeks index van een vierkant zijn (dat zou maximaal 3 enen in het hele vierkant betekenen). Er zijn weinig (126) indices in de tabel die rechtstreeks corresponderen met een groep vierkanten, de functie is niet surjectief. Hij biedt wel een evenwichtige verdeling, als er bij indices vierkanten horen, dan zijn het er steeds evenveel. Maar het feit dat veel indices in eerste instantie ongebruikt blijven, is geen goede eigenschap voor een hashfunctie. Methode 2 is, ondanks zijn kwaliteiten als hashfunctie, toch onbruikbaar. De ruimte in de tabel is namelijk veel te klein om alle vierkanten (er is sprake van 500) in op te slaan. Chaining zou hier wel werken, maar probing niet. In dit opzicht is methode 1 veel beter. Omdat hij het gebrek heeft dat hij niet surjectief is, zal er vaak sprake zijn van collisions, en het aantal probes om iets te vinden of op te slaan zal daardoor hoog zijn, maar je kunt in de tabel waarschijnlijk wel alle vierkanten kwijt. 3
Opgave 2 1. Schrijf een algoritme dat een left shift uitvoert op een binaire boom. Dat wil zeggen: uit een gegeven boom T wordt het eerste element als resultaat opgeleverd, terwijl de boom en zijn inhoud veranderen. De laatste knoop uit de boom wordt verwijderd, in alle andere knopen wordt de oorspronkelijk waarde vervangen door de waarde van de opvolgerknoop. Neem als voorbeeld een binaire boom met drie knopen, een wortel waar twee bladeren van afstammen. Het gevraagde algoritme levert dan de waarde van het linkerblad op. Bovendien verandert het de boom. De waarde van de wortel schuift door naar het linkerblad en de waarde van het rechterblad schuift door naar de wortel. Het rechterblad verdwijnt, er blijft een boom over met twee knopen, namelijk een wortel waar alleen een linkerblad van afstamt. U moet uitgaan van gegeven klassen bintree, node en elt met de volgende features : node root voor bintree-objecten, node left, right, parent en elt value voor node-objecten. Uw algoritme mag geen gebruik maken van een willekeurige lijst of rij van eltobjecten waarin waarden uit de boom worden opgeslagen. U mag wel gebruik maken van een stack of een queue en hun operaties elt pop( ), elt dequeue( ), void push(elt e), void enqueue(elt e) en bool isempty( ) voor de tijdelijke opslag van waarden uit de boom. In de aanduidingen eerste knoop, laatste knoop en opvolgerknoop wordt aan de inorder-ordening van de knopen van de boom gerefereerd. 25 pt 2. Beschrijf de (asymptotische) complexiteit van uw algoritme, zowel in tijd als in ruimte. 4
Uitwerking opgave 2 Veel oplossingen zijn denkbaar voor onderdeel 1, de bedoelde oplossing is de volgende (toelichting volgt). elt Lef tshif t(bintree t){ stack s = new stack( ); node n = t.root; if (n nil ) if (n.right == nil ){ t.root = n.left; if (n.left == nil ) return n.value else {s.push(n.value); return ls(n.lef t, s)}} else return ls(n, s) } // wortel is laatste knoop //wortel is enige knoop //wortel is opvolger linker subboom elt ls(node n, stack s){ elt x = n.value; if (n.right == nil ){ if (s.isempty( )) n.parent.right = n.lef t else n.value = s.pop( ) else n.value = ls(n.right, s); if (n.left == nil ) return x else {s.push(x); return ls(n.lef t, s)} } // laatste knoop in subboom // laatste knoop van de gehele boom //knoop is eerste knoop van subboom // opvolger linkersubboom 5
De feitelijke left shift wordt uitgevoerd door aanroepen van ls, die de left shift uitvoert op de afzonderlijke knopen van de boom en daarvoor als extra parameter een stack meekrijgt met waarden van knopen uit de boom. De volgorde waarin knopen van de boom worden afgehandeld is right-to-left-inorder. De stack heeft de volgende rol: op de top van de stack ligt het eerste element uit de boom dat volgt op de waarden in de subboom waarvan de huidige knoop de top is. Dit eerstvolgende element (de top van de stack dus), moet uiteindelijk terechtkomen in de laaste knoop van de huidige subboom. Veranderingen in de stack treden op: als ls recusrsief wordt aangeroepen op het linkerkind van de huidige knoop. De waarde van de huidige knoop wordt dan op de stack gelegd. als ls ontdekt dat de huidige knoop de laatste knoop van de huidige subboom is. De top van de stack wordt weggenomen en als nieuwe waarde in de huidige knoop geplaatst. Waarden van knopen veranderen als volgt: als de knoop een rechterkind heeft dan is de nieuwe waarde van de knoop het resultaat van de recursieve aanroep van ls op dit rechterkind als het rechterkind van de knoop ontbreekt wordt de nieuwe waarde van de knoop van de stack gehaald Het resultaat van ls wordt als volgt bepaald: als de huidige knoop een linkerkind heeft, dan is het resultaat het resultaat van de recursieve aanroep van ls op dit linkerkind ontbreekt het linkerkind, dan is de waarde van de huidige knoop het opgeleverde resultaat Tenslotte, het verwijderen van de laatste knoop is als volgt georganiseerd: Als de huidige knoop geen rechterkind heeft, en de stack is leeg, dan is de huidige knoop de laatste knoop van de boom. Hij wordt verwijderd door het linkerkind van de huidige knoop tot het rechterkind van de vaderknoop te maken (hij moet ook zelf het rechterkind geweest zijn!) Als de wortel van de boom de laatste knoop blijkt te zijn (geen rechterkind, stack is dan sowieso leeg), moet de verwijdering op een speciale manier plaatsvinden, niet door een verandering van rechterkind voor de parent, maar door een verandering van de root van de gehele boom. Dit wordt niet door ls, maar voorafgaand aan aanroepen van ls binnen LeftShift afgehandeld. 6
De manier waarop de opgave is gesteld, bevat de suggestie dat er iets met een queue te doen zou zijn. Die suggestie van de queue is in de tekst van de opgave terechtgekomen omdat niet al te expliciet in een bepaalde denkrichting (stack) te sturen. Maar natuurlijk kan de queue ook gebruikt worden als alternatief voor het array dat in de opgave werd verboden. Haal alle elementen inorder uit de boom naar de queue (eerste doorloop), haal het eerste element uit de queue (het opgeleverde resultaat) en plaats de rest weer terug in de boom (tweede doorloop). Dat was dus niet de bedoeling (maar wel goed). Oplossingen die het probleem aanpakken met een grote hoeveelheid doorlopen van de boom (je kunt natuurlijk vanuit elke knoop gewoon de opvolger gaan ophalen) zijn overigens ook goed. Ten aanzien van de complexiteitsvraag (onderdeel 2): het goede antwoord hangt af van de gegeven oplossing. Bij de oplossing hierboven is de tijdscomplexiteit lineair in het aantal knopen van de boom, iedere knoop wordt 1 maal bezocht, bij ieder bezoek wordt een begrensd aantal operaties uitgevoerd (assignments, controles op het nil zijn van referenties naar kinderen (maximaal 2), controles op het leeg zijn van de stack (maximaal 1 keer), pushes en pops op en van de stack (van beide maximaal 1). De ruimtecomplexiteit is evenredig aan de diepte van de boom, dat is de ruimte die door de stack wordt ingenomen (en ook de ruimte die nodig is om de recursie af te handelen). Als n het aantal knopen van de boom is, en we schrijven T (n) en S(n) voor tijd en ruimtecomplexiteit, dan geldt worst-case voor deze oplossing: T (n) Θ(n), S(n) Θ(n), maar voor een gebalanceerde boom kunnen we (best-case), veel scherper, zeggen S(n) Θ(log n). (De uitspraak die klopt voor alle gevallen is dus S(n) O(n).) 7
Opgave 3 1. Schrijf een algoritme dat onderzoekt of een gegeven gerichte graaf een (ongeordende) boom is. Een gerichte graaf is een (ongeordende) boom als aan de volgende eisen voldaan is: 25 pt (a) Hij bevat geen cykels. (b) Hij bezit een unieke knoop R, de wortel, zodanig dat elke andere knoop vanuit R via precies 1 pad te bereiken is. U kunt zelf de representatie van de gegeven graaf (adjacency lists, adjacency matrix,..) kiezen. 2. Beschrijf de (asymptotische) complexiteit van uw algoritme, zowel in tijd als in ruimte. Uitwerking opgave 3 Ook hier zijn verschillende oplossingen mogelijk, de bedoelde oplossing is de volgende (toelichting volgt). } bool IsT ree(intlist AdjV ; int n){ int [1 : n] color; bool IsF orrest = true, IsT ree = false ; int k = 1; for (i = 1; i n; i + +) color[i] = white; while (k n && IsF orrest){ for each (j AdjV [k]) if (color[j] = black) IsF orrest = false else color[j] = black; k = k + 1} if IsF orrest{ k = 1; IsF orrest = false ; while (k n &&!IsF orrest){ if (color[k] = white) if (IsT ree) IsF orrest = true else IsT ree = true ; k = k + 1}; return IsT ree &&!IsF orrest} else return false 8
Het algoritme bestaat uit twee doorlopen van de graaf. De eerste doorloop voert langs alle kanten van de graaf om de knopen te kleuren, en bijzonderheden tijdens dat kleuren te signaleren. De essentie van het kleuren is: nagaan of er geen knopen zijn met meer dan 1 inkomende pijl. Als een inkomende pijl naar een knoop wordt gedetecteerd, kleuren we deze knoop zwart. Daarvoor dient het array color. Als naar een eerder zwart gekleurde knoop een tweede pijl wordt ontdekt, is een probleem gesignaleerd. Voor de signalering van dat probleem dient de variabele IsForrest. De tweede doorloop voert langs alle knopen, om vast te stellen dat er precies een knoop is overgebleven die in de eerste doorloop niet is gekleurd. Dat moet de wortel van de boom zijn. Het vinden van de eerste ongekleurde knoop wordt gesignaleerd door de variabele IsTree, de (hergebruikte) variabele IsForrest signaleert een probleem: er is een tweede ongekleurde knoop gevonden. Beide doorlopen worden (uiteraard) afgebroken als een probleem is gesignaleerd. Belangrijk voor het begrip van de correctheid van deze aanpak is de constatering dat de ongeordende boom zoals die hier bedoeld wordt, precies is gekarakteriseerd door de eigenschap dat al zijn knopen op 1 na precies 1 inkomende pijl hebben (indegree =1), en dat de unieke knoop die deze eigenschap niet heeft de wortel is, waar geen enkele pijl naar toe gaat. Voor wat betreft onderdeel 2. Het antwoord hangt af van de gegeven uitwerking. Het algoritme zoals het hier gepresenteerd wordt, heeft een worst-case tijds- en ruimtecomplexiteit van O(N + E), waarin N het aantal knopen is, en E het aantal kanten. Voor de tijdscomplexiteit volgt dat onmiddelijk uit de beschrijving van het algoritme (twee doorlopen, eerst alle kanten, daarna alle knopen). Voor de ruimtecomplexiteit: de omvang van het array met adjacency lijsten is evenredig aan N + E, de lijst kleuren is zo groot als N. Maar: als de gegeven graaf inderdaad een boom is, zijn N en E even groot. Als het geen boom is, zal het algoritme bij het doorlopen van de verzameling kanten, nadat N kanten zijn afgewerkt een knoop ontdekken met indegree groter dan 1. Daar zal het stoppen. Voor de tijdscomplexiteit kunnen we zelfs concluderen dat die worst-case Θ(N) is (en in het algemeen O(N)). 9
Opgave 4 We beschouwen het volgende probleem. Gegeven is een spoorwegemplacement met baanvakken en wissels, waarop zich een locomotief en een aantal wagons bevinden. Een verplaatsing op het emplacement is een beweging van de locomotief alleen, of van de locomotief met daaraan gekoppelde wagons. Veronderstel een beginpositie van de wagons en de locomtief verspreid over baanvakken, en definieer een gewenste eindpositie. Veronderstel dat alle wagons identiek zijn, voor begin- en eindpositie is alleen het aantal wagons per baanvak relevant, welke wagons het precies zijn doet niet terzake. Is er een reeks verplaatsingen die van de begin- naar de eindpositie leidt, en wat is de kortste reeks verplaatsingen die daarvoor nodig is? 25 pt 1. Voor veel problemen biedt een (gerichte) graaf een goed model. Kunt u de knopen en kanten van een graaf beschrijven die een goed model is voor de hier voorgelegde vraag? Aanwijzing: de structuur van het emplacement is voor deze graaf van ondergeschikt belang. Kunt u uw graaf concreet maken in een heel eenvoudige situatie (bijvoorbeeld 1 wagon, 1 locomotief, 1 wissel, 3 baanvakken). 2. Kent u een graafalgoritme dat het antwoord op de vraag naar de optimale reeks verplaatsingen kan leveren, als we het toepassen op uw graaf? Wat kunt u zeggen over de complexiteit van dat algoritme in termen van het aantal baanvakken (noem dit aantal n), voor situaties waarin 1 locomotief en 2 wagons aanwezig zijn. En wat is de complexiteit als er 3 wagons zijn en 1 locomotief? 10
Uitwerking opgave 4 1. Voor de beantwoording van de gestelde vraag naar (optimale) bewegingen op het emplacement is een toestandsgraaf het handige model. De knopen van de toestandsgraaf zijn situaties op het emplacement: hoe is de verdeling van wagons en locomotief over de baanvakken (in een knoop zit dus heel veel informatie verborgen, en er zullen al heel gauw heel veel knopen zijn). De kanten (pijlen) in de graaf verbinden twee knopen als de situatie uit de ene knoop door 1 beweging van de locomotief (al dan niet met een aantal wagons) naar de sitatie van de andere knoop leidt. In deze bijzondere toestandsgraaf zijn alle overgangen omkeerbaar, alle pijlen lopen twee kanten uit. Voorbeeld: drie baanvakken A,B en C. Via de wissel kan je rijdend naar rechts van A naar B of van A naar C. Vanuit C kan je via de wissel, rijdend naar links, alleen naar A, en voor B geldt hetzelfde. Situaties (knopen van de toestandsgraaf zijn de volgende 12: aa, Aa, Ab, Ac ab, bb, Bb, Bc ac, bc, cc, Cc Dit is als volgt te begrijpen. De hoofdletter geeft het baanvak van de locomotief, de kleine letter die van de wagon. Als beide zich in hetzelfde baanvak bevinden, is de onderlinge positie significant. In Aa staat de wagon rechts van de locomotief, aan de kant van de wissel, in aa staat niet de wagon maar de locomotief aan de kant van de wissel en de wagon links van de locomotief. De volgende tabel laat wat overgangen (kanten) zien van naar aa ab, ac, bb, cc Aa Bb, Cc Ab Bb, Cb Ac Bc, Cc ab aa bb aa Bb Aa, Ab Bc Ac enz... 2. Het bekende algoritme dat inzicht geeft in de mogelijkheid om van de ene situatie naar de andere te komen, en dat ook kan tellen hoeveel stappen daarvoor minimaal nodig zijn, is het algoritme van Floyd (all pairs). Als we de beginsituatie fixeren helpt ook Dijkstra. Voor de toepassing van beide moet het gewicht van elke overgang op 1 gesteld worden (dan is het bepalen van afstand namelijk gewoon het tellen van stappen). Om een uitspraak te doen over de complexiteit van deze algoritmen, uitgedrukt in het aantal baanvakken van het emplacement, moeten we begrijpen hoe we uit het aantal baanvakken (n) het aantal situaties op het emplacement en dus het aantal knopen van de graaf (N) zullen bepalen. 11
De formule voor het aantal mogelijke situaties bij 2 wagons en 1 locomotief op n baanvakken is n(n + 1 + 1 2n(n + 1)). Dat is eenvoudig in te zien. Plaats eerst ergens de locomotief, dat kan op n manieren. Nu zijn er n + 1 posities voor elk van de wagons (immers de locomotief heeft een baanvak in tweeën gesplitst). De 2 wagons kunnen op op n + 1 manieren samen in hetzelfde baanvak worden geplaatst, en op 1 2n(n + 1) in twee verschillende baanvakken. M.a.w. het aantal knopen N van de toestandsgraaf voor twee wagons en 1 locomtief op n baanvakken is in Θ(n 3 ). Met onze kennis van de complexiteit van het algoritme van Floyd (Θ(N 3 )) concluderen we dat alle mogelijke overgangen en het aantal benodigde stappen daarbij volledig kunnen berekenen in een tijd T (n) Θ(n 9 ) (voor 2 wagons en n baanvakken). Voor drie wagons wordt de ordegrootte in n 12, in plaats van n 9. In zijn algemeenheid geldt dat k wagons plus locomotief op n baanvakken geplaatst kunnen worden op N manieren, met N Θ(n k+1 ), gegeven de bekende tijdscomplexiteit van het algoritme van Floyd is het antwoord op de vraag naar de tijdscomplexiteit uitgedrukt in het aantal baanvakken dus in zijn algemeenheid: T (n) Θ(n 3k+3 ). Commentaar Er wordt gevraagd naar een complexiteitsbeschouwing in termen van het aantal baanvakken In het bovenstaande staat uitgewerkt wat het asymptotisch gedrag is voor 2 wagons, en voor k wagons. Daarbij is (gezien het feit dat het asymptotische in deze setting slaat op het aantal baanvakken, dat zeer groot mag worden gedacht) het terechte uitgangspunt dat het aantal wagons kleiner is dan het aantal baanvakken. Maar het realistische probleem is misschien precies het omgekeerde van wat hier gevraagd wordt: druk de complexiteit uit in termen van het aantal wagons (voor een vast aantal baanvakken). Maar die meer realistische vraag is niet gesteld, dus dit commentaar is een terzijde. 12