Formele talen en automaten [1 ed.] [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Introductie tot de cursus 1

2

3 4

5

Plaats en functie van de cursus 7 1.1 Positie in de opleiding 7 1.2 Functie 7 Cursusmateriaal 8 2.1 Tekstboek 8 2.2 Werkboek 8 2.3 Software 9 2.4 Cursussite 9 Planning 10 Inhoud 10 4.1 Opbouw van de cursus 10 4.2 Studielast 11 4.3 Leerdoelen 11 Tentaminering 11

Introductie tot de cursus

Om u wegwijs te maken in de cursus Formele talen en automaten informeren wij u eerst over de bedoeling van de cursus, de samenstelling en inhoud van het cursusmateriaal, de manier waarop u de cursus kunt bestuderen en de wijze waarop deze getoetst wordt. 1

Plaats en functie van de cursus

De cursus Formele talen en automaten is een cursus met een studielast van 5 EC, wat overeenkomt met circa 140 uur in totaal. In paragraaf 4.1 vindt u een specificatie van de studielast. 1.1

POSITIE IN DE OPLEIDING

De cursus is een verplicht onderdeel van de bacheloropleiding Informatica. Voorkennis

Om de cursus te kunnen bestuderen, veronderstellen we de volgende voorkennis: – basiskennis van verzamelingen, grafen, bomen, relaties, functies, propositie- en predikaatlogica en bewijzen – kennis van een programmeertaal is wel handig (in verband met het begrijpen van toepassingen van formele-talentheorie), maar niet noodzakelijk.

Ingangseis

Als ingangseis geldt daarom de cursus Logica, verzamelingen en relaties (IB0402). 1.2

FUNCTIE

Een van de gebieden waar de formele-talentheorie naar kijkt is berekenbaarheid. Dat gaat over de vraag ‘welke problemen kun je op welke manieren oplossen?’, en uiteindelijk ook over ‘zijn er problemen die je niet kunt oplossen met een computer?’. Het antwoord op die laatste vraag is: er zijn inderdaad problemen die je niet kunt oplossen met een computer. Dat berekenbaarheidsverhaal wordt verteld met behulp van een serie steeds krachtiger wordende automaten (wij behandelen daarvan de eindige automaat, de stapelautomaat en de turingmachine). In dit vakgebied wordt algemeen aangenomen dat als je kunt bewijzen dat je voor een bepaald probleem geen turingmachine kunt ontwerpen die dat probleem oplost, je dan ook niet hoeft te proberen een programma voor een moderne computer te schrijven dat dat probleem zou moeten oplossen.

Tot nu toe hebben we het alleen gehad over ‘kan het wel of kan het niet?’, en nog niet over ‘hoe lang duurt het dan om het op te lossen?’. Die laatste vraag staat centraal in de complexiteitstheorie, waar we aan het eind van deze cursus kort naar kijken. Bij iedere soort automaat blijkt een soort grammatica te horen. Een automaat wordt over het algemeen gezien als een machine die een bepaalde invoer krijgt en die dan nagaat of die invoer aan bepaalde eisen voldoet. Een grammatica wordt over het algemeen gezien als een generator van dingen (strings) die aan die eisen voldoen. Een ander belang van de formele-talentheorie heeft te maken met deze grammatica’s. Die worden namelijk gebruikt bij het definiëren en compileren van programmeertalen. Berekenbaarheid, complexiteit en programmeertalen zijn dus redenen om deze cursus op te nemen in het curriculum. Vooral berekenbaarheid en complexiteit zijn heel moeilijke onderwerpen. Voordat we daaraan toekomen zullen we eerst uitgebreid ‘oefenen’ op de eenvoudigere automaten en grammatica’s. In paragraaf 3.1 leest u meer over de opzet van deze cursus. 2

Cursusmateriaal

Het cursusmateriaal bestaat uit een werkboek, een tekstboek, een tool en de cursussite. 2.1

TEKSTBOEK

We gebruiken het Engelstalige tekstboek An introduction to formal languages and automata, sixth edition, van Peter Linz (2017, Jones & Bartlett Learning). Het tekstboek bevat de leerstof die behandeld wordt in de cursus. Het is een introductie op de theorie van formele talen, automaten, grammatica’s, allerlei modellen van berekenbaarheid en complexiteit. U zult misschien tot de conclusie komen dat het tekstboek nogal wiskundig en formeel is, en dat het meer tijd kost dan u vooraf zou denken om de stof te lezen, begrijpen en beheersen. Dat is heel normaal, want het onderwerp is nu eenmaal formeel en wiskundig (en soms moeilijk). Dit tekstboek is echter een van de informelere boeken die over dit onderwerp geschreven zijn. 2.2

WERKBOEK

Naast het tekstboek hebben we een (Nederlandstalig) werkboek geschreven. Dit werkboek geeft per leereenheid aan welk deel van het tekstboek bestudeerd moet worden, en is dus leidend voor het bestuderen van de cursus.

Verwijzingen naar het tekstboek

Voor de verwijzingen naar het tekstboek handhaven we de Engelse termen van het tekstboek. Zo hebben we het over chapter, section, subsection, theorem, definition, enzovoort, als we het over onderdelen van het tekstboek hebben. We gebruiken ook vaak kortweg ‘Linz’ om te verwijzen naar het tekstboek. De titels van leereenheden en paragrafen in het werkboek dragen dezelfde Engelse namen als de corresponderende onderdelen in het tekstboek. Zo is de relatie tussen werkboek en tekstboek meteen duidelijk. Behalve de studeeraanwijzingen bevat het werkboek aanvullingen, opgaven en uitwerkingen. Elke leereenheid bevat ook een zelftoets. De opgaven uit deze zelftoets zijn representatief voor de soort opgaven die u op het tentamen kunt verwachten. De oorspronkelijke versie (T22321) van deze cursus is ontwikkeld onder het toeziend oog van prof. dr. W.J. Fokkink, Vrije Universiteit Amsterdam. In het werkboek is op een aantal plaatsen dankbaar gebruik gemaakt van het lesmateriaal dat hij ter beschikking heeft gesteld. Voor de huidige versie (IB0802) hebben we de errata die in T22321 gevonden waren verwerkt, en een leereenheid toegevoegd over complexiteit. 2.3

SOFTWARE

We gebruiken de open source tool JFLAP (Java Formal Language and Automata Package) ter ondersteuning bij de cursus. Meer informatie hierover vindt u in leereenheid 1 en op de cursussite. 2.4

CURSUSSITE

De cursussite is een onlosmakelijk onderdeel van de cursus. Op deze site staat alle informatie die aan verandering onderhevig is en alles wat te maken heeft met de gebruikte software. Meer specifiek treft u hier het volgende aan: – informatie over de studiebegeleiding: de discussiegroep voor vragen, en de toegang tot de virtuele klas voor de online begeleidingsbijeenkomsten – informatie over tentaminering: details over de toetsvorm en tentamendata – eventuele aanvullingen op en wijzigingen van de verplichte leerstof – een eindtoets die representatief is voor het tentamen en een voorbeeldtentamen – een lijst met bekende errata voor het tekstboek en het werkboek – links naar relevante of interessante websites – informatie over het installeren van en het werken met de software – bouwstenen bij sommige opgaven. We raden u aan om aan het begin van het bestuderen van de cursus de cursussite goed te verkennen en alle aanvullende informatie te lezen.

3 Variabele cursus

Semestercursus

Rooster

Belangrijk!

Planning

Deze cursus is een variabele semestercursus. De term variabel betekent dat u in principe helemaal zelf kunt bepalen wanneer u de cursus aanschaft en bestudeert (als u maar voldoet aan de ingangseis). U kunt de cursus geheel in zelfstudie doen, en/of daarnaast eventueel opnames van eerdere begeleidingssessies beluisteren. Voor diegenen die behoefte hebben aan een duidelijke structuur is de cursus ook opgenomen in het begeleidingsrooster. Het is een semestercursus, wat wil zeggen dat de begeleidingsperiode verspreid is over twee kwartielen. Voor deze cursus zijn dat het derde en vierde kwartiel, ofwel de periode februari – juli. In deze periode is er ‘live’ online begeleiding. De data hiervan, en een rooster om de bijbehorende stof op tijd bestudeerd te hebben, vindt u op de cursussite. Ook als u de cursus geheel in zelfstudie doorloopt kunt u natuurlijk baat hebben bij de structuur van dat rooster. U hebt vanaf het moment van inschrijven twaalf maanden de tijd om in maximaal drie pogingen een voldoende voor het tentamen te halen. De drie vastgestelde tentamendata voor uw inschrijfperiode vindt u onder andere op de cursussite. Er zijn ieder jaar tentamens in juli (aansluitend aan de begeleiding), in november, en in februari (net voor de start van een nieuwe cyclus). U moet zich op tijd zelf aanmelden voor een tentamen. 4

Inhoud

4.1

OPBOUW VAN DE CURSUS

Blok 1 studielast 10 uur

Het eerste blok bestaat uit één introductieleereenheid over de basisconcepten taal, grammatica en automaat. In dit stadium worden deze concepten, die nauw verband met elkaar houden, slechts globaal beschreven. In twee voorbeelden wordt meteen de link van de theorie met computers en computerprogramma's gelegd.

Blok 2 studielast 29 uur

In dit blok komen reguliere talen en hun eigenschappen aan de orde. Een reguliere taal kan op verschillende manieren formeel of informeel beschreven worden. Wij verdiepen ons in drie formele beschrijvingen: eindige automaten, lineaire grammatica's en reguliere expressies.

Blok 3 studielast 32 uur

In het vorige blok hebben we gezien dat sommige talen niet regulier zijn. Het thema van blok 3 zijn de context-vrije talen, een andere familie van talen, waarvoor andere formele beschrijvingen bestaan. We leren wat de eigenschappen van context-vrije talen zijn en hoe we een context-vrije taal kunnen beschrijven met behulp van een context-vrije grammatica en een stapelautomaat.

Blok 4 studielast 19 uur

Er bestaan ook talen die niet context-vrij zijn. Talen blijken ingedeeld te kunnen worden in een hiërarchie: de Chomsky-hiërarchie. Deze hiërarchie bestuderen we in het laatste blok van de cursus. In dit blok bestuderen we ook de turingmachine, een automaat die krachtig genoeg is om een algoritmisch probleem te kunnen oplossen. Maar ook in dit blok ontdekken we dat er grenzen zijn aan wat mogelijk is. Er bestaan namelijk problemen die niet algoritmisch zijn en die dus niet door een turingmachine opgelost kunnen worden. Tot slot gaan we nog (vrij kort en vrij informeel) in op een onderwerp dat nauw verbonden is met berekenbaarheid maar waaraan we tot nu toe nog weinig aandacht hebben besteed: complexiteit. 4.2

STUDIELAST

Met alleen de verplichte theorie komen we op 90 uur studielast. Samen met de (sterk aanbevolen) bijeenkomsten (7 uur), tentamenvoorbereiding (10 uur) en het daadwerkelijk maken van het tentamen (3 uur) komen we op 110 uur studielast voor alle essentiële onderdelen samen. Van de in totaal 140 uur die voor de cursus staan, blijven dan nog 30 uur over voor het lezen van deze introductieleereenheid, het verkennen van de cursussite, het installeren en uitproberen van de tool, het uitwerken van de eindtoets en een voorbeeldtentamen, en eventueel nog extra tentamenvoorbereiding of herhaling. 4.3

LEERDOELEN

Na het bestuderen van de cursus wordt verwacht dat u – kunt aangeven wat een formele taal is en een globale indeling van formele talen op eigenschappen kunt maken – de belangrijkste eigenschappen van reguliere en context-vrije talen kunt aangeven – een grammatica kunt interpreteren, construeren en transformeren – kunt aangeven hoe een eindige automaat, een stapelautomaat en een turingmachine werken – het verband kunt uitleggen tussen automaten, talen en grammatica's – het begrip beslisbaarheid kunt omschrijven – de begrippen complexiteit, P, NP en NP-compleet kunt omschrijven – praktische toepassingen van de theorie kunt noemen. 4

Tentaminering

De cursus wordt getoetst door middel van een geheim regulier schriftelijk tentamen. Alle details hierover (zoals wat u mag meenemen naar het tentamen) vindt u op de cursussite. Ook de drie vastgestelde tentamendata per jaar zijn daar te vinden, met de bijbehorende uiterste aanmelddata.

Blok 1

Introductie

Introduction to the theory of computation Introductie Leerkern 1 2 3 4

15 16

Mathematical preliminaries and notation Three basic concepts 16 Some applications 19 Kennismaking met JFLAP 20

Zelftoets

22

Terugkoppeling 1 2

23

Uitwerking van de opgaven 23 Uitwerking van de zelftoets 32

16

Leereenheid 1

Introduction to the theory of computation

INTRODUCTIE

Strings en talen zijn voor de theorie van formele talen wat elementen en verzamelingen zijn voor de verzamelingenleer, of getallen voor de rekenkunde: de objecten waar de theorie over gaat. In deze leereenheid maakt u kennis met deze objecten en alle basisbegrippen die in deze cursus aan bod komen. Voor de beheersing van deze kernbegrippen is het goed om veel te oefenen. Dat doen we met pen en papier en ook met behulp van de tool JFLAP (Java Formal Language and Automata Package), een interactieve softwaretool voor het visualiseren en experimenteren met formele talen en automaten. Aan het einde van deze leereenheid maakt u kennis met JFLAP. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – van een taal kunt aangeven of deze eindig of oneindig is – de concatenatie van twee strings en van twee talen kunt bepalen – het verschil kunt aangeven tussen de lege string, de lege taal en de taal met alleen de lege string – het complement, het spiegelbeeld, de n-de macht, de Kleeneafsluiting en de positieve afsluiting van een taal kunt geven – met gebruik van de op strings, talen en verzamelingen gedefinieerde operaties, talen nauwkeurig kunt beschrijven – zulke beschrijvingen kunt interpreteren – een grammatica met behulp van een viertupel kunt definiëren – de productieregels van een grammatica kunt gebruiken om een afleiding van een string te geven – kunt omschrijven wat een deterministische en een niet-deterministische automaat is – twee voorbeelden van toepassingen van de theorie van formele talen en automaten kunt geven – de betekenis kunt geven van de volgende kernbegrippen uit deze leereenheid: alfabet, symbool, string, taal, formele taal, prefix, suffix, substring, grammatica, automaat, accepter, transducer. Studeeraanwijzingen Bij deze leereenheid hoort chapter 1 van het tekstboek van Linz. Section 1.1 geldt als verplichte voorkennis. Het bestuderen van paragraaf 1 is daarom facultatief. Lees section 1.1 door of bestudeer paragraaf 1 als u uw voorkennis wilt opfrissen. Bij deze leereenheid hoort ook materiaal van de website bij het tekstboek van Linz; zie paragraaf 4. De studielast van deze leereenheid bedraagt circa 10 uur.

LEERKERN Studeeraanwijzing

Lees in Linz de introductie op de chapter. 1

Mathematical preliminaries and notation

Facultatief

Deze paragraaf correspondeert met section 1.1 in Linz en is facultatief. In deze paragraaf worden basisprincipes uit de wiskunde in vogelvlucht uitgelegd.

Proof techniques

In subsection Proof techniques in Linz wordt gesproken over een sequence of statements P1, P2, … Daarmee wordt bedoeld dat er een bewering P is waarin een natuurlijk getal voorkomt, zoals bijvoorbeeld “1 + 2 + … + n = n(n + 1) / 2”. Met P1 bedoelen we dan de bewering “1 = 1(1 + 1) / 2”, met P2 bedoelen we “1 + 2 = 2(2 + 1) / 2” enzovoort. Je kunt dit inderdaad zien als een rij beweringen, maar je zou ook kunnen zeggen dat het één bewering is voor willekeurige n – dat maakt voor de rest van het verhaal niet uit. Het idee achter inductie is als volgt. Als je weet dat bijvoorbeeld P1, P2 en P3 waar zijn èn dat vanaf dat moment (dus voor n ≥ 3) geldt dat Pn+1 volgt uit Pn, dan weet je dat Pn geldt voor alle n. Je kunt dan namelijk uit de waarheid van P3 de waarheid van P4 afleiden, en daaruit die van P5 enzovoort. Er zijn drie stappen te herkennen in een inductiebewijs: – de basis: het controleren van de bewering voor een eindig aantal startwaarden, – de inductiehypothese: de aanname dat Pn geldt voor een zekere n, en – de inductiestap: het bewijs dat Pn+1 volgt uit Pn. In sommige inductiebewijzen worden de afzonderlijke onderdelen (basis, inductiehypothese en inductiestap) duidelijk en in deze volgorde aangegeven, maar het kan ook voorkomen dat de basis pas achteraf wordt gecontroleerd en dat de inductiehypothese niet expliciet genoemd wordt. In sommige gevallen is de hier beschreven inductiehypothese niet sterk genoeg, en hebben we als inductiehypothese de aanname “Pn geldt voor alle n tot en met een zekere k” nodig; dit heet volledige inductie. In de inductiestap moet dan bewezen worden dat Pk+1 volgt uit P1, P2, …,Pk.

OPGAVE 1.1

Maak exercise 38 uit section 1.1 van Linz. 2

Three basic concepts

Studeeraanwijzing

Bestudeer in Linz section 1.2, subsection Languages.

Erratum in Linz

Pagina 18, example 1.8, regel 10: in plaats van “by induction characters” moet staan “by induction on the number of symbols of v”.

Substring Prefix Suffix

Als v een substring is van w, dan gebruiken we de notatie v sub w. Als geldt w = uv, dan gebruiken we de notatie u pref w en v suf w om aan te geven dat u een prefix is van w en v een suffix is van w.

Kleene-afsluiting Positieve afsluiting

De oneindige verzameling van alle machten van een taal, L*, heet de Kleene-afsluiting (of gewoon afsluiting). Naast de Kleene-afsluiting gebruikt men ook wel de positieve afsluiting, L+. Deze afsluiting bestaat uit de vereniging van alle machten van de taal L, met uitzondering van L0.

OPGAVE 1.2

We zijn het woord ‘taal’ intussen een aantal keer tegengekomen: in de uitdrukkingen programmeertaal, formele taal en natuurlijke taal, en ook gewoon los. Wat zijn de verschillen en overeenkomsten tussen deze voorkomens van het woord taal? Geef ook voorbeelden van iedere soort taal. Denk ook eens na over het verband tussen het alfabet van een formele taal en dat van een natuurlijke taal, en over strings (woorden) in beide soorten talen.

Taal Alfabet String

In de rest van deze cursus gebruiken we de begrippen taal, string en alfabet over het algemeen in de formele-talenbetekenis: een (formele) taal is een mogelijk oneindige deelverzameling van *, ofwel een (mogelijk oneindige) verzameling strings over een zeker alfabet . Een alfabet is een eindige niet-lege verzameling symbolen, en een string is een eindig rijtje symbolen.

OPGAVE 1.3

Gegeven is het alfabet  = {a, b, ..., z, _} waar _ een spatie representeert. Geef een paar strings over het alfabet . OPGAVE 1.4

Is  een taal over ? OPGAVE 1.5

In subsection Languages heeft u in example 1.9 en 1.10 twee manieren gezien om een taal te definiëren. Wat zijn de twee manieren? OPGAVE 1.6

Maak exercise 5 uit section 1.2 van Linz. OPGAVE 1.7

Maak exercise 6 uit section 1.2 van Linz. OPGAVE 1.8

Gegeven is de taal L van alle even binaire getallen, dat wil zeggen alle getallen gevormd met de cijfers 0 en 1 waarvan het laatste cijfer 0 is. Geef een formele specificatie van deze taal. OPGAVE 1.9

Gegeven zijn de twee talen L1 = {a, ab} en L2 = {a, ac, acc}. Wat is L1L2 ? OPGAVE 1.10

a De lege taal is de taal die geen strings bevat. De lege taal wordt genoteerd met het symbool . Geldt  = ? b Wat is {}? Geldt {} = ?

OPGAVE 1.11

Gegeven zijn de volgende talen: – L1 = {, ab, aabb, aaabbb, aaaabbbb} – L2 = {x  {a, b}* : a sub x}: de verzameling van alle strings over {a, b} met ten minste één a. – L3 = {x  {a, b}* : niet (ba suf x)}: de verzameling van alle strings over {a, b} die niet op ba eindigen. – L4 = {x  {a, b}* : x = xR}: de verzameling van alle palindromen over {a, b}. – L5 = {x  {a, b, c}* : x = 3}: de verzameling van alle strings over {a, b, c} met lengte 3. a Welke talen zijn eindig? b Welke talen bevatten de lege string? c Geef een omschrijving van L2  L3, van L2  L4 en van L5 – L3. OPGAVE 1.12

a Is *, de Kleene-afsluiting van een niet-leeg alfabet , eindig of oneindig? Motiveer uw antwoord. b Bepaal voor een taal L over  en voor L , het complement van L ten opzichte van , de vereniging L  L . Wat kunt u zeggen over de waarde vanL  L ? c Maak, door gebruik te maken van het resultaat van a en b, exercise 7 uit section 1.2 van Linz. Studeeraanwijzing

Bestudeer in Linz section 1.2, subsection Grammars. Merk op dat definition 1.1 in Linz heel algemeen is, maar dat alle voorbeeldgrammatica's in chapter 1 heel specifiek zijn omdat ze aan strengere eigenschappen (van contextvrije grammatica's; zie hiervoor leereenheid 5) voldoen.

OPGAVE 1.13

a Aan het begin van subsection Grammars is een grammatica gegeven voor het vormen van simpele zinnen in het Engels. Wat zijn de productieregels, het startsymbool, de hulpsymbolen en de eindsymbolen? b Als we slechts de productieregels geven, is deze grammatica dan nog eenduidig gedefinieerd? c Geef andere zinnen die met deze grammatica gevormd kunnen worden. OPGAVE 1.14

Wat is in verband met grammatica's het verschil tussen de symbolen  en  ? OPGAVE 1.15

Welke definitie van een taal kunnen we geven in termen van een grammatica? OPGAVE 1.16

Leg precies uit hoe het bewijs in example 1.11 in Linz in elkaar zit. OPGAVE 1.17

Geef een andere set productieregels voor de grammatica uit example 1.12, waarmee precies dezelfde taal wordt gegenereerd.

OPGAVE 1.18

a Laat  = {a, b}. Geef een grammatica die alle strings met precies één a genereert. Geef eerst een paar voorbeeldstrings uit de gegeven verzameling. b Maak exercise 14c uit section 1.2 van Linz. Geef eerst een paar voorbeeldstrings uit de gegeven verzameling. c Maak exercise 14d uit section 1.2 van Linz. OPGAVE 1.19

Maak exercise 15 uit section 1.2 van Linz. OPGAVE 1.20

Maak exercise 16 uit section 1.2 van Linz. OPGAVE 1.21

a Maak exercise 17a uit section 1.2 van Linz. b Maak exercise 17d uit section 1.2 van Linz. OPGAVE 1.22

Maak exercise 18a uit section 1.2 van Linz. OPGAVE 1.23

Maak exercise 20 uit section 1.2 van Linz. Geef bij de gevraagde voorbeeldstrings ook hun afleidingen. Geef ook een paar strings die niet door de grammatica kunnen worden gegenereerd. Probeer tot slot de taal die door de grammatica wordt gegenereerd te omschrijven. OPGAVE 1.24

Maak exercise 23 uit section 1.2 van Linz. OPGAVE 1.25

Maak exercise 24 uit section 1.2 van Linz. Studeeraanwijzing

Lees in Linz section 1.2, subsection Automata.

OPGAVE 1.26

Welke informatie is nodig om een overgangsfunctie te definiëren? 3 Studeeraanwijzing

Some applications

Bestudeer section 1.3 in Linz.

OPGAVE 1.27

Volgens de naamgevingconventie in Java krijgen klassen een naam die begint met een hoofdletter en methoden een naam die begint met een kleine letter. Stel dat de eindsymbolen in een grammatica voor Java bestaan uit kleine letters (a,.. , z), hoofdletters (A, .., Z) en cijfers (0, 1, .. 9). Andere symbolen worden niet gebruikt. Geef, met gebruik van de notatie op pagina 31 in Linz, een grammatica voor klassennamen en een grammatica voor methodenamen. Teken ook in beide gevallen een automaat zoals in figure 1.6 in Linz.

OPDRACHT 1.28

Zoek op internet een definitie van de grammatica van Java. Kunt u hierin de definitie van een if-opdracht vinden? NB U hoeft de gevonden informatie niet te interpreteren. OPGAVE 1.29

In het geheugen van een computer of tijdens de overdracht van informatie tussen twee computers kan een storing optreden waardoor de opgeslagen bits verminkt worden. Om zulke verminkingen op te kunnen sporen, voegt men aan een reeks bits vaak extra bits toe ter controle. Zo wordt wel eens één extra bit vooraan de reeks toegevoegd: een 1 geeft aan dat het totaal aantal 1’en (inclusief extra bit) even is, een 0 dat het oneven is. Vervolgens worden pariteitscontroles uitgevoerd, om te kijken of het aantal 1’en nog klopt met wat de pariteitsbit (de extra bit) aangeeft. Het zal duidelijk zijn dat deze controle alleen zinvol is als men aanneemt dat er een oneven aantal bits verminkt kan zijn. Geef een automaat die nagaat of het aantal 1’en in een bitstring even of oneven is. 4

Kennismaking met JFLAP

(Java Formal Language and Automata Package) is een interactieve softwaretool voor het visualiseren van en experimenteren met formele talen en automaten. De software is open source. In de cursus gebruiken we versie 7 die u van de cursussite kunt ophalen. Op de cursussite vindt u, naast de software, een link naar de homepage van JFLAP. Deze homepage bevat veel informatie en materiaal voor het werken met de tool. Er is onder andere een uitgebreide tutorial die als helpfunctie gebruikt kan worden. Op de cursussite staat ook een document met handige tips voor het werken met JFLAP. Hierin staat allerlei informatie die anders misschien moeilijk te vinden is. Houd het document (in het begin) paraat als u met JFLAP werkt. Voor sommige opdrachten in het werkboek zijn bouwstenen samengesteld. Deze treft u ook aan op de cursussite. JFLAP

Software Homepage JFLAP Tutorial Handige tips

Bouwstenen

Studeeraanwijzing

Lees in Linz appendix B. Ga dan naar de website go.jblearning.com/LinzJPAK die daar wordt genoemd, en download vanaf tabblad Sample Materials de JFLAP Activities. Dit is een zip-file, die u op uw computer kunt uitpakken. Zoek daarin het bestand JFLAP Activities.pdf en lees daarvan chapter 1.

Installatie

Haal nu het bestand JFLAP.jar op van de cursussite en plaats het bestand in een eigen map op uw computer. JFLAP wordt gestart door te dubbelklikken op de bestandsnaam. U krijgt dan het scherm van figuur 1.1 te zien.

FIGUUR 1.1

Startscherm van JFLAP

Het werken met JFLAP is niet moeilijk. Raadpleeg bij problemen de tutorial op de JFLAP homepage, de helpfunctie van de software of het document met handige tips van de cursussite. Studeeraanwijzing

Lees nu in JFLAP Activities.pdf van chapter 2 de introductie en section 2.1, zonder daarbij de opdrachten uit te voeren. Start dan, indien nodig, JFLAP.

OPDRACHT 1.30 a Voer in JFLAP

de grammatica van example 1.12 uit Linz in en sla de grammatica op in een bestand. b Voer nu in JFLAP de grammatica in die u in opgave 1.17 gemaakt hebt. Sla ook deze grammatica op, onder een zelfgekozen naam. c Probeer in beide grammatica's de invoerstrings aabbb, aabb, abb, abbb en . Gebruik hiervoor de menuopties InputCYK Parse, InputMultiple CYK Parse en InputBrute Force Parse. Bekijk ook hoe de afleidingen van de strings eruit zien. NB – JFLAP

toont meer informatie dan wat u op dit moment (en ook na afloop van de cursus) kunt begrijpen. Probeer daar overheen te kijken en alleen te concentreren op wat wel te begrijpen is en wat in de opdrachten gevraagd wordt. – Het kan zijn dat de optie InputBrute Force Parse te traag werkt op uw computer. Sla dit onderdeel dan over.

OPDRACHT 1.31

a Open het bestand Jexample1.13.jff uit de map JFLAP ActivitiesJFLAP FilesJFLAP Examples. Deze grammatica accepteert de lege string, maar het is niet mogelijk om dit in JFLAP te controleren. Ga dit na. b Wijzig nu de grammatica opdat deze niet meer de lege string genereert maar voor de rest dezelfde taal genereert. Controleer het in JFLAP. OPDRACHT 1.32 a JFLAP kan, behalve

voor het specificeren van grammatica's, ook goed gebruikt worden voor het tekenen van automaten. In paragraaf 3 heeft u al een paar voorbeelden gezien. Bekijk de automaat van figure 1.6 in Linz en teken de automaat in JFLAP. NB Twee van de drie labels geven verschillende opties. Zo moet u de overgang met label letter of undrscr interpreteren als een overgang met label letter en een overgang met label undrscr. b Het is in JFLAP ook mogelijk om een toestand als eindtoestand (final) te markeren. Hoe moeten de toestanden 2 en 3 gemarkeerd worden als eindtoestand? biedt vele manieren om te experimenteren met grammatica's, automaten en andere onderwerpen uit de theorie. We raden u aan om veel gebruik te maken van deze tool bij de bestudering van de cursus. Het kan wel eens gebeuren dat we u afraden om voor een bepaald onderdeel JFLAP te gebruiken. De werkwijze in JFLAP is dan naar ons oordeel te omslachtig of misleidend. JFLAP

ZELFTOETS

1

Geef een definitie van het begrip formele taal.

2

Kan de verzameling van de natuurlijke getallen dienen als alfabet voor een taal? Motiveer uw antwoord.

3

Gegeven is het alfabet  = {a, b}. Bepaal L* en L voor a L = . b L = {}. c L = {a} d L = {a}*{b}

4

Gegeven is de taal L = {wwR : w  {a, b}+}. a Geef drie strings uit deze taal. b Geef een grammatica die deze taal genereert. c Geef een afleiding voor ieder van de drie strings uit onderdeel a.

5

a Geef een beschrijving van de taal die gegenereerd wordt door de grammatica met startsymbool S en met de volgende productieregels: S  abSSbaa b Geef een andere verzameling productieregels die dezelfde taal genereert.

TERUGKOPPELING 1

Uitwerking van de opgaven

1.1

We geven een bewijs met inductie. Basisstap: De bewering is waar voor n = 4: de waarde van 24 is 16, en de waarde van 4! is 24. Inductiehypothese: We nemen aan dat de bewering waar is voor een waarde n  4. Inductiestap: We bewijzen dat de bewering dan ook waar is voor n + 1. 2n < n! is waar volgens onze aanname (de inductiehypothese). We moeten nu laten zien dat 2(n+1) < (n + 1)!, en daarvoor moeten we zorgen dat we ergens een factor 2n en ergens een factor n! krijgen, want dan kunnen we hopelijk de inductiehypothese gebruiken. Gelukkig geldt dat 2(n+1) = 2 * 2n en dat (n + 1)! = (n + 1) * n! De bewering 2(n+1) < (n + 1)! is dus gelijk aan 2 * 2n < (n + 1) * n! Deze bewering is waar omdat 2 < (n + 1) en volgens onze aanname 2n < n! Hiermee is bewezen dat de bewering waar is voor alle waarden van n  4.

1.2

Een gangbare definitie van het algemene begrip taal is al gegeven in Linz: een systeem dat geschikt is om zekere ideeën, feiten of concepten uit te drukken, inclusief een verzameling symbolen en regels om met die symbolen te werken. Het duidelijkste verschil tussen een natuurlijke en een formele taal is dat een natuurlijke taal vanzelf in de loop der eeuwen ontstaan is (zoals het Nederlands, Frans, Engels en Arabisch), terwijl een formele taal kunstmatig is (zoals bijvoorbeeld Esperanto, en programmeertalen). In de context van deze cursus bedoelen we met een formele taal over het algemeen heel abstract een deelverzameling van *, ofwel een verzameling strings over een zeker alfabet . In die zin is een programmeertaal misschien niet echt een formele taal, want onder ‘de programmeertaal Java’ verstaan we niet (alleen) de verzameling Java-programma’s, maar eerder het geheel van sleutelwoorden, syntaxdefinitie, libraries, typesysteem, compiler enzovoort van Java. Dat lijkt dus erg op de algemene definitie van het begrip taal die we hierboven gaven. Het Nederlands heeft een alfabet dat bestaat uit 26 letters. Met die letters kunnen woorden (strings) gemaakt worden (we laten even in het midden of daar een duidelijke verzameling regels voor bestaat; het is in ieder geval duidelijk dat niet ieder rijtje letters een goed woord oplevert). Woorden kunnen dan weer worden samengevoegd tot zinnen, volgens de grammatica van het Nederlands. Dat levert een oneindige verzameling syntactisch goedgevormde Nederlandse zinnen op (dat wil zeggen, de structuur is goed, maar qua semantiek (betekenis) kan er nog van alles mis zijn!). Als we hier op de formele-talenmanier naar kijken, dan zouden we misschien geneigd zijn te zeggen dat de Nederlandse taal de verzameling bestaande Nederlandse woorden is. Aan de andere kant, formele talen houden zich bezig met de structuur van een taal en niet zozeer met de betekenis, en zo bezien lijkt het logischer om de verzameling goedgevormde zinnen als invulling van het begrip Nederlandse taal te nemen.

Bij programmeertalen geldt ook zoiets: je hebt de verzameling sleutelwoorden (keywords), dat is een (formele) taal over een bepaald alfabet (het is zelfs een reguliere taal, zie leereenheid 2 tot en met 4). Verder heb je de verzameling identifiers, dat is ook een formele (reguliere) taal over een (eventueel ander) alfabet. Dan heb je de verzameling welgevormde programma’s, en je zou kunnen zeggen dat dat strings zijn over het alfabet bestaande uit sleutelwoorden, identifiers en bijzondere symbolen zoals {, ; en ). Deze verzameling welgevormde programma’s is ook weer een formele taal, maar geen reguliere: het is een context-vrije taal (leereenheid 5 tot en met 8). Kortom: de begrippen alfabet, string (woord) en taal betekenen niet in iedere context precies hetzelfde, maar de overeenkomsten zijn (natuurlijk) groot. 1.3

De strings aaa, als en ik_zing zijn drie strings over het alfabet . Ze zijn namelijk allemaal opgebouwd uit symbolen uit het alfabet. De string dit_een_boek_is is ook een string over het alfabet , maar geen string (geen zin) uit de Nederlandse taal.

1.4

Een taal over  is een deelverzameling van *.   *, dus  is een taal over .

1.5

Eén manier is door een formele specificatie te geven, gebruikmakend van de notaties uit de verzamelingenleer. Tussen accolades worden de regels gegeven waarmee alle correcte strings uit de taal kunnen worden opgebouwd. Zie bijvoorbeeld example 1.10. Een andere manier, voor een eindige taal, is een opsomming te geven van alle strings uit de taal. Zie het voorbeeld {a, aa, aab} in example 1.9. NB In de cursus zult u nog andere manieren tegenkomen; zie bijvoorbeeld opgave 1.8 met een beschrijving van een taal.

1.6

Gegeven is dat L = {ab, aa, baa}. abaabaaabaa is op te delen als abaabaaabaa, dus een element van L*. aaaabaaaa is op te delen als aaaabaaaa, dus een element van L*. Als we van links naar rechts werken is baaaaabaaaab maar op één manier op te delen in strings uit L: baaaaabaaaa… We missen nu de laatste b nog, dus baaaaabaaaab  L*. baaaaabaa = baaaaabaa, dus baaaaabaaa  L*.

1.7

Zie de terugkoppeling in Linz op pagina 398. Alternatieven zijn L = {w  {a, b}* : w ≠ aa en w ≠ bb} en L = Σ* – {aa, bb}.

1.8

L= {x  {0, 1}* : 0 suf x}.

1.9

L1L2 = {aa, aac, aacc, aba, abac, abacc}.

1.10

a Nee,  is de lege string, dus een leeg rijtje symbolen.  is de lege taal, ofwel de lege verzameling. Een string en een taal, ofwel een rijtje en een verzameling, kunnen nooit gelijk zijn. b {} is de taal die alleen de lege string bevat. Deze taal heeft één element en is dus niet de lege taal: {} ≠ .

1.11

a

De talen L1 en L5 zijn eindig. De andere talen zijn oneindig.

b De talen L1, L3 en L4 bevatten de lege string; de andere talen niet. c L2  L3 = {a, b}*. Beschouw immers een willekeurige string x  {a, b}*. Als x op ba eindigt, dan bevat x een a en zit dus in L2. Eindigt x daarentegen niet op ba, dan zit x in L3. We hebben nu aangetoond dat {a, b}*  L2  L3. De inclusie de andere kant op is duidelijk. L2  L4 bestaat uit alle palindromen over {a, b} met ten minste één a. L5 – L3 bestaat uit alle strings over {a, b, c} met lengte 3, met uitzondering van de strings met alleen a's en b's die niet op ba eindigen, dus met uitzondering van aaa, abb, baa, en bbb. Schrijven we deze taal uit, dan krijgen we: L5 – L3 ={aac, aba, abc, aca, acb, acc, bac, bba, bbc, bca, bcb, bcc, caa, cab, cac, cba, cbb, cbc, cca, ccb, ccc}. 1.12

a De Kleene-afsluiting * van een niet-leeg alfabet is altijd oneindig. * = 0  1  2  ...  n  ... met n  . Omdat oneindig is, is * oneindig. b Uit de definitie van L volgt direct dat L  L = *. Omdat L en L disjunct zijn geldtL  L  =L +  L . c We geven een bewijs uit het ongerijmde. Stel L en L zijn beide eindig. Dan is de somL + L  een natuurlijk getal. Dit is in tegenspraak met L  L = * en het feit dat * oneindig is. Onze aanname is dus niet juist: op zijn minst één van de twee talen is oneindig.

1.13

a

De productieregels zijn:

sentence noun_phrase predicate article article noun noun verb verb

 noun_phrasepredicate  articlenoun  verb a  the  boy  dog  runs  walks

Het startsymbool is sentence. De hulpsymbolen zijn sentence, noun_phrase, predicate, article, noun en verb. Eindsymbolen zijn symbolen die niet meer met behulp van productieregels afgeleid kunnen worden. Hier zijn de eindsymbolen a, the, boy, dog, runs en walks. In tegenstelling tot wat we gewend zijn (een eindsymbool is meestal één symbool uit een alfabet), zijn eindsymbolen hier complete woorden. b Uit de productieregels kunnen we eenvoudig opmaken wat de bedoeling van alle symbolen is. Symbolen van de vorm … zijn hulpsymbolen, en in de gegeven grammatica komen zij ook allemaal voor in de linkerkant van een productie. Cursief gedrukte symbolen zijn hier eindsymbolen, en die komen hier alleen voor in de rechterkant van een productie.

Ook het startsymbool is eenvoudig te herkennen: dit is hier het enige hulpsymbool dat niet in een rechterkant voorkomt. Bovendien geldt dat als we de vorming van een Engelse zin bijvoorbeeld bij noun starten, we met de grammatica geen volledige zinnen meer kunnen maken. Om deze grammatica eenduidig te definiëren, kunnen we dus volstaan met het geven van de productieregels en het startsymbool. NB

– Dit hoeft niet te gelden voor alle grammatica's. Geef bij twijfel het volledige viertupel bij de definitie van een grammatica. In ieder geval moet u zich er bij het geven van een definitie van een grammatica altijd van vergewissen dat de gegeven definitie eenduidig is. – Vaak wordt S als startsymbool gebruikt. Vaak is ook de eerst gegeven productieregel van een grammatica de productieregel die van het startsymbool uitgaat. In Linz wordt het viertupel achterwege gelaten als deze conventies van toepassing zijn. c

De volgende zinnen kunnen ook gevormd worden:

the boy walks the boy runs the dog runs a boy walks a dog walks a dog runs Merk op dat we voor de leesbaarheid spaties tussen de eindsymbolen hebben geplaatst. 1.14

Het symbool  wordt gebruikt in een productieregel, om aan te geven welke herschrijfstappen toegestaan zijn in een afleiding: de linkerkant van de productieregel mag dan worden vervangen door de rechterkant. Het symbool  wordt gebruikt in een afleiding, om aan te geven dat er * één (of een onbepaald aantal, dan staat er  ) afleidingsstappen gedaan zijn.

1.15

Een taal is de verzameling van alle strings met alleen eindsymbolen die gegenereerd (gevormd) kunnen worden vanuit het startsymbool van een gegeven grammatica.

1.16

Eerst wordt bewezen dat L(G)  {anbn : n  0}, ofwel dat alle strings uit de taal de vorm anbn hebben (“We first show that all sentential forms … Thus, G can derive only strings of the form anbn”). Onderdeel van dat bewijs is een inductiebewijs van de bewering “… all sentential forms must have the form wi = aiSbi”. Eerst wordt de inductiehypothese geformuleerd: “Suppose that (1.7) holds for all sentential forms wi of length 2i + 1 or less.” Vervolgens wordt de inductiestap bewezen: “To get another sentential form … so that every sentential form of length 2i + 3 is also of the form (1.7).” Merk op dat de inductie hier in stappen van 2 gaat: van 2i + 1 naar 2i + 3. Nu komt de basisstap, voor i = 1: “Since (1.7) is obviously true for i = 1”. Hiermee is het inductiebewijs klaar (“…it holds by induction for all i”), maar hebben we nog een observatie nodig om het bewijs van L(G)  {anbn : n  0} af te ronden: “Finally, to get a sentence, … G can derive only strings of the form anbn.”

Vervolgens wordt bewezen dat ook L(G)  {anbn : n  0}, ofwel: alle strings met de vorm anbn behoren tot de taal: “We also have to show …followed by S  .” 1.17

Als we voor G de volgende productieregels nemen, wordt dezelfde taal gegenereerd: S  aSb S b

1.18

a Een paar voorbeelden zijn a, bba, babbb. De strings bestaan uit eerst een willekeurig aantal (0, 1 of meer) b's, dan één a en ten slotte een willekeurig aantal b's. Een grammatica die deze taal genereert is de grammatica G = ({B, S}, {a, b}, S, P) met als verzameling productieregels P: S  BaB B  bB b Een paar voorbeelden zijn a, aa, aaa, babb, abab, bbaaba. De strings bevatten maximaal drie a's en tussen alle voorkomens van het symbool a een willekeurig aantal b's. Een grammatica die deze taal genereert is de grammatica G = ({A, B, S}, {a, b}, S, P) met als productieregels P: S  BABABAB A  a B  bB c We moeten zeker weten dat er ten minste drie a’s worden gegenereerd, en daaromheen en ertussen mogen nog zoveel a’s en b’s als we willen. Dit laatste, zoveel a’s en b’s als we willen (en de volgorde maakt niet uit), krijgen we bijvoorbeeld met A  aA  bA   Om dit op alle mogelijke manieren om en tussen precies drie a’s te krijgen, gebruiken we de productie S  AaAaAaA Samen vormen deze twee producties de bedoelde grammatica.

1.19

De enige productieregel voor A is A  bS, dus S  aaA kan gewoon vervangen worden door S  aabS. Dan zijn de productieregels S  aabS en is de gegenereerde taal meteen duidelijk: {(aab)n : n  0}.

1.20

Alle rechterkanten van productieregels bevatten nog een hulpsymbool. De grammatica kan dus geen enkele terminale string genereren: L(G) = .

1.21

a Zie de terugkoppeling in Linz op pagina 399. Een alternatieve uitwerking is: S  aSaT T  aTb

b We schrijven eerst de definitie van de strings van L4 wat handiger op: anbn-2 = a2an-2bn-2, en omdat n  3 kunnen we dit ook schrijven als a2akbk met k  1. De gevraagde grammatica is nu eenvoudig op te schrijven: S  aaT T  aTbab 1.22

Zie de terugkoppeling in Linz op pagina 399. Een alternatieve uitwerking is S  aaaSaaa

1.23

De volgende strings kunnen worden afgeleid: Sa S  aSb  aaSbb  aaabb S  bSa  baSba  baaba S  aSb  abSab  abaSbab  abaabab De strings aaa en abb behoren niet tot de taal van de grammatica. We zien dat de strings allemaal een oneven aantal symbolen bevatten, met het symbool a in het midden. De taal bevat dus strings van de vorm w = uav, met u  = v . De substrings u en v houden wel verband met elkaar. De string u is een willekeurige string over het alfabet. De string v wordt verkregen door de string u in spiegelbeeld te nemen en daarin alle a’s te vervangen door een b en alle b's te vervangen door een a.

1.24

Als we in de tweede grammatica de productie S  aaSbb weglaten, houden we precies de productieregels van de eerste grammatica over. De tweede grammatica kan dus alles genereren wat de eerste kan genereren, plus misschien nog iets extra’s vanwege S  aaSbb. Maar het enige dat die productie doet is twee extra a’s voor de rest van de a’s zetten, en twee extra b’s achter de rest van de b’s. En dat kan de eerste grammatica ook, door gewoon tweemaal de productie S  aSb toe te passen.

1.25

Om te bewijzen dat de twee grammatica's niet equivalent zijn is het voldoende om een tegenvoorbeeld te geven. Zie de terugkoppeling in Linz op pagina 399.

1.26

Om een overgangsfunctie te definiëren, moeten we voor elke mogelijke combinatie van waarden voor de huidige toestand, het huidige invoersymbool en de informatie in de huidige geheugenlocatie weten wat de nieuwe toestand (of de nieuwe toestanden in geval van niet-determinisme) wordt, wat de gegenereerde output is, en wat de nieuwe inhoud van de geheugenlocatie wordt.

1.27

Voor klassennamen kunnen we de volgende productieregels geven: klassennaam  hoofdlettersymbolen symbolen  symbool symbolen symbool  hoofdletterkleinelettercijfer hoofdletter  AB...Z kleineletter  ab ...z cijfer  01 ...9 Het startsymbool van deze grammatica is klassennaam Voor methodenamen vervangen we de productieregel voor klassennaam door: methodenaam  kleineletter symbolen De overige productieregels blijven gelijk. Het startsymbool is nu methodenaam. De automaat voor klassennamen is:

De automaat voor methodenamen is:

In beide automaten is toestand 2 de ja-toestand en toestand 3 de neetoestand.

1.28

Door de trefwoorden ‘Java’ en ‘grammar’ in een zoekmachine te geven, krijgen we veel links naar de gezochte informatie. Op de cursussite geven we een paar links. We geven hier de productieregels voor if-opdrachten uit één van de gevonden pagina's: IfThenStatement: if ( Expression ) Statement IfThenElseStatement: if ( Expression ) StatementNoShortIf else Statement IfThenElseStatementNoShortIf: if ( Expression ) StatementNoShortIf else StatementNoShortIf Merk op dat hier drie verschillende productieregels worden gegeven waarvan de eerste een if-opdracht zonder else betreft. Merk ook op dat de notatie afwijkend is van de notatie in section 1.3 in Linz.

1.29

De automaat kan er als volgt uitzien.

Na het lezen van de reeks bits staat de automaat in toestand even of oneven, afhankelijk van het gelezen aantal bits met de waarde 1. 1.30

a Kies voor het opslaan van de grammatica de menuoptie FileSave As ... Zie ook Jexample1.12.jff in de map JFLAP ActivitiesJFLAP FilesJFLAP Examples. b Een nieuwe grammatica kan ingevoerd worden door in het actieve venster de menuoptie FileNew te kiezen. c We bespreken in het algemeen de drie manieren. – CYK Parse: Selecteer in de afrollijst de optie Derivation Table. Na de invoer van een string geeft een klik op de knop Start (of op Enter) het antwoord of de string wel of niet wordt geaccepteerd. Is de string geaccepteerd, dan levert elke klik op de knop Step een nieuw afleidingsstap op. – Multiple CYK Parse: Het is nu mogelijk om in een tabel meerdere strings te geven. Een klik op de knop Run Inputs geeft meteen antwoord voor alle invoer. Het selecteren van een string in de tabel brengt de string over naar het venster links, waar het mogelijk is om de afleidingsstappen en te bekijken. – Brute Force Parse: Hier zien we hetzelfde venster als bij CYK Parse.

1.31

a Het is niet mogelijk om de lege string als invoer voor deze grammatica te geven. Er verschijnt een foutmelding die betrekking heeft op het feit dat het startsymbool  genereert. Dat is blijkbaar niet gewenst. Andere strings kunnen wel ingevoerd worden.

b De volgende grammatica genereert dezelfde taal als de oorspronkelijke grammatica, maar zonder de lege string: S S S S S

 SS  aSb  bSa  ab  ba

Er is een inconsistentie in JFLAP waargenomen. De grammatica accepteert de string babb niet via Brute Force Parse maar wel via CYK Parse. Er gaat hier dus iets mis met de CYK Parse. NB

1.32

a Kies de optie Finite Automaton. Er verschijnt een scherm waarop u een automaat kunt tekenen. Gebruik de tweede knop van links, de State creator, voor het toevoegen van toestanden, de derde knop Transition creator voor het toevoegen van overgangen en de vierde knop Deleter voor het wissen van een element. Als de eerste knop, Attribute editor, ingeschakeld is, kunt u toestanden verplaatsen voor een betere lay-out en kunt u, door te rechtsklikken op een toestand, eigenschappen van deze toestand wijzigen. We gaan als volgt te werk: We plaatsen eerst drie toestanden op het scherm. Daarna wijzigen we de eigenschappen van de toestanden door elk een andere naam te geven (met Set Name) en toestand 1 als begintoestand aan te wijzen (Initial). Dan klikken we op de knop Transition creator en tekenen we de overgangen. Let op: er moeten zes verschillende overgangen getekend worden. JFLAP groepeert ze automatisch. Een overgang die in dezelfde toestand start en eindigt wordt getekend door de muiswijzer op de toestand te plaatsen, dan met ingetrokken linkerknop buiten de toestand te gaan en weer terug in de toestand te komen (dit moet drie keer gebeuren omdat er drie labels zijn). We krijgen het volgende resultaat:

b Als in het scherm de eerste knop links is geselecteerd, dan kunnen we toestand 2 en 3 als eindtoestand markeren door met de rechtermuisknop op een toestand te klikken en dan de optie Final aan te klikken. De toestanden krijgen dan een dubbele rand.

2

Uitwerking van de zelftoets

1

Een formele taal is een verzameling strings over een alfabet.

2

Nee, een alfabet is immers eindig en de verzameling natuurlijke getallen niet.

3

a De Kleene-afsluiting van  is * = {}. Zie hiervoor de definitie van * op pagina 19 en 20 in Linz: als je 0 elementen uit een of andere verzameling achter elkaar zet, krijg je de lege string. Het complement van  bevat alle strings over het alfabet.  = * = {a, b}*. b {} bevat als enig element de lege string. {}* bevat alle mogelijke combinaties van de lege string en dus alleen de lege string. {}* = {}. Het complement van {} bevat alle strings over het alfabet behalve de lege string. { } = +. c

L* = {a}*, de verzameling van alle strings die uit 0 of meer a’s bestaan.

L = Σ* – {a}, alle strings uit {a, b}* behalve a.

d L* bestaat uit de lege string en alle strings over {a, b} die eindigen op een b. L bevat alle strings behalve strings van de vorm anb met n  0. Dus L = Σ* – {a}*{b}. 4

a

Drie voorbeelden zijn bb, abba, abaaba.

b Een string begint en eindigt altijd met hetzelfde symbool. De taal kan gegenereerd worden door de grammatica G = ({S}, {a, b}, S, P) met P bestaand uit de volgende productieregels: S  aSabSbaabb c

De drie afleidingen zijn:

S  bb S  aSa  abba S  aSa  abSba  abaaba 5

a We geven eerst een paar voorbeeldstrings: a, aba, ababa, abababa. De taal is de verzameling strings over het alfabet {a, b} van oneven lengte die beginnen en die eindigen met een a. In de string staan nooit twee a's of twee b's naast elkaar. De taal is dus {(ab)na : n  0} b De volgende verzameling productieregels genereert dezelfde taal: S  abSa

Blok 2

Reguliere talen

Finite automata Introductie Leerkern 1 2 3

35 36

Deterministic finite accepters 36 Nondeterministic finite accepters 38 Equivalence of deterministic and nondeterministic finite accepters 41

Zelftoets

44

Terugkoppeling 1 2

45

Uitwerking van de opgaven 45 Uitwerking van de zelftoets 56

Leereenheid 2

Finite automata

INTRODUCTIE

In leereenheid 1 hebt u kennisgemaakt met drie begrippen die heel belangrijk zijn voor deze cursus: talen, grammatica’s en automaten. U weet nu dat een (formele) taal een verzameling is, bestaande uit strings over een zeker alfabet. Zowel een grammatica als een automaat kan worden gebruikt om exact aan te geven welke strings over een alfabet tot een bepaalde taal behoren, en – vaak impliciet – welke niet. Vaak wordt een grammatica gezien als een mechanisme waarmee de strings van een taal kunnen worden gegenereerd (geproduceerd, voortgebracht, gemaakt), terwijl een automaat wordt gezien als een machine waarmee alle strings van de taal kunnen worden herkend. De automaat accepteert alleen strings uit die taal – vandaar de Engelse term accepter – en wel precies alle woorden uit die taal. In deze leereenheid bestuderen we de eindige automaat; dat is de eenvoudigste automaat die met eindige middelen toch talen met oneindig veel strings kan herkennen. Die eindige middelen bestaan uit: een eindig aantal toestanden, een eindig alfabet en een (daardoor ook eindig) aantal toestandsovergangen. Verder heeft een eindige automaat een begintoestand en een aantal eindtoestanden. Ondanks het feit dat eindige automaten zo eenvoudig zijn, zijn er toch nog verschillende varianten van. De verschillen zitten in de structuur van de onderliggende graaf. Een eindige automaat is deterministisch als er maar één manier is waarop die string door die automaat herkend kan worden. Er bestaan ook niet-deterministische eindige automaten. Het blijkt dat het verschil in structuur tussen beide soorten groot is, maar dat ze toch precies even krachtig zijn. Deze leereenheid is ook onze eerste kennismaking met het begrip familie van talen. We zullen zien dat alle eindige automaten samen de familie van reguliere talen herkennen. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – een definitie van een deterministische eindige automaat kunt geven – een definitie van een niet-deterministische eindige automaat kunt geven – een definitie van equivalentie van eindige automaten kunt geven – bij een gegeven reguliere taal een (niet-)deterministische eindige automaat kunt construeren die die taal accepteert – de overeenkomsten en verschillen tussen deterministische en nietdeterministische eindige automaten kunt uitleggen – het verband tussen de begrippen totaliteit en (niet-)determinisme, en de aan- of afwezigheid van -overgangen kunt uitleggen – een gegeven niet-deterministische automaat kunt omzetten in een equivalente deterministische automaat

Studeeraanwijzingen Bij deze leereenheid hoort chapter 2 van het tekstboek van Linz. Section 2.4 daaruit is facultatief. De studielast van deze leereenheid bedraagt circa 10 uur.

LEERKERN Studeeraanwijzing

Lees de introductie op chapter 2 in het tekstboek van Linz. 1

Studeeraanwijzing

Deterministic finite accepters

Bestudeer in section 2.1 van Linz de subsection Deterministic accepters and transition graphs. Laten we de definitie van de overgangsfunctie in definition 2.1 nader bekijken. De overgangsfunctie heet  en is gedefinieerd als een totale functie van Q   naar Q. Dat wil zeggen dat aan iedere waarde (p, a) uit Q   precies één waarde q uit Q wordt toegekend – precies één omdat de functie totaal is. In termen van de eindige automaat betekent dat: als de automaat in toestand p is en het symbool a leest, dan moet hij zichzelf in toestand q zetten en naar het volgende invoersymbool gaan. In de onderliggende graaf is er dan een pijl met label a van toestand p naar toestand q.

OPGAVE 2.1

Maak exercise 1 van section 2.1. Neem in plaats van 00001101 de string 0000110. We gaan nu verder met de uitgebreide overgangsfunctie (extended transition function)  : Q  *  Q. We herhalen de recursieve definitie uit Linz: *(q, ) = q *(q, wa) = (*(q, w), a)

(2.1) (2.2)

Probeer in uw eigen woorden uit te leggen wat de uitgebreide overgangsfunctie doet, en in het bijzonder wat regel (2.2) zegt. De uitgebreide overgangsfunctie legt vast wat het resultaat is van het toepassen van een aantal overgangen (volgens de ‘gewone’ overgangsfunctie) achter elkaar. Met andere woorden, de uitgebreide overgangsfunctie geeft aan in welke toestand de automaat komt als hij vanuit een gegeven toestand een gegeven string leest in plaats van een symbool, zoals bij de gewone overgangsfunctie. Overigens is * vooral een hulpmiddel om bij het redeneren over de werking van automaten wat grotere stappen te kunnen nemen dan we met  kunnen. We hebben  nog steeds nodig om de eindige automaat te specificeren. Regel (2.2) van de recursieve definitie zegt dat het lezen van wa vanuit toestand q precies hetzelfde is als: eerst w lezen vanuit q, en vervolgens a lezen vanuit de toestand waarin de automaat na het lezen van w terechtgekomen is. Die laatstgenoemde toestand is precies *(q, w). Natuurlijk werkt zo’n recursieve definitie alleen als er een ‘bodem’ in de recursie zit; daartoe dient regel (2.1).

OPGAVE 2.2

Bepaal *(q0, 111) en *(q2, 100010) voor de automaat van figure 2.1. U hoeft hiervoor de stappen van de recursieve definitie niet helemaal uit te schrijven (dat mag wel natuurlijk); het gaat erom dat u begrijpt wat * doet. Studeeraanwijzing

Bestudeer in section 2.1 van Linz de subsection Languages and dfa’s.

Trap state

Een trap state is een toestand waar de eindige automaat niet meer uitkomt. Alle uitgaande overgangen zijn een lus naar de trap state. Trap states kunnen final zijn of niet. Een trap state is final als de toestand een eindtoestand is. Nonfinal trap states worden vaak gebruikt om ervoor te zorgen dat de overgangsfunctie totaal is. We beginnen de bespreking van deze subsection met een paar opgaven over talen en bijbehorende eindige automaten.

OPGAVE 2.3

Laat  = {a, b}. Construeer dfa’s die de verzamelingen accepteren die bestaan uit: a alle strings met precies één a b alle strings met minstens één a c alle strings met hoogstens drie a’s d alle strings met ten minste één a en precies twee b’s OPGAVE 2.4

Construeer dfa’s voor de volgende talen: a L1 = {x  {a, b}* : x bevat de substring bab} b L2 = {x  {a, b}* : x bevat geen substring bab} OPGAVE 2.5

Maak exercise 9 van section 2.1. OPGAVE 2.6

Maak exercise 10 van section 2.1. In opgave 2.5 en 2.6 hebben we een constructie gezien waarmee we bij iedere gegeven dfa M een dfa M’ kunnen maken zodat M’ precies het complement van M accepteert. In leereenheid 4 zien we nog meer van zulke constructies. Uit de uitwerking bij opgave 2.5 kunnen we nog iets anders afleiden: het totaal zijn van de overgangsfunctie van een dfa en het deterministisch zijn van die dfa zijn twee aparte begrippen. Het determinisme volgt immers uit het definiëren van de verzameling overgangen met behulp van een functie van Q   naar Q, terwijl de totaliteit een extra eis aan die functie is. Een dfa zoals gedefinieerd in definition 2.1 is totaal en deterministisch. Stel dat we de eis weglaten dat de overgangsfunctie van een dfa totaal is. Is er dan nog een manier om vast te stellen dat een bepaalde invoerstring niet wordt geaccepteerd?

Normaalvorm

Ja, maar we moeten dan twee gevallen onderscheiden: voor invoerstrings die niet geaccepteerd moeten worden, geldt óf dat de dfa na het lezen van de gehele string niet in een eindtoestand is, óf dat de dfa blijft ‘hangen’ (in een eindtoestand of in een niet-eindtoestand) voordat het einde van de invoerstring bereikt is. We kunnen afspreken dat in beide gevallen de string niet geaccepteerd wordt. Voor strings die wel geaccepteerd moeten worden, verandert er niets. In de literatuur zien we dan ook vaak definities van eindige automaten die niet totaal hoeven te zijn, onder andere omdat we ons dan niet druk hoeven te maken om een nonfinal trap state, en we daardoor kleinere, overzichtelijkere automaten krijgen. Aan de andere kant is het voor sommige resultaten over eindige automaten handig of zelfs noodzakelijk om aan te kunnen nemen dat de overgangsfunctie totaal is (zoals voor het nemen van het complement – zie opgave 2.5 en 2.6). Gelukkig zijn totale eindige automaten een normaalvorm voor eindige automaten: voor iedere eindige automaat kunnen we eenvoudig een eindige automaat met een totale overgangsfunctie construeren die dezelfde taal accepteert.

OPGAVE 2.7

Laat zien dat (en hoe) voor iedere niet-totale eindige automaat een totale eindige automaat geconstrueerd kan worden die precies dezelfde taal accepteert. Afspraak

Als u in deze cursus gevraagd wordt een dfa voor een gegeven taal te construeren, dan hoeft u deze niet totaal te maken, tenzij daar expliciet om gevraagd wordt. Dit geldt ook voor opdrachten in JFLAP, want JFLAP accepteert ook niet-totale automaten.

OPDRACHT 2.8

a Maak exercise 11a van section 2.1. b Teken uw dfa in JFLAP en controleer de gegeven voorbeeldstrings. Studeeraanwijzing

Bestudeer van section 2.1 in Linz de subsection Regular languages. Een belangrijk punt in deze subsection is het verschil tussen een taal en een familie van talen. Een taal is een verzameling strings, en een familie van talen is een verzameling talen. De talen in een familie hebben qua structuur iets met elkaar te maken: voor iedere taal in de familie van reguliere talen bijvoorbeeld kan een dfa geconstrueerd worden die die taal accepteert. We leren later nog andere families van talen kennen, en het verband tussen die families en de familie van reguliere talen.

OPGAVE 2.9

Maak exercise 15 van section 2.1. 2 Studeeraanwijzing

Nondeterministic finite accepters

Bestudeer section 2.2 van Linz. Het belangrijkste verschil tussen een dfa en een nfa is dat in een nfa een toestand meer dan één uitgaande pijl met hetzelfde label mag hebben – zie bijvoorbeeld figure 2.8. Let hier ook op het woord mag: een nfa kan toevallig ook een dfa zijn. Met andere woorden: iedere dfa is een nfa, maar niet andersom.

OPGAVE 2.10

Wat zijn de verschillen en overeenkomsten tussen de manier waarop een dfa strings accepteert en de manier waarop een nfa dat doet? OPGAVE 2.11

Construeer een nfa voor de taal L = {01, 010}*. Probeer ook (heel even) een dfa voor L te vinden. OPGAVE 2.12

Construeer een nfa voor de taal L = {w  {a, b}* : w bevat de substring bab}. OPGAVE 2.13

Bekijk onderstaande nfa.

Wat is volgens u de taal die door deze nfa wordt geaccepteerd? Motiveer uw antwoord. U kunt uw antwoord controleren met JFLAP voordat u de uitwerking achteraan deze leereenheid bekijkt: de nfa is beschikbaar in het bestand Jexercise2.2.jff in de map JFLAP ActivitiesJFLAP FilesJFLAP Exercises . Zoals in de subsection Definition of a nondeterministic accepter in het tekstboek wordt opgemerkt, zijn er drie belangrijke verschillen tussen de definities van de overgangsfunctie in een dfa en in een nfa. We herhalen beide definities hier en bespreken ze dan: :Q Q  : Q  (  {})  2Q

(dfa, definition 2.1) (nfa, definition 2.4)

Merk om te beginnen op dat er een vierde verschil tussen beide definities is: de  van een nfa lijkt qua structuur veel op de  van een dfa, maar betekent toch iets heel anders. Als een dfa de overgang (p, a) = q heeft, dan kunnen we q weer ‘in  stoppen’ om zo nog een stap te doen: bijvoorbeeld (q, b) = r. De dfa heeft dan een pijl met label a van toestand p naar toestand q, en vanuit q gaat er een pijl met label b naar toestand r. Als daarentegen een nfa een overgang (p, a) = {q, r} heeft, dan kunnen we het resultaat (de verzameling {q, r}) niet weer ‘in  stoppen’, want het eerste argument van  moet een toestand zijn, en niet een verzameling toestanden. Om uit de  van een nfa te kunnen afleiden hoe de automaat

er uit ziet, is de uitleg onder definition 2.4 onontbeerlijk: als we weten dat (p, a) = {q, r}, dan zoeken we daarna (q, …) en (r, …) op (waarbij we op de puntjes ieder invoersymbool mogen invullen). Als we dan bijvoorbeeld vinden dat (q, a) = {q}, (q, b) = {p, r} en (r, a) = {q}, dan weten we dat de automaat in ieder geval de volgende overgangen heeft:

Waaraan kunt u in bovenstaand plaatje zien dat de (onvolledige) automaat die daar getekend is niet-deterministisch is? De automaat heeft een toestand van waaruit twee pijlen met hetzelfde label vertrekken, en daarom is de automaat niet-deterministisch. Deze automaat heeft zelfs twee van zulke toestanden: vanuit p vertrekken twee pijlen met een a, en vanuit q vertrekken er twee met een b. Laten we nu deze observatie eens vergelijken met definition 2.4, in het bijzonder met de definitie van  daar, namelijk  : Q  (  {})  2Q. Linz heeft er dus voor gekozen om niet-deterministische eindige automaten te definiëren als eindige automaten die niet deterministisch hoeven te zijn, èn die -overgangen mogen hebben. Vindt u dat een logische keuze? Aan de ene kant is dat een heel logische keuze: iedere automaat die een -overgang bevat, is niet-deterministisch (zie opgave 2.14). Aan de andere kant kun je best niet-determinisme hebben zonder -overgangen, zoals we net gezien hebben. Net als determinisme en totaliteit in de vorige paragraaf hoeven niet-determinisme en -overgangen dus niet per se tegelijkertijd voor te komen. Overigens maakt het niet zoveel uit of je wel of geen -overgangen toelaat: eindige automaten zonder -overgangen zijn een normaalvorm, dat wil zeggen, voor iedere eindige automaat kun je een eindige automaat maken die geen -overgangen bevat maar die wel precies dezelfde taal accepteert. We bespreken die constructie hier echter niet. OPGAVE 2.14

Waarom is de eindige automaat van figure 2.10 in Linz, met overgangen (q0, a, q1), (q1, , q2) en (q2, , q0), en eindtoestand q1, niet-deterministisch? OPGAVE 2.15

Maak exercise 13 van section 2.2. OPGAVE 2.16

a Construeer een nfa voor de taal La = {b}*{a}  {b}{a}*{b}*. b Construeer een nfa voor de taal Lb = {01}*  {10}*.

We begonnen deze deelparagraaf met de opmerking dat er drie verschillen zijn tussen de definities van  in definition 2.1 en definition 2.4. Als eerste noemt Linz het feit dat bij de nfa rechts van de pijl 2Q staat, terwijl dat bij de dfa Q was. We hebben al gezien dat dit verschil zorgt voor het mogelijke niet-determinisme in een nfa: de waarde van (q, a) is nu een verzameling toestanden (met mogelijk meer dan één element), terwijl dat eerst altijd één toestand was. Als tweede noemt Linz het mogelijke voorkomen van -overgangen in een nfa; ook dat verschil hebben we besproken. Het derde verschil is dat in de overgangsfunctie van een nfa de verzameling (q, a) leeg mag zijn. Dit betekent dan dat er geen overgang is gespecificeerd voor de combinatie (q, a). Deze uitleg is een logisch vervolg op wat we hiervoor besproken hebben:  als (q, a) = {p, r}, dan zijn er twee overgangen voor de combinatie (q, a),  als (q, a) = {p}, dan is er maar één overgang voor die combinatie, en  als (q, a) = , dan is er geen overgang voor die combinatie. OPGAVE 2.17

Gegeven is de taal L = {vwv : v, w  {a, b}* en v = 2}. a Is aabaa een element van L? En ababba, bab en baba? b Construeer een nfa zonder -overgangen die L accepteert. c Construeer nu een dfa voor L. OPGAVE 2.18

Maak exercise 19 van section 2.2. 3

Studeeraanwijzing

Equivalence of deterministic and nondeterministic finite accepters

Bestudeer section 2.3 uit het tekstboek van Linz. In de vorige paragraaf hebben we gemerkt dat het soms makkelijker is een nfa voor een taal te construeren dan een dfa. Maar we hadden daarvoor, bij de complementconstructie, al gezien dat het in sommige gevallen heel handig is als we kunnen uitgaan van een deterministische automaat. Dat is ook vaak zo als we iets willen bewijzen over een specifieke automaat of over automaten in het algemeen. Gelukkig is de deterministische eindige automaat een normaalvorm: voor iedere willekeurige eindige automaat kunnen we een equivalente deterministische eindige automaat construeren. Het grote verschil tussen een nfa en een dfa is dat je in een nfa voor één invoerstring verschillende paden vanuit de begintoestand kunt hebben (zie bijvoorbeeld de uitwerking van opgave 2.15). Het idee achter de constructie om van een willekeurige nfa een equivalente dfa te maken (de procedure nfa-to-dfa uit Linz) is om die verschillende paden als het ware samen te vatten in één pad in de dfa. Iedere toestand op dat pad in de dfa houdt dan bij in welke toestanden de nfa op dat moment (op die positie van de invoerstring) allemaal kan zijn. Het bijhouden van meerdere nfa-toestanden in één dfa-toestand gaat het makkelijkst met een verzameling, en iedere dfa-toestand heeft dan als label een deelverzameling van de verzameling toestanden van de nfa. De constructie verandert niet als de nfa -overgangen bevat, maar wordt er wel wat lastiger door uit te voeren. We kijken dus eerst naar een voorbeeld zonder -overgangen (opgave 2.19).

OPGAVE 2.19

Gegeven is de nfa M:

Construeer een equivalente dfa. In de uitwerking van opgave 2.19 merkten we op dat de resulterende dfa totaal was, terwijl de oorspronkelijke nfa dat niet was. Als we de procedure nfa-to-dfa heel precies uitvoeren, is het resultaat altijd een totale deterministische automaat, want de procedure zegt expliciet dat we daarvoor moeten zorgen. We vinden het echter ook goed genoeg om tijdens het uitvoeren van nfa-to-dfa alleen die toestanden en overgangen te introduceren waar we op natuurlijke wijze tegenaan lopen. Dat wil zeggen, als de dfa-onder-constructie een toestand {p, q} heeft, en de oorspronkelijke nfa heeft noch vanuit p noch vanuit q een overgang met label a, dan geven we de dfa ook geen overgang met label a vanuit toestand {p, q}. Mocht het later wenselijk zijn om toch een totale deterministische automaat te hebben, dan is een nonfinal trap state met bijbehorende overgangen zo toegevoegd. Verder zegt de procedure nfa-to-dfa niets over het aanhouden van een bepaalde volgorde bij het afwerken van de nieuw gevonden toestanden en de invoersymbolen. Dat hoeft natuurlijk ook niet, maar enige systematiek geeft wel meer overzicht. We raden u dan ook aan om, net als in de procedure, altijd te beginnen met {q0}, en om dan eerst voor alle a   te bepalen in welke (eventueel nieuwe) toestand(en) u met die a vanuit {q0} kunt komen. Vervolgens werkt u dan de nieuwe toestanden af, bijvoorbeeld in de volgorde waarin u ze gevonden hebt. Zorg steeds dat u alle mogelijke overgangen vanuit een toestand bekeken hebt voordat u met de volgende toestand verder gaat. Op deze manier krijgt u niets meer en niets minder dan alle toestanden en overgangen die u nodig hebt. We gaan nu ook opgaven maken waarin een nfa die -overgangen bevat ‘deterministisch gemaakt moet worden’. Dat is duidelijk een stuk moeilijker dan wanneer de nfa geen -overgangen bevat. Het kan enorm helpen om eerst een tabel te maken waarin u voor iedere toestand q uit de nfa en voor ieder invoersymbool a aangeeft welke elementen de verzameling *(q, a) bevat. Let op de *: die geeft aan dat die ene a gelezen kan worden door meer dan één overgang te volgen! In het uiterste geval namelijk eerst een aantal -overgangen, dan een a-overgang en dan weer een aantal -overgangen. OPGAVE 2.20

Maak exercise 1 van section 2.3.

OPGAVE 2.21

Maak exercise 2 van section 2.3. Daar wordt gevraagd om de nfa van exercise 13 van section 2.2 te converteren naar een equivalente dfa. OPDRACHT 2.22

a Construeer een equivalente dfa bij de volgende nfa :

b Gegeven is de volgende nfa uit bouwsteen opdracht02.22b.jff. Is deze nfa equivalent aan die van onderdeel a (bouwsteen opdracht02.22a.jff)? Gebruik JFLAP om dit te controleren.

OPGAVE 2.23

Maak exercise 9 van section 2.3. OPGAVE 2.24

Als we een gegeven nfa met behulp van de procedure nfa-to-dfa omschrijven naar een dfa, kan die dfa dan -overgangen bevatten? heeft ook een mogelijkheid om een gegeven nfa te converteren naar een dfa: kies ConvertConvert to DFA. U kunt via internet in de JFLAP Tutorial opzoeken hoe dit precies werkt. Er zijn twee mogelijkheden: u laat JFLAP per toestand uitzoeken welke overgangen en nieuwe toestanden de dfa nodig heeft (met de knop State Expander), of u geeft dit zelf aan per toestand (met de knop Expand Group on Terminal). De eerste manier kan heel handig zijn om de procedure nfa-to-dfa te leren (met JFLAP in de rol van docent die laat zien hoe de constructie werkt), maar het moge duidelijk zijn dat het minstens zo belangrijk is om de procedure helemaal zelf te oefenen. De tweede manier gebruikt een wat onhandige notatie, met een naam èn een label voor iedere toestand; de naam is daarbij wat verwarrend, want die komt meestal ook voor in de nfa maar heeft daar niets mee te maken: in het label wordt bijgehouden in welke verzameling toestanden de nfa op dat moment kan zijn. JFLAP

OPDRACHT 2.25

Open bouwsteen opdracht02.22a.jff van opdracht 2.22. Converteer deze nfa met behulp van JFLAP naar een dfa. Probeer beide hierboven genoemde methoden uit.

ZELFTOETS

1

a Construeer een dfa voor de taal K = {w  {a, b}* : als w een a bevat, dan bevat w precies twee b’s}. b Construeer nu een nfa voor K.

2

Gegeven is de eindige automaat M:

a Is M totaal? Is M deterministisch? Motiveer uw antwoord. b Bepaal *(q, s) voor alle q  Q en alle s  . c Construeer een dfa die equivalent is met M. 3

Laat zien dat alle eindige talen regulier zijn.

4

Kunt u beredeneren waarom de taal L = {anbn : n ≥ 1} niet regulier is?

TERUGKOPPELING 1

Uitwerking van de opgaven

2.1

Voor 0001 komt de automaat na de begintoestand q0 achtereenvolgens in toestand q0, q0, q0 en q1, en dan is de hele string gelezen. De laatste toestand is een eindtoestand, dus 0001 wordt geaccepteerd. Voor 01101 komt de automaat na de begintoestand q0 achtereenvolgens in toestand q0, q1, q2, q2 en q1, dus 01101 wordt geaccepteerd. Voor 0000110 komt de automaat na de begintoestand q0 achtereenvolgens in q0, q0, q0, q0, q1, q2 en q2. De hele string is gelezen, maar q2 is geen eindtoestand, dus 0000110 wordt niet geaccepteerd.

2.2

*(q0, 111) = q1, dat wil zeggen: als de automaat van figure 2.1 in de begintoestand begint, dan is hij na het lezen van 111 in toestand q1. *(q2, 100010) = q0, dus 100010 lezen vanuit q2 brengt ons weer terug in de begintoestand.

2.3

a

Een dfa voor de taal La = {x  {a, b}* : x bevat precies één a} is:

We gebruiken (de indices van) de namen van de toestanden om te onthouden hoeveel a’s er al gelezen zijn: in q0 zijn dat er geen, in q1 is dat er één, en in q2 zijn dat er twee of meer. Ter controle proberen we een aantal goed gekozen strings uit; sommige moeten geaccepteerd worden (bijvoorbeeld a, ab, ba, bbbbabbbb) en andere juist niet (bijvoorbeeld , b, bbbb, abab, aa). In al deze gevallen geeft de gegeven dfa het goede antwoord. Ga dit na! b Een dfa voor de taal Lb = {x  {a, b}* : x bevat minstens één a} is:

Ter controle: , b, bbbbb moeten afgekeurd worden, en a, aa, babababab moeten geaccepteerd worden. Dat gebeurt ook inderdaad. c

Een dfa voor de taal Lc = {x  {a, b}* : x bevat hoogstens drie a’s} is:

d Een dfa voor de taal Ld = {x  {a, b}* : x bevat minstens één a en precies twee b’s} is:

We hebben de toestanden in deze dfa namen gegeven die aangeven welke informatie in die toestand bekend is. In dit geval gebruiken we bijvoorbeeld a0,b1 om aan te geven dat er nog geen a’s gelezen zijn (van de minstens één die we uiteindelijk moeten hebben), maar wel al één b (van de precies twee die we moeten hebben). Door het kiezen van de juiste eindtoestand zorgen we er dan voor dat precies Ld geaccepteerd wordt. 2.4

a Het idee achter onze oplossing is om in de toestanden bij te houden welk deel van bab al herkend is. De toestand ba. staat bijvoorbeeld voor de situatie dat de automaat net de (sub)string ba heeft gelezen. Als hij in die situatie een a tegenkomt, dan is het laatstgelezen stuk van de invoer blijkbaar baa en kan hij weer opnieuw beginnen met zoeken naar bab: in de substring baa zit geen beginstuk (prefix) van bab. Hij gaat dus weer naar de begintoestand … en leest het volgende symbool van de invoer. Hij accepteert alleen strings waarvoor hij eindigt in de toestand bab; vanwege de lus met label a,b bij die toestand accepteert hij niet alleen strings die op bab eindigen, maar ook strings die bab als substring hebben. De dfa voor L1 = {x  {a, b}* : x bevat de substring bab} die we zo krijgen is:

In opgave 2.12 zullen we een makkelijkere manier zien om een eindige automaat voor deze taal te maken. b Voor L2 = {x  {a, b}* : x bevat geen substring bab} gebruiken we precies dezelfde strategie, en we krijgen ook precies dezelfde onderliggende graaf. Het enige verschil is de eindtoestand: we moeten nu alleen strings accepteren die niet de volledige substring bab bevatten.

We krijgen dan de volgende dfa voor L2:

2.5

(Eigenlijk hebben we hetzelfde net al gedaan in opgave 2.4b, maar dan voor een andere automaat, en zonder uit te leggen waarom het ‘verwisselen’ van eind- en gewone toestanden het gewenste resultaat oplevert.) De overgangsfunctie van de eindige automaat in figure 2.6 is totaal (want dat moet volgens de definitie van dfa), dus voor iedere toestand en ieder invoersymbool is er precies één pijl vanuit die toestand naar een (eventueel andere) toestand, met dat invoersymbool als label van de pijl. Dat betekent dat deze automaat alle strings over {a, b}* helemaal uitleest. Bovendien is de dfa deterministisch (omdat de overgangsfunctie een functie is). Dat wil zeggen dat er voor iedere invoerstring hoogstens één pad door de automaat is. Deze twee aspecten samen (totaal en deterministisch) zorgen ervoor dat er voor iedere invoerstring precies één pad door de automaat is. Als dat pad eindigt in een eindtoestand wordt de string geaccepteerd, als het pad eindigt in een niet-eindtoestand wordt de string niet geaccepteerd. En als we dus eindtoestanden en nieteindtoestanden ‘verwisselen’, zoals gevraagd in de opgave, dan worden door de nieuwe automaat precies die strings geaccepteerd die door de oorspronkelijke niet werden geaccepteerd, en andersom. Inderdaad is de taal die door de nieuwe automaat wordt geaccepteerd dan het complement van de taal van de oude automaat. Merk op dat dit alles niets te maken heeft met de taal in kwestie; alleen de structuur van de oorspronkelijke eindige automaat is van belang.

2.6

Laat M = (Q, , , q0, F) en M1 = (Q, , , q0, Q – F) twee totale deterministische eindige automaten zijn. Dan geldt: L(M1) = {w  * : *(q0, w)  Q – F} (definition 2.2 toegepast) = {w  * : *(q0, w)  F} (dat klopt alleen omdat beide automaten totaal en deterministisch zijn) = L(M )

2.7

Laten we uitgaan van een eindige automaat M = (Q, , , q0, F) die wel deterministisch is, maar niet totaal (we laten dus de kwalificatie ‘totaal’ weg uit de beschrijving van  in definition 2.1). We voegen nu een nieuwe toestand qtrap aan Q toe, die dienst gaat doen als trap state: we voegen ook de overgangen (qtrap, a) = qtrap toe voor alle a  . Verder voegen we voor iedere combinatie van een toestand q  Q – {qtrap} en een a   waarvoor de oorspronkelijke automaat geen overgang heeft, de overgang (q, a) = qtrap toe. Kortom: we vullen de oorspronkelijke automaat gewoon aan met de ontbrekende overgangen, en laten die allemaal ‘doodlopen’ in een nieuwe trap state. Verder komt er voor ieder invoersymbool een overgang van de trap state naar zichzelf.

2.8

a Een oplossing is de volgende automaat, die als volgt is geconstrueerd:  in toestand q0 is nog niets van 00 gelezen, en zolang we ook niets van 00 lezen (dus alleen 1’en zien) kunnen we daar blijven en ook meteen alles accepteren  in toestand q1 is de eerste 0 van 00 gelezen, maar zolang we de tweede 0 niet zien hoeven we nergens op te letten, dus we kunnen weer accepteren. Als we nu een 1 lezen weten we dat het niet ging om ‘de eerste 0 van 00’, dus kunnen we terug naar toestand q0 (‘niets van 00 gelezen’). Als we echter na de 0 nog een 0 zien hebben we wel 00 gelezen, en moeten we gaan opletten dat er nu een 1 volgt. We gaan dus naar een nieuwe toestand q2, waarin we niet mogen accepteren.  vanuit q2 moeten we een 1 lezen, en daarna kunnen we eventueel doorlezen en op zoek gaan naar een volgende 00; we gaan dus naar toestand q0.

Merk op dat we (volgens onze afspraak) de nonfinal trap state achterwege hebben gelaten. De automaat is dus niet totaal maar wel deterministisch. b Het controleren van de voorbeeldstrings gaat eenvoudig door InputMultiple Run te kiezen. Aan de rechterkant kunt u dan onder Input de voorbeeldstrings invoeren (geef steeds een enter na iedere invoerstring). Als u alle gewenste strings hebt ingevoerd klikt u op Run Inputs. Naast iedere geaccepteerde string verschijnt dan Accept, naast iedere niet geaccepteerde string verschijnt Reject. Met de andere drie opties uit het menu Input (Step with Closure…, Step by State… en Fast Run…) kunt u de strings één voor één testen, waarbij u dan ook nog kunt zien welk pad door de automaat gekozen wordt. 2.9

2.10

De voor de hand liggende oplossing is de volgende automaat:

Een string wordt geaccepteerd door een dfa als de string correspondeert met een wandeling van de begintoestand naar een eindtoestand. In een dfa is deze wandeling uniek: er is maar één manier om met die string van de begintoestand in een eindtoestand te komen.

Voor een nfa geldt hetzelfde: een string wordt geaccepteerd door een nfa als de string correspondeert met een wandeling van de begintoestand naar een eindtoestand. Het verschil is dat in een nfa deze wandeling niet uniek hoeft te zijn: er kan meer dan één manier zijn om met die string van de begintoestand in een eindtoestand te komen. 2.11

Een nfa voor L is snel gemaakt:

Een dfa voor dezelfde taal is veel lastiger (direct) te vinden; we gaan ons daar dan ook niet in verdiepen. In de volgende paragraaf bespreken we een manier om bij een gegeven nfa een dfa te construeren die precies dezelfde taal accepteert. 2.12

Een nfa voor L is veel eenvoudiger te maken dan een dfa (zie daarvoor opgave 2.4 a):

De tactiek van de dfa van opgave 2.4 a is om het eerste voorkomen van de substring bab in de invoerstring te herkennen. De nfa daarentegen herkent eender welk voorkomen van de substring bab. 2.13

Het enige wat toestand q1 doet is een extra manier bieden om van q0 met een a in q3 te komen; dat kan ook al rechtstreeks. We kunnen toestand q1 dus veilig weglaten, inclusief de twee pijlen die eraan vast zitten. Het driehoekje q0 – q3 – q5 (en weer terug naar q0) herkent de taal {aa, ab, ba, bb}+. De vraag is nu of de toestanden q2 en q4 met de bijbehorende overgangen daar nog iets aan toevoegen. Het antwoord is nee: er zijn twee zinvolle paden die gebruik maken van q2 en q4, en beide kunnen worden ‘nagedaan’ binnen de driehoek q0 – q3 – q5. Een alternatief voor het pad q3 – q2 – q4 – q3 met label ab is het pad q3 – q5 – q0 – q3. Een alternatief voor het pad q3 – q2 – q4 – q5 met label aba of abb is q3 – q5 – q0 – q3 – q5. We kunnen toestanden q2 en q4 met de bijbehorende overgangen dus ook verwijderen zonder de geaccepteerde taal te veranderen. De automaat die dan overblijft, accepteert duidelijk precies {aa, ab, ba, bb}+. U kunt dit in JFLAP controleren door Jexercise2.2.jff op te slaan onder een andere naam, in dat tweede bestand toestanden q1, q2 en q4 te verwijderen, en dan (zorg dat beide bestanden geopend zijn!) in een van beide vensters te kiezen voor TestCompare Equivalence.

2.14

De automaat van figure 2.10, met overgangen (q0, a, q1), (q1, , q2) en (q2, , q0), is niet-deterministisch omdat er een -overgang in zit: als de automaat in toestand q1 is, dan kan hij kiezen of hij daar blijft of dat hij naar toestand q2 gaat, in beide gevallen zonder een symbool van de invoerstring te lezen. Er zijn weliswaar niet twee uitgaande pijlen met hetzelfde label vanuit toestand q1, maar op basis van dezelfde actie op de invoerstring (namelijk het niet lezen van het volgende symbool), kan de automaat wel kiezen uit twee verschillende ‘bestemmingen’, namelijk q1 en q2. Omdat er ook een -overgang is vanuit q2 naar q0, kan hij er zelfs ook nog voor kiezen geen invoersymbool te lezen en naar q0 te gaan. Het is dus wel zo dat een automaat met (minstens) een -overgang niet-deterministisch is, maar een niet-deterministische automaat hoeft geen -overgangen te hebben.

2.15

We introduceren even een notatie voor paden door een automaat: p ab  q b betekent dat de automaat in toestand p begint met het lezen van de invoerstring ab; na het lezen van het symbool a is de automaat in toestand q en moet dan nog b lezen. 00 wordt niet geaccepteerd door de automaat van exercise 13, want alle mogelijke paden vanuit de begintoestand met label 00 zijn: q0 00  q1 0  q0 (de string is helemaal gelezen, maar q0 is geen eindtoestand) q0 00  q1 0  q2 (ook niet geëindigd in eindtoestand) q0 00  q1 0  q2 0  ?? (loopt dood) 01001 wordt wel geaccepteerd, want er is een pad van de begintoestand naar de eindtoestand met label 01001: q0 01001  q1 1001  q1 001  q0 01  q1 1  q1, en dat is de eindtoestand. 10010 wordt niet geaccepteerd: q0 10010  q1 0010  q0 010  q1 10  q1 0  q0q0 10010  q1 0010  q0 010  q1 10  q2 10  q1 0  q0 q0 10010  q1 0010  q0 010  q1 10  q2 10  q1 0  q2 q0 10010  q1 0010  q0 010  q1 10  q2 10  q1 0  q2 0  ?? q0 10010  q1 0010  q2 010  ?? q0 10010  q1 0010  q2 0010  ?? Geen van deze mogelijkheden leidt naar een eindtoestand. 000 wordt wel geaccepteerd, maar 0000 weer niet.

2.16

a De taal La = {b}*{a}  {b}{a}*{b}* is ook een van die talen waarvoor veel makkelijker een nfa dan een dfa gemaakt kan worden, en waarbij -overgangen ook goed van pas komen. Een oplossing is de volgende:

b Een nfa (die toevallig wel deterministisch, maar niet totaal is) voor Lb is:

2.17

a Voor de eerste string geldt aabaa  L, want die begint en eindigt met dezelfde string van lengte 2 (alles over {a, b}*). De tweede string doet dat niet, dus ababba  L. De string bab is te kort; alle strings in L hebben immers ten minste lengte 4. Dus bab  L. De laatste string, baba, is wel een element van L, met w =  en v = ba. b De meest voor de hand liggende eindige automaat voor L is nietdeterministisch en ziet er als volgt uit:

c Uitgaande van de nfa van onderdeel b kunnen we de toestanden q1 en q5 samennemen, evenals q9 en q13. Dan moeten we de dubbele a-overgangen vanuit q2 en die vanuit q6 nog oplossen, en de dubbele b-overgangen vanuit q10 en die vanuit q14. Bij q2 komt dat neer op het verzinnen van een deterministische eindige automaat voor “eindigt op aa”. Het resultaat wordt dan als volgt:

2.18

Zie de uitwerking op pagina 405 in Linz. Het idee is om een nieuwe toestand aan de automaat toe te voegen, met daarvandaan een -overgang naar iedere begintoestand van de oorspronkelijke automaat. Vervolgens worden alle begintoestanden weer gewone toestanden (ze blijven wel eindtoestand als ze dat al waren), en de nieuw toegevoegde toestand wordt de enige begintoestand.

2.19

De enige toestand waarin een (zinnig) pad door de nfa kan beginnen is toestand s, dus de begintoestand van de dfa wordt {s}. Vanuit s kunnen we in de nfa met een 0 naar s èn naar p. In de dfa vatten we deze twee paden (in dit geval zijn het gewoon overgangen) samen in het pad (de overgang) ({s}, 0, {s, p}), naar een nieuwe toestand {s, p}. Er is nog een overgang vanuit toestand s in de nfa, met label 1. Deze overgang eindigt in toestand s. We hoeven dus niet nóg een nieuwe toestand aan de dfa toe te voegen: we maken alleen een overgang ({s}, 1, {s}). We zijn nu klaar met toestand {s}. Het plaatje is nu als volgt:

We gaan verder met de enige nog niet afgewerkte toestand van de dfa, dus met {s, p}. In de nfa kunnen we vanuit s met een 0 naar s en naar p (dat hadden we net ook al gezien, maar het levert nu een bijdrage aan een andere overgang in de dfa!), en vanuit p met een 0 naar q. Samengevat: vanuit {s, p} kunnen we met een 0 naar {s, p, q}, en deze laatste toestand bestond nog niet in de dfa, dus die voegen we toe, met de bijbehorende overgang. In de nfa is q een eindtoestand, dus wordt {s, p, q} ook een eindtoestand. We kijken ook meteen waar we in de nfa vanuit {s, p} met een 1 kunnen komen: vanuit s is dat s zelf weer, vanuit p nergens. De dfa krijgt dus ook een overgang ({s, p}, 1, {s}). Het plaatje wordt dan:

Weer is er maar één toestand bij gekomen: {s, p, q}. Met een 0 kunnen we in de nfa vanuit s in s en p komen, vanuit p in q en vanuit q nergens; de dfa krijgt dus een overgang ({s, p, q}, 0, {s, p, q}). Met een 1 kunnen we in de nfa vanuit dit drietal toestanden alleen maar in s komen; de dfa krijgt dus een overgang ({s, p, q}, 1, {s}). Er zijn geen nieuwe toestanden bij gekomen, dus het eindresultaat is:

Merk op dat deze automaat totaal is, terwijl de oorspronkelijke nfa dat niet is. Als u de procedure nfa-to-dfa precies volgt is het resultaat altijd een totale deterministische automaat; als u de pragmatische benadering volgt die we in deze opgave beschreven hebben, kan het resultaat ook een niet-totale deterministische automaat zijn. 2.20

We beginnen met de dfa een begintoestand {q0} te geven. Hiervandaan loopt natuurlijk nog geen overgang met label a, dus die gaan we nu toevoegen. Maar waar moet die heen? Vanuit q0 kunnen we in de nfa met een a naar q1. Verder kunnen we vanuit q1 via de -overgang ‘doorlopen’ naar q2, en daarvandaan zelfs weer doorlopen naar q0. Dan kunnen we niet meer verder zonder het volgende invoersymbool te lezen (en bovendien hebben we alle toestanden van de nfa al gehad), dus de dfa krijgt een overgang ({q0}, a, {q0, q1, q2}). Omdat q1  {q0, q1, q2} wordt de nieuwe toestand meteen eindtoestand. De voorlopige dfa is:

Volgens de procedure nfa-to-dfa moeten we nu *(q0, a)  *(q1, a)  *(q2, a) berekenen. We beginnen vooraan, met *(q0, a), en zien dat we die in de eerste stap al berekend hebben: *(q0, a) = {q0, q1, q2}. En omdat {q0, q1, q2} alle toestanden van de nfa bevat, kan deze verzameling niet uitgebreid worden door vereniging met *(q1, a) en *(q2, a). We zijn dus klaar, en het resultaat is:

We hadden dit resultaat ook direct kunnen bereiken, dus zonder de procedure nfa-to-dfa uit te voeren, door in te zien dat de taal die door de nfa van figure 2.10 wordt geaccepteerd gelijk is aan {an : n > 0}. De voor de hand liggende dfa voor die taal is bovenstaande automaat. 2.21

Dit is zo’n opgave waarbij veel verwarring en vergissingen voorkomen kunnen worden door eerst een tabel met de waarden van * voor de nfa te maken. Die tabel ziet er als volgt uit: *

0

1

q0 q1 q2

{q1, q2} {q0, q2} 

{q1, q2} {q1, q2} {q1, q2}

We hebben hier niets anders gedaan dan de mogelijkheden in de nfa opsommen, bijvoorbeeld: vanuit q0 kunnen we met een 1 naar q1 (via de 1-overgang) of naar q2 (via de 1-overgang gevolgd door de -overgang). We hebben dus alvast wat voorwerk gedaan voor de procedure nfa-todfa, want daar hebben we deze informatie ook nodig. Merk op dat we nog steeds wel goed moeten opletten dat we de tabel vullen met de juiste waarden, vanwege de -overgangen! Het ‘echte’ werk is nu eenvoudig: we beginnen de dfa met een begintoestand {q0}, en geven die een 0-overgang naar {q1, q2} en ook een 1-overgang naar {q1, q2}. Nu komt het moment waarop we voordeel hebben van de tabel: om te bepalen naar welke toestanden we overgangen vanuit {q1, q2} moeten toevoegen, nemen we voor de 0-overgang *(q1, 0)  *(q2, 0) = {q0, q2}   = {q0, q2}, en voor de 1-overgang *(q1, 1)  *(q2, 1) = {q1, q2}  {q1, q2} = {q1, q2}. Zo gaan we verder tot we uiteindelijk de volgende dfa krijgen:

2.22

a

We maken eerst weer de tabel voor *:

*

0

1

q0 q1 q2

{q0, q1, q2} {q0, q1, q2} {q2}

{q1, q2} {q1, q2} {q1}

Daarna is het construeren van de dfa eenvoudig:

b De nfa's zijn equivalent. Dit kunnen we in JFLAP controleren door beide bestanden te openen en dan vanuit één van beide de optie TestCompare Equivalence te gebruiken. 2.23

Voeg een nieuwe eindtoestand pf toe, en voeg voor iedere q  F een overgang toe van q naar deze pf. Maak dan alle ‘oude’ eindtoestanden gewone toestanden, zodat pf de enige eindtoestand is. Het is dan eenvoudig om aan te tonen dat als eerst *(q0, w)  F   gold, nu pf  *(q0, w) geldt, en andersom. Deze constructie werkt niet voor dfa’s, omdat daarin geen -overgangen mogen voorkomen. Er zou natuurlijk een andere constructie te verzinnen kunnen zijn die wel werkt, maar dat blijkt niet zo te zijn. Het is namelijk zo dat een dfa voor de taal {, a} alleen met twee eindtoestanden gemaakt kan worden:

Het is dus inderdaad zo dat zo’n constructie voor dfa’s niet bestaat. Dat ligt overigens geheel aan het feit dat we rekening moeten houden met talen die  bevatten, maar daar gaan we verder niet op in. 2.24

Nee, want de procedure nfa-to-dfa (en ook onze praktische versie daarvan) zegt dat we overgangen moeten blijven toevoegen zolang er toestanden zijn die nog niet voor alle a   een overgang hebben. Er komen dus alleen overgangen voor invoersymbolen, en  is geen invoersymbool.

2.25

Als u op de knop State Expander hebt geklikt (voor een ‘demonstratie’ per toestand), kunt u daarna steeds een toestand in de dfa-in-aanbouw aanklikken die u afgehandeld wilt zien. We raden u aan om steeds voor uzelf na te gaan wat er gebeurd is en waarom. Denk eraan dat in de dfa de labels (dus niet de namen) van de toestanden aangeven wat de corresponderende toestanden in de nfa zijn: label 0,1 staat voor {q0, q1}. Met de knop Complete kunt u de dfa in één keer af laten maken. Met de knop Done? Kunt u een aanwijzing krijgen over wat er nog moet gebeuren. Als u niet de knop State Expander kiest maar de knop Expand Group on Terminal kunt u daarna per toestand zelf aangeven voor welk invoersymbool er een overgang naar welke toestand moet komen. Als u in een toestand klikt, neemt JFLAP aan dat u een lus bij die toestand wilt maken. Als u een overgang naar een nieuwe toestand wilt maken moet u daarom in een toestand klikken, de muis ingedrukt houden en de muiswijzer een eindje uit de toestand bewegen. Als u de muis dan loslaat, vraagt JFLAP u eerst welk label de overgang moet hebben, en vervolgens wat het label van de nieuwe toestand moet zijn. U kunt dan bijvoorbeeld 1,2 intypen als u {q1, q2} bedoelt. De knoppen Complete en Done? werken zoals eerder beschreven. 2

1

Uitwerking van de zelftoets

a We moeten eerst bepalen welke strings de taal bevat. De omschrijving zegt “als w een a bevat, …”. Dus K bevat ook strings zonder a’s, met andere woorden, K bevat alle strings die uit 0 of meer b’s bestaan (want a en b zijn hier de enige invoersymbolen). Verder zegt de omschrijving dat alle strings waar één of meer a’s in zitten precies twee b’s moeten bevatten. Dit deel van de taal is dus ook te omschrijven als {a}*{b}{a}*{b}{a}*. Kortom, een andere definitie van K is {b}*  {a}*{b}{a}*{b}{a}*. Deze definitie suggereert een eenvoudige nfa, maar helaas wordt in onderdeel a om een dfa gevraagd. Een mogelijke oplossing is om toch eerst de nfa (zie onderdeel b) te maken, en die dan met de procedure nfa-to-dfa om te zetten naar een dfa. Wij kiezen nu echter voor een directe aanpak. We gebruiken het inmiddels bekende recept: we houden in de namen van de toestanden bij of we al a’s hebben gelezen (ja/nee), en hoeveel b’s we hebben gelezen (0, 1, 2, 3). Vervolgens zetten we de toestanden in een soort matrixvorm, en bepalen voor iedere toestand waar we met een a heen moeten kunnen en waar met een b. Dat levert de volgende automaat:

b Een nfa voor K is veel sneller gemaakt: aangezien de taal bestaat uit twee duidelijk te onderscheiden delen, geven we de automaat ook twee onafhankelijke delen.

2

a M is niet totaal, want bijvoorbeeld (3, a) is niet gedefinieerd (niet voor iedere combinatie toestand-invoersymbool is er een overgang). M is ook niet deterministisch, want bijvoorbeeld (1, c) = {1, 3} (er zijn combinaties toestand-invoersymbool waarvoor er een keuze is). b We geven weer een tabel voor *: *

a

b

c

1 2 3 4

{1, 2}   {4}

{1} {4} {4} {4}

{1, 3}   {4}

c Als we de procedure systematisch aanpakken (we lopen de invoersymbolen op alfabet af, en de nieuwe toestanden in de volgorde waarin we ze tegenkomen), vinden we achtereenvolgens de toestanden {1}, {1, 2}, {1, 3}, {1, 4}, {1, 2, 4} en {1, 3, 4}. De toestanden die 4 bevatten worden eindtoestanden. De dfa ziet er als volgt uit:

Als u goed naar het cluster van eindtoestanden in de dfa kijkt, ziet u dat de constructie om een nfa deterministisch te maken niet altijd de meest efficiënte automaten oplevert: we kunnen toestand {1, 4} een extra lus met label a en een met label c geven, en de toestanden {1, 2, 4} en {1, 3, 4} weglaten.

3

De eenvoudigste manier om dit te laten zien is aan de hand van een voorbeeld. Een nfa voor de eindige taal L = { ab, bab, abc } is bijvoorbeeld:

In het algemeen komt het erop neer dat je gewoon voor iedere string uit de taal zijn eigen pad van de begintoestand naar een eindtoestand maakt; in een nfa gaat dat heel eenvoudig (we gebruiken hier -overgangen, maar het kan ook zonder). Omdat er maar eindig veel strings in de taal zitten, wordt de automaat ook eindig. 4

De taal L = {anbn : n ≥ 1} kan niet worden herkend door een eindige automaat (van welke soort dan ook – deterministisch of nietdeterministisch, totaal of niet, met -overgangen of niet), omdat de informatie die tijdens het doorlopen van een invoerstring moet worden bijgehouden niet eindig is. Voor iedere string moet eerst het aantal a’s geteld worden, zodat daarna kan worden gecontroleerd dat er precies evenveel b’s volgen. Omdat het aantal a’s willekeurig groot kan worden zijn daarvoor oneindig veel toestanden nodig. NB Het is wel mogelijk een eindige automaat te maken voor de taal {a}*{b}*, omdat we dan niet hoeven te tellen: we hoeven alleen maar te zorgen dat we eerst alle a’s krijgen en dan alle b’s. Het is ook mogelijk een eindige automaat te maken voor de taal {anbn : 1 ≤ n ≤ m}, voor een of andere vaste m ≥ 1, omdat die laatste taal eindig is. In leereenheid 4 bewijzen we formeel dat L inderdaad niet regulier is.

Regular languages and regular grammars Introductie Leerkern 1 2 3 4

61 62

Regular expressions 62 Connection between regular expressions and regular languages Regular grammars 68 Oefeningen met JFLAP 68

Zelftoets

69

Terugkoppeling 1 2

70

Uitwerking van de opgaven 70 Uitwerking van de zelftoets 80

63

Leereenheid 3

Regular languages and regular grammars

INTRODUCTIE

In de vorige leereenheid hebt u kennis gemaakt met eindige automaten en hebt u geleerd dat deze automaten aan de hand van bepaalde kenmerken te verdelen zijn in twee groepen: deterministische en nietdeterministische eindige automaten. Eindige automaten representeren de talen van de familie van de reguliere talen. In deze leereenheid bekijken we twee andere manieren om reguliere talen te representeren: reguliere expressies en reguliere grammatica’s. U maakt kennis met een systematische methode om uit een eindige automaat een reguliere expressie te construeren die dezelfde taal representeert, en een systematische methode om uit een eindige automaat een rechtslineaire grammatica voor dezelfde taal af te leiden. Aan het einde van de leereenheid kunnen we concluderen dat de drie representatievormen equivalent zijn. Het bestuderen van eigenschappen van reguliere talen stellen we uit tot de volgende leereenheid. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – kunt uitleggen wat een reguliere expressie is en kunt aangeven hoe een reguliere expressie samengesteld is – voor een gegeven reguliere taal een reguliere expressie kunt opstellen die de taal representeert – uit een reguliere expressie een eindige automaat voor dezelfde taal kunt construeren – de procedure nfa-to-rex kunt gebruiken om uit een eindige automaat een reguliere expressie voor dezelfde taal te kunnen afleiden – de relatie tussen reguliere expressies en reguliere talen kunt aangeven – de kenmerken van een rechtslineaire en een linkslineaire grammatica kunt geven – voor een gegeven eindige automaat een rechtslineaire grammatica voor dezelfde taal kunt construeren – voor een gegeven rechtslineaire grammatica een eindige automaat voor dezelfde taal kunt construeren – de relatie tussen rechtslineaire (en linkslineaire) grammatica’s en reguliere talen kunt aangeven – drie manieren kunt noemen om te bewijzen dat een taal regulier is – de betekenis kunt geven van de volgende kernbegrippen uit deze leereenheid: gegeneraliseerde overgangsgraaf (GTG), reguliere grammatica, lineaire grammatica, patroonherkenning.

Studeeraanwijzingen Bij deze leereenheid hoort chapter 3 van het tekstboek van Linz. De studielast van deze leereenheid bedraagt circa 9 uur.

LEERKERN 1

Regular expressions

Studeeraanwijzing

Bestudeer in Linz de introductie en section 3.1.

Operator 

De operator  wordt in een reguliere expressie gebruikt voor de concatenatie van symbolen. Vaak wordt de operator weggelaten. De expressie ab is dus gelijk aan ab.

OPGAVE 3.1

Welke talen worden door de volgende reguliere expressies beschreven? a a+b b a + bc c (a + bc)* d c +  en c +  e c en c f De reguliere expressie in example 3.1 in Linz: (a + bc)*(c + ). OPGAVE 3.2

Geef voor elk van de volgende expressies aan of deze een reguliere expressie is. Zo ja, geef een beschrijving van de bijbehorende taal, zo nee, geef aan waarom niet. a (a + b + c)(a + b + c)(a + b + c) b 1*(01*01*)* c (ab + ba)+ d (a + b)*cc* OPGAVE 3.3

a Geldt L(ab*) = L((ab)*)? b Welke taal is L(*)? OPGAVE 3.4

Maak exercise 2 uit section 3.1 van Linz. OPGAVE 3.5

Maak exercise 5 uit section 3.1 van Linz. OPGAVE 3.6

Maak exercise 7 uit section 3.1 van Linz. OPGAVE 3.7

Maak exercise 8 uit section 3.1 van Linz. OPGAVE 3.8

a Maak exercise 9a uit section 3.1 van Linz. _ b Behoren de strings aabb, aaaabbbbb en aaaabba tot L of tot L ? c Maak exercise 9c uit section 3.1 van Linz.

OPGAVE 3.9

Maak exercise 11 uit section 3.1 van Linz. OPGAVE 3.10

Maak exercise 16 uit section 3.1 van Linz. OPGAVE 3.11

Gegeven zijn de volgende vier reguliere expressies. Vind voor elke expressie een simpelere reguliere expressie voor dezelfde taal. a (a + )* b aa* +  c ( + a)(a + b)*( + a) d (a + b + ab + ba)* OPGAVE 3.12

Maak exercise 4 uit section 3.1 van Linz. OPGAVE 3.13

Maak exercise 25 uit section 3.1 van Linz. Merk op dat we nog helemaal niets gezegd hebben over het soort talen dat met reguliere expressies beschreven kan worden. Het feit dat alle reguliere expressies die we tot nu toe gezien hebben een reguliere taal beschrijven (ga dat na) en de naam ‘reguliere expressie’ suggereren natuurlijk wel een bepaald verband tussen reguliere talen en reguliere expressies. Wat dat verband precies is wordt in de volgende paragraaf duidelijk. 2

Studeeraanwijzing

Connection between regular expressions and regular languages

Bestudeer in Linz section 3.2. In deze section wordt vaak verwezen naar equations. Dat zijn de formules die in het tekstboek voorzien zijn van een blauw nummer tussen haakjes, rechts van de formule. In deze paragraaf komen in Linz de twee volgende constructies aan de orde: – van reguliere expressie naar nfa (theorem 3.1) – van nfa naar reguliere expressie (theorem 3.2). Het werkboek geeft bovendien een pragmatische manier om van een nfa of een dfa een reguliere expressie voor dezelfde taal te maken.

Theorem 3.1

De constructie in het bewijs van theorem 3.1 in Linz is bedoeld om in het algemeen aan te tonen hoe een nfa voor een reguliere expressie samengesteld kan worden uit de nfa’s van deelexpressies. De -overgangen dienen ervoor om de deel-nfa’s aan elkaar te koppelen. De constructie levert snel grote onoverzichtelijke nfa’s, mede door de hoeveelheid -overgangen. Pas de constructie daarom slechts toe als er geen andere (eenvoudige) nfa voor de hand ligt.

OPGAVE 3.14

In deze opgave gaan we voor simpele reguliere expressies bekijken wat het verschil is tussen een geconstrueerde nfa, zoals beschreven in het bewijs van theorem 3.1, en een voor de hand liggende nfa. a Geef een voor de hand liggende nfa voor de reguliere expressies aa en (a + b)*. b Pas de constructie in het bewijs van theorem 3.1 toe op dezelfde reguliere expressies en bekijk het verschil. OPGAVE 3.15

Maak exercise 3 uit section 3.2 van Linz. Aanwijzing: bedenk eerst een nfa voor de twee deelexpressies en construeer dan de gevraagde nfa door beide onderdelen te combineren. OPGAVE 3.16

a De eindige automaat in de terugkoppeling op opgave 3.15 is niet deterministisch. Hoe kunnen we met een simpele ingreep de automaat deterministisch maken? b Maak nu met behulp van de automaat uit a exercise 4 uit section 3.2 van Linz. OPGAVE 3.17

Maak exercise 5 uit section 3.2 van Linz. OPGAVE 3.18

Wanneer is een gegeneraliseerde overgangsgraaf (GTG, generalized transition graph) volledig? OPGAVE 3.19

In example 3.10 in Linz worden voor vier bestaande overgangen de labels uitgebreid, van q1 naar q1, van q1 naar q3, van q3 naar q1 en van q3 naar q3. Verklaar de nieuwe waarden van de labels. OPGAVE 3.20

Bekijk in Linz figure 3.10 en equation 3.1. Beschrijf de wandeling in de eindige automaat van figure 3.10 die tot equation 3.1 leidt. Pragmatische benadering nfa-to-rex

De procedure nfa-to-rex gaat uit van een volledige GTG (en om die te verkrijgen moeten vaak -overgangen worden toegevoegd). Het voordeel daarvan is dat dan één beschrijving (equation (3.3)) van de conversie van een stukje GTG naar een bijbehorende reguliere expressie voldoende is om alle mogelijk voorkomende situaties aan te kunnen. Het heeft echter ook een nadeel: we moeten die -overgangen introduceren, en de ’s die daardoor in de reguliere expressies terechtkomen op de juiste manier verwerken (voor reguliere expressies r en s geldt dat  + r = r +  = r, en r*s = rs = rs, maar r = r = ). We kunnen beide nadelen omzeilen door toch onderscheid te maken tussen vier verschillende situaties (eigenlijk één situatie met drie speciale gevallen).

Bovendien kunnen we de bijbehorende conversies (het verwijderen van één toestand, die geen begin- of eindtoestand is) aanschouwelijk maken in een plaatje. Hierin zijn r, s, t en u reguliere expressies, en q2 is de te verwijderen toestand. Zie hiervoor figuur 3.1.

FIGUUR 3.1

Vier alternatieven met drie toestanden

Links staat steeds een situatie waarin q2 verwijderd moet worden (dus q2 mag geen begin- of eindtoestand zijn), rechts staat het resultaat. Het moge duidelijk zijn dat hier precies het idee achter de procedure nfa-torex is afgebeeld. Als u wilt, kunt u controleren dat het klopt: vul een van de GTG’s aan de linkerkant aan met -overgangen tot een volledige GTG, pas daarop equation (3.3) toe, en herschrijf indien nodig de resulterende reguliere expressie (werk de ’s weg) tot u de bijbehorende, equivalente expressie aan de rechterkant hebt.

Eén bijzonderheid hebben we nog niet vermeld: q1 en q3 mogen dezelfde toestand voorstellen. De conversie is dan eigenlijk als in figuur 3.2 (het is nog steeds de bedoeling om q2 te verwijderen):

FIGUUR 3.2

Conversie met twee toestanden

Links staat weer de uitgangssituatie; rechts het resultaat van het verwijderen van q2. We laten het aan u over de drie speciale gevallen erbij te bedenken. De constructie om een nfa te converteren naar een reguliere expressie is nu verder precies hetzelfde als de procedure nfa-to-rex: – Start met een nfa met één begintoestand en één (andere) eindtoestand. – Kies steeds een ‘gewone’ toestand (dus niet de begin- of eindtoestand) om te verwijderen en pas daar de passende conversiestap op toe (vaak is meer dan één conversie nodig om de toestand te verwijderen). De volgorde waarin u toestanden verwijdert maakt voor de correctheid van het resultaat niet uit; andere volgordes kunnen wel andere (maar dus wel equivalente) uitkomsten geven. – Stop als u alleen nog de begin- en eindtoestand over hebt. OPGAVE 3.21

Gegeven is de volgende eindige automaat:

a

Welke overgangen moeten worden toegevoegd voor een volledige welk label krijgen ze? b Welke toestand moet worden verwijderd als we de procedure nfa-to-rex toepassen? c Welke vier overgangen moeten worden gecreëerd en welke labels horen daarbij? d Bepaal door toepassing van equation 3.2 de reguliere expressie die verkregen wordt door toepassing van procedure nfa-to-rex. e Doe nu hetzelfde maar dan op de pragmatische manier. GTG en

Aanwijzing: om een toestand te verwijderen moet u in twee stappen werken. Zoek eerst een combinatie van drie toestanden met bijbehorende overgangen, die overeenkomt met één van de vier mogelijkheden van figuur 3.1. Dat levert één nieuwe overgang op. Zoek daarna een combinatie van twee toestanden zoals in figuur 3.2 op. Dat levert een tweede overgang op. Dan pas kan de toestand verwijderd worden. OPGAVE 3.22

Maak exercise 10 uit section 3.2 van Linz. Patroonherkenning

Voorbeeld

Voor het zoeken van bestanden in een map of voor het valideren van invoer van de gebruiker kan een computerprogramma gebruik maken van patroonherkenning. Hiertoe wordt een patroon opgegeven waaraan een bepaalde (invoer)string moet voldoen. Het patroon kan beschreven worden met een soort van reguliere expressie. Het volgende algoritme (bijvoorbeeld voor de opdracht grep in Unix) zoekt of de string w een substring bevat die in L(r) zit. Hier is r een reguliere expressie: – Transformeer de reguliere expressie *.r in een nfa. – Genereer ‘on-the-fly’ het pad van w in de bijbehorende nfa. – Bereikt het pad een eindtoestand (w hoeft dan niet helemaal verwerkt te zijn), dan bevat w een substring van L(r). Neem bijvoorbeeld r = a(aa*b + bb*a)b en w = aaababbb. Na transformatie krijgen we de nfa voor *.r; zie figuur 3.3:

FIGUUR 3.3

De nfa van *.r

Na verwerking van de substring aaabab van w wordt de eindtoestand bereikt. Dus w bevat een substring van L(a(aa*b + bb*a)b). OPDRACHT 3.23

Voor patroonherkenning heeft de taal Java de package java.util.regex met als belangrijke klasse de klasse Pattern die een reguliere expressie representeert. In de Java API van de klasse java.util.regex.Pattern staat beschreven welke vorm een reguliere expressie moet aannemen om als parameter gegeven te kunnen worden bij de aanroep van methode matches. Zoek in de API de betekenis van de volgende patronen op. a ".*[0-9]{10}.*" b "[a-z&&[^aeiouy]]" c "(1[012][1-9]):[0-5][0-9](\\s)+uur"

3 Studeeraanwijzing

Regular grammars

Bestudeer in Linz section 3.3. In deze paragraaf komen in Linz de twee volgende constructies aan de orde: – van reguliere grammatica naar nfa (theorem 3.3) – van nfa naar reguliere grammatica (theorem 3.4).

OPGAVE 3.24

Kunnen, gezien definition 3.3 in Linz, een rechtslineaire en een linkslineaire grammatica een productieregel hebben met de vorm A  ? OPGAVE 3.25

De grammatica van example 3.14 is niet regulier, maar lineair. Welke taal genereert deze grammatica? Is die taal regulier? OPGAVE 3.26

Maak exercise 1 uit section 3.3 van Linz. OPGAVE 3.27

Teken de eindige automaat voor de taal L(aab*a) uit example 3.16 in Linz, die correspondeert met de overgangsgraaf van figure 3.18. OPGAVE 3.28

Maak exercise 5 uit section 3.3 van Linz. Geef in beide gevallen ook de bijbehorende reguliere expressie. OPGAVE 3.29

a Maak exercise 6 uit section 3.3 van Linz. b Maak exercise 11 uit section 3.3 van Linz. OPGAVE 3.30

Noem drie manieren om te bewijzen dat een taal regulier is. 4 Studeeraanwijzing

Oefeningen met JFLAP

Lees in JFLAP-Activities section 2.3. In deze paragraaf oefenen we de volgende technieken: – de conversie van een rechtslineaire grammatica naar een nfa – de conversie van een nfa naar een rechtslineaire grammatica – het testen of twee grammatica’s equivalent zijn – de conversie van een nfa naar een reguliere expressie. De conversie van een reguliere expressie naar een nfa laten we achterwege omdat deze conversie snel leidt tot een onoverzichtelijke nfa.

OPDRACHT 3.31

Voor deze opdracht gebruiken we de bestanden Jexercise3.4a.jff en Jexercise3.4b.jff van de map JFLAP Exercises bij JFLAP Activities. a Open het bestand Jexercise3.4a.jff in JFLAP. Welke soort grammatica bevat het bestand? b Converteer de grammatica stapsgewijs tot een nfa. Probeer ook een foutieve overgang te tekenen en bekijk hoe JFLAP reageert. Exporteer het resultaat naar een nieuw scherm.

c Open het bestand Jexercise3.4b.jff in JFLAP. Converteer in een keer de rechtslineaire grammatica tot een nfa en exporteer het resultaat in een nieuw scherm. d Gebruik JFLAP om te onderzoeken of de twee verkregen eindige automaten equivalent zijn. Zijn ze equivalent? e Geef een string die door de ene grammatica wel en door de andere grammatica niet wordt geaccepteerd. Controleer dit in JFLAP. OPDRACHT 3.32

Maak exercise 1 van section 2.3 uit JFLAP-Activities door de eindige automaat uit het bestand Jexercise3.1a.jff stapsgewijs te converteren naar een rechtslineaire grammatica en het resultaat te vergelijken met de grammatica uit Jexercise3.1b.jff. OPDRACHT 3.33

In deze opdracht gaan we de eindige automaat uit opdracht 3.32 converteren naar een reguliere expressie. Open in JFLAP het bestand Jexercise3.1a.jff. Welke reguliere expressie levert de conversie op?

ZELFTOETS

1

Bepaal een reguliere expressie voor de volgende taal: {x  {a, b}* : x bevat ten minste één a en ten minste één b}

2

Gegeven is de volgende reguliere expressie: c*((a + b)c*(a + b)c*)* a Geef een nfa voor deze reguliere expressie. b Welke taal wordt gerepresenteerd door deze reguliere expressie?

3

Gegeven is de volgende GTG:

Geef de reguliere expressie die hieruit afgeleid kan worden. 4

Construeer een rechtslineaire grammatica die de volgende reguliere taal genereert: {x  {a, b}* : x eindigt niet op ba} Aanwijzing: construeer eerst een eindige automaat voor deze taal.

TERUGKOPPELING 1

3.1

a

Uitwerking van de opgaven De reguliere expressie a + b beschrijft de taal {a}  {b} ofwel {a, b}.

b De reguliere expressie a + bc beschrijft de taal {a}  {bc} ofwel {a, bc}. c De reguliere expressie (a + bc)* beschrijft de Kleene-afsluiting (star-closure) van {a, bc} ofwel {a, bc}*. Ook  behoort tot deze taal. d De reguliere expressie c +  beschrijft de taal {c}   ofwel {c}. De reguliere expressie c +  is dus equivalent met de reguliere expressie c. De reguliere expressie c +  beschrijft de taal {, c}. e De reguliere expressie c staat voor {c}   = {cw : w  }, en beschrijft dus de lege taal . De reguliere expressie c beschrijft de taal {c}{} = {c} = {c}. f De reguliere expressie (a + bc)*(c + ) beschrijft de taal waarvan de strings zijn opgebouwd uit een willekeurige combinatie van de strings a en bc, gevolgd door een c. Voorbeelden uit deze taal zijn c, ac, bcc, abcc en bcabcaaac. 3.2

a De expressie (a + b + c)(a + b + c)(a + b + c) is een reguliere expressie. De expressie beschrijft de verzameling van alle strings over {a, b, c} met lengte 3, ofwel {a, b, c}3. b De expressie 1*(01*01*)* is een reguliere expressie. De expressie beschrijft de verzameling van alle strings over {0, 1} met een even aantal nullen. Let op de voorrangsregels van de operatoren * en  (die laatste is volgens afspraak weggelaten): in de subexpressie 01*01* horen de sterretjes alleen bij de 1’en, niet bij de 0’en. c De expressie (ab + ba)+ is geen reguliere expressie. Hier wordt een positieve afsluiting bedoeld van de taal {ab, ba}. Als we de expressie vervangen door (ab + ba)(ab + ba)*, dan hebben we wel een reguliere expressie voor dezelfde taal. d De expressie (a + b)*cc* is een reguliere expressie. De expressie beschrijft de verzameling van alle strings over {a, b, c} die beginnen met een willekeurig aantal a’s en/of b’s gevolgd door minimaal één c, ofwel {a, b}*{c}+.

3.3

a Nee, want L(ab*) = {a, ab, abb, abbb, ...} en L((ab)*) = {, ab, abab, ababab, ...}. b L(*) = * = 0  1  2  … = 0 = {}

( en * zijn hier reguliere expressies) ( en * zijn hier talen/verzamelingen)

3.4

Gevraagd worden alle strings met maximale lengte 3. Zulke strings bevatten minimaal één b en verder maximaal 2 andere symbolen. We proberen deze strings hier systematisch te geven, en onderstrepen voor de duidelijkheid steeds de ‘vaste’ b uit het ‘midden’ van de reguliere expressie: b, abb, bb, bbb, ba, baa, bab en bba.

3.5

Zie de terugkoppeling in Linz op pagina 409.

3.6

De taal {anbm : n  3, m is oneven} wordt beschreven door de reguliere expressie aaaa*b(bb)*.

3.7

Er zijn twee mogelijkheden: – n is even, en dan is m oneven – n is oneven, en dan is m even. Deze constatering levert de volgende reguliere expressie op: (aa)*b(bb)* + a(aa)*(bb)* of een of andere variant daarop, zoals (aa)*b(bb)* + (aa)*a(bb)* of zelfs (aa)*(a + b)(bb)*.

3.8

a

Zie de terugkoppeling in Linz op pagina 409. _

b De drie strings behoren tot L . Bij de string aabb is n < 3. Bij de string aaaabbbbb is m > 4. De string aaaabba heeft niet de vorm anbm. c Het complement van L is een stuk lastiger te vangen in een reguliere expressie. Het bevat:  alle strings van de vorm anbm waarvoor niet geldt dat n  3 en m  4, ofwel alle strings van de vorm anbm waarvoor n < 3 of m > 4. De strings waarvoor n < 3 worden beschreven door ( + a + aa)b*, de strings waarvoor m > 4 door a*bbbbbb*.  alle strings die niet van de vorm a*b* zijn. Deze strings worden gekarakteriseerd door de reguliere expressie (a + b)*ba(a + b)*. Samen geeft dit ( + a + aa)b* + a*bbbbbb* + (a + b)*ba(a + b)*. 3.9

De taal bevat alle strings die óf van de vorm “een b met ervoor en erachter een even aantal a’s” zijn óf van de vorm “een b met ervoor en erachter een oneven aantal a’s”. De aantallen a’s voor en achter de b hoeven niet gelijk te zijn.

3.10

Zie de terugkoppeling in Linz op pagina 409.

3.11

De vereenvoudigde reguliere expressies zijn: a a* b a* c

De gegeven reguliere expressie kan als volgt geschreven worden:

(a + b)* + a(a + b)* + (a + b)*a + a(a + b)*a = (a + b)* d (a + b)*

3.12

De eindige automaat kan als volgt:

3.13

De expressie voor een oneindige taal moet in ieder geval een subexpressie-met-* bevatten. Zodra er één subexpressie-met-* is die niet de lege string beschrijft, kan deze substring willekeurig vaak herhaald worden en kunnen we dus oneindig veel strings beschrijven.

3.14

a Voor de reguliere expressie aa krijgen we de volgende eindige automaat:

Voor de reguliere expressie (a + b)* krijgen we de volgende eindige automaat:

b Door toepassing van de constructie in het bewijs van theorem 3.1 krijgen we voor de reguliere expressie aa:

En voor (a + b)*:

In beide gevallen ziet u dat de eindige automaat minder overzichtelijk is dan de voor de hand liggende equivalenten in de terugkoppeling van opgave a.

3.15

Net zoals in example 3.7 beginnen we met een automaat voor ab*aa en een automaat voor bba*ab; die zijn rechttoe rechtaan:

ab*aa

bba*ab Dan gebruiken we de constructie uit figure 3.3 om een eindige automaat voor ab*aa + bba*ab te maken:

3.16

a Eerst moeten we de -overgangen weghalen door de begin- en de eindtoestand te veranderen. De automaat bevat verder één toestand met twee overgangen met label a. Door het a-lusje één toestand naar rechts te verschuiven krijgen we een equivalente automaat die wel deterministisch is:

b Voor het complement van de taal moeten we eerst de eindige automaat totaal maken door een trap state toe te voegen en dan eindtoestanden en niet-eindtoestanden verwisselen zoals we dat al in opgave 2.5 hebben gedaan:

3.17

Zie de terugkoppeling in Linz op pagina 409-410. Een alternatieve oplossing is:

3.18

Een GTG is volledig als elk paar knooppunten verbonden is door een overgang (kant, tak, pijl). Dit is niets anders dan de definitie van een volledige (complete) graaf, maar wel iets anders dan het begrip totale overgangsfunctie! Bij dat laatste hoeft er namelijk alleen maar een overgang te zijn voor iedere combinatie (toestand, invoersymbool), en dus niet per se een overgang tussen ieder tweetal toestanden.

3.19

We bekijken eerst de overgang van q1 naar q1. In de GTG van figure 3.11 bestaat al een overgang van q1 naar q1 met als label e. We voegen een overgang toe met als label het label van de wandeling van q1 naar q1 via de toestand q2 die we willen weghalen. Dat is af*b (vergeet de f* niet!). De bestaande en de nieuwe overgang combineren we in de overgang met label e + af*b in figure 3.12. De overgang van q1 naar q3 is een combinatie van de overgang van q1 naar q3 van figure 3.11 (met label h) en de overgang met als label het label van de wandeling van q1 naar q3 via de toestand q2. Dat is af*c. Dit levert de overgang met label h + af*c in figure 3.12. Voor de andere twee overgangen kunnen we een vergelijkbare verklaring geven.

3.20

Met r1*r2 komen we in een eindtoestand. Van daaruit kunnen we een willekeurig aantal keren een lus maken die ons weer terugbrengt in de eindtoestand. De lussen zijn r4 of r3r1*r2. Dat levert equation 3.1 op.

3.21

a We moeten drie overgangen toevoegen met label , van q0 naar q0, van q1 naar q2 en van q2 naar q0.

b Toestand q1 moet worden verwijderd. c We moeten de volgende vier overgangen creëren: – van q0 naar q0 (via q1) met label r00 =  + 11*0 = 11*0 – van q0 naar q2 (via q1) met label r02 = 0 + 11* = 0 – van q2 naar q0 (via q1) met label r20 =  + 01*0 = 01*0 – van q2 naar q2 (via q1) met label r22 = 1 + 01* = 1 Voor de bepaling van de labels passen we de volgende regels toe: σ+=+σ=σ σ = σ =  waar σ een reeks symbolen is (zie opgave 3.1). d We passen equation 3.2 toe: r = r00*r02(r22 + r20r00*r02)* = (11*0)*0(1 + 01*0(11*0)*0)* e De combinatie van drie toestanden is de tweede mogelijkheid in figuur 3.1 (van q2 via q1 naar q0) met s = 0, t = 1 en u = 0. Hiermee krijgen we een overgang van q2 naar q0 met label 01*0. De combinatie met twee toestanden is die van q0 en q1 met s = 1, t = 1 en u = 0. Hiermee krijgen we de overgang van q0 naar q0 met label 11*0. Nu zijn alle in- en uitgaande pijlen van q1 verwerkt. Dit levert dezelfde GTG op als in c en dus dezelfde reguliere expressie op als in d.

3.22

a

We benoemen de toestanden als volgt:

Net als in de voorgaande opgave passen we in twee stappen de pragmatische aanpak toe voor de constructie. Eerst bekijken we de overgangen s = a + b van q1 naar q2 en u = ab van q2 naar q3. Deze twee overgangen kunnen (overeenkomstig figuur 3.2) vervangen worden door een overgang met label (a + b)ab van q1 naar q3. Dan bekijken we de overgangen r = bb van q3 naar q3, s =  van q3 naar q2 en ab van q2 naar q3. Deze drie overgangen kunnen (overeenkomstig figuur 3.1) vervangen worden door een overgang met label bb +ab van q3 naar q3. Dit levert de GTG van de terugkoppeling in Linz op pagina 410. b Zie de terugkoppeling in Linz op pagina 411. 3.23

a Dit is een patroon voor een serie van 10 aaneengesloten cijfers in een woord. Met ".*" wordt bedoeld 0, 1 of meer voorkomens van een willekeurige karakter. b Dit is een patroon voor een karakter dat een willekeurige kleine letter kan zijn behalve een klinker. c Dit is een patroon voor een tijdsaanduiding. Voorbeelden die voldoen zijn "11:59 uur" en "3:20 uur". Het getal voor de uren begint niet met het cijfer 0. Na de minuten moet minimaal één spatie staan en de tijdsaanduiding eindigt met "uur".

3.24

Ja, dat kan omdat   T*.

3.25

We kunnen de productieregels herschrijven tot S A A  aAb (Ga na dat deze grammatica equivalent is met de oorspronkelijke grammatica.) We kunnen zelfs nog een stap verder gaan: als we A als startsymbool gebruiken kunnen we het hulpsymbool S weglaten. We krijgen dan een grammatica die we al eerder gezien hebben (maar toen met S in plaats van A): A  aAb Deze grammatica genereert de taal {anbn : n ≥ 0}, en daarvan hebben we in de zelftoets van leereenheid 2 beredeneerd dat deze niet regulier is. Met een niet-reguliere grammatica kunnen we dus niet-reguliere talen genereren.

3.26

Voor de dfa gebruiken we de toestanden S, A, B en Vf en een aantal extra toestanden:

Merk op dat deze dfa niet totaal is. 3.27

De eindige automaat is:

3.28

Strings uit L beginnen met aaa, eventueel gevolgd door nog meer a’s, dan bb, eventueel gevolgd door nog meer b’s. Deze constatering levert de volgende rechtslineaire grammatica: S aaaA A aAbbB B bB De reguliere expressie is (opgebouwd van rechts naar links): aaaa*bbb*. Strings uit L eindigen met bb, eventueel voorafgegaan door nog meer b’s, dan aaa, eventueel voorafgegaan door nog meer a’s. Deze constatering levert de volgende linkslineaire grammatica: S Abb A AbBaaa B Ba De reguliere expressie is (opgebouwd van links naar rechts): a*aaab*bb.

3.29

a We bepalen eerst de productieregels van de rechtslineaire grammatica voor de reguliere expressie aaab*ab door te kijken hoe strings vanaf de linkerkant worden opgebouwd: S aaaA A bAab De grammatica voor (aaab*ab)* is dan ({S, A}, {a, b}, P, S) met productieregels: S aaaA A bAabS

Een andere manier om dit resultaat te bereiken is door eerst een eindige automaat te maken zoals hier volgt en die om te zetten in een rechtslineaire grammatica.

b We bekijken hoe strings uit de taal worden gevormd vanaf de rechterkant en leiden daaruit de productieregels voor een linkslineaire grammatica af: S Aab A AbSaaa 3.30

Om te bewijzen dat een taal regulier is, is het voldoende om – een reguliere (rechtslineaire of linkslineaire) grammatica voor de taal te geven – een reguliere expressie voor de taal te geven – een eindige automaat (dfa of nfa) voor de taal te geven.

3.31

a Het bestand bevat een rechtslineaire grammatica. Zoals blijkt uit de mededeling bij menuoptie TestTest for Grammar Type van JFLAP is het dus ook een reguliere grammatica, en ook een contextvrije grammatica (zie daarvoor leereenheid 5). b Kies de optie ConvertConvert Right-Linear Grammar to FA. Er verschijnt een venster met vijf toestanden waarvan vier met een label. We selecteren de eerste productieregel en klikken rechts op de knop Create Selected. Dit leidt tot de creatie van een overgang van S naar A met label b. Voor de tweede productieregel gaan we handmatig de overgang van S naar S met label a tekenen. U ziet dat, na het tekenen van de overgang, de corresponderende productieregel geselecteerd wordt en dat er een vinkje verschijnt ten teken dat deze productieregel afgehandeld is. Een foutieve overgang leidt tot een foutmelding. Na een paar stappen kiezen we voor Show All. JFLAP voert dan alle stappen in een keer uit. Als we nu op de knop Export klikken, wordt de eindige automaat in een nieuw scherm geplaatst. De naam van het scherm is waar Nr een volgnummer is. c Om de rechtslineaire grammatica in een keer te converteren, klikken we meteen op de knop Show All.

d De twee grammatica’s zijn niet equivalent. In het scherm van een van de twee eindige automaten kiezen we voor de optie TestCompare Equivalence. Als JFLAP vraagt naar de naam van de andere automaat, selecteren we de goede naam (als het goed is, is de naam al geselecteerd). Een klik op OK vertelt ons dat beide automaten, en dus beide grammatica’s, niet equivalent zijn. e Om een string te vinden is het handig om eerst de lay-out van de eindige automaten zo te wijzigen dat de begintoestand links staat en de eindtoestand rechts. Dan zien we meteen dat een automaat de string b accepteert terwijl de tweede automaat dat niet doet. Dit kunnen we controleren door beide grammatica’s de invoer b te laten verwerken (met bijvoorbeeld de optie InputCYK Parse). 3.32

Open het bestand in JFLAP en kies de optie ConvertConvert to Grammar. Het klikken op een label van een overgang in de automaat levert een productieregel op. Het klikken op een eindtoestand levert een -overgang op. Met een klik op de knop What’s Left? kunt u controleren of alles al gedaan is. Het bestand Jexercise3.1b.jff bevat dezelfde grammatica als de net gegenereerde grammatica. De eindige automaat uit Jexercise3.1a.jff en de grammatica uit Jexercise3.1b.jff representeren dus dezelfde taal. NB U kunt ook de vraag beantwoorden door de grammatica te converteren naar een eindige automaat en het resultaat dan vergelijken met de gegeven automaat. Dit was echter niet in de opdracht gevraagd.

3.33

Voor de conversie kiezen we de optie ConvertConvert FA to RE. Eerst wordt gevraagd om de automaat zo te wijzigen dat er maar één eindtoestand is. Dat doen we met behulp van de State Creator en de Transition Creator. Daarna moet de GTG volledig worden gemaakt. Dat kunnen we handmatig doen of er JFLAP om vragen door op de knop Do It te klikken. Trek daarna de toestanden een beetje uit elkaar (klik daarvoor eerst op de knop Attribute Editor) om nog enig overzicht te hebben. Er moeten nu drie toestanden weggewerkt worden: q1, q2 en q3. Gebruik hiervoor de State Collapser. Klik in het scherm Transitions op een regel om te zien welke wandeling is gevolgd voor het verkrijgen van een label. Een klik op de knop Finalize in dit scherm verwijdert de geselecteerde toestand en past de benodigde labels op de overgebleven overgangen aan. Als de GTG tot twee toestanden is gereduceerd, wordt de reguliere expressie gegeven.

2

1

Uitwerking van de zelftoets

Een eindige automaat die de gegeven taal representeert is bijvoorbeeld:

Het is nu eenvoudig (met de pragmatische aanpak voor nfa-to-rex) om de reguliere expressie te vinden: (aa*b + bb*a)(a + b)* Een andere manier is om de reguliere expressie direct te construeren, bijvoorbeeld door te bedenken dat de a dan voor de b moet komen, of de b voor de a. Wat er verder omheen staat maakt niet uit. Dat levert de volgende reguliere expressie: (a + b)*a(a + b)*b(a + b)* + (a + b)*b(a + b)*a(a + b)* 2

a

De eindige automaat kan als volgt:

of:

b De reguliere expressie laat zien dat het aantal c’s en hun plaats willekeurig is. Aan c wordt dus geen voorwaarde gesteld. In de deelexpressie (a + b)c*(a + b)c* is het aantal a’s plus het aantal b’s gelijk aan 2. Omdat deze deelexpressie een willekeurig aantal keer herhaald kan worden, betekent het dat de expressie de volgende taal beschrijft: {x  {a, b, c}* : het aantal a’s plus het aantal b’s in x is even}

3

De reguliere expressie is: b*aa(b + ab*aa)*.

4

We construeren eerst een nfa voor de taal {x  {a, b}* : x eindigt op ba}, converteren het resultaat tot een dfa (die totaal is) en nemen daar het complement van. De taal wordt gerepresenteerd door de volgende dfa:

De corresponderende rechtslineaire grammatica wordt nu: G = ({S, T, U}, {a, b}, P, S) met de volgende verzameling productieregels: S a S b U U a Tb U T a S b U

Properties of regular languages Introductie Leerkern 1 2 3

83 84

Closure properties of regular languages 84 Elementary questions about regular languages Identifying nonregular languages 87

Zelftoets

90

Terugkoppeling 1 2

91

Uitwerking van de opgaven 91 Uitwerking van de zelftoets 104

87

Leereenheid 4

Properties of regular languages

INTRODUCTIE

In deze leereenheid maakt u kennis met een aantal afsluitingseigenschappen van de familie van reguliere talen, dat wil zeggen allerlei manieren waarop reguliere talen kunnen worden aangepast of met elkaar gecombineerd zodat weer een reguliere taal ontstaat. Verder beantwoorden we enkele fundamentele vragen over reguliere talen: bestaat er een algoritme om na te gaan of een gegeven string in een gegeven reguliere taal zit? Bestaat er een algoritme om na te gaan of een gegeven reguliere taal oneindig is? Zulke vragen heten beslissingsproblemen. Later in de cursus komen we andere families van talen tegen, waarvoor we dezelfde vragen zullen proberen te beantwoorden, met soms andere antwoorden. Tot slot bespreken we een manier om aan te tonen dat een gegeven taal niet regulier is: het pomplemma voor reguliere talen. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – weet wat het betekent dat een verzameling gesloten is onder een operatie – kunt uitleggen waarom de familie van reguliere talen gesloten is onder vereniging, doorsnede, concatenatie, complement en Kleene-afsluiting (star-closure) – vanuit ieder van de drie standaardrepresentaties de constructies voor vereniging, concatenatie en Kleene-afsluiting van twee reguliere talen kunt uitvoeren – de constructies voor complement en doorsnede van gegeven eindige automaten kunt uitvoeren – de beschrijving van een (nieuwe) operatie op reguliere talen kunt lezen en toepassen, en kunt beredeneren of de familie van reguliere talen gesloten is onder die operatie – een aantal elementaire vragen over reguliere talen kent, en hun antwoorden kunt beredeneren – weet wat het pomplemma voor reguliere talen inhoudt – het pomplemma voor reguliere talen kunt toepassen – de betekenis kunt geven van de volgende begrippen uit deze leereenheid: homomorfisme, rechterquotiënt. Studeeraanwijzingen Bij deze leereenheid hoort chapter 4 van het tekstboek van Linz. Alle sections hiervan zijn verplicht. De studielast van deze leereenheid bedraagt circa 10 uur.

LEERKERN 1 Studeeraanwijzing

Gesloten

Closure properties of regular languages

Lees in Linz de introduction van chapter 4, en bestudeer section 4.1. De terminologie van afsluitingseigenschappen is niet beperkt tot de formeletalentheorie: van iedere verzameling kunnen we onderzoeken of die gesloten is onder bepaalde operaties op de elementen van die verzameling. Het feit dat een verzameling gesloten is onder een bepaalde operatie betekent, heel informeel gezegd, dat als je in die verzameling zit, je er niet uit kunt komen door het toepassen van die operatie. Het maakt daarbij niet uit wat voor soort elementen de verzameling bevat: getallen, strings, verzamelingen, of iets anders. Ook maakt het aantal operanden (argumenten) waarop de operatie toegepast moet worden niet uit: een taal kan bijvoorbeeld gesloten zijn onder spiegelbeeld (één operand: je neemt het spiegelbeeld van een string) of onder concatenatie (twee operanden: je plakt de ene string achter de andere).

OPGAVE 4.1

a Is de verzameling van natuurlijke getallen gesloten onder optelling? En onder deling? b Is de verzameling * gesloten onder concatenatie? En onder spiegelbeeld? c Is de verzameling {, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3}} gesloten onder vereniging? En onder complement ten opzichte van {1,2,3}? De situatie in het laatste onderdeel van opgave 4.1 lijkt het meest op de situatie bij afsluitingseigenschappen van families van talen. In opgave 4.1 c gaat het om een verzameling van verzamelingen van getallen, bij een familie van talen gaat het om een verzameling van verzamelingen van strings. In theorem 4.1 wordt onder andere bewezen dat de familie van reguliere talen (dus de verzameling {L : L is een reguliere taal}) gesloten is onder vereniging: als je twee reguliere talen met elkaar verenigt krijg je altijd weer een reguliere taal. Bij sommige afsluitingseigenschappen is het nut ervan meteen duidelijk: omdat we weten dat de familie van reguliere talen gesloten is onder doorsnede, wordt het in sommige gevallen een stuk eenvoudiger om een automaat te vinden die een gegeven taal accepteert. Bij andere afsluitingseigenschappen, zoals het feit dat de familie van reguliere talen gesloten is onder rechterquotiënt, is dat nut helemaal niet zo duidelijk. Over dat rechterquotiënt kunnen we toch twee dingen opmerken. Ten eerste is het bedenken en bewijzen van de constructie in example 4.4 en theorem 4.4 een goede oefening in het denken over de werking van eindige automaten. Ten tweede blijkt er een grotere familie van talen te bestaan (de familie van context-gevoelige talen, zie leereenheid 9) die niet gesloten is onder rechterquotiënt: we kunnen deze operatie dus blijkbaar gebruiken om onderscheid te maken tussen twee families van talen. We gaan daar verder niet op in. Het is overigens interessant dat de complementconstructie (uit het bewijs van theorem 4.1) en de rechterquotiëntconstructie (uit het bewijs van theorem 4.4) precies dezelfde techniek gebruiken – namelijk niets veranderen behalve de verzameling eindtoestanden – en toch een heel ander resultaat opleveren!

OPGAVE 4.2

Iemand doet de uitspraak ‘Reguliere talen zijn gesloten onder concatenatie’. Wat is er mis met deze uitspraak? OPGAVE 4.3

a Leg in uw eigen woorden het idee uit achter de constructie voor de doorsnedeautomaat uit het bewijs van theorem 4.1. b Hoeveel toestanden heeft de doorsnedeautomaat? c Is het essentieel dat de oorspronkelijke automaten totaal en deterministisch zijn? OPGAVE 4.4

Gebruik de constructie uit theorem 4.1 om nfa’s te vinden voor de talen a L((a + b)a*)  L(baa*) b L(ab*a*)  L(a*b*a) OPGAVE 4.5

Construeer een eindige automaat voor de doorsnede van de taal Lac = {w  {a, b, c}* : w heeft een substring ac} en de taal Lcb = {w  {a, b, c}* : w heeft een substring cb}. OPGAVE 4.6

Maak exercise 7 van section 4.1 van Linz. Denk hierbij goed na over de vraag of de oorspronkelijke automaten totaal en/of deterministisch moeten zijn. OPGAVE 4.7

a Maak exercise 9 van section 4.1 van Linz. b Denkt u dat de familie van reguliere talen ook gesloten is onder oneindige vereniging? Motiveer uw antwoord. OPGAVE 4.8

Maak exercise 10 van section 4.1 van Linz. OPGAVE 4.9

Maak exercise 13 van section 4.1 van Linz. OPGAVE 4.10

a Maak exercise 14 van section 4.1 van Linz door zelf de definitie van de operator rechterquotiënt toe te passen op de twee gegeven talen (dus nog zonder automaten). b Maak exercise 14 nogmaals, maar nu door de constructie uit het bewijs van theorem 4.4 toe te passen. OPGAVE 4.11

Maak exercise 15 van section 4.1 van Linz. OPGAVE 4.12

Deze opgave gaat over het bewijs van theorem 4.4. ˆ? a Is  de overgangsfunctie van M of van M b Als je wilt bewijzen dat twee verzamelingen A en B gelijk zijn, doe je dat vaak door A  B én B  A te bewijzen. Voor welke twee verzamelingen wordt deze techniek in dit bewijs gebruikt, waar precies, en wat wordt ermee bewezen?

OPGAVE 4.13

Probeer exercise 16 van section 4.1 van Linz. De uitwerking staat achterin Linz op pagina 413. Het geeft niets als u de oplossing niet zelf kunt bedenken; het lezen van de uitwerking is op zich al een leerzame oefening in verzamelingenleer en het gebruik van afsluitingseigenschappen. OPGAVE 4.14

We definiëren een operatie changeleft op strings als volgt, voor w  * en iedere a  : changeleft(aw) = {bw : b   – {a}} en breiden deze op de voor de hand liggende manier uit naar talen: changeleft( L) 

wL

changeleft( w )

a Wat zijn changeleft(abc) en changeleft(bc) als  = {a, b, c}? b Gegeven is de volgende automaat M:

Geef een automaat voor changeleft(L(M)). Neem weer aan dat  = {a, b, c}. c Generaliseer de constructie van onderdeel b, zodat u kunt bewijzen dat de familie van reguliere talen gesloten is onder de operatie changeleft. Bedenk daarbij ook of het nodig (of handig) is dat de oorspronkelijke automaat deterministisch of totaal is. OPGAVE 4.15

Maak exercise 21 bij section 4.1 van Linz. OPGAVE 4.16

Lees exercise 27 bij section 4.1 van Linz, en de uitwerking op pagina 414 in Linz. Geef uw commentaar op die uitwerking. OPGAVE 4.17

Dezelfde vraag als bij opgave 4.16, maar nu voor eindige automaten. Laat M1 en M2 twee eindige automaten zijn. Laat zien hoe daaruit eindige automaten kunnen worden geconstrueerd voor de volgende talen: a L(M1)  L(M2) b L(M1)L(M2) c L(M1)*

Merk op dat we de geslotenheid van de familie van reguliere talen onder vereniging, concatenatie en Kleene-afsluiting intussen via alle drie de standaardrepresentaties hebben aangetoond; afsluiting onder complement en doorsnede alleen via eindige automaten. Dat maakt voor het eindresultaat – de familie van reguliere talen is gesloten onder al deze operaties – niet uit. Het toont alleen maar weer eens aan dat de ene representatie soms handiger is dan de andere, en dat het in andere gevallen niet uitmaakt. 2 Studeeraanwijzing

Elementary questions about regular languages

Bestudeer section 4.2 van Linz.

OPGAVE 4.18

Maak exercise 4 van section 4.2 van Linz. OPGAVE 4.19

Maak eerst exercise 5 van section 4.2 van Linz. OPGAVE 4.20

Maak exercise 7 van section 4.2 van Linz. OPGAVE 4.21

Maak exercise 3 van section 4.2 van Linz. OPGAVE 4.22

Maak exercise 13 van section 4.2 van Linz. 3 Studeeraanwijzing

Identifying nonregular languages

Bestudeer section 4.3 van Linz tot en met example 4.7.

OPGAVE 4.23

In het bewijs van theorem 4.8 staat “… such a repetition must start no later than the nth move.” Bent u het daarmee eens? Motiveer uw antwoord. OPGAVE 4.24

Lees de alinea net na theorem 4.8 in Linz nog eens, en leg precies uit waarom het pomplemma ook geldt voor eindige talen. OPGAVE 4.25

Leg uit waar en hoe precies in example 4.7 het pomplemma wordt gebruikt: wat gebeurt er in iedere zin, wat is w, wat zijn x, y en z? En waarom wordt m gebruikt in de specificatie van w? De tweede alinea na theorem 4.8 bespreekt hoe het pomplemma gebruikt wordt: in een bewijs uit het ongerijmde (“The demonstration is always by contradiction”). We hebben een taal waarvan we willen aantonen dat hij niet regulier is, maar we beginnen met aan te nemen dat hij wel regulier is. Volgens het pomplemma moet hij dan ook pompbaar zijn. We tonen vervolgens echter aan dat de taal niet pompbaar is, en concluderen daaruit dat die taal dus niet regulier kan zijn. Example 4.7 volgt deze aanpak.

(Regulier) pompbaar

Contrapositie

Als we zorgvuldig willen zijn moeten we eigenlijk spreken over ‘regulier pompbaar’ in plaats van gewoon ‘pompbaar’. Dat komt omdat er nog andere pomplemma’s bestaan, voor andere families van talen (zie leereenheid 8). Als er geen verwarring kan ontstaan, zullen we het ‘regulier’ echter vaak weglaten. We kunnen de redenering net na opgave 4.25 ook kortsluiten door eerst contrapositie toe te passen op de formulering van het pomplemma. Contrapositie is een standaardequivalentie uit de logica, die zegt dat “als p dan q” equivalent is met “als niet q dan niet p”. Als we dat toepassen op de formulering van het pomplemma (“als regulier, dan pompbaar”) krijgen we “als een taal niet pompbaar is, dan is hij ook niet regulier”. We kunnen dus meteen beginnen met aantonen dat de taal in kwestie niet pompbaar is, en dan zeggen dat uit het pomplemma volgt dat hij niet regulier is. Example 4.10 en 4.11 gebruiken deze aanpak. Het aantonen dat een taal niet pompbaar is, is vaak nog een hele onderneming. Dat komt omdat de formulering voor “L is regulier pompbaar” (*) nogal ingewikkeld is:

L is regulier pompbaar

er is een m  0 voor alle w  L er x, y, z bestaan

voor alle i ≥ 0 geldt

zodat metw≥ m met w = xyz enxy≤ m eny≥ 1, zodat wi = xyiz  L

()

De formulering voor “L is niet regulier pompbaar” geven we hieronder (). We gebruiken voor het omschrijven van () naar () de volgende standaardequivalenties uit de logica: “niet er is een x zodat …” is hetzelfde als “voor alle x geldt niet …”, en “niet voor alle x geldt …” is hetzelfde als “er is een x waarvoor niet …”. Je kunt de ‘niet’ dus als het ware naar binnen duwen door de er-is-een’s en de voor-alle’s heen, waarbij iedere er-is-een verandert in een voor-alle en andersom. L is niet regulier pompbaar

voor alle m  0 bestaat een w  L voor alle x, y, z

er een i ≥ 0 bestaat

metw≥ m, zodat met w = xyz enxy≤ m eny≥ 1 zodat wi = xyiz  L

()

Merk op dat de ‘niet’ door alle er-is-een’s en voor-alle’s heen is gegaan en uiteindelijk is blijven steken in de , die daardoor een  is geworden. Formulering () zegt het volgende: als we willen laten zien dat een taal niet regulier pompbaar is, dan moet er, ongeacht welke waarde m heeft, altijd een w  L metw≥ m te vinden zijn zodat er voor alle verdelingen van w in drie stukken x, y en z een manier van pompen te vinden is die een string buiten L oplevert.

Omdat de contrapositie van het pomplemma ongeacht de waarde van m moet gelden, mogen we geen specifieke waarde invullen voor m. Bij het benoemen van een geschikte w gebruiken we dan ook altijd die m. Dat heeft nog als extra voordeel dat we er dan meteen voor kunnen zorgen dat die w inderdaad minstens lengte m heeft. De kunst bij het gebruiken van het pomplemma is om een geschikte w  L te vinden, die niet zo op te delen is dat hij voor alle i gepompt kan worden. Het hangt vaak van de taal en de gekozen string af of we laten zien dat pompen niet kan omdat we niet omhoog kunnen pompen (voor een of andere i > 1) of omdat we niet omlaag kunnen pompen (i = 0). Studeeraanwijzing

Bestudeer nu de rest van section 4.3 van Linz. Daar wordt het bovenstaande op weer een iets andere manier verwoord, en wordt een strategie beschreven – in de vorm van een spel met een tegenstander – die u kan helpen bij het correct toepassen van het pomplemma.

OPGAVE 4.26

Maak exercise 4 bij section 4.3 van Linz. OPGAVE 4.27

Laat zien dat de volgende talen niet regulier zijn: a L = { anblak : k  n + l } b L = { w  {a, b}* : na(w)  nb(w) } c L = { ww : w  {a, b}* } OPGAVE 4.28

Maak exercise 7 bij section 4.3 van Linz. OPGAVE 4.29

Maak exercise 17 bij section 4.3 van Linz. OPGAVE 4.30

Maak exercise 18a en 18b bij section 4.3 van Linz. Zeg voor de andere onderdelen van exercise 18 alleen of u denkt dat de taal regulier is of niet. OPGAVE 4.31

Maak exercise 29 bij section 4.3 van Linz. OPGAVE 4.32

Toon aan dat de reguliere taal {a}*{b}* voldoet aan het pomplemma, dat wil zeggen, toon aan dat {a}*{b}* pompbaar is. OPGAVE 4.33

Welke van de volgende methoden zijn geschikt om aan te tonen dat een taal L niet regulier is? Leg steeds uit waarom wel of niet. a Doorsnijd L met een reguliere taal Lr. Als L  Lr een taal is waarvan bekend is dat hij niet regulier is, dan is L ook niet regulier. b Laat zien dat L de doorsnede is van twee niet-reguliere talen. c Laat zien dat L de doorsnede is van een reguliere taal Lr en een taal waarvan we weten dat hij niet regulier is. d Doorsnijd L met een niet-reguliere taal. Als het resultaat van deze doorsnede een taal is waarvan we weten dat hij niet regulier is, dan is L ook niet regulier.

De pumping lemma game die na example 4.7 beschreven wordt is geïmplementeerd in de tool JFLAP. We raden u aan om, voordat u het spel daadwerkelijk in JFLAP gaat spelen, eerst de handleiding van het spel in JFLAP Activities.pdf te lezen (section 3.2, en exercise 6 in section 2.4). We benadrukken bovendien dat u het pomplemma goed moet kennen, en het gebruik ervan moet begrijpen, voordat u het spel kunt spelen. Zeker de eerste keren is het aan te raden om in te stellen dat de computer mag beginnen. Examples 4.7, 4.8, 4.9, 4.10 en 4.12 uit Linz zijn geïmplementeerd in het spel. Let bij het spelen goed op: er staan talen tussen die regulier zijn!

ZELFTOETS

1

Bewijs met afsluitingseigenschappen: als L  {a, b, c}* regulier is, dan is de taal K die bestaat uit alle woorden van L, maar dan met alle voorkomens van b verwijderd, ook regulier. Als L = {abc, bb}, dan is K dus gelijk aan {ac, }.

2

Construeer eindige automaten die de volgende talen accepteren, en gebruik daarbij de aangegeven constructie(s) op eindige automaten: a De taal bestaande uit alle strings over {a, b, c} met een prefix uit {a}+{b} en een suffix uit {b}+{c} (doorsnedeconstructie) b De taal {a, b, c}* – ({a}*{b}  {b}*{c}) (constructie voor vereniging, en complementconstructie)

3

Gegeven is de taal L die bestaat uit alle strings over {a, b} van de vorm ww’, waarbij w’ uit w verkregen wordt door iedere a in w te vervangen door een b, en omgekeerd. Tot L behoren dus onder andere , ab en aababbab. Toon aan dat L niet regulier is.

4

Geef van de volgende twee beweringen aan of ze waar zijn en waarom. a Elke regulier pompbare taal is regulier. b Als een taal L een string z bevat die niet regulier pompbaar is in L, dan is L niet regulier.

5

Toon aan dat de taal L = {a, b, c}* – {canbn : n > 0} niet regulier is. Gebruik daarbij een afsluitingseigenschap en het pomplemma.

TERUGKOPPELING 1

4.1

Uitwerking van de opgaven

a Ja, de verzameling van natuurlijke getallen is gesloten onder optelling: welk tweetal natuurlijke getallen je ook neemt, als je ze optelt heb je nog steeds een natuurlijk getal. Nee, de verzameling natuurlijke getallen is niet gesloten onder deling: neem bijvoorbeeld de natuurlijke getallen 3 en 6. Als je de eerste door de tweede deelt krijg je 3/6, en dat is geen natuurlijk getal. Het maakt hierbij niet uit dat je ze in omgekeerde volgorde wel kunt delen met een natuurlijk getal als resultaat: 6/3 = 2. Eén combinatie van twee natuurlijke getallen waarvan de deling een niet-natuurlijk getal oplevert is al genoeg om aan te tonen dat de verzameling niet gesloten is onder deling. NB Merk op dat zowel optelling als deling binaire operatoren zijn: ze hebben allebei twee operanden (argumenten) nodig. b Ja, de verzameling * is per definitie gesloten onder concatenatie (daarom heet het ook star-closure). En zelfs niet alleen onder concatenatie van twee elementen (strings), maar zelfs van een willekeurig (maar wel eindig) aantal elementen. Ja, de verzameling * is ook gesloten onder spiegelbeeld: * bevat alle strings over het alfabet . Als je een string over  neemt en daar dan het spiegelbeeld van neemt, heb je weer een string over . NB Merk op dat concatenatie een binaire operatie is en spiegelbeeld een unaire (spiegelbeeld heeft maar één operand nodig). c De gegeven verzameling is precies de machtsverzameling van {1, 2, 3}: de verzameling die alle deelverzamelingen van {1, 2, 3} bevat, en niets anders. Als je twee deelverzamelingen van {1, 2, 3} verenigt krijg je natuurlijk weer een deelverzameling van {1, 2, 3}, dus de gegeven verzameling is gesloten onder vereniging. Omdat het complement van een deelverzameling van {1, 2, 3} ten opzichte van {1, 2, 3} natuurlijk weer een deelverzameling van {1, 2, 3} is, is de machtsverzameling van {1, 2, 3} gesloten onder complement.

4.2

Een taal is een verzameling strings. Als een taal gesloten is onder concatenatie betekent dit dat het achter elkaar plakken van twee willekeurige strings uit die taal weer een string uit die taal oplevert. Voor sommige reguliere talen klopt dat, zoals voor {a}*: het concateneren van twee strings die alleen a’s bevatten geeft weer een string die alleen a’s bevat. Voor andere reguliere talen klopt dat echter niet, zoals voor {ab} (abab is geen element van {ab}) en voor {a}+{b}+ (concatenatie van twee willekeurige strings uit {a}+{b}+ levert een string uit {a}+{b}+{a}+{b}+). Het is dus niet zo dat alle reguliere talen gesloten zijn onder concatenatie. Wat waarschijnlijk bedoeld werd is ‘De familie van reguliere talen is gesloten onder concatenatie’, ofwel: als we de ene willekeurige reguliere taal achter de andere willekeurige reguliere taal plakken, dan krijgen we weer een reguliere taal. NB Het feit dat de uitspraak het heeft over concatenatie en niet over bijvoorbeeld doorsnede bemoeilijkt de zaak een beetje: van de uitspraak ‘Reguliere talen zijn gesloten onder doorsnede’ zou het al snel duidelijk zijn dat er iets niet klopt. Wat is immers de doorsnede van twee strings?

4.3

a Het idee achter de doorsnedeconstructie uit het bewijs van theorem 4.1 is om de twee gegeven automaten tegelijkertijd te doorlopen, en om alleen stappen te doen die in beide automaten gedaan kunnen worden. In de toestanden van de doorsnedeautomaat houden we bij in welke toestanden de beide oorspronkelijke automaten op dat punt tijdens het lezen van de invoerstring zijn. In iets meer detail: de toestanden van de doorsnedeautomaat zijn van de vorm (q, p), met q  Q en p  P. Het startpunt van de doorsnedeautomaat is duidelijk: (q0, p0). Alleen als beide oorspronkelijke automaten M1 en M2 op dat punt een overgang gelabeld a hebben – zeg 1(q0, a) = q1 en 2(p0, a) = p1 – dan krijgt de doorsnedeautomaat M’ een overgang ’((q0, p0), a) = (q1, p1). De doorsnedeautomaat mag alleen strings accepteren die door beide oorspronkelijke automaten worden geaccepteerd. b Het aantal toestanden van de doorsnedeautomaat is niet gespecificeerd in de constructie in het bewijs van theorem 4.1. In zulke constructies is het voor het bewijs meestal het handigst om te zeggen dat de doorsnedeautomaat als toestandsverzameling heel Q  P heeft, dus gewoon alle combinaties bestaande uit een toestand uit Q en een toestand uit P. In de praktijk, dus als we bezig zijn een concrete doorsnedeautomaat te maken, is het vaak handiger om alleen die toestanden (q, p) toe te voegen die we tijdens het systematisch uitvoeren van de constructie tegenkomen; in de praktijk is het aantal toestanden van de doorsnedeautomaat dus maximaal Q  P. c De oorspronkelijke automaten hoeven niet totaal te zijn: als de ene automaat op een bepaald punt in de invoerstring een a kan lezen, en de andere automaat heeft op dat punt geen a-overgang, dan kunnen ze duidelijk niet allebei hetzelfde doen en hoort die a-overgang niet thuis in de doorsnedeautomaat. De oorspronkelijke automaten hoeven niet deterministisch te zijn: als de ene automaat op een bepaald moment kan kiezen uit 1(q1, a) = q2 en 1(q1, a) = q3, terwijl de ander alleen maar 2(p4, a) = p5 kan doen, krijgt de doorsnedeautomaat ook twee mogelijkheden: ’((q1, p4), a) = (q2, p5) en ’((q1, p4), a) = (q3, p5). Overigens bedoelen we hier met ‘niet deterministisch’ alleen dat toestanden meerdere uitgaande pijlen met hetzelfde label mogen hebben; we hebben liever geen -overgangen, omdat die de constructie nogal bemoeilijken. Voor de geldigheid van de doorsnedeconstructie maakt dat niet uit: voor iedere eindige automaat met -overgangen kan een equivalente eindige automaat zonder -overgangen geconstrueerd worden.

4.4

a We moeten eerst automaten construeren voor de talen L((a + b)a*) en L(baa*). Als we die dan tekenen en handig neerzetten, kunnen we vervolgens de doorsnedeautomaat in de tekening construeren. We tekenen hieronder onze automaat voor L((a + b)a*) horizontaal, en onze automaat voor L(baa*) verticaal. We bouwen dan de doorsnedeautomaat op in het gebied dat tussen de twee oorspronkelijke automaten ontstaat.

We zijn begonnen met de gecombineerde begintoestand (0, p). Van daaruit kan maar in één van beide automaten een a gelezen worden, dus de doorsnedeautomaat krijgt geen overgang met een a vanuit (0, p). Beide automaten kunnen wel een b lezen vanuit (0, p), dus de doorsnedeautomaat krijgt wel een b-overgang vanuit (0, p), en wel naar (1, q). Toestand (0, p) is nu afgewerkt, dus we gaan nu voor de nieuwe toestand (1, q) na of er overgangen met a en/of b moeten komen enzovoort. In het plaatje hierboven ziet u het resultaat. Het is eenvoudig in te zien dat L((a + b)a*)  L(baa*) = L(baa*), en het is dan ook niet vreemd dat de doorsnedeautomaat, op de toestandsnamen na, gelijk is aan de automaat voor L(baa*) waar we mee begonnen. Merk op dat alle automaten in dit onderdeel niet-totaal zijn (dat is ook de reden dat gevraagd wordt om nfa’s: het is helemaal niet nodig om de automaten eerst totaal te maken en dat scheelt weer werk). b We maken eerst weer een dfa voor L(ab*a*) en een nfa voor L(a*b*a); de eerste is niet moeilijk, voor de tweede maken we daarna toch maar een equivalente deterministische automaat, om de lastige -overgangen te verwijderen. We hebben de dfa voor L(ab*a*) hieronder verticaal getekend en die voor L(a*b*a) horizontaal, en we passen weer onze ‘grafische’ versie van het algoritme in het bewijs van theorem 4.1 toe.

4.5

We tonen hieronder twee (niet-totale, niet-deterministische) automaten voor Lac (horizontaal) en Lcb (verticaal), en de automaat voor Lac  Lcb die daaruit ontstaat door het uitvoeren van de doorsnedeconstructie.

4.6

In example 4.1 wordt het idee eigenlijk al gegeven: L1  L2  L1  L2 . Ofwel: als we dfa’s hebben voor L1 en L2, dan kunnen we een dfa voor L1 – L2 construeren door eerst een dfa voor het complement van L2 te maken volgens de complementconstructie uit het bewijs van theorem 4.1, en vervolgens gebruiken we de doorsnedeconstructie uit datzelfde bewijs om een (deterministische) automaat voor L1 – L2 te maken. De automaat voor L2 moet echt totaal en deterministisch zijn (anders werkt de complementconstructie niet), de automaat voor L1 mag niet-totaal en/of niet-deterministisch zijn.

___

We kunnen deze twee constructies zelfs eenvoudig samenvoegen tot de volgende ‘verschilconstructie’: Laat L1 = L(M1) en L2 = L(M2), met M1 = (Q, , 1, q0, F1) en M2 = (P, , 2, p0, F2) eindige automaten, en met M2 totaal en deterministisch. We construeren uit M1 en M2 een automaat Mv = (Q  P, , v, (q0, p0), Fv), met als toestanden paren (qi, pj), en waarvan de overgangsfunctie v een overgang v((qi, pj), a) = (qk, pl) bevat dan en slechts dan als 1(qi, a) = qk én 2(pj, a) = pl. Merk op dat we tot zover precies hetzelfde gedaan hebben als in de doorsnedeconstructie. Nu komt het verschil: de verzameling eindtoestanden van de verschilautomaat bestaat uit alle toestanden (qi, pj) waarvoor geldt dat qi  F1 en pj  F2. 4.7

a Neem aan dat we voor iedere Li een reguliere expressie ri hebben zodat Li = L(ri). Dan is het eenvoudig in te zien dat L1  L2 = L(r1 + r2) en dus regulier, dat (L1  L2)  L3 = L((r1 + r2) + r3) en dus regulier, enzovoort. Omdat er in totaal altijd eindig veel talen verenigd worden (namelijk n), geldt dat de bijbehorende expressie inderdaad regulier is en de daarbij behorende taal dus ook. We zouden deze redenering ook nog formeel kunnen bewijzen, met inductie naar het aantal talen, maar dat laten we aan u over. Voor doorsnede geldt hetzelfde, maar dan via eindige automaten in plaats van reguliere expressies. b Nee, de familie van reguliere talen is zelfs niet gesloten onder oneindige vereniging van eindige talen: {ab}  {a2b2}  {a3b3}  … geeft {anbn : n ≥ 1}. In de zelftoets van leereenheid 2 hebt u beredeneerd dat deze taal niet regulier is; later in deze leereenheid zullen we dat formeel bewijzen.

4.8

We laten dit zien met ‘set identity’ (ook wel formulemanipulatie genoemd). Een alternatieve definitie van het symmetrisch verschil van S1 en S2 is (S1 – S2)  (S2 – S1). Als L1 en L2 reguliere talen zijn, dan is (L1 – L2)  (L2 – L1) ook regulier, omdat de familie van reguliere talen gesloten is onder verschil (example 4.1 en opgave 4.6) en onder vereniging (theorem 4.1). Er zijn nog andere mogelijkheden om de opgave met formulemanipulatie op te lossen: een andere definitie van het symmetrisch verschil van L1 en L2 is (L1  L2) – (L1  L2), en omdat de familie van reguliere talen gesloten is onder , – , en  is het resultaat weer regulier.

4.9

a Er geldt inderdaad dat h(L1  L2) = h(L1)  h(L2). We beredeneren eerst dat ieder element van h(L1  L2) ook een element van h(L1)  h(L2) is. Een element van h(L1  L2) wordt gevormd door een element w van L1  L2 te nemen en daar h op toe te passen. Als w in L1 zat, dan zit h(w) in h(L1) en dus ook in h(L1)  h(L2). Als w in L2 zat, dan zit h(w) in h(L2) en dus ook in h(L1)  h(L2). Dus h(L1  L2)  h(L1)  h(L2). Aantonen dat ook h(L1)  h(L2)  h(L1  L2) gaat analoog. De gelijkheid volgt uit beide inclusies. b De uitdrukking h(L1  L2) = h(L1)  h(L2) is niet waar. Het is misschien even zoeken naar een geschikt tegenvoorbeeld, maar het volgende voldoet: neem L1 = L(aa*), L2 = L(bb*) en h(a) = h(b) = a. Dan geldt h(L1  L2) = h() = , terwijl h(L1) = aa* en h(L2) = aa*, dus h(L1)  h(L2) = aa*. NB Merk op dat we dus kunnen bewijzen dat iets niet waar is door één geschikt tegenvoorbeeld te geven. Als we daarentegen moeten bewijzen dat iets waar is, mag dat niet door middel van voorbeelden, maar moeten we zo algemeen en precies mogelijk alle gevallen af gaan (zoals in onderdeel a).

c De laatste uitdrukking, h(L1L2) = h(L1)h(L2), is waar. Laat x  L1 en y  L2. Door nu h op de strings x, y en xy toe te passen, zoals gedefinieerd in definition 4.1, is het duidelijk dat h(xy) = h(x)h(y). 4.10

a We moeten van iedere string uit L1 alle prefixen bewaren die overblijven nadat we er aan het eind een string uit L2 hebben afgehakt. Anders gezegd: we moeten voor iedere string uit L1 nagaan of hij eindigt op een string uit L2, en zo ja, dan moeten we dat stuk uit L2 eraf halen en het stuk dat overblijft in L1/L2 stoppen. Merk op dat iedere string uit L1 dus meer dan één string in L1/L2 kan opleveren! In dit specifieke geval bevatten alle strings van zowel L1 als L2 precies één b, en kunnen we een string x uit L1 en een string y uit L2 dus op precies één manier ‘over elkaar leggen’: met de b’s precies op elkaar. x = aaaaabaaaa y = aaaaabaaaa In het voorbeeld hierboven hebben we de ‘vaste’ stukken van x en y (iedere string uit L1 moet de substring ba bevatten, en iedere string uit L2 de substring ab) onderstreept, en de b’s precies op elkaar gelegd. Het is duidelijk dat in dit geval aaaa  L1/L2. Het is nu eenvoudig na te gaan dat L1/L2 = L(a*). b In dit geval gaat het sneller als we de constructie uit het bewijs van theorem 4.4 toepassen. Een dfa voor L1 staat al in figure 4.3. We berekenen L(M0)  L2 = L(abaa*) L(M1)  L2 =  L(M2)  L2 =  L(M3)  L2 =  Een dfa voor L1/L2 is dus de dfa uit figure 4.3, maar dan met q0 als enige eindtoestand, in plaats van q2. Het is meteen duidelijk dat die automaat de taal L(a*) accepteert.

4.11

Het kan even zoeken zijn naar een geschikt tegenvoorbeeld, maar dit is er een: neem L1 = {ba} en L2 = {a}*. Dan geldt L1L2 = {ban : n ≥ 1}. Van iedere string uit L1L2 moeten we nu één voor één alle suffixen uit L2 afhakken. Voor bijvoorbeeld baa  L1L2 levert dat de strings baa, ba en b. Het is dus duidelijk dat L1L2/L2 meer bevat dan alleen ba, en dus niet gelijk is aan L1.

4.12

ˆ zijn helemaal gelijk, behalve a  is de overgangsfunctie van allebei: M en M hun verzameling eindtoestanden. ˆ ) = L1/L2. Hiertoe wordt eerst aangetoond b Het doel is te bewijzen dat L( M ˆ accepts x ˆ ): “…, let x be any element of L1/L2. … and M dat L1/L2  L( M ˆ ˆ )  L1/L2 : because *(q0, x) is in F ”. Vervolgens wordt bewezen dat L( M ˆ , …, and x is in L1/L2”. “Conversely, for any x accepted by M ˆ ) = L1/L2, ofwel dat de gegeven constructie een Nu is bewezen dat L( M dfa oplevert die precies het rechterquotiënt van de reguliere taal L1 met de reguliere taal L2 accepteert. Omdat L1 en L2 willekeurige reguliere talen zijn, is hiermee aangetoond dat de familie van reguliere talen gesloten is onder rechterquotiënt.

4.13

Zie de uitwerking achterin Linz (pagina 413). Zoals gezegd wordt niet van u verwacht dat u de daar gegeven set identity zelf bedenkt; we raden u wel aan om, bijvoorbeeld met behulp van een venndiagram, na te gaan dat het___klopt. Een andere manier om dat na te gaan is de observatie dat ( L1  L2 )  L1 niets anders is dan L2 – L1; het is daarna niet zo moeilijk om in te zien dat___ (L2 – L1)  (L2  L1) inderdaad gelijk is aan L2. De reden dat er ( L1  L2 )  L1 staat en niet gewoon L2 – L1 is dat in de opgave gegeven is dat L1  L2 regulier is; daar kunnen we alleen gebruik van maken als L1  L2 ook daadwerkelijk in de uitdrukking voorkomt.

4.14

a changeleft(abc) = {bbc, cbc} en changeleft(bc) = {ac, cc}, we moeten immers het eerste symbool van abc vervangen door ieder mogelijk ander symbool. Merk op dat het toepassen van changeleft op één string dus een verzameling met meer dan één string kan opleveren (namelijk als  ≥ 3). b Het eerste symbool van iedere geaccepteerde string moet veranderd worden. Dat symbool vinden we natuurlijk op de uitgaande takken van de begintoestand. Een eerste gedachte is dan misschien om de overgang (q0, a) = q1 te vervangen door een nieuwe overgang (q0, d) = q1 voor alle d   – {a}, en de overgang (q0, b) = q2 door een nieuwe overgang (q0, d) = q2 voor alle d   – {b}. Dat werkt alleen niet, omdat de automaat nog terug kan keren in de begintoestand, en dan nog steeds de oorspronkelijke overgangen (q0, a) = q1 en (q0, b) = q2 moet kunnen nemen. Een oplossing is om een nieuwe begintoestand s in te voeren, en dan nieuwe overgangen (s, d) = q1 voor alle d   – {a} en (s, d) = q2 voor alle d   – {b} toe te voegen. De oude overgangen vanuit de begintoestand blijven dus gewoon bestaan; het enige verschil met de oorspronkelijke automaat is dat we nu op een andere manier met het eerst gelezen invoersymbool in toestand q1 of q2 terechtkomen. De rest van de automaat is niet veranderd, dus de string kan daarna als vanouds afgemaakt worden. Het resultaat voor onze voorbeeldautomaat is als volgt:

c We kiezen ervoor om uit te gaan van een deterministische, maar niet per se totale automaat. Totaal zijn is niet nodig (de automaat uit onderdeel b is immers niet totaal), en determinisme zorgt er meestal voor dat bewijzen en constructies wat makkelijker op te schrijven zijn: voor iedere q en a heeft (q, a) hoogstens één waarde.

Uitgaande van een deterministische automaat M = (Q, , , q0, F) construeren we een (eveneens deterministische) automaat M’ voor changeleft(L(M)) als volgt: M’ = (Q  {s}, , ’, s, F), met Q  {s} =  (dat wil zeggen: s is een nieuwe toestand) en waarbij ’ alle overgangen van  bevat, plus een aantal nieuwe overgangen vanuit s: voor alle a   en alle q  Q waarvoor  een overgang (q0, a) = q heeft krijgt ’ overgangen ’(s, d) = q voor alle d   – {a}. NB changeleft() is niet gedefinieerd en dat kan een probleem opleveren bij changeleft(L), als  in L. Als we afspreken dat changeleft() = {} (als je het meest linkse symbool van  wilt veranderen ben je meteen klaar, en heb je nog steeds ), kunnen we het algoritme als volgt aanpassen: We gaan nog steeds uit van een niet noodzakelijk totale, maar wel deterministische automaat. Als  door die automaat geaccepteerd wordt, betekent het dat de begintoestand ook eindtoestand is. We voeren de constructie precies zo uit als beschreven, maar maken de nieuwe begintoestand nu ook eindtoestand. 4.15

Omdat de tail(L) letterlijk alle staarten van strings uit L bevat, is een logische eerste gedachte: zorg dat je in alle toestanden kunt beginnen. Dat is inderdaad een goed idee, alleen zien we dan een bijzondere situatie over het hoofd:

We hebben aan de automaat van opgave 4.14 twee onbereikbare toestanden toegevoegd: q3 en q4 zijn onbereikbaar vanuit de begintoestand. We keren nu terug naar ons idee om een automaat voor tail(L) te maken door in de automaat voor L te zorgen dat we in alle toestanden kunnen beginnen (we laten nog even in het midden hoe we dat gaan regelen): het is duidelijk dat we in bovenstaand plaatje niet willen dat we in q4 kunnen beginnen. Daarom passen we de formulering van onze constructie aan: we zorgen dat we kunnen beginnen in alle toestanden die op een pad van de begintoestand naar een eindtoestand liggen (inclusief de begintoestand en de eindtoestanden zelf). Nu is de vraag nog hoe we daarvoor kunnen zorgen. De eenvoudigste oplossing is om in plaats van één begintoestand een verzameling begintoestanden te nemen, die precies alle genoemde toestanden bevat. Volgens opgave 2.18 van leereenheid 2 mag dat. Dus als L regulier is, dan is tail(L) dat ook. 4.16

a

Geen commentaar.

b De oplossing achterin Linz werkt alleen voor rechtslineaire grammatica’s!

c Ook deze uitwerking in Linz werkt alleen voor rechtslineaire grammatica’s. NB Merk op dat dit verder geen problemen oplevert: we kunnen uit de gegeven constructies nog steeds afleiden dat de familie van reguliere talen gesloten is onder vereniging, concatenatie en star-closure, en we weten nu dat die constructies voor rechtslineaire grammatica’s (bijna) net zo eenvoudig zijn als voor reguliere expressies. In opgave 4.17 vragen we u hetzelfde probleem nog eens op te lossen, maar dan met eindige automaten. 4.17

a Deze constructie is net zo eenvoudig als die voor reguliere expressies of rechtslineaire grammatica’s: we zetten gewoon de beide automaten bij elkaar in één plaatje, dan hebben we een nfa (met twee begintoestanden, zie opgave 2.18 van leereenheid 2) die precies L(M1)  L(M2) accepteert. b We verbinden de beide automaten door -overgangen vanuit iedere eindtoestand van M1 naar iedere begintoestand van M2 toe te voegen. De begintoestanden van de nieuwe automaat zijn die van M1, de eindtoestanden van de nieuwe automaat zijn de eindtoestanden van M2. c We voegen -overgangen toe vanuit iedere eindtoestand van M1 naar iedere begintoestand. Dat levert een automaat voor de positieve afsluiting (positive closure) van M1. Als   L(M1) zijn we nu klaar, als   L(M1) dan voegen we nog een extra, losse toestand toe die zowel begin- als eindtoestand is.

4.18

Omdat L1  L2 regulier is, bestaat er zo’n algoritme, volgens theorem 4.5.

4.19

Er geldt dat L1  L2 dan en slechts dan als L1  L2 = L2 (ga na dat beide richtingen gelden en noodzakelijk zijn). Aangezien L1  L2 regulier is, en er een algoritme bestaat voor gelijkheid van reguliere talen, is er ook een algoritme om te bepalen of de ene reguliere taal een deelverzameling is van de andere. Er zijn overigens nog andere mogelijkheden: L1  L2 is ook equivalent met L1 – L2 = . Omdat L1 – L2 regulier is en er een algoritme bestaat om na te gaan of een reguliere taal leeg is, bestaat er dus ook een algoritme voor L1  L2.

4.20

De vraag is of er een algoritme bestaat dat voor een willekeurige reguliere taal L kan bepalen of   L. Als L regulier is, bestaat er een eindige automaat die L accepteert. We mogen aannemen dat die automaat deterministisch is (want als hij dat niet is kunnen we hem altijd deterministisch maken). Een deterministische automaat bevat geen -overgangen, dus de enige manier om dan  te accepteren is als de begintoestand tegelijkertijd ook eindtoestand is. Dat is eenvoudig na te gaan.

4.21

Construeer uit de dfa voor L een dfa voor LR, met de constructie van theorem 4.2. Gebruik daarna het algoritme voor gelijkheid van reguliere talen uit theorem 4.7.

4.22

Als er geen strings van even lengte zijn in L, dan geldt L((aa + ab + ba + bb)*)  L = . Het resultaat volgt nu uit theorem 4.6.

4.23

Het gaat om een string met lengte minstens n + 1, terwijl de automaat n + 1 toestanden heeft (gelabeld q0 tot en met qn). Het is een dfa, dus in iedere move wordt precies één symbool van de invoerstring gelezen. De rij toestanden waar de automaat achtereenvolgens langs komt bij het lezen van de invoerstring is q0, qi, qj, …, qf en bevat minstens n + 2 toestanden. De eerste plaats waar een herhaling op moet treden (als er eerder nog geen toestand herhaald is) is dus de overgang van de (n + 1)-ste toestand in die rij naar de (n + 2)-de toestand in die rij. Dat is de (n + 1)-ste move, niet de n-de move. NB Voor de correctheid van het pomplemma maakt dit natuurlijk niets uit!

4.24

Laat L een eindige taal zijn; L is dus ook regulier. Volgens het pomplemma moet er dan een of ander positief getal m bestaan zodat iedere string w  L die langer is dan m gepompt kan worden (dat wil zeggen na pompen nog steeds een string in L oplevert). Er worden verder geen eisen aan m gesteld, dus zodra we er een gevonden hebben waarvoor de rest van de uitspraak in het pomplemma waar is, zijn we klaar. Als we m groter dan de lengte van de langste string in L kiezen, dan hebben we een m zoals bedoeld in het pomplemma gevonden: het pomplemma zegt namelijk dat er nu iets moet gelden voor alle w  L met w ≥ m. Zulke w bestaan niet, dus het maakt ook niet uit wat daar nu precies voor zou moeten gelden (“voor alle x geldt …” is waar als er geen x’en zijn, ongeacht wat er op de … staat).

4.25

In example 4.7 wordt een bewijs uit het ongerijmde gegeven. Eerst nemen we aan dat L = {anbn : n ≥ 0} regulier is, en dus pompbaar: “Assume that L is regular, so that the pumping lemma must hold”. Volgens het pomplemma moet er dus een m zijn zodat voor iedere w  L met lengte m of langer iets geldt (namelijk dat w binnen L gepompt kan worden). De volgende zin (“We do not know the value of m, but whatever it is, we can always choose n = m”) gaat daarover, maar de formulering ervan zou voor verwarring kunnen zorgen: er staat niet “we weten niet wat m is, dus kunnen we net zo goed n kiezen als waarde voor m”, maar er staat “we weten niet wat m is, dus we blijven hem maar m noemen”. Impliciet staat er nog iets heel belangrijks: “we kiezen w = ambm ”. We mogen w zelf kiezen, want de pompeigenschap moet voor alle w  L gelden. Omdat alle woorden van L dezelfde vorm hebben (anbn voor een of andere n) ligt de keuze voor de hand: ambm. We kunnen nu meteen uitleggen waarom we m gebruiken in de specificatie van w: we weten niet wat m is, maar we moeten wel zorgen dat w minstens lengte m heeft. De enige manier om daarvoor te zorgen is door m te gebruiken. We blijven de formulering van het pomplemma volgen, en zijn nu toe aan het ophakken van w in drie stukken x, y en z met bepaalde eigenschappen: w = xyz,xy≤ m eny≥ 1. Volgens het pomplemma moeten er zulke x, y en z te vinden zijn voor onze w. Uit onze keuze voor w en uit de drie eisen aan x, y en z kunnen we nu afleiden dat “Therefore, the substring y must consist entirely of a’s”: x en y moeten samen helemaal binnen de eerste m symbolen van w vallen, en daar staan alleen a’s. Om het vervolg wat makkelijker op te kunnen schrijven zeggen we vervolgens “Suppose thaty = k”. We keren weer terug naar de formulering van het pomplemma en zien dat nu moet gelden dat wi = xyiz  L voor alle i ≥ 0. We kiezen dus een i waarmee we ons doel bereiken, namelijk een tegenspraak, waarmee we aantonen dat L niet pompbaar is: “Then the string obtained by using i = 0 in Equation (4.2) is w0 = am-kbm and is clearly not in L”. Dan kunnen we het geheel afronden met “This contradicts the pumping lemma and thereby indicates that the assumption that L is regular must be false”.

4.26

We tonen aan dat L = {w : na(w) = nb(w)} niet pompbaar is. Met het pomplemma volgt dan dat L niet regulier is. We kiezen w = ambm, dan geldt w  L en w ≥ m. De enige mogelijkheid voor y is dan ak, voor een k met 1 ≤ k ≤ m. Welke i ≠ 1 we dan ook kiezen, de string xyiz zit nooit in L: als i = 0, dan zitten er te weinig a’s in, en als i > 1, dan zitten er te veel a’s in. De taal is dus niet pompbaar, en daarom ook niet regulier. Er wordt ook nog gevraagd of L* regulier is. Laten we eerst eens bedenken welke strings er allemaal in L* zitten. Strings in L* zijn van de vorm w1w2…wn, waarbij iedere wi  L. Je zou kunnen zeggen dat zo’n string begint met een stuk met evenveel a’s als b’s (w1). Daar wordt een stuk (w2) achter geplakt dat ook evenveel a’s als b’s heeft (maar niet noodzakelijk evenveel als w1). Het aantal a’s van het resultaat w1w2 is dus met evenveel opgehoogd (ten opzichte van w1) als het aantal b’s, en het resultaat heeft dan natuurlijk ook evenveel a’s als b’s. Enzovoort. Kortom, L* bevat alleen maar strings die al in L zitten. De vraag is: bevat L* alle strings uit L? Het antwoord is natuurlijk ja: L  L*. Hier geldt dus dat L* = L, en we hadden net bewezen dat L niet regulier is.

4.27

a Laat m de constante uit het pomplemma zijn, en kies w = ambma2m. Dan geldt duidelijk w  L en w  m. Omdat xy  m moet zijn, kan y alleen maar gelijk zijn aan ak, voor een of andere 1  k  m. Omhoog pompen levert nu een probleem op: xy2z = am+kbma2m met 2m + k  2m. NB Er zijn ook verkeerde keuzes voor w mogelijk, waarvan sommige ook nog eens meer werk opleveren bij het zoeken van decomposities (dat laatste is op zich geen probleem, maar we proberen ons altijd onnodig werk te besparen). Neem bijvoorbeeld w = abam+2. Daarvoor geldt ook w  L en w ≥ m. Bij het zoeken van alle mogelijke decomposities van w = xyz kan het handig zijn een schema te tekenen zoals dat in figure 4.5 bij example 4.8. We noemen hier de mogelijkheden voor y; y kan gelijk zijn aan: de eerste a, of aan ab, of aan abak voor een of andere k met 1 ≤ k ≤ m – 2, of aan b, of aan bak voor een of andere k met 1 ≤ k ≤ m – 2, of aan ak voor een of andere k met 1 ≤ k ≤ m – 2 (uit het laatste rijtje a’s). In alle gevallen behalve het laatste is een i te vinden zodat xyiz  L. Omdat het in het laatste geval niet mogelijk is zo’n i te vinden, kunnen we deze w niet gebruiken om met het pomplemma aan te tonen dat L niet regulier is. b Het pomplemma toepassen lijkt in dit geval niet zo makkelijk, dus we kiezen een andere manier. Stel dat L regulier is. Omdat de familie van reguliere talen gesloten is onder complement, zou het complement van L dan ook regulier zijn. Maar dat complement is gelijk aan {w  {a, b}* : na(w) = nb(w)} en daarvan weten we dat het niet regulier is. Kortom: als L regulier is, moet het complement van L ook regulier zijn, maar we weten dat dat laatste niet waar is. We hebben dus een tegenspraak, en blijkbaar was onze aanname niet juist. L is dus niet regulier.

c Kies w = ambmambm of w = ambamb, dan voldoet w duidelijk aan de voorwaarden, en bovendien is er maar één mogelijkheid voor y, namelijk y = ak (eigenlijk zijn dat natuurlijk k mogelijkheden). Zolang we maar niet i = 1 kiezen, levert pompen altijd een string die niet van de vorm uu met u  {a, b}* is. Merk op dat we de variabele w uit de definitie van L in de vorige zin even hebben vervangen door u, om conflicten met de w uit het pomplemma te voorkomen. Het is bij dit onderdeel belangrijk een goede keuze voor w te maken. Keuzes die niet werken zijn bijvoorbeeld w = a2m (dan is er wel een pompbare verdeling in drie stukken te vinden, namelijk die met y = aa) of w = (ab)2m (neem y = abab). 4.28

a Het eerste deel van L is bevat in het tweede deel: {anbn : n ≥ 1}  {anbm : n ≥ 1, m ≥ 1}. Het laatste deel is regulier, dus het geheel ook. b Deze taal is niet regulier. We bewijzen dat met behulp van het pomplemma. Neem w = ambm, dan is y = ak en levert omhoog pompen een string buiten L op. Merk op dat omlaag pompen hier niet werkt, omdat het resultaat dan in het tweede deel van L kan zitten.

4.29

De bewering is niet waar. Neem bijvoorbeeld L1 = { anbm : n  m} en L2 = {anbm : n > m}. Beide zijn niet regulier, maar L1  L2 = L(a*b*), en die is wel regulier.

4.30

a De taal is regulier. Er zijn maar eindig veel manieren om niet-negatieve gehele getallen n, l en k te kiezen zodat n + l + k = 6 (0+0+6, 0+6+0, 6+0+0, 0+1+5, 2+3+1 enzovoort). Voor ieder van die manieren kan eenvoudig een reguliere expressie worden geconstrueerd. Omdat de specificatie niet eist dat n + l + k precies gelijk is aan 6, maar groter is dan 5, moet ieder van die reguliere expressies dan nog uitgebreid worden met a* of b* op strategische plekken: a*b*aaaaaaa* + a*bbbbbbb*a* + … + aaa*bbbb*aa* + … b Deze taal is niet regulier, wat we bewijzen met het pomplemma. We kiezen w = a6bm+3am+3, dan geldt k = 6 > 5, l = m + 3 > 3, en k = m + 3  m + 3 = l (dus w  L) en w m. Dan zijn er drie mogelijkheden voor y zodat xy m:  y is een substring van de eerste 6 a’s. Omlaag pompen levert dan te weinig a’s in het eerste stuk.  y = apbq met 1 ≤ p ≤ 6 en 1 ≤ q ≤ m – p. Door omhoog pompen krijgen we dan een string die niet van de vorm a*b*a* is en dus niet in L zit.  y bevat alleen b’s. Omlaag pompen zorgt er dan voor dat k  l niet langer geldt. De taal is dus niet regulier pompbaar, en daarom niet regulier. c, d, e en g zijn niet regulier, f is wel regulier.

4.31

a Nee, het pomplemma zegt “als een taal regulier is, dan is hij ook pompbaar”. Je kunt het pomplemma maar in twee gevallen gebruiken: – als je al weet dat een taal regulier is (dan weet je door het pomplemma dat die taal ook pompbaar is), of

– als je al weet (of kunt bewijzen) dat een taal niet pompbaar is (dan kun je het pomplemma gebruiken om te zeggen dat die taal dus niet regulier kan zijn). Het pomplemma wordt vrijwel altijd op de tweede manier gebruikt. b Nee, want L is wel regulier: maak eerst een automaat voor ap met p ≥ 100 (een lange ‘slang’ met 100 opeenvolgende a-overgangen en tot slot een lusje met een a), en breid deze dan zo uit dat vervolgens alleen nog maar 1 of 2 of … of 50 b’s kunnen worden gelezen. 4.32

De taal {a}*{b}* is regulier, dus volgens het pomplemma moet er een positief getal m zijn zodat iedere string w  L metw ≥ m kan worden opgedeeld in drie stukken x, y en z zodat w = xyz,xy ≤ m,y ≥ 1 en wi = xyiz  L voor alle i ≥ 0. Volgens het bewijs van het pomplemma kunnen we voor dat getal m altijd het aantal toestanden van een dfa die {a}*{b}* accepteert nemen. Het maakt niet uit welke dfa, dus we nemen gewoon de eerste die ons te binnen schiet, met (s, a, s), (s, b, f) en (f, b, f), waarin s de begintoestand is, en s en f beide eindtoestand zijn. Deze dfa heeft twee toestanden, dus we kiezen m = 2. Vervolgens moeten we iets laten zien voor iedere w  L metw ≥ m. We nemen daarom een willekeurige string w = akbn  L metw ≥ 2. Een deel van deze strings begint met een a (als k > 0), de rest begint met een b. We moeten nu laten zien dat er voor beide soorten strings een manier is om de string op te delen in drie stukken, volgens de specificaties in het pomplemma. Als w met een a begint, dan nemen we x = , y = a, en z = ak-1 bn. Dan is aan alle voorwaarden voldaan: w = xyz,xy ≤ 2,y ≥ 1 en wi = xyiz = aiak-1 bn  L voor alle i ≥ 0. Als w met een b begint, dan nemen we x = , y = b, en z = bn-1. Ook dan is aan alle voorwaarden voldaan: w = xyz,xy ≤ 2,y ≥ 1 en wi = xyiz = bibn-1  L voor alle i ≥ 0.

4.33

a Alternatief a is geschikt. De doorsnede van twee reguliere talen is altijd regulier. Als een doorsnede van twee talen niet regulier is, moet dus minstens één van die twee talen nietregulier zijn. Omdat van Lr gegeven is dat hij regulier is, moet L niet-regulier zijn. b Alternatief b is niet geschikt: neem bijvoorbeeld de niet-reguliere talen {anbn : n ≥ 0} en {bncn : n ≥ 0}. De doorsnede van deze talen is {}, en dat is een eindige taal en dus regulier. c Alternatief c is niet geschikt: de doorsnede van een reguliere en een nietreguliere taal kan best regulier zijn. Neem bijvoorbeeld de doorsnede van de reguliere taal {c} met de niet-reguliere taal {anbn : n ≥ 0}. Die doorsnede is gelijk aan  en dus regulier. d Ook alternatief d is niet geschikt: als de doorsnede van L met een nietreguliere taal niet-regulier is kan L best regulier zijn. Neem bijvoorbeeld L = {a, b}* en maar weer eens {anbn : n ≥ 0}: de doorsnede is {anbn : n ≥ 0} zelf en dus niet regulier, maar L is wel regulier.

2

Uitwerking van de zelftoets

1

We gebruiken het feit dat de familie van reguliere talen gesloten is onder homomorfismen. De taal K is namelijk gelijk aan de taal h(L), met het homomorfisme h gedefinieerd door h(a) = a, h(b) =  en h(c) = c.

2

a De taal kan geschreven worden als L  K, met L = {a}+{b}{a, b, c}* en K = {a, b, c}*{b}+{c}. We maken dus eerst automaten voor L en K, en passen daar dan de doorsnedeconstructie op toe.

b Automaten voor {a}*{b} en {b}*{c} zijn eenvoudig te maken. De automaat voor {a}*{b}  {b}*{c} is dan ook eenvoudig, en heeft vier toestanden, waarvan twee begintoestanden:

De automaat is dus niet-deterministisch. Omdat we uiteindelijk een automaat voor het complement van {a}*{b}  {b}*{c} willen hebben, moeten we bovenstaande automaat eerst deterministisch én totaal maken. We gebruiken daarvoor de constructie uit leereenheid 2.

De automaat voor {a, b, c}* – ({a}*{b}  {b}*{c}) wordt nu eenvoudig geconstrueerd door ‘eindtoestanden verwisselen’:

3

Stel L is regulier. Dan is L  {a}+{b}+ ook regulier, want {a}+{b}+ is regulier en de doorsnede van twee reguliere talen is weer een reguliere taal. Een string in L  {a}+{b}+ moet:  van de vorm anbm zijn, met n, m ≥ 1, en  uit twee helften w en w’ bestaan, waarbij geldt dat als in w alle a’s door b’s vervangen worden (en andersom), we w’ krijgen. Een string van de vorm anbm voldoet alleen maar aan de tweede eis als n = m. Dus geldt L  {a}+{b}+ = {anbn : n > 0}. Deze laatste taal is niet regulier, en dus kan L niet regulier zijn. Een alternatieve uitwerking gebruikt het pomplemma (en verder hetzelfde idee): kies u = ambm  L, met m de positieve integer uit het pomplemma. Dan is w ≥ m. De enige mogelijkheid voor y is nu ak met 1 ≤ k ≤ m. Pompen met bijvoorbeeld i = 0 levert de string am - kbm, en die is niet van de vorm ww’ zoals boven beschreven.

4

a Nee, dat is niet waar. Volgens het pomplemma is elke reguliere taal pompbaar, niet andersom. Dit is pure logica: het pomplemma is van de vorm ‘als p, dan q’ en dat is in het algemeen niet equivalent met ‘als q, dan p’ (maar wel met ‘als niet q, dan niet p’, en dat is precies de manier waarop we het pomplemma gebruiken). b Nee, alleen strings die lang genoeg zijn (namelijk strings waarvoor meer dan Q toestanden nodig zijn om ze symbool voor symbool helemaal te lezen) moeten pompbaar zijn. L mag dus best (kortere) strings bevatten die niet pompbaar zijn. Merk op dat dit precies de reden is dat we in een bewijs met behulp van het pomplemma altijd een string kiezen waar het getal m dat in het pomplemma wordt genoemd in voorkomt: dat moet omdat we niet weten wat ‘lang genoeg’ precies betekent.

5

We vermoeden natuurlijk meteen dat {canbn : n > 0} niet regulier is, en dat we dat kunnen bewijzen met het pomplemma. Het probleem is alleen dat er niet letterlijk {canbn : n > 0} staat: L is juist het complement daarvan. Gelukkig kunnen we dat ‘ongedaan maken’ door nogmaals het complement te nemen: het complement van het complement van een verzameling V is weer de verzameling V. Samen met de wetenschap dat de familie van reguliere talen gesloten is onder complement hebben we nu de ingrediënten voor het bewijs bij elkaar. Het wordt een bewijs uit het ongerijmde: Stel dat L wel regulier is. Omdat de familie van reguliere talen gesloten is onder complement geldt dan dat het complement van L ook regulier is. Het complement van L is het complement van het complement van {canbn : n > 0}, ofwel {canbn : n > 0} zelf. Er zou nu dus moeten gelden dat {canbn : n > 0} regulier is. Dat laatste is echter niet waar, omdat we met het pomplemma kunnen aantonen dat {canbn : n > 0} niet regulier pompbaar is: neem w = cambm , met m de positieve integer uit het pomplemma. Dan geldtw ≥ m en w  L. We laten nu zien dat alle mogelijke verdelingen van w in drie stukken x, y en z, metxy ≤ m eny ≥ 1, door middel van pompen een string buiten L op kunnen leveren. Er zijn maar twee keuzes voor y mogelijk: y = cak met 0 ≤ k ≤ m – 1, en y = ak met 1 ≤ k ≤ m. In het eerste geval geeft pompen met iedere i ≠ 1 een string buiten L (óf te weinig óf te veel c’s), in het tweede geval geldt ook dat pompen met iedere i ≠ 1 een string buiten L geeft (maar dan vanwege óf te weinig óf te veel a’s). Kortom, de taal {canbn : n > 0} is niet pompbaar, en kan dus ook niet regulier zijn. Onze aanname dat L regulier is leidt dus tot een tegenspraak, en de conclusie is dat die aanname fout was: L is niet regulier.

Blok 3

Context-vrije talen

Context-free languages Introductie Leerkern 1 2 3

109 110

Context-free grammars 110 Parsing and ambiguity 111 Context-free grammars and programming languages

Zelftoets

115

Terugkoppeling 1 2

117

Uitwerking van de opgaven 117 Uitwerking van de zelftoets 124

113

Leereenheid 5

Context-free languages

INTRODUCTIE

In het eerste blok van de cursus zijn reguliere talen aan de orde geweest. Dat zijn talen waarvan de grammatica aan bepaalde eisen moet voldoen. Bij reguliere talen horen reguliere grammatica’s, eindige automaten en reguliere expressies. Maar er zijn talen, zoals de taal van goed geneste haakjesparen, die met geen van deze drie mechanismen beschreven kunnen worden. Voor zulke talen moeten we aan de grammatica minder strenge eisen stellen. Worden de beperkingen aan de productieregels van de grammatica iets versoepeld, dan krijgen we de familie van context-vrije talen. Hiermee kunnen we meer talen genereren. Context-vrije talen blijken heel nuttig te zijn voor het specificeren van programmeertalen. Dit blok is gewijd aan context-vrije talen. In deze leereenheid bestuderen we de structuur van zulke talen en besteden we aandacht aan een belangrijke toepassing daarvan, namelijk het genereren van een parser voor een programmeertaal. Het parsen van de invoer (een programma geschreven in de programmeertaal) is de eerste stap in de verwerking van een computerprogramma. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – een definitie kunt geven van een context-vrije grammatica en van een simpele grammatica – voor een eenvoudige context-vrije taal een bijbehorende context-vrije grammatica kunt opstellen – van een eenvoudige context-vrije grammatica de taal kunt bepalen – voor een gegeven context-vrije grammatica en een string kunt bepalen wat een linkspreferente afleiding en een rechtspreferente afleiding van de string is – kunt uitleggen wat een afleidingsboom is – de relatie tussen een partiële afleiding en een afleidingsboom kunt uitleggen – bij een gegeven afleiding een afleidingsboom kunt construeren en andersom – een definitie kunt geven van dubbelzinnigheid van strings, van dubbelzinnigheid van context-vrije grammatica’s en van inherente dubbelzinnigheid van context-vrije talen – de problemen kunt schetsen die in deze leereenheid met het parsen van strings worden uitgelegd – het lidmaatschapsalgoritme kunt omschrijven – de relatie tussen BNF en context-vrije grammatica’s kunt omschrijven – kunt uitleggen wat tokens zijn – kunt uitleggen wat lexicale analyse en parsen inhouden – de betekenis kunt geven van de volgende kernbegrippen uit deze leereenheid: afleidingsdeelboom, yield van een afleidingsboom, brute force parsing, syntaxis, semantiek, parser generator.

Studeeraanwijzingen Bij deze leereenheid hoort chapter 5 van het tekstboek van Linz. Alle sections hiervan zijn verplicht. De studielast van deze leereenheid bedraagt circa 8 uur.

LEERKERN 1 Studeeraanwijzing

Context-free grammars

Bestudeer in chapter 5 van Linz de introductie en section 5.1. Aan het begin van section 5.1 wordt vermeld dat elke reguliere grammatica ook een context-vrije grammatica is. Dit is een belangrijke opmerking. Hieruit kunnen we afleiden dat elke reguliere taal ook een context-vrije taal is en dat de familie van reguliere talen bevat is in de familie van context-vrije talen. Ook elke lineaire grammatica is een context-vrije grammatica. Let op, andersom is het niet waar. Er bestaan context-vrije grammatica’s die niet regulier of lineair zijn.

OPGAVE 5.1

a Transformeer de productieregels van de context-vrije grammatica van example 5.2 in Linz zo dat hulpsymbool B niet meer voorkomt. De hulpsymbolen zijn dan S en A. b Maak drie afleidingen waarbij hulpsymbool A meteen de eerste keer, pas na twee keer en na drie keer vervangen wordt door . Behoren de resultaten van de drie afleidingen tot de taal {ab(bbaa)nbba(ba)n : n ≥ 0} die in example 5.2 gegeven is? c Hoe kunt u bewijzen dat de grammatica G van example 5.2 de taal {ab(bbaa)nbba(ba)n : n ≥ 0} representeert? NB U hoeft het bewijs niet te geven. Het is voldoende om te schetsen hoe het mogelijk is. OPGAVE 5.2

De grammatica G = ({S}, {1, 2, +, , (, )}, S, P) is een context-vrije grammatica met de volgende productieregels: S  S + SS  S(S)12 G is een grammatica voor (heel eenvoudige) aritmetische expressies (rekenkundige uitdrukkingen). Geef vier voorbeelden van afleidingen. OPGAVE 5.3

Geef context-vrije grammatica’s voor de volgende talen: a L = { anbm : n = m – 1, m  0 } b L = { anbm : n  m – 1 } c L = { w  {a,b}* : na(w)  nb(w) } OPGAVE 5.4

a Geef een context-vrije grammatica voor de taal L = {w  {a, b, c}* : w = 3na(w)}. b Ga na (op papier of met JFLAP) of de grammatica de volgende strings accepteert: abc, bac, aabbbb, cbabac, bc, ccccccaaa, cca, abccba, cbaabc.

OPGAVE 5.5

Maak exercise 24 uit section 5.1 van Linz. OPGAVE 5.6

Gegeven is een context-vrije grammatica ({S, A, B}, {a, b}, S, P) met de volgende productieregels: S  BA A  aAa B  Bab Construeer twee verschillende afleidingsbomen voor de afleiding * S  BA  BaA  baa. OPDRACHT 5.7

Gegeven is de grammatica G = ({S, A, B}, {a, b}, S, P) met de volgende productieregels: S  AB A  aB B  Sb a Voer deze grammatica in JFLAP in. Geef als invoer (menu InputCYK Parse) de string aabbbb en bekijk de afleidingsboom. Hiervoor moet u de optie Noninverted Tree selecteren en op de knop Step klikken totdat de afleidingsboom helemaal gevormd is. b Geef een linkspreferente afleiding en een rechtspreferente afleiding van de string aabbbb. OPGAVE 5.8

Maak exercise 23 uit section 5.1 van Linz. OPGAVE 5.9

Maak exercise 25 uit section 5.1 van Linz. 2 Studeeraanwijzing

Parsing and ambiguity

Bestudeer in Linz section 5.2. Het bewijs van theorem 5.2 in Linz vertelt ons dat, op voorwaarde dat een grammatica geen producties van de vorm A   en A  B heeft, een string w in maximaal 2w afleidingsstappen gegenereerd wordt als de string tot de taal van de grammatica behoort. Als geen enkele afleiding van hoogstens 2wstappen de string genereert, dan behoort de string niet tot de taal van de grammatica. Wordt er met brute force een afleiding voor een string gezocht, dan moeten alle mogelijkheden worden uitgeprobeerd. Dit levert een orde van grootte die exponentieel groeit met de lengte van de string.

OPGAVE 5.10

Leg de volgende zinnen uit het bewijs van theorem 5.2 uit: a “Each step in the derivation increases at least one of these.” b “Since neither the length of a sentential form nor the number of terminal symbols can exceed w” c “a derivation cannot involve more than 2wrounds”.

Theorem 5.3 vertelt ons dat er een algoritme bestaat voor het zoeken van een afleiding voor een string met een orde van grootte w3. Dat is minder dan de exponentiële groei die eerder begroot is. Voor simpele grammatica’s wordt de orde van grootte zelfs gereduceerd tot w. OPGAVE 5.11

a Verander de context-vrije grammatica van example 5.6 in Linz zo dat de grammatica voldoet aan de criteria van theorem 5.2. b Is deze grammatica een simpele grammatica volgens definition 5.4 van Linz? Motiveer uw antwoord. OPGAVE 5.12

Construeer een simpele grammatica voor de taal L(aaa*b + b). OPGAVE 5.13

Construeer een simpele grammatica voor de taal L = { anbn : n  1 }. OPGAVE 5.14

De definitie van een simpele grammatica (definition 5.4 in Linz) maakt gebruik van het paar (A, a) om uit te drukken dat een productieregel de vorm A  ax heeft. Gegeven is de string s = σ1σ2... σk-1 σk...σn van lengte n. Gegeven is ook de simpele grammatica G = (V, T, S, P) met {σ1, σ2, ..., σn}  T. We willen de string s door G laten genereren met een linkspreferente afleiding. a Aan welke voorwaarde moet G voldoen voor de eerste afleidingsstap? Formuleer uw antwoord in termen van paren. b Tijdens het proces bereiken we de partiële afleiding σ1σ2...σk-1A. Aan welke voorwaarde moet G voldoen voor de volgende afleidingsstap? Formuleer uw antwoord in termen van paren. c Kan met G voor dezelfde string meer dan één linkspreferente afleiding worden gemaakt? d Geef een algemene uitspraak over dubbelzinnigheid bij simpele grammatica’s. OPGAVE 5.15

a Mag een context-vrije grammatica een productieregel hebben met de vorm A  ? b Mag een simpele grammatica een productieregel hebben met de vorm A  ? OPGAVE 5.16

Maak exercise 8 uit section 5.2 van Linz. OPGAVE 5.17

Maak exercise 9 uit section 5.2 van Linz. OPGAVE 5.18

Maak exercise 18 uit section 5.2 van Linz.

3 Studeeraanwijzing

Context-free grammars and programming languages

Bestudeer in Linz section 5.3.

OPGAVE 5.19

Leg in eigen woorden uit wat de syntaxis en de semantiek van een programmeertaal is in de context van deze leereenheid. OPGAVE 5.20

Als we een grammatica voor een programmeertaal definiëren, wat is dan een string uit de taal die door die grammatica wordt gegenereerd?

Compiler Interpreter

Lexicale analyse Token

Voorbeeld lexicale analyse

Bij het definiëren van een programmeertaal wordt voor de syntaxis van de taal een context-vrije grammatica gebruikt. Dubbelzinnigheid moet daarbij vermeden worden. De andere regels, voor de semantiek, worden op de een of andere manier ook vastgelegd. Naast de taaldefinitie moeten de ontwerpers van de taal een gereedschap verstrekken waarmee programma’s kunnen worden verwerkt. Dit gereedschap heet een compiler of een interpreter. Het compileren of interpreteren gebeurt in een aantal fasen die soms gegroepeerd worden. In deze cursus zijn we alleen geïnteresseerd in de eerste twee fasen, de lexicale analyse en het parsen (deze twee fasen worden vaak samengevoegd onder de naam parser). Bij de lexicale analyse wordt de programmacode uit een bestand gelezen en veranderd in een rij tokens. Daarbij worden spaties en tekens voor regeleinde weggelaten. Tokens zijn groepen karakters (eindsymbolen) die bij elkaar horen. Voorbeelden in Java zijn sleutelwoorden, operatoren, leestekens, getallen en namen van variabelen. De lexicale analyse is over het algemeen regulier. Bij elke soort token hoort een reguliere expressie. De lexicale analyse zoekt herhaaldelijk naar de langste prefix van de invoerstring (de programmacode) die correspondeert met één van de reguliere expressies. Om dit te doen worden de reguliere expressies voor de tokens r1, ..., rn omgezet in een nfa voor r1 + ... + rn. Deze nfa wordt dan omgezet in een dfa. Elke gevonden prefix wordt omgezet in het bijbehorende token. Als er geen prefix met een reguliere expressie correspondeert, volgt een foutmelding. Als de langste prefix correspondeert met meerdere reguliere expressies, wordt er één gekozen. Ter illustratie geven we een voorbeeld: Gehele getallen worden in FORTRAN beschreven door de BNF-grammatica: digit integer sign signed-integer

::= ::= ::= ::=

01...9 digit+ +- signinteger

(Een deel van) de bijbehorende dfa staat in figuur 5.1.

FIGUUR 5.1

Deterministische eindige automaat voor een natuurlijk getal in FORTRAN

Doordat gezocht wordt naar een langste prefix, wordt bijvoorbeeld +52 niet geïnterpreteerd als losse patronen +5 en 2, maar als één patroon. OPGAVE 5.21

Gegeven is het volgende programmafragment in Java: public static void main(String[] args) throws Exception

Wat zijn de tokens in dit fragment? Parsen

Compiler generator Compiler compiler

Bij het parsen wordt gecontroleerd of de rij tokens gegenereerd wordt door de grammatica. Goed gevormde code wordt wel door de parser geaccepteerd, andere code niet. Na het parsen volgen dan de interpretatie van de semantiek en de uitvoering van de programmacode. Hier gaan we verder niet op in. Voor het maken van een parser (inclusief lexicale analyse), een compiler of een interpreter (of iets dergelijks) bestaan tools. Deze tools zijn compiler generators of compiler compilers. Voorbeelden van zulke tools zijn YACC (Yet Another Compiler-Compiler) en ANTLR (ANother Tool for Language Recognition). De ontwerper van een nieuwe taal moet aan de tool alle gegevens verstrekken over de specificatie van de taal die hij ontworpen heeft. Dan wordt een parser, compiler of interpreter gegenereerd die de gewenste bewerking op de invoer van programmacode in de ontworpen taal kan verrichten. De parser, compiler of interpreter zal automatisch door de tool in een gekozen taal (bijvoorbeeld Java) worden gegenereerd. We beschrijven de gang van zaken met een voorbeeld: – De ontwerper ontwerpt een nieuwe programmeertaal ZZ. – Hij wil een parser (en later ook een interpreter) hebben voor programma’s geschreven in ZZ. De parser moet controleren of ZZ-programma’s syntactisch correct zijn. – Hij selecteert als tool ANTLR en Java als programmeertaal voor ANTLR. – Hij specificeert in ANTLR de grammatica van ZZ. – Hij laat ANTLR een parser voor ZZ genereren. De parser is een Javaprogramma. – Hij schrijft een testprogramma in ZZ dat geparset moet worden. – Hij verwerkt de Java-parser met als invoer het testprogramma in ZZ en controleert of het testprogramma geaccepteerd wordt door de parser. Specificaties schrijven voor een tool is een secuur werk. Het ‘laten genereren’ gaat niet zonder inspanning. In plaats van een tool te gebruiken kan een ontwerper er ook voor kiezen om de parser, compiler of interpreter helemaal zelf te schrijven (dus het werk niet door een tool laten verrichten). Dit is ook een secuur werk. Welke weg ook bewandeld wordt (met behulp van een tool of zelf schrijven), een parser, compiler of interpreter maken kost de nodige inspanning. Een tool zoals ANTLR moet voor het bouwen van een parser beschikken over de volgende informatie over de gedefinieerde taal die geparset moet worden: – Wat zijn eenvoudige tokens als ‘+’, ‘-’, ‘(’? – Wat zijn de samengestelde tokens als getallen, variabelen, sleutelwoorden en hoe worden deze samengesteld? – Welke tekens worden gebruikt voor spaties en regeleinden? – Wat zijn de grammaticaregels voor het parsen van de invoer?

ZELFTOETS

1

G = ({S, A, B}, {a, b}, S, P) is een context-vrije grammatica met de volgende productieregels: S AB A aAbaAa B aBbBbb a Geef afleidingen van de strings aabbbb en aaabb. b Welke taal wordt gegenereerd door deze grammatica? c Laat zien dat G dubbelzinnig is. d Construeer een ondubbelzinnige grammatica G', die equivalent is met de grammatica G.

2

Construeer een context-vrije grammatica voor de taal L = { anbmck : n, m, k  0 en k = n + m }.

3

Gegeven is de volgende afleidingsboom:

a Wat is de yield van de afleidingsboom? b Welke productieregels behoren tot de context-vrije grammatica die met de gegeven boom deze string afleidt? 4

De grammatica G = ({S, A, B}, {0, 1}, S, P) met de volgende productieregels genereert de taal van de reguliere expressie 0*1(0 + 1)*: S A1B A 0A B 0B1B Geef een linkspreferente en een rechtspreferente afleiding voor de string 00101.

5

Gegeven is de volgende BNF-notatie voor gehele getallen: intnumber ::= signdigitsigndigitintnumber sign ::= +- digit ::= 0123456789 Geef voor de volgende gevallen aan of de controle van de juistheid van de invoer tot de syntaxis of de semantiek behoort: a Een getal bevat geen decimale punt. b Een getal mag niet groter zijn dan 232. c Een getal mag niet met een 0 beginnen.

TERUGKOPPELING

Uitwerking van de opgaven

1

5.1

a We kunnen hulpsymbool B verwijderen door de rechterkant van de (enige) productie voor B te substitueren op alle plaatsen waar B in de rechterkant van een productie voorkomt: S abbbAa A aabbAab b De drie afleidingen zijn: S  abbbAa  abbba S  abbbAa  abbbaabbAaba  abbbaabbaba S  abbbAa  abbbaabbAaba  abbbaabbaabbAababa  abbbaabbaabbababa De drie afgeleide zinnen behoren tot de taal {ab(bbaa)nbba(ba)n : n ≥ 0}. Het zijn de strings voor n = 0, n = 1 en n = 2. c Het bewijs kan met inductie geleverd worden. Er moet bewezen worden dat als een afgeleide zin ab(bbaa)nbba(ba)n tot de taal behoort, dit dan ook geldt voor ab(bbaa)n+1bba(ba)n+1. Gebruikt wordt de partiële afleiding: S  ab(bbaa)nbbAa(ba) n *

We hebben in onderdeel b gezien dat de stelling waar is voor de bijzondere gevallen n = 0, 1 en 2. 5.2

In de vier hier gegeven voorbeelden beginnen we steeds met een andere productieregel: S  S+S  S+(S)  1+(S)  1+(2) S  SS  (S)S  (S+S)S *  (1+2)2 S  (S)  (S+S)  (S+SS) *  (1+22) S 2

5.3

a De context-vrije grammatica kan zijn G = ({S}, {a, b}, S, P) met de productieregels: S aSbb b We bekijken eerst het geval n < m – 1: er moeten dan minstens 2 extra b’s zijn. Dat kunnen we bereiken met de volgende productieregels: S  aS>bA A  aA Ten slotte combineren we beide gevallen door toevoeging van één productieregel. Het eindresultaat is de context-vrije grammatica met startsymbool S en met de volgende productieregels: S S< S> A B

 S  aSbA  aA  bBbb

De grammatica kan nog vereenvoudigd worden: S → aSb  A  B A → aA   B → bB  bb c Ook voor deze grammatica moeten we twee gevallen onderscheiden, namelijk: het aantal a’s is kleiner dan het aantal b’s en het aantal a’s is groter dan het aantal b’s. We gebruiken het idee en de grammatica van example 1.13, en voegen daaraan productieregels toe om minstens één extra a (S>  AaA en A  aA) of minstens één extra b (S<  BbB en B  bB) te genereren. Dat levert de context-vrije grammatica met startsymbool S en met de volgende productieregels: S S< S> A B

 S  BbB  AaA  aAbbAaaAAA  aBbbBabBBB

Als alternatief geven we: S  AB A  TaTATaT B  TbTBTbT T  aTbbTaTT

5.4

a We bekijken eerst een paar voorbeelden van strings die tot de taal behoren: –  behoort tot de taal (string met lengte 0) – alle strings van lengte 3 die tot de taal behoren zijn abb, bab, bba, abc, bac, bca, acb, cab, cba, acc, cac, cca – andere voorbeelden zijn: aabbbb, cbaabc, acccca, abacbc. Een string uit de taal bevat voor elke a twee andere symbolen, b’s en/of c’s. De a’s kunnen op elke willekeurige plek in de string voorkomen. Voor elke toevoeging van een a moeten ook twee andere symbolen worden toegevoegd. Uit deze constatering leiden we de grammatica af met startsymbool S en met de volgende productieregels: S SSATTTATTTA A aSSa T bc Het zal duidelijk zijn dat met deze grammatica alleen strings uit L kunnen worden gegenereerd. Het is iets minder duidelijk dat inderdaad alle strings uit L kunnen worden gegenereerd. We kunnen dat aannemelijk maken door eenzelfde argument als in example 1.13 gegeven is, alleen nemen we nu +2 voor een a en –1 voor een b of een c. Steeds als we onderweg op 0 uitkomen hebben we een substring x gelezen die ook in L zit, dus waarvoor x = 3na(x). Alle strings uit L kunnen worden opgedeeld in dit soort stukjes (desnoods in maar één stuk: de string zelf). Merk op dat dit bewijs niet gevraagd is in de opdracht. U hoeft alleen een grammatica voor de taal te geven en niet te bewijzen dat uw antwoord klopt. b De string bc behoort niet tot de taal, en dat blijkt ook als we dat testen met JFLAP. De andere strings behoren wel tot de taal, en ook dat wordt bevestigd door JFLAP.

5.5

Een haakjespaar bestaat uit een openingshaakje en een bijbehorend sluithaakje. Zo vormt ( samen met ) een haakjespaar en [ samen met ] ook. Eerst komt een openingshaakje en daarna het bijbehorende sluithaakje. Strings opgebouwd uit goed geneste haakjesparen leveren strings zoals ()([(()[])]). We kunnen een recursieve definitie geven: Zo’n string is of leeg, of bestaat uit zo’n zelfde soort string met haakjesparen eromheen gevolgd door nog zo’n soort string. Dus: – De lege string is opgebouwd uit goed geneste haakjesparen. – Als s1 en s2 strings zijn die opgebouwd zijn uit goed geneste haakjesparen, dan ook (s1)s2 en [s1]s2. Een context-vrije grammatica voor het genereren van strings, opgebouwd uit goed geneste haakjesparen (we gebruiken ronde () en rechte [] haken), is G = ({S}, {(, ), [, ]}, S, P) met als productieregels: S  (S)[S]SS

5.6

BaA kan ontstaan uit BA door de B te vervangen door Ba, of door de A te vervangen door aA. De twee bijbehorende afleidingsbomen zijn:

5.7

a

De afleidingsboom ziet er als volgt uit:

b In de afleidingsboom kunnen we de volgende linkspreferente afleiding lezen: S  AB  aBB  aSbB  aABbB  aaBBbB  aaSbBbB

 aabBbB  aabSbbB  aabbbB  aabbbSb  aabbbb  aabbbb De rechtspreferente afleiding is: S  AB  ASb  Ab  aBb  aSbb  aABbb  aASbbb  aAbbb  aaBbbb  aaSbbbb  aabbbb = aabbbb 5.8

We moeten laten zien dat de string aabbabba niet door de grammatica gegenereerd kan worden. Alle afleidingen in de gegeven grammatica starten met S  aaB  aaAa. Omdat de gegeven string langer is dan 3 symbolen moeten we nu verdergaan met A  bBb, waarna alleen maar B  Aa kan volgen. Dat levert S  aaB  aaAa  aabBba  aabAaba, en vanwege het suffix aba kan dit nooit meer aabbabba worden.

5.9

E  E + E  E  E  E*  (E)      a  b

5.10

a “Each step in the derivation increases at least one of these.” We kijken naar de lengte van mogelijke partiële afleidingen, en naar het aantal eindsymbolen in die partiële afleidingen. Als een grammatica geen producties van de vorm A   en A  B heeft, dan is de enige soort productie met een rechterkant van lengte 1 de productie A  a, met a  T. Als zo’n productie wordt toegepast op een partiële afleiding, zal de lengte van die partiële afleiding niet veranderen, maar er komt één extra eindsymbool in (in de plaats van één hulpsymbool). Alle andere producties hebben een rechterkant met lengte ten minste 2, en zullen dus als ze worden toegepast de lengte van de partiële afleiding met één laten toenemen, en het aantal eindsymbolen in die partiële afleiding met 0, 1 of meer. b “Since neither the length of a sentential form nor the number of terminal symbols can exceedw” Als we een string van lengtewproberen af te leiden met een grammatica die in iedere afleidingsstap de partiële afleiding in ieder geval niet korter kan laten worden (zie onderdeel a), kunnen we geen partiële afleidingen gebruiken die langer zijn danw, en natuurlijk ook geen partiële afleidingen met meer danw eindsymbolen. We hoeven dus alleen te kijken naar partiële afleidingen die hoogstens lengtew hebben en die hoogstensw eindsymbolen bevatten.

c “a derivation cannot involve more than 2w rounds” Zulke partiële afleidingen kunnen in hoogstens 2w stappen worden afgeleid, want in het ergste (langzaamste) geval wordt in de eerste w stappen een partiële afleiding bestaande uitw hulpsymbolen gegenereerd (één extra hulpsymbool per stap, met productieregels van de vorm A  BC), die daarna nog één voor één worden vervangen door eindsymbolen, in nog eens w stappen (met producties van de vorm A  a). Als er producties zijn met langere rechterkanten kan het natuurlijk sneller gaan. 5.11

a

Eerst verwijderen we . We krijgen de volgende productieregels:

S aABaA A bBbbb B A Nu moet de laatste productieregel worden verwijderd: S aAAaA A bAbbb b Nee, deze grammatica is geen simpele grammatica omdat de paren (S, a) en (A, b) twee keer voorkomen, en omdat de rechterkanten bAb en bb niet van de vorm cC1C2…Cn zijn, met c  T en Ci  V voor 0  i  n. 5.12

De grammatica is G = ({S, A, B}, {a, b}, S, P) met de productieregels: S aAb A aB B aBb Merk op dat de volgende productieregels niet bij een simpele grammatica horen omdat er bij een simpele grammatica precies één eindsymbool vooraan moet staan aan de rechterkant van een productieregel: S aaAb A aAb Merk ook op dat, om dezelfde reden, een simpele grammatica niet  kan genereren.

5.13

De meest evidente productieregels voor de grammatica zijn: S abaSb Deze productieregels zijn niet conform de definitie van een simpele grammatica omdat het paar (S, a) twee keer voorkomt. De volgende transformatie, waarbij we de overeenkomstige prefixen van de rechterkanten (de a) samennemen en pas daarna (met de A) onderscheid maken tussen de twee producties, voldoet evenmin omdat na een hulpsymbool geen eindsymbool meer mag voorkomen: S aA A bSb

Dit lossen we als volgt op: S aA A baAB B b Nu hebben we een simpele grammatica. 5.14

a Het paar (S, σ1) moet voorkomen in een (zelfs precies één) productie van de grammatica. b Het paar (A, σk) moet voorkomen in precies één productie van de grammatica. c Nee, er kan per string maar één linkspreferente afleiding worden gemaakt omdat elk paar (A, a) maximaal één keer mag voorkomen. d Simpele grammatica’s zijn ondubbelzinnig omdat elke gegenereerde string een unieke linkspreferente afleiding heeft.

5.15

a Ja, dat mag omdat volgens definition 5.1 in Linz de enige eis aan een context-vrije grammatica is dat alle productieregels van de vorm A  x moeten zijn met x  (V  T)*, en   (V  T)*. b Nee, dat mag niet omdat volgens definition 5.4 in Linz alle productieregels van een simpele grammatica met een eindsymbool moeten beginnen, en  doet dat niet.

5.16

Zie de terugkoppeling in Linz op pagina 417.

5.17

Als we een afleiding starten met de productieregel S  AB, kunnen we de strings ab, aab, aaab, aaaab enzovoort maken. De productieregel S  aaaB voegt dus geen nieuwe mogelijkheid toe. Deze productieregel kan geschrapt worden. We krijgen dan de ondubbelzinnige grammatica met de productieregels: S AB A aAa B b

5.18

De grammatica is ambigu omdat er twee linkspreferente afleidingen voor de string ab bestaan: S  aSb  ab S  SS  aSbS abS  ab Een taal is niet inherent dubbelzinnig als er minstens één ondubbelzinnige grammatica bestaat. De volgende grammatica is ondubbelzinnig: S  aSbS

5.19

De syntaxis van een programmeertaal is alles wat vastgelegd kan worden met behulp van een context-vrije grammatica. De verzameling van de overige regels waaraan een programma in de taal moet voldoen is de semantiek.

5.20

Ieder syntactisch correct programma in de gedefinieerde taal is dan een string uit de taal die door die grammatica wordt gegenereerd.

5.21

In dit fragment zijn 12 verschillende tokens: ( , ) , [ , ] , public, static, void, main, String, args, throws en Exception. 2

1

a

Uitwerking van de zelftoets De afleidingen zijn als volgt:

S  B  aBb  aaBbb  aaBbbb  aabbbb S  A  aAb  aaAbb  aaabb b Vanuit A groeit een rij a’s naar links en een rij b’s naar rechts. Bij iedere afleidingsstap worden of alleen een a, of een a en een b geproduceerd. De laatste stap produceert altijd alleen een a. Uit A zijn dus alle strings ambn afleidbaar waarvoor m > n. Vanuit B groeit ook een rij a’s naar links en een rij b’s naar rechts, maar nu wordt alleen een b of een a en een b geproduceerd. De laatste stap produceert altijd alleen een b. Uit B zijn dus alle strings ambn afleidbaar waarvoor m < n. Uit S zijn dus alle strings ambn afleidbaar waarvoor m < n of m > n. De gegenereerde taal is daarom {ambnm, n  , m ≠ n} c De grammatica is dubbelzinnig omdat er meerdere afleidingsbomen mogelijk zijn voor alle strings aibj met i – j  2. Voor aabbbb zijn de volgende afleidingsbomen mogelijk:

d We kunnen van G een ondubbelzinnige grammatica maken als we ervoor zorgen dat in een afleiding(sboom) steeds eerst de regels A  aAb, respectievelijk B  aBb moeten worden toegepast en dat pas daarna de extra a’s respectievelijk b’s kunnen worden gegenereerd (andersom kan eventueel ook). Dit lukt door hulpsymbolen A' en B' te gebruiken: S  AB A  aAbA' A' aA'a B  aBbB' B'  B'bb Elke string heeft in deze nieuwe grammatica maar één afleidingsboom. Ga dit na voor de string aabbbb. 2

Merk op dat we anbmck met k = m + n ook kunnen schrijven als anbmcm + n = anbmcmcn. Voor elke niet-lege string uit de taal zorgen we eerst dat voor elke a aan de linkerkant een c komt aan de rechterkant. In de volgende stap zorgen we dat daartussen nog bmcm komt. De waarden n, m en k kunnen gelijk zijn aan 0. De grammatica moet dan ook de lege string genereren. De gevraagde context-vrije grammatica is G = ({S, B}, {a, b, c}, S, P) met de productieregels: S aScB B bBc

3

a

De yield van de afleidingsboom is aabab.

b De volgende productieregels behoren tot de context-vrije grammatica die met deze boom de string aabab afleidt: S  aSbaSab 4

De linkspreferente afleiding van 00101 is: S  A1B  0A1B  00A1B  0010B  00101B  00101 De rechtspreferente afleiding is: S  A1B  A10B  A101B  A101  0A101  00A101  00101

5

a Deze controle behoort tot de syntaxis. Bij de gegeven productieregels komt geen decimale punt voor. b Deze controle behoort tot de semantiek. De productieregels geven aan hoe getallen gevormd worden, niet wat de waarde van de getallen is en ook niet wat de bovengrens is. c Gezien de specificatie behoort deze controle tot de semantiek. Een geheel getal kan volgens de productieregels wel met een 0 beginnen. De controle zou syntactisch uitgevoerd kunnen worden door een lichte aanpassing aan de specificatie. In dat geval moet onderscheid worden gemaakt tussen digit (van 0 t/m 9) en notnull (van 1 t/m 9). NB

Simplification of context-free grammars and normal forms Introductie Leerkern 1 2 3

129 130

Methods for transforming grammars 130 Two important normal forms 132 A membership algorithm for context-free grammars

Zelftoets

135

Terugkoppeling 1 2

136

Uitwerking van de opgaven 136 Uitwerking van de zelftoets 145

133

Leereenheid 6

Simplification of context-free grammars and normal forms

INTRODUCTIE

Deze leereenheid gaat over normaalvormen. Een normaalvorm zou je kunnen omschrijven als een structurele vorm – in dit geval van de productieregels van een context-vrije grammatica – waarnaar je iedere willekeurige context-vrije grammatica kunt transformeren, zonder dat je daarmee de taal verandert die door die grammatica wordt gegenereerd. Hierbij is het cruciaal dat er een algoritme bestaat dat beschrijft hoe die transformatie in het algemeen in zijn werk gaat. Bij context-vrije grammatica’s houden normaalvormen meestal in dat producties van een vorm die om de een of andere reden ongewenst is, verwijderd worden, of juist dat alle producties in een bepaalde gewenste vorm gebracht worden. En dat allemaal zonder de gegenereerde taal te veranderen! Alleen de lege string wil nog wel eens voor problemen zorgen; die halen we dan ook vaak bij voorbaat al uit de taal. Hoewel we de transformaties die we in deze leereenheid bespreken ook wel aanduiden met de Engelse term simplification, worden de grammatica’s er niet in alle opzichten simpeler op. Het doel van zo’n transformatie is namelijk vaak dat het makkelijker wordt om bepaalde bewerkingen op die grammatica uit te voeren, of dat het makkelijker wordt om bewijzen over grammatica’s op te schrijven. Helaas gaat dat vaak gepaard met meer producties, of met producties die voor een menselijke lezer minder makkelijk te begrijpen zijn. Dat geeft niet: we kiezen gewoon altijd de vorm die ons het beste uitkomt, want ze zijn toch allemaal equivalent. Tot slot van deze leereenheid bespreken we een toepassing van een van de normaalvormen die we introduceren (de Chomsky-normaalvorm): een algoritme om te bepalen of een string gegenereerd wordt door een bepaalde grammatica. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – kunt omschrijven wat het begrip normaalvorm inhoudt – de substitutieregel kunt toepassen – een gegeven grammatica -vrij kunt maken – een gegeven grammatica keten-vrij kunt maken – de nutteloze producties uit een gegeven grammatica kunt verwijderen – bovenstaande drie normaalvormen kunt combineren – een gegeven grammatica in Chomsky-normaalvorm kunt brengen – een gegeven eenvoudige grammatica in Greibach-normaalvorm kunt brengen – het CYK-algoritme kunt uitvoeren op een gegeven string en een gegeven grammatica.

Studeeraanwijzingen Bij deze leereenheid hoort chapter 6 van het tekstboek van Linz. Alle sections daarvan zijn verplicht. De studielast van deze leereenheid bedraagt circa 8 uur.

LEERKERN 1

Methods for transforming grammars

Studeeraanwijzing

Bestudeer de introductie van chapter 6 van Linz, en section 6.1.

-vrij?

In de laatste zin van de introductie van section 6.1 staat “For the rest of this chapter, unless otherwise stated, we will restrict our discussion to -free languages”. Maar eigenlijk gebeurt er precies het omgekeerde: in drie gevallen wordt expliciet gezegd dat  niet mee mag doen (theorem 6.3, 6.4 en 6.5), in de andere gevallen is  geen bezwaar. De boodschap is in ieder geval duidelijk: als het bij het bewijzen van een stelling lastig is om niet te weten of  nu wel of niet in de taal kan zitten, kunnen we gewoon aannemen dat  niet in de taal zit; dat maakt voor de bruikbaarheid van de stelling niets uit. U hebt waarschijnlijk wel gemerkt dat we theorem 6.1 allang een keer toegepast hebben: in opgave 5.1. Ook het verwijderen van nutteloze producties hebben we daar al toegepast. Dat konden we doen omdat het intuïtief duidelijk correcte transformaties zijn. Voor de volledigheid worden ze in deze leereenheid nog netjes vastgelegd en bewezen. Iets dergelijks geldt voor de transformaties in deze leereenheid die we nog niet zelf gebruikt hebben: -vrij maken en keten-vrij maken.

OPGAVE 6.1

Maak exercise 2 van section 6.1 van Linz. Theorem 6.2

Let op!

We maken twee opmerkingen bij het bewijs van theorem 6.2 in Linz: – In de derde regel van dat bewijs komt de verzameling T2 voor; daar mag gewoon T zelf staan (alle eindsymbolen ‘leiden een eindsymbool af’). – In stap 2 staat “A  x1x2…xn, met alle xi in V1  T”. Dat wil zeggen dat A ook toegevoegd moet worden aan V1 als x1x2…xn gelijk is aan de lege string, dus als n = 0. (Dat volgt uit de interpretatie van “voor alle xi geldt dat xi in V1  T” in de logica: deze hele expressie is ook waar als er geen x’en zijn.)

OPGAVE 6.2

Elimineer alle nutteloze producties uit de grammatica S  aS AB A  bA B  AA Welke taal genereert deze grammatica? OPGAVE 6.3

Maak exercise 7 van section 6.1 van Linz.

OPGAVE 6.4

Maak exercise 9 van section 6.1 van Linz. OPGAVE 6.5

Maak exercise 11 van section 6.1 van Linz. Theorem 6.5 zegt dat de drie normaalvormen te combineren zijn, en het bewijs geeft aan in welke volgorde je de afzonderlijke transformaties moet toepassen: eerst -vrij maken, dan keten-vrij en tot slot de nutteloze producties verwijderen. In de volgende drie opgaven gaan we na dat die volgorde inderdaad werkt. OPGAVE 6.6

Maak exercise 17 van section 6.1 van Linz. OPGAVE 6.7

Maak exercise 18 van section 6.1 van Linz. OPGAVE 6.8

Maak exercise 19 van section 6.1 van Linz. OPGAVE 6.9

Maak exercise 10 van section 6.1 van Linz. OPGAVE 6.10

Maak exercise 15 van section 6.1 van Linz. OPGAVE 6.11

Maak exercise 22 van section 6.1 van Linz. Besteed hier niet al te veel tijd aan; als het niet lukt kunt u de uitwerking bekijken. -vrij, keten-vrij en ‘zonder nutteloze producties’ zijn normaalvormen

Linz presenteert alle transformaties en substituties in deze section als ‘hulpmiddelen’ bij de vereenvoudiging (simplification) van grammatica’s. En inderdaad zorgen ze er allemaal voor dat een grammatica in een of ander opzicht eenvoudiger wordt. Het elimineren van - en keten-producties levert vooral voordeel op bij het parsen (ontleden) of afleiden: het wordt duidelijker hoe lang een succesvolle afleiding kan zijn, omdat er geen variabelen kunnen verdwijnen of eindeloos vervangen worden door één andere variabele. Het voordeel van het elimineren van nutteloze variabelen en producties spreekt voor zich. Al deze ‘hulpmiddelen’ voldoen echter ook aan de criteria van ‘normaalvorm’: er bestaat steeds een algemeen algoritme waarmee we van iedere context-vrije grammatica een equivalente (eventueel modulo ) context-vrije grammatica kunnen maken die -vrij is, of keten-vrij, of die geen nutteloze producties bevat. In de volgende paragraaf bespreken we nog twee andere normaalvormen, die vooral bedoeld zijn om alle producties in een bepaalde gewenste vorm te brengen, en dus niet zozeer om specifieke ongewenste structuren te elimineren: Chomsky-normaalvorm en Greibach-normaalvorm. Beide zijn handig bij het parsen.

2

Two important normal forms

Studeeraanwijzing

Bestudeer section 6.2 van Linz.

Chomskynormaalvorm (CNV)

We kunnen de constructie om een willekeurige grammatica in Chomskynormaalvorm te brengen als volgt samenvatten: – Maak de grammatica -vrij en keten-vrij. – Neem alle producties van de vorm A  a over. – Vervang in alle producties waarvan de rechterkant langer is dan 1 ieder eindsymbool a door een (nieuw!) hulpsymbool Ba, en voeg Ba  a toe voor ieder van die a’s. – Verdeel producties met te lange rechterkanten over zoveel producties (met nieuwe tussenhulpsymbolen) als nodig om ze op te breken in stukjes van 2.

OPGAVE 6.12

Breng de grammatica S  aSb  ab in Chomsky-normaalvorm. OPGAVE 6.13

Breng de grammatica S  aSaAA A  abAb in Chomsky-normaalvorm. OPGAVE 6.14

Breng de grammatica S  abAB A  bAB B  BAaA in Chomsky-normaalvorm. OPGAVE 6.15

Maak exercise 5 van section 6.2 in Linz. Een voordeel van CNV is dat afleidingsbomen van grammatica’s in CNV binaire bomen zijn, waarin iedere knoop óf twee interne knopen als kinderen óf één blad als kind heeft. Een gevolg daarvan is dat het voor grammatica’s in CNV mogelijk is een redelijk efficiënte parser te bouwen; we zullen daar in de volgende paragraaf een voorbeeld van zien. Met die parser hebben we dan meteen het lidmaatschapsprobleem voor context-vrije talen opgelost (zie ook leereenheid 8). Greibachnormaalvorm (GNV)

Een andere nuttige normaalvorm is Greibach-normaalvorm (GNV). Een voordeel van deze normaalvorm is dat je, voor een grammatica in GNV, iedere string in de taal van die grammatica letter voor letter van links naar rechts kunt parsen. Omdat de algemene constructie en het bewijs van GNV nogal lastig zijn, zullen we alleen een paar grammatica’s in GNV brengen waarvoor we ‘met het blote oog’ kunnen zien hoe dat kan.

OPGAVE 6.16

Maak exercise 10 van section 6.2 in Linz. OPGAVE 6.17

Maak exercise 11 van section 6.2 in Linz. OPGAVE 6.18

Maak exercise 13 van section 6.2 in Linz. 3 Studeeraanwijzing

A membership algorithm for context-free grammars

Bestudeer section 6.3 van Linz. Het CYK-algoritme zoals beschreven in section 6.3 is een voorbeeld van een bottom-up parser: het begint bij de afzonderlijke eindsymbolen van de te parsen string, en probeert dan van onder naar boven door mogelijke afleidingsbomen van die string te lopen, om te zien of er een tussen zit met als wortel het startsymbool van de grammatica. Eigenlijk is het algoritme nog geen volwaardige parser, maar ‘alleen’ een oplossing van het lidmaatschapsprobleem voor context-vrije talen: in deze vorm wordt het gebruikt om te bepalen of een gegeven string wordt gegenereerd door een gegeven grammatica. Zoals Linz ook al zegt, is het algoritme eenvoudig uit te breiden tot een volwaardige parser. De techniek die in het algoritme gebruikt wordt – dynamic programming – is welbekend in de informatica en de wiskunde. Ze bestaat eruit dat eerst kleine deelproblemen worden opgelost, en de oplossingen daarvan worden steeds samengevoegd tot oplossingen van iets grotere problemen, net zolang tot het gehele probleem is opgelost. In het geval van CYK is het gehele probleem het parsen van een bepaalde string, gegeven een context-vrije grammatica. Laten we aannemen dat we een context-vrije grammatica hebben, en dat we willen weten of die grammatica de string aabab genereert. De kleinst mogelijke deelproblemen zijn de afzonderlijke eindsymbolen (“hoe kun je met de gegeven grammatica het eindsymbool a maken?”), en dat is dan ook waar CYK mee begint. De string wordt verdeeld in stukjes (substrings) van lengte 1, en voor ieder van die stukjes bepalen we welke variabelen die substring kunnen genereren. Dit is stap 1 van het algoritme (zie bovenaan pagina 179 van Linz). Nu komt een moeilijke stap: we moeten bedenken hoe we de substrings moeten samenvoegen tot grotere strings, waarvan we dan ook weer moeten bepalen door welke variabelen ze gegenereerd kunnen worden. De vraag is dus: hoe komen we er nu achter of onze voorbeeldstring is opgebouwd als a(aba)b of als ((((aa)b)a)b) of nog anders? Het antwoord: dat hangt af van de producties van de grammatica in kwestie. En dat levert een probleem op: voor één specifieke combinatie (grammatica, string) kunnen we er misschien nog wel achter komen (misschien heeft de grammatica in kwestie producties C  AabaB, A  a en B  b, en komen we daarmee verder), maar het CYKalgoritme heeft een algemene beschrijving nodig, die werkt voor iedere willekeurige combinatie (grammatica, string). En hier komt de Chomskynormaalvorm goed van pas: we mogen eisen dat de grammatica in kwestie in CNV is. Dan weten we precies hoe alle producties eruit zien: iedere productie is ofwel van de vorm A  BC ofwel van de vorm A  a. Iedere string in de

taal heeft dus een afleiding die bestaat uit een aantal toepassingen van producties van de vorm A  BC en een aantal toepassingen van producties van de vorm A  a. We weten nu dus dat we steeds twee (en niet drie, of nog meer) substringetjes moeten samenvoegen tot een grotere (sub)string, net zolang tot we de gehele string hebben (of niet, dan zit de string niet in de taal). Merk op dat we nog steeds niet weten welke substrings we als eerste moeten samenvoegen: moet het ((((aa)b)a)b) of ((a(ab))(ab)) of nog iets anders zijn? Gelukkig is dat niet erg: de string die we willen parsen is per definitie eindig, en heeft dus maar eindig veel substrings. We bepalen gewoon voor iedere substring, van klein naar groot, door welke variabelen hij gegenereerd kan worden. Dit zijn de stappen 2, 3 en verder van het algoritme (zie bovenaan pagina 179 van Linz). Als we uiteindelijk bij de totale string uitkomen, kijken we of deze door (onder andere) het startsymbool gegenereerd kan worden. Zo ja, dan zit onze string in de taal, zo nee, dan niet. Let op

U moet dus in principe voor iedere string opnieuw het hele algoritme uitvoeren, ook als het steeds om dezelfde grammatica gaat! We zullen in de opgaven echter zien dat we in sommige gevallen toch resultaten kunnen hergebruiken.

OPGAVE 6.19

Het doel van deze opgave is om duidelijk te krijgen welke combinatie(s) van verzamelingen Vij we in iedere stap van het CYK-algoritme in het algemeen nodig hebben om de doelverzameling(en) Vij van die stap te kunnen bepalen. Anders gezegd: het gaat er in deze opgave om dat u in iedere stap weet welke indices u nodig hebt, en een gevoel hebt voor wat u precies aan het doen bent. We gaan uit van de string aabab. Let op: u moet de vragen beantwoorden zonder dat er een specifieke grammatica is gegeven. a Kunnen we de verzamelingen Vij ook daadwerkelijk bepalen met de gegevens die we hebben? b Het algoritme begint met de substrings van lengte 1 van aabab. Welke Vij moet deze stap opleveren, en wat is de betekenis ervan? c De volgende stap betreft de substrings van lengte 2. Welke Vij moet deze stap opleveren, welke eerder bepaalde Vij zijn daarvoor nodig, en wat is de betekenis ervan? d De derde stap betreft de substrings van lengte 3. Welke Vij moet deze stap opleveren, welke eerder bepaalde Vij zijn daarvoor nodig en in welke combinaties, en wat is de betekenis ervan? e Dezelfde vraag voor lengte 4. f Idem voor lengte 5. OPGAVE 6.20

Gebruik het CYK-algoritme om te bepalen of de grammatica uit example 6.11 de strings aabb, aabba en abbbb kan genereren. OPGAVE 6.21

Maak exercise 2 van section 6.3 van Linz.

ZELFTOETS

1

Gegeven is de volgende context-vrije grammatica G: S  TTCT T  AB A  aaA B  bB C  cc Construeer een -vrije context-vrije grammatica die equivalent is met G (modulo ).

2

Gegeven is de volgende grammatica G: S  ACDA A  aaAa B  bbbB C  cCC D  ddDF E  bcBC F  FF Construeer een context-vrije grammatica zonder nutteloze producties, die equivalent is met G.

3

Gegeven is de context-vrije grammatica G, met verzameling eindsymbolen {+, *, e, (, )}: S  TS + T T  FT *F F  e(S) Construeer een equivalente context-vrije grammatica in Chomsky-normaalvorm.

4

Gebruik het CYK-algoritme om te bepalen of de strings (e) en (e)* door de grammatica van opgave 3 worden gegenereerd.

TERUGKOPPELING 1

Uitwerking van de opgaven

6.1

Als we in de eerste grammatica de rechterkanten van de producties voor B substitueren in de eerste productie voor S, dan kunnen we de producties voor B weglaten en krijgen we de tweede grammatica.

6.2

We volgen de procedure beschreven in het bewijs van theorem 6.2. We moeten eerst de verzameling V1 van variabelen van de nieuwe grammatica op  initialiseren. Vervolgens moeten we er alle variabelen aan toevoegen die in één stap een terminale string kunnen genereren, maar die zijn er niet! Alle rechterkanten van alle producties bevatten immers minstens een variabele. Er worden dus geen variabelen toegevoegd aan V1 en de procedure stopt meteen. De resulterende grammatica heeft dus geen variabelen (dus ook geen startsymbool) en krijgt in stap 3 van de procedure ook geen producties. Deze grammatica genereert de lege verzameling (met andere woorden: niets); en dat klopt, want dat doet de oorspronkelijke grammatica ook.

6.3

We zoeken eerst uit welke variabelen een terminale string kunnen genereren. We volgen daarbij de stappen van de eerste helft van het bewijs van theorem 6.2: stap 1 stap 2a stap 2b stap 2c stap 3

V1 =  V1 = {S, A, D}, vanwege S  a, A   en D  ddd V1 = {S, A, D, B}, vanwege B  Aa V1 verandert niet, dus we zijn klaar P1 wordt S  aaAB A  aB B  Aa D  dddCd

Nu moeten we nog bepalen welke variabelen onbereikbaar zijn vanuit het startsymbool. Daarvoor tekenen we de afhankelijkheidsgraaf van P1:

Variabele D is niet bereikbaar, en variabele C (dus) ook niet. We kunnen daarom alle producties waarin D en C voorkomen weglaten. Het resultaat is: S  aaAB A  aB B  Aa 6.4

De gegeven grammatica kan  niet genereren, dus we kunnen theorem 6.3 toepassen. We volgen de procedure beschreven in het bewijs van theorem 6.3. We beginnen met het vullen van de verzameling VN van verdwijnende variabelen (nullable variables).

stap 1: VN = {A, B}, vanwege A   en B   stap 2: VN blijft gelijk aan {A, B}, want er zijn geen producties waarin in de rechterkant alleen A’s en/of B’s voorkomen Nu gaan we de nieuwe verzameling productieregels bepalen: iedere productie die niet  als rechterkant heeft nemen we mee, plus alle producties die we daaruit kunnen maken door op alle mogelijke manieren de A’s en B’s in de rechterkanten door  te vervangen: S  AaB S  aaB B  bbA

levert levert levert

S  AaBaBAaa S  aaBaa B  bbAbb

Dus de equivalente grammatica zonder -producties is S  AaBaBAaa S  aaBaa B  bbAbb Merk op dat de A’s in de rechterkanten nu nutteloos zijn geworden! Die rechterkanten kunnen we dus weghalen. Dan krijgen we de grammatica S  aBaaaBaa B  bb 6.5

We mogen theorem 6.4 alleen toepassen als we eerst de -producties verwijderen. Daar is er maar één van, en eliminatie daarvan volgens de procedure in het bewijs van theorem 6.3 geeft de grammatica G: S  aaABC A  aB B  Aaa C  cCD D  ddd Om deze grammatica keten-vrij te maken gebruiken we de strategie die beschreven is in het bewijs van theorem 6.4: we zoeken eerst het begin en eind van alle zo lang mogelijke ketens op. In dit geval zijn dat S  B en S  C. We nemen alle niet-keten-producties van G over: S  aaA A  aB B  Aaa C  cCD D  ddd We voegen daar de ‘kortsluiting’ van de keten S  B aan toe: S  Aa

(en S  a, maar die was er al)

en de kortsluiting van de keten S  C: S  cCD

6.6

Zie de uitwerking op pagina 419 in Linz. Merk op dat er vier manieren zijn om in BB de B wel of niet door  te vervangen: BB, B, B en . De laatste manier levert een nieuwe -productie, en die willen we dus niet (dit is de exception die aan het eind van het bewijs van theorem 6.3 genoemd wordt). De tweede en de derde manier leveren beide B (en dus de bedoelde ‘previously nonexistent unit-production’).

6.7

De constructie van theorem 6.4 bestaat uit twee delen: – het overnemen van alle niet-keten-producties van de oorspronkelijke grammatica; omdat de oorspronkelijke grammatica geen -producties bevat, worden die hiermee ook niet geïntroduceerd – het toevoegen van een aantal producties om de ketens kort te sluiten; omdat de rechterkanten van die nieuwe producties al voorkwamen in de oorspronkelijke grammatica worden hiermee ook geen -producties geïntroduceerd.

6.8

Met de constructie van theorem 6.2 worden alleen volledige producties verwijderd (er worden dus geen hulpsymbolen uit rechterkanten geschrapt). Er worden dus geen producties geïntroduceerd.

6.9

Zie de uitwerking op pagina 419 in Linz. Let op de volgorde: volgens theorem 6.5 moeten we eerst de -producties elimineren, dan de keten-producties en tot slot de nutteloze producties.

6.10

De vraag is dus om een grammatica die wèl  genereert, -vrij te maken met de constructie uit het bewijs van theorem 6.3, en te kijken wat er dan gebeurt. De enige verdwijnende variabele is A, en de enige productie waarin A voorkomt in de rechterkant is S  A. Als we daarin op alle mogelijke manieren A wel en niet vervangen door , krijgen we alleen S  A zelf weer, want S   willen we natuurlijk niet toevoegen. Het resultaat is dus S  AB B  aBb Bb en de taal die deze grammatica genereert is gelijk aan L(G) – {}: alles wat de oorspronkelijke grammatica via B kon genereren kan nog steeds, en het enige dat met A gegenereerd werd was .

6.11

Nee, de twee delen van de constructie mogen niet omgedraaid worden. Neem bijvoorbeeld de grammatica S  abcAB A  ac Als we daarin, zoals het hoort, eerst de variabelen opzoeken die een string die volledig uit eindsymbolen bestaat kunnen genereren, dan vinden we S en A. De tussengrammatica wordt dus S  abc A  ac Als we nu de hulpsymbolen die onbereikbaar zijn vanuit S opzoeken, dan vinden we A, en het resultaat wordt S  abc

Nu gaan we, uitgaande van de oorspronkelijke grammatica, de constructie in de verkeerde volgorde uitvoeren. We zoeken dus eerst de variabelen die niet bereikbaar zijn vanuit S: die zijn er niet, en de grammatica blijft S  abcAB A  ac Nu zoeken we de variabelen die geen terminale string af kunnen leiden. Dat is alleen B, en we moeten de producties waarin B voorkomt dus verwijderen: S  abc A  ac Hiermee is A onbereikbaar geworden, en het resultaat bevat dus nog steeds een nutteloze productie. 6.12

De grammatica S  aSbab is -vrij en keten-vrij, zoals vereist. Om vervelende subscripts te vermijden gebruiken we een nieuwe variabele A in plaats van Ba, en B in plaats van Bb. We vervangen dus de eindsymbolen a en b door hun ‘synoniemen’ A en B en voegen daarvoor twee producties toe: S  ASBAB A a Bb Er is nu één productie met een te lange rechterkant, dus die breken we op in twee producties: S  ATAB T  SB A a Bb Het resultaat is een grammatica in CNV die equivalent is met de gegeven grammatica.

6.13

Gegeven is de grammatica S  aSaAA A  abAb Deze is -vrij, maar nog niet keten-vrij. De enige keten is S  A, en die sluiten we kort als volgt: S  aSaAabAb A  abAb Nu kunnen we dit tussenresultaat op de gebruikelijke manier in CNV brengen. Eerst krijgen we, met T als synoniem voor a en B als synoniem voor b: S  TSTATBAb A  TBAb Ta Bb

Vervolgens breken we TSTA in drieën, en TBA en ABA in tweeën: S  TXTZb X  SY Y  TA Z  BA A  TUb in plaats van U kunnen we Z hergebruiken… U  BA …en dan kan deze productie weg Ta Bb 6.14

Gegeven is de grammatica S  abAB A  bAB B  BAaA Het elimineren van de -producties leidt tot S  abABabBabAab A  bABbBbAb B  BAaAaBaaA Nu de keten-producties elimineren: S  abABabBabAab A  bABbBbAb B  BAaAaBaabABbBbAb Tot slot brengen we de grammatica in CNV. We maken eerst synoniemen aan: S  EFABEFBEFAEF A  FABFBFAb B  BAEAEBEaFABFBFAb Ea F b Daarna breken we te lange rechterkanten in stukjes: S  ETEVEWEF T  FU U  AB V  FB W FA A  FUFBFAb B  BXAEBEaFUFBFAb X  AE Ea F b Deze grammatica is in Chomsky-normaalvorm en equivalent (modulo ) met de oorspronkelijke grammatica.

6.15

We moeten eerst de -producties verwijderen: S  AB BaB A  abb B bbAbb Nu hebben we een ketenproductie geïntroduceerd, die we weer moeten elimineren: S  AB aBbbAbb A  abb B bbAbb Tot slot moeten we zorgen dat iedere rechterkant precies 2 variabelen of 1 eindsymbool bevat. Omdat de A en de B al gebruikt zijn, kiezen we voor C  a en D  b. De eerste stap geeft dan: S A B C D

 AB CBDDADD  CDD DDADD a b

We zien dat de drie producties met een te lange rechterkant alle drie DD bevatten, dus daarvan kunnen we mooi gebruikmaken: S A B C D T 6.16

 AB CBTADD  CT TADD a b  DD

Gegeven is de grammatica S  aSbbSaab ab. Om deze in Greibachnormaalvorm te brengen, vervangen we de eindsymbolen die niet vooraan een rechterkant staan door een synoniem, en voegen producties toe voor die synoniemen. S  aSBbSAab aB A a Bb Deze grammatica is in GNV en equivalent met de gegeven grammatica.

6.17

Gegeven is de grammatica S  aSbabbb. De tactiek om deze in GNV te brengen, is dezelfde als in de vorige opgave: S  aSBaBbB Bb

6.18

Gegeven is de grammatica S  ABbab A  aaAB B  bAb Om deze in GNV te brengen, vervangen we eerst de B in de keten-productie door de rechterkant van de productie voor B: S  ABbab A  aaAbAb B  bAb Nu is de productie S  ABb nog een probleem. Gelukkig beginnen beide rechterkanten van de producties voor A wel met een eindsymbool, dus dat probleem is met een eenvoudige substitutie opgelost: S  aaABbbAbBbab A  aaAbAb B  bAb Nu nog synoniemen toevoegen, en we zijn klaar: S  aEABFbAFBFab A  aEAbAF B  bAF Ea F b

6.19

a Nee, want we hebben de grammatica niet. We kunnen dus alleen aangeven welke Vij we in iedere stap moeten bepalen, welke eerder bepaalde Vij we daarvoor nodig hebben, en hoe we die dan moeten combineren. En we kunnen een algemene omschrijving geven van de betekenis van de verzameling Vij: die bevat alle variabelen van de grammatica die op een of andere manier een string af kunnen leiden die gelijk is aan de substring van aabab die begint op positie i en eindigt op positie j (posities i en j doen beide mee). b Er is een substring van lengte 1 die begint en eindigt op positie 1 (namelijk a), er is er een die begint en eindigt op positie 2 (namelijk de tweede a), …, en ten slotte is er een die begint en eindigt op positie 5 (de laatste b). We zoeken dus de verzamelingen V11 tot en met V55. De elementen van bijvoorbeeld V22 zijn alle variabelen X waarvoor geldt dat er een productie X  a is, en de elementen van V33 zijn alle X waarvoor geldt dat er een productie X  b is. Maar omdat we de grammatica niet kennen, kunnen we de inhoud van de Vij niet nader bepalen. Let op: u hoeft er geen rekening mee te houden dat de ene a op positie 1 van aabab staat en de andere op positie 2. Met andere woorden, het zou kunnen dat uiteindelijk blijkt dat X wel gebruikt kan worden om de eerste a te genereren, maar niet voor de tweede. Dat geeft niet, dat wordt er in een latere stap wel uitgefilterd. Voor aabab geldt dus V11 = V22 = V44 en V33 = V55. c De string aabab heeft vier substrings van lengte 2: aa van positie 1 tot en met 2, ab van positie 2 tot en met 3, ba van positie 3 tot en met 4, en ab van positie 4 tot en met 5. We moeten dus V12, V23, V34 en V45 bepalen.

Omdat de grammatica in Chomsky-normaalvorm is, zijn er alleen producties van de vorm A  BC en A  a. Strings van lengte 2 kunnen alleen door de eerste soort producties gegenereerd worden, en het moet dan zo zijn dat B de eerste letter van de substring maakt, en C de tweede. V12 moet dus alle A bevatten waarvoor er een productie A  BC is met B  V11 en C  V22. Evenzo moet V23 alle A bevatten waarvoor er een productie A  BC bestaat met BC  V22V33 enzovoort. d Het doel is V13, V24 en V35 te bepalen, want aabab heeft drie substrings van lengte 3: de eerste (aab) begint op positie 1 en eindigt op positie 3, de tweede (aba) begint op positie 2 en eindigt op positie 4, en de derde (bab) begint op positie 3 en eindigt op positie 5. V13 hoort bij het prefix aab, en stelt de verzameling variabelen voor die aab kunnen genereren. Dat moet gebeurd zijn met een productie van de vorm A  BC, waarbij B en C ieder een stukje van aab hebben gegenereerd. Dat * * laatste kan op twee manieren: B  a en C  ab, of B  aa en C  b. Om erachter te komen welke variabelen de taak van B en C bij de eerste manier kunnen vervullen hebben we de eerder geconstrueerde verzamelingen V11 en V23 nodig. Voor de tweede manier hebben we dan V12 en V33 nodig. V13 moet dus alle variabelen X bevatten waarvoor geldt dat er een productie X  YZ is met YZ  V11V23  V12V33. De substring aba, die begint op positie 2 en eindigt op positie 4, is ook op twee manieren in twee stukjes op te delen: aba en aba. V24 moet dus alle variabelen X bevatten waarvoor geldt dat er een productie X  YZ is met YZ  V22V34  V23V44. Tot slot geldt dat V35 alle variabelen X moet bevatten waarvoor geldt dat er een productie X  YZ is met YZ  V33V45  V34V55. e Er zijn twee substrings van lengte 4. Voor aaba moeten we V14 bepalen, en die moet alle variabelen X bevatten waarvoor geldt dat er een productie X  YZ is met YZ  V11V24  V12V34  V13V44. Voor abab moeten we V25 bepalen, en die moet alle variabelen X bevatten waarvoor geldt dat er een productie X  YZ is met YZ  V22V35  V23V45  V24V55. f Tot slot moeten we V15 bepalen, en die moet alle variabelen X bevatten waarvoor geldt dat er een productie X  YZ is met YZ  V11V25  V12V35  V13V45  V14V55. 6.20

De gegeven grammatica G is al in Chomsky-normaalvorm: S  AB A  BBa B  ABb Om te bepalen of aabb in L(G) zit gaan we de verzamelingen Vij bepalen voor 1  i  j  4. We beginnen met de substrings van lengte 1. Voor de Vii moeten we opzoeken welke variabelen de substrings a op de 1e en 2e positie kunnen genereren, en welke variabelen de substrings b op de 3e en 4e positie. Dat levert V11 = V22 = {A} en V33 = V44 = {B}. We gaan verder met de substrings van lengte 2. We moeten dan V12, V23 en V34 bepalen. We vinden: V12 = {X : X  YZ met Y  V11 en Z V22} = {X : X  AA} = , V23 = {X : X  YZ met Y  V22 en Z V33} = {X : X  AB} = {S, B}, en V34 = {X : X  YZ met Y  V33 en Z V44} = {X : X  BB} = {A}.

Nu zijn de substrings van lengte 3 aan de beurt: V13 = {X : X  YZ met (Y  V11 en Z V23) of met (Y  V12 en Z V33)} = {X : X  AS, X  AB} = {S, B}, en V24 = {X : X  YZ met (Y  V22 en Z V34) of met (Y  V23 en Z V44)} = {X : X  AA, X  SB, X  BB} = {A}, Tot slot de volledige string: V14 = {X : X  YZ met (Y  V11 en Z V24) of met (Y  V12 en Z V34) of met (Y  V13 en Z V44)} = {X : X  AA, X  SB, X  BB} = {A}. Deze laatste verzameling bevat niet het startsymbool S, dus aabb  L(G). De string aabba heeft de vorige string, aabb, als prefix. We kunnen dus alle Vij met i  5 en j  5 overnemen van de berekening voor aabb. We missen nog: V55 = {A}, V45 = {X : X  YZ met Y  V44 en Z V55} = {X : X  BA} = , V35 = {X : X  YZ met (Y  V33 en Z V45) of met (Y  V34 en Z V55)} = {X : X  AA} = , V25 = {X : X  YZ met (Y  V22 en Z V35) of met (Y  V23 en Z V45) of met (Y  V24 en Z V55)} = {X : X  AA} = , en V15 = {X : X  YZ met (Y  V11 en Z V25) of met (Y  V12 en Z V35) of met (Y  V13 en Z V45) of met (Y  V14 en Z V55)} = {X : X  AA} = . De string aabba kan dus door geen enkele variabele van G afgeleid worden: aabba  L(G). Voor de laatste string, abbbb, moeten we helaas weer wel alle verzamelingen opnieuw bepalen. We geven meteen de eindresultaten (in de linkerkolom), en ter controle de gezochte rechterkanten (in de rechterkolom): V11 = {A} V22 =V33 = V44 = V55 = {B}

a b

V12 = {S, B} V23 = V34 = V45 = {A}

AB BB

V13 = {A} V24 = V35 = {S, B}

AA, SB, BB BA, AB

V14 = {S, B} V25 = {A}

AS, AB, SA, BA BS, BB, AA, SB

V15 = {A}

AA, SS, SB, BS, BB

We zien dat S  V15, dus abbbb  L(G).

6.21

Omdat aab een prefix is van de string uit example 6.11, kunnen we de Vij gebruiken die daar berekend zijn. Omdat S  V13 zit aab in de taal die door de grammatica wordt gegenereerd, en kan dus geparset worden. Voor het parsen bepalen we welke producties gebruikt zijn om te laten zien dat S  V13: S  V13 omdat S  AB, met A  V11 en B  V23 A  V11 omdat A  a B  V23 omdat B  AB, met A  V22 en B  V33 A  V22 omdat A  a B  V33 omdat B  b We weten nu alle producties die nodig zijn, en kunnen de gevraagde afleiding opstellen: S  AB  aB  aAB  aaB  aab 2

1

Uitwerking van de zelftoets

We bepalen eerst de verdwijnende variabelen, in stappen zoals aangegeven in het bewijs van theorem 6.3: stap 1 stap 2a stap 2b stap 2c

VN = {A, B}, vanwege A   en B   VN = {A, B, T}, vanwege T  AB VN = {A, B, T, S}, vanwege S  T VN verandert niet meer, dus we zijn klaar.

We laten nu de -producties weg, en vervangen in de overblijvende producties alle verdwijnende variabelen in rechterkanten op alle mogelijke manieren door . Dat levert de volgende grammatica, die equivalent is met G (modulo ): S  TTCTCTTCC T  ABBA A  aaA B  bBb C  cc 2

Volgens theorem 6.2 (en opgave 6.11) moeten we eerst de verzameling V1 bepalen, met daarin alle variabelen die een terminale string kunnen genereren. stap 1 stap 2a stap 2b stap 2c stap 3

V1 =  V1 = {A, B, C, E} V1 = {A, B, C, E, S} V1 verandert niet meer We laten alle producties weg die variabelen bevatten die niet in V1 voorkomen. Wat overblijft, is dan de grammatica G’: S  AC A  aaAa B  bbbB C  cCC E  bcBC

Vervolgens tekenen we de afhankelijkheidsgraaf van G’:

Hierin zien we dat alleen A en C bereikbaar zijn vanuit S. We mogen dus de producties die B en/of E bevatten weglaten, en krijgen zo een grammatica G’’ die equivalent is met G, maar die geen nutteloze producties meer bevat: S  AC A  aaAa C  cCC 3

We moeten eerst zorgen dat de grammatica -vrij en keten-vrij wordt. G heeft geen -producties, maar wel twee keten-producties. Om die te verwijderen zoeken we eerst de ketens op met behulp van de afhankelijkheidsgraaf:

We nemen nu alle niet-keten-producties van G over: S S+T T  T *F F  e | (S) We voegen daaraan toe: * – producties om de keten S  T kort te sluiten: S  T *F * – producties om de keten S  F kort te sluiten: S  e(S) * – en tot slot producties om de keten T  F kort te sluiten: T  e(S) Alles bij elkaar hebben we nu de grammatica G’, die equivalent is met G, maar -vrij en keten-vrij: S  S + TT *Fe(S) T  T *Fe(S) F  e(S)

Nu kunnen we G’ in Chomsky-normaalvorm gaan brengen. We introduceren eerst nieuwe variabelen P, M, O en D voor de eindsymbolen ‘Plus’, ‘Maal’, ‘haakje Open’ en ‘haakje Dicht’. Dan vervangen we alle eindsymbolen in rechterkanten die langer zijn dan 1 symbool door de bijbehorende nieuwe variabele: S  SPTTMFeOSD T  TMFeOSD F  e |OSD P+ M * O ( D ) Tot slot breken we te lange rechterkanten in stukjes, met behulp van nog drie nieuwe variabelen X, Y en Z: S  SXTYeOZ X  PT Y  MF Z  SD T  TYeOZ F  e |OZ P+ M * O ( D ) Deze grammatica is in Chomsky-normaalvorm en equivalent met G. 4

Om het CYK-algoritme te kunnen gebruiken, moet de grammatica eerst in Chomsky-normaalvorm gebracht worden; gelukkig hebben we dat in opgave 3 al gedaan. We bepalen eerst de Vij voor de string (e). V11 V22 V33 V12

V23

V13

= {O} = {S, T, F} = {D} = {X : X  YZ met YZ  V11V22} = {X : X  OS of X  OT of X  OF} = = {X : X  YZ met YZ  V22V33} = {X : X  SD of X  TD of X  FD} = {Z} = {X : X  YZ met YZ  V11V23  V12V33} = {X : X  OZ} = {S, T, F}

Omdat S  V13 geldt dat de string (e) wordt gegenereerd door G.

Omdat de string (e) een prefix is van de string (e)*, kunnen we bij het toepassen van het CYK-algoritme op (e)* gebruik maken van de Vij die we net berekend hebben. We moeten er alleen nog een paar extra berekenen, voor j = 4. V44 = {M} V34 = {X : X  YZ met YZ  V33V44} = {X : X  DM} = V24 = {X : X  YZ met YZ  V22V34  V23V44} = {X : X  ZM} = V14 = {X : X  YZ met YZ  V11V24  V12V34  V13V44} = {X : X  SM of X  TM of X  FM} = Omdat S  V14 geldt dat (e)* niet wordt gegenereerd door G.

Pushdown automata Introductie Leerkern 1 2 3

151 152

Nondeterministic pushdown automata 152 Pushdown automata and context-free languages 156 Deterministic pushdown automata and deterministic context-free languages 158

Zelftoets

159

Terugkoppeling 1 2

161

Uitwerking van de opgaven 161 Uitwerking van de zelftoets 171

Leereenheid 7

Pushdown automata

INTRODUCTIE

Tot nu toe hebben we twee families van talen bestudeerd, namelijk de familie van reguliere talen in blok 2 en de familie van context-vrije talen aan het begin van blok 3. Reguliere talen worden vaak gerepresenteerd door eindige automaten. Hoe zit dat nu met context-vrije talen? Een eindige automaat kan een taal als {anbn : n  0} niet herkennen omdat de automaat daarvoor altijd te weinig toestanden heeft. Om te controleren of het tweede deel van de string net zoveel b’s bevat als a’s, zou de automaat in staat moeten zijn de eerste helft op de een of andere manier te kunnen onthouden. Omdat die eerste helft willekeurig lang kan zijn, lukt dat niet. Om een automaat te krijgen die wel in staat is om willekeurige hoeveelheden informatie op te slaan, voegen we aan de eindige automaat een geheugen toe. Dan krijgen we de stapelautomaat met een stapel (stack) die dienst doet als geheugen. In deze leereenheid bestuderen we stapelautomaten. We zien dat determinisme een belangrijke rol speelt bij stapelautomaten en we onderzoeken wat de relatie is tussen stapelautomaten en context-vrije talen. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – een definitie kunt geven van een stapelautomaat – de werking van een stapelautomaat kunt beschrijven – voor een gegeven stapelautomaat de bijbehorende overgangsgraaf kunt geven – met behulp van momentane beschrijvingen de werking van een gegeven stapelautomaat kunt demonstreren – de relatie tussen stapelautomaten en context-vrije talen kunt aangeven – het verschil tussen wel en niet deterministische stapelautomaten kunt uitleggen – kunt aangeven wanneer een context-vrije taal deterministisch is – de taal kunt bepalen die door een gegeven stapelautomaat wordt gerepresenteerd – bij een gegeven context-vrije taal een stapelautomaat kunt construeren voor deze taal – de betekenis kunt geven van de volgende kernbegrippen uit deze leereenheid: momentane beschrijving, beweging, berekening. Studeeraanwijzingen Bij deze leereenheid hoort chapter 7 van het tekstboek van Linz. Section 7.4 daaruit behoort niet tot de leerstof. De studielast van deze leereenheid bedraagt circa 9 uur.

LEERKERN 1

Nondeterministic pushdown automata

Terminologie

Zoals u in deze leereenheid gaat leren, bestaan er niet-deterministische en deterministische stapelautomaten (afgekort npda en dpda). We zullen de algemene term stapelautomaat gebruiken als de precieze aanduiding nietdeterministisch of deterministisch niet relevant is (de opmerking geldt dan voor beide soorten) of uit de context blijkt.

Studeeraanwijzing

Lees in chapter 7 van Linz de introductie op de chapter en bestudeer van section 7.1 de introductie en de subsection Definition of a pushdown automaton. U krijgt hier extra uitleg bij figure 7.1 en bij definition 7.1 van Linz. Raadpleeg deze extra uitleg tijdens het lezen van het tekstboek.

Uitleg bij figure 7.1

Figure 7.1 in Linz laat een invoerbestand, een controle-eenheid en een stapel zien. Het invoerbestand bevat de symbolen van de string die aangeboden wordt aan de stapelautomaat. Tussen het invoerbestand en de controle-eenheid staat een leeskop. De controle-eenheid leest één voor één de symbolen van het invoerbestand. Na het lezen van één symbool staat de leeskop op het volgende symbool. De leeskop verplaatst zich uitsluitend in één richting, zeg maar van links naar rechts. De stapel bevat informatie in de vorm van symbolen (stapelsymbolen). De oudste informatie staat aan de onderkant. Nieuwe informatie wordt boven op de stapel geplaatst. Tussen de controle-eenheid en de stapel staat een lees-schrijfkop. De controleeenheid kan lezen van de stapel (een pop-operatie waarbij het bovenste stapelsymbool van de stapel wordt gehaald) en schrijven naar de stapel (een push-operatie waarbij stapelsymbolen boven op de stapel worden geplaatst). De lees-schrijfkop kan zich in twee richtingen verplaatsen, naar boven (als er stapelsymbolen op de stapel worden geplaatst) of naar beneden (als er stapelsymbolen van de stapel worden gelezen). De lees-schrijfkop staat altijd aan de top van de stapel. De controle-eenheid bevindt zich in een bepaalde toestand (we zullen vanaf nu zeggen dat de stapelautomaat zich in een bepaalde toestand bevindt). Het lezen van een symbool van het invoerbestand veroorzaakt, in combinatie met het lezen van de inhoud van de top van de stapel en de huidige toestand van de stapelautomaat, een mogelijke verandering van toestand van de stapelautomaat en een mogelijke schrijfactie op de stapel. Eén stap van de stapelautomaat veroorzaakt het verplaatsen van de leeskop één plaats naar rechts in het invoerbestand. Tijdens de stap wordt de lees-schrijfkop één plaats naar beneden verplaatst (tijdens de pop-actie) en daarna, afhankelijk van de bewerking, 0, 1 of meer plaatsen naar boven verplaatst (tijdens de push-operatie). Een stapelautomaat mag ook een stap doen zonder een invoersymbool te lezen; -overgangen zijn dus toegestaan. We geven een paar voorbeelden, waarbij ↦ een stap van de stapelautomaat weergeeft: – toestand q0, invoersymbool a gelezen, stapelsymbool 0 bovenaan ↦ toestand q1, stapelsymbolen 10 op de stapel geplaatst (stapelsymbool 0 is vervangen door de twee stapelsymbolen 1 en 0 ofwel: er wordt een 1 bovenop de 0 die er al stond gezet) – toestand q1, invoersymbool a gelezen, stapelsymbool 1 bovenaan ↦ toestand q1, stapelsymbool 1 op de stapel geplaatst (stapelsymbool 1 is ongewijzigd)

– toestand q1, invoersymbool b gelezen, stapelsymbool 1 bovenaan ↦ toestand q1, geen stapelsymbolen geplaatst (stapelsymbool 1 van de stapel gehaald) – toestand q0, geen symbool gelezen (wordt aangeduid met  gelezen), stapelsymbool 1 bovenaan ↦ toestand q1, stapelsymbool 0 op de stapel geplaatst (stapelsymbool 1 is vervangen door stapelsymbool 0). We kunnen de mogelijke acties op de stapel als volgt onderverdelen: – De stapel wordt één symbool kleiner: het bovenste stapelsymbool wordt weggehaald, ofwel vervangen door . – De stapel blijft even groot. Het bovenste stapelsymbool kan hierbij gewijzigd worden of gelijk blijven. – De stapel wordt groter: het bovenste stapelsymbool wordt vervangen door twee of meer stapelsymbolen. Hierbij kan het oorspronkelijk bovenste stapelsymbool veranderen, maar dat hoeft niet. Voorbeeld

We illustreren de werking van stapelautomaten met een klein voorbeeld. Bekijk de stapelautomaat van figuur 7.1. We gaan onderzoeken welke strings de stapelautomaat naar de eindtoestand q2 kunnen brengen. Het resultaat wordt getoond in tabel 7.1.

Stapelautomaat met drie toestanden

FIGUUR 7.1

Bij de start (stap 0 in tabel 7.1) staat het stapelstartsymbool Z op de stapel. In de begintoestand q0 kunnen we alleen het symbool a lezen. In stap 1 hebben we die a gelezen en op de stapel is het stapelsymbool Z vervangen door de twee stapelsymbolen 1 en Z, met 1 bovenop. Hiermee bereiken we dat het stapelstartsymbool Z onderop de stapel blijft. Dit wordt in figuur 7.1 weergegeven door de overgang van q0 naar q0 met label a, Z; 1Z. In stap 2 is weer een a gelezen en de top van de stapel (een 1) is vervangen door tweemaal het stapelsymbool 1 (ofwel: er is een 1 bovenop gezet). De stapelautomaat is nu in toestand q1. Vanaf nu zijn er twee mogelijkheden voor het lezen van invoer: we kunnen een b lezen of we kunnen niets () lezen. Om een b te kunnen lezen moet op de stapel het stapelsymbool 1 bovenop staan en om  te lezen moet het stapelsymbool Z bovenop staan. In stap 3 en 4 lezen we daarom een b en halen telkens het bovenste stapelsymbool 1 van de stapel. Na uitvoering van stap 4 is de stapelautomaat in toestand q1 met Z op de stapel. Zonder iets te lezen (we lezen ) en zonder de stapel te wijzigen (Z wordt vervangen door Z) komt de stapelautomaat in eindtoestand q2 (stap 5). De werking van de stapelautomaat van figuur 7.1

TABEL 7.1 stap

0

1

2

3

4

5

toestand

q0

q0

q1

q1

q1

q2

a

a

b

b

λ

1 Z

1 1 Z

1 Z

Z

Z

gelezen symbool stapel Z

In dit eenvoudig voorbeeld is altijd duidelijk welk symbool moet worden gelezen om verder te gaan richting eindtoestand. Er is telkens één mogelijkheid. De string aabb is de enige string waarmee de stapelautomaat na het lezen van alle symbolen van de string in de eindtoestand komt. De stapelautomaat accepteert dus de taal {aabb} (dit komt later nog aan de orde in definition 7.2 in Linz). NB We hebben voor deze taal natuurlijk helemaal geen stapelautomaat nodig: de taal is eindig en kan dus ook ‘gewoon’ met een eindige automaat herkend worden. Het enige doel van dit voorbeeld is dan ook de werking van de stapel te demonstreren. Uitleg bij definition 7.1

Bij definition 7.1 in Linz worden alle gegevens opgesomd die nodig zijn voor het definiëren van een niet-deterministische stapelautomaat. Er kunnen overgangen gespecificeerd worden voor symbolen uit het alfabet  en ook voor . In het laatste geval wordt niets van de invoer gelezen. Voor configuraties waarin de stapel leeg is (geen stapelsymbolen bevat) kunnen geen overgangen gedefinieerd worden. In die gevallen blokkeert de stapelautomaat. Het stapelstartsymbool (het symbool onderaan de stapel) wordt bij de definitie van een stapelautomaat gespecificeerd en kan gebruikt worden om te testen of de stapel leeg is op het stapelstartsymbool na. De lees-schrijfkop staat dan onderaan. Dit is uiteraard alleen mogelijk als het stapelstartsymbool niet elders in de stapel voorkomt.

OPGAVE 7.1

Bekijk de overgangsfunctie in example 7.2 in Linz. Interpreteer in eigen woorden de volgende overgangen: a (q0, , 0) = {(q3, )} b (q0, a, 0) = {(q1, 10), (q3, )} c (q2, , 0) = {(q3, )} OPGAVE 7.2

a Wat gebeurt er als aan de stapelautomaat van example 7.2 de invoerstring ba wordt aangeboden? b In opgave 7.1.b zagen we dat de overgangsfunctie twee alternatieven kent voor toestand q0, invoer a en stapelsymbool 0. Hoe wordt bepaald welk alternatief gebruikt moet worden? Momentane beschrijving

Beweging Berekening

Een momentane beschrijving van een stapelautomaat is een drietal (q, w, u) waarmee de staat van de stapelautomaat wordt beschreven. Toestand q is de huidige toestand, w is het nog niet gelezen deel van de invoerstring en u is een string met de actuele inhoud van de stapel, waarbij de top van de stapel aan de linkerkant van de string staat. De overgang van een momentane beschrijving naar de volgende is een beweging (ofwel een stap van de automaat). Het stapteken is ⊢. Een rij van nul, een of meer opeenvolgende stappen van een stapelautomaat vormt een berekening. Voor een berekening kunnen we alle bewegingen apart noteren of de eerste en de laatste momentane beschrijving noteren, gescheiden door het stappenteken * .

OPGAVE 7.3

Deze opgave gaat over de stapelautomaat van example 7.2. Geef voor elk van de volgende invoerstrings aan of er een berekening bestaat die de stapelautomaat van begin- naar eindtoestand brengt en waarbij de gehele invoerstring gelezen wordt. Als een berekening bestaat, geef die dan. a De invoerstring a. b De invoerstring aabb. c De invoerstring aaba. Studeeraanwijzing

Let op!

Bestudeer in section 7.1 subsection The language accepted by a pushdown automaton. Bij de stapelautomaat van figure 7.3 in Linz is niet vermeld dat het stapelstartsymbool z (met een kleine letter) is. Het tekstboek hanteert als conventie dat het stapelstartsymbool altijd z is, tenzij anders vermeld. We zullen in opdracht 7.5 zien dat JFLAP altijd Z (met een hoofdletter) gebruikt. Wees altijd attent op dit verschil.

OPGAVE 7.4

Wanneer accepteert een stapelautomaat een string? OPDRACHT 7.5 a Teken in JFLAP de stapelautomaat (overgangsgraaf) van figure 7.2 in Linz. Let op, JFLAP gebruikt altijd het stapelsymbool Z (met een hoofdletter) als

stapelstartsymbool. Het symbool 0 in de figure moet dus vervangen worden door Z. b Test de invoer a, aabb en aaba eerst met Multiple Run en daarna met Fast Run. Bevestig telkens de optie Accept by Final State. OPGAVE 7.6

Geef een definitie van de stapelautomaat uit figure 7.3 in Linz met behulp van het zevental zoals in example 7.2 in Linz. OPDRACHT 7.7

a Beschrijf de functie van de drie toestanden van de stapelautomaat in example 7.5 in Linz. b Leg uit wat de betekenis is van de overgangen (q0, , a) = {(q1, a)} en (q0, , b) = {(q1, b)}. Op welk moment worden ze uitgevoerd? c Teken in JFLAP de automaat van example 7.5. d Test de invoer aa met de optie InputStep by State .... Beschrijf wat u ziet. e Probeer op dezelfde wijze de invoer abba te testen. OPGAVE 7.8

Construeer een npda voor de taal L(aaa*b).

OPGAVE 7.9

a

Gegeven is de volgende stapelautomaat met stapelstartsymbool Z.

Welke strings worden door de automaat geaccepteerd? b Idem met de volgende stapelautomaat met stapelstartsymbool Z:

OPGAVE 7.10

Construeer een pda voor de taal L = { anb2n : n  0 }. OPGAVE 7.11

Maak exercise 7 uit section 7.1 van Linz. OPGAVE 7.12

Maak exercise 11 uit section 7.1 van Linz. OPGAVE 7.13

Maak exercise 13 uit section 7.1 van Linz. OPGAVE 7.14

Maak exercise 16 uit section 7.1 van Linz. Met ‘internal state’ wordt gewoon een toestand bedoeld; de gevraagde automaat mag dus maar twee toestanden hebben. 2 Studeeraanwijzing

Alternatieve constructie

Pushdown automata and context-free languages

Bestudeer in section 7.2 van Linz de introductie en subsection Pushdown automata for context-free languages. Zoals Linz al aangeeft is het voor de constructie van een npda bij een context-vrije grammatica niet strikt noodzakelijk dat die grammatica in Greibach-normaalvorm is. We geven hier een alternatieve constructie zonder Greibach-normaalvorm, die vergelijkbaar is met de constructie waar Linz net na example 7.7 op doelt.

Gegeven is G = (V, T, S, P), een context-vrije grammatica. Maak een nietdeterministische stapelautomaat M = (Q, , Γ, , q0, z, F) met drie toestanden Q = {q0, q1, q2} en één eindtoestand F = {q2}. Neem het alfabet gelijk aan de eindsymbolen van de grammatica,  = T, en laat het stapelalfabet naast het stapelstartsymbool en de hulpsymbolen ook de eindsymbolen van de grammatica bevatten, Γ = T  V  {z}. – Maak een -overgang van q0 naar q1 waar de z wordt vervangen door Sz, (q0, , z) = {(q1, Sz)}. – Maak een -overgang van q1 naar q2 als z op de stapel staat, (q1, , z) = {(q2, z)}. – Maak voor elke productieregel A  x in P een -overgang van q1 naar zichzelf, die ervoor zorgt dat, als de linkerkant (A) bovenop de stapel staat, deze vervangen wordt door de rechterkant (x): (q1, x)  (q1, , A). – Maak voor elk eindsymbool a  T een productie die een a leest van de invoer als er een a bovenop de stapel staat, (q1, )  (q1, a, a). OPGAVE 7.15

a

Construeer een npda voor de grammatica

S  aABB aAA A  aBBa B  bBB A Pas beide constructiemethoden toe: die van Linz en die van het werkboek. b Welke aanpassing moet plaatsvinden aan beide stapelautomaten opdat deze tevens de lege string accepteren? OPGAVE 7.16

a Construeer een npda voor de taal die wordt gegenereerd door de grammatica met producties S  AAa A  SAb Doe dit eventueel met behulp van JFLAP. b Geef een paar strings die door de stapelautomaat worden geaccepteerd. c Geef ook strings die niet door de automaat worden geaccepteerd. Studeeraanwijzing

Bestudeer in section 7.2 van Linz subsection Context-free grammars for pushdown automata.

Constructie contextvrije grammatica

We vatten hier samen wat de stappen zijn om uit een gegeven stapelautomaat een context-vrije grammatica te construeren.

Stap 1 Stap 2

Stap 3

Gegeven is de stapelautomaat M = (Q, , Γ, , q0, z, F). Zorg ervoor dat de stapelautomaat één eindtoestand heeft (F = {qf}) die alleen bereikt wordt als de stapel leeg is. Wijzig M opdat de stapelautomaat voldoet aan de tweede eis, op pagina 197 in Linz. Elke beweging voegt dan precies één stapelsymbool toe aan de stapel of haalt precies één stapelsymbool van de stapel. Voor elke beweging die precies één stapelsymbool van de stapel haalt (criterium 7.5) geldt (qj, )  (qi, a, A) met qi, qj  Q, a    {}, A  Γ. Dat levert een productieregel op van de vorm (qiAqj)  a.

Stap 4

Stap 5

Stap 6

Voor elke beweging die precies één stapelsymbool aan de stapel toevoegt (criterium 7.6) geldt (qj, BC)  (qi, a, A) met qi, qj  Q, a    {}, A, B, C  Γ. Neem voor elke beweging van deze vorm een set productieregels op (qiAqk)  a(qjBql)(qlCqk) waar qk en ql alle mogelijke toestanden uit Q zijn. Vereenvoudig de context-vrije grammatica door alle productieregels te schrappen waarin hulpsymbolen voorkomen die nutteloos zijn. Een hulpsymbool (qiAqj) is nutteloos als (qiAqj) niet aan de linkerkant voorkomt of als er geen pad is in de oorspronkelijke overgangsfunctie van qi naar qj. Het startsymbool van de grammatica is (q0zqf).

OPGAVE 7.17

Deze opgave gaat over example 7.8 en example 7.9 in Linz. a Teken (bijvoorbeeld in JFLAP) de twee versies van de stapelautomaat van example 7.8, de oorspronkelijke en de versie die ook voldoet aan conditie 2. b Waarom voldoet de automaat vóór transformatie wel aan conditie 1 maar niet aan conditie 2? c Leg in eigen woorden uit hoe de stapelautomaat wordt gewijzigd opdat deze aan de tweede conditie voldoet. d Welke overgangen van de getransformeerde stapelautomaat zijn van de vorm (7.5) en welke zijn van de vorm (7.6)? e Ga vanaf nu uit van de getransformeerde stapelautomaat die gebruikt wordt voor het afleiden van een context-vrije grammatica. Hoeveel productieregels zullen stap 3 en stap 4 voor de constructie van de grammatica opleveren volgens de opgegeven formules? f Voor het verwijderen van nutteloze productieregels noemt het tekstboek het feit dat er geen pad is van q1 naar q0, van q1 naar q1, van q1 naar q3 en van q2 naar q2. Zijn er nog meer paren van toestanden in de graaf aan te wijzen waar geen pad tussen is? Zo ja, waarom worden ze dan in het tekstboek niet genoemd? OPGAVE 7.18

Maak exercise 15 uit section 7.2 van Linz. OPGAVE 7.19

Maak exercise 16 uit section 7.2 van Linz. Hoeveel productieregels zijn te verwachten? NB U hoeft stap 5 van de constructiemethode niet uit te voeren. 3

Studeeraanwijzing

Erratum Linz

Deterministic pushdown automata and deterministic context-free languages

Bestudeer section 7.3 van Linz. Van example 7.11 hoeft u niet alle details te lezen en te begrijpen. U moet wel opgave 7.21 kunnen maken. De dpda in example 7.10 accepteert  niet, terwijl dat wel zou moeten. Om dat te repareren voegen we een nieuwe begintoestand p toe, met (p, , 0) = {(q0, 0)}.

OPGAVE 7.20

Bekijk de stapelautomaat van figure 7.2 op pagina 185 in Linz. Deze stapelautomaat is niet deterministisch. Aan welke conditie(s) van definition 7.3 voldoet de stapelautomaat niet?

OPGAVE 7.21

Beschrijf in eigen woorden wat de essentie is van example 7.11. OPGAVE 7.22

Maak exercise 5 uit section 7.3 van Linz. OPGAVE 7.23

Maak exercise 7 van section 7.3 uit Linz. OPGAVE 7.24

Is de taal L = {anbm : n = m of n = m + 2} deterministisch? OPGAVE 7.25

Laat zien dat, in example 7.11, Mˆ niet de strings anbnck met k  n accepteert.

ZELFTOETS

1

a Geef een stapelautomaat met twee toestanden voor de taal L = {(ab)nb : n  0}. b Is de stapelautomaat deterministisch? Motiveer uw antwoord. c Geef met behulp van momentane beschrijvingen de berekening van de string abb die de string accepteert.

2

Gegeven is de volgende stapelautomaat met stapelstartsymbool Z:

Beredeneer zonder constructie toe te passen welke taal de stapelautomaat accepteert. 3

Gegeven is de context-vrije grammatica G = (V, T, S, P) met V = {S}, T = {a, b} en met de productieregels: S  aSbbab Construeer een stapelautomaat M met L(M) = L(G).

4

Gegeven is de volgende stapelautomaat met stapelstartsymbool Z:

a Is de stapelautomaat deterministisch? Motiveer uw antwoord. b Construeer met behulp van de constructie uit het tekstboek een context-vrije grammatica voor deze stapelautomaat. Aanwijzing: u hoeft de verkregen grammatica niet te vereenvoudigen. 5

Maak exercise 6 van section 7.3 uit Linz.

TERUGKOPPELING 1

7.1

Uitwerking van de opgaven

a Als de stapelautomaat in de begintoestand q0 is met op de top van de stapel het stapelsymbool 0 (het stapelstartsymbool), dan gaat de stapelautomaat zonder het lezen van invoer over naar de eindtoestand q3, en haalt de 0 van de stapel. De stapel is dan leeg. b Is de stapelautomaat in de begintoestand met stapelsymbool 0 op de stapel en wordt symbool a gelezen, dan zijn er twee mogelijkheden:  De stapelautomaat gaat over naar toestand q1 en het stapelsymbool 0 wordt vervangen door 10 (ofwel: er wordt een 1 bovenop de 0 die er al stond gezet). Of:  De stapelautomaat gaat over naar eindtoestand q3. Stapelsymbool 0 wordt van de stapel gehaald en er wordt niets meer op de stapel geplaatst. Merk op dat de stapelautomaat niet-deterministisch is omdat er in de begintoestand twee mogelijkheden zijn bij het lezen van a. Bovendien is er een derde mogelijkheid in de begintoestand, namelijk niets lezen, die ook gekozen kan worden als de invoer als eerste symbool een a bevat (zie onderdeel a). c Als de stapelautomaat in toestand q2 is en op de stapel het stapelstartsymbool 0 staat, dan kan de stapelautomaat zonder iets te lezen overgaan naar eindtoestand q3. Ook hier wordt stapelsymbool 0 van de stapel gehaald en er wordt niets meer op de stapel geplaatst.

7.2

a Er geen overgang gedefinieerd is voor de combinatie toestand q0 en invoer b. Er wordt gekozen voor de combinatie toestand q0 en  als invoer. Zonder iets te lezen gaat de stapelautomaat naar toestand q3. De string is niet gelezen en de stapel is leeg. b Bij een gegeven invoer kunnen alle mogelijkheden worden geprobeerd (brute force). Alle combinaties die het mogelijk maken om verder te gaan en de hele invoer te verwerken worden onderzocht.

7.3

a

Voor de string a krijgen we de volgende berekening:

(q0, a, 0) ⊢ (q3, , ) b Voor de string aabb krijgen we de volgende berekening: (q0, aabb, 0) ⊢ (q1, abb, 10) ⊢ (q1, bb, 110) ⊢ (q2, b, 10) ⊢ (q2, , 0) ⊢ (q3, , ) c

Voor de string aaba is er geen berekening die naar de eindtoestand leidt:

(q0, aaba, 0) ⊢ (q1, aba, 10) ⊢ (q1, ba, 110) ⊢ (q2, a, 10) Hier blokkeert de stapelautomaat. Er is namelijk geen overgang gedefinieerd voor de combinatie van toestand q2 met invoer a. Er is wel een -overgang vanuit toestand q2, maar die kan alleen genomen worden als er een 0 bovenaan de stapel staat, en dat is nu niet het geval. Andere berekeningen die naar de eindtoestand leiden én de hele invoerstring lezen zijn niet te vinden.

7.4

Een stapelautomaat accepteert een string als er een wandeling van begin- naar eindtoestand door de stapelautomaat is, waarbij alle invoer (alle symbolen van de string) gelezen wordt. De inhoud van de stapel in de eindtoestand is niet relevant.

7.5

a Kies de optie Pushdown Automaton en bevestig met OK het geselecteerde type invoer (Multiple Character Input). Het enige verschil met eindige automaten is dat nu drie waarden ingevuld moeten worden voor een overgang. b Zoals verwacht worden de eerste twee strings geaccepteerd en de derde niet. Bij de optie Fast Run worden voor een string die geaccepteerd is, alle stappen van de berekening getoond. U ziet voor elke stap wat de toestand van de stapelautomaat is, welke invoer gelezen is, welke invoer nog te lezen is en wat de inhoud van de stapel is.

7.6

Figure 7.3 in Linz toont de stapelautomaat M = (Q, , Γ, , q0, z, F) met Q = {q0, qf}  = {a, b} Γ = {0, 1, z} F = {qf} met de overgangsfunctie: (q0, a, 0) = {(q0, 00)} (q0, a, z) = {(q0, 0z)} (q0, a, 1) = {(q0, )} (q0, b, 0) = {(q0, )} (q0, b, 1) = {(q0, 11)} (q0, b, z) = {(q0, 1z)} (q0, , z) = {(qf, z)}

7.7

a Toestand q0 is de begintoestand. De stapelautomaat bevindt zich in deze toestand zolang symbolen uit de string w worden gelezen. Toestand q1 is de toestand waar symbolen uit de string wR worden gelezen. Toestand q2 is de eindtoestand. De stapelautomaat gaat vanuit toestand q1 naar deze toestand als de string gelezen is. b Op een gegeven moment besluit de controle-eenheid van de stapelautomaat dat de string w gelezen is en dat begonnen moet worden met het lezen van wR. Ongeacht welk karakter van de string op de top van de stapel staat, en zonder dat er invoer wordt gelezen, gaat de stapelautomaat in toestand q1. Het gelezen stapelsymbool wordt bij deze overgang op de stapel teruggezet. Omdat bij het analyseren van een string door een automaat alle mogelijke berekeningen worden geprobeerd, zal ook de berekening waarin de bewuste overgang op het juiste moment plaatsvindt gevonden worden (als de invoer van de vorm wwR is natuurlijk). c

Zie bestand opdracht07.7.jff in de bouwsteen.

d Bij de start is de stapelautomaat in toestand q0, de invoer is aa en op de stapel staat Z. Na één stap blijft de stapelautomaat in toestand q0, is één symbool a gelezen en op de stapel staat aZ. Bij de volgende stap zijn er twee mogelijke bewegingen die verder onderzocht worden. Bij de tweede mogelijkheid gaat de stapelautomaat zonder invoer te lezen naar toestand q1.

De stapel blijft ongewijzigd: stapelsymbool a is gelezen en wordt weer op de stapel teruggeplaatst. Na een aantal stappen worden de alternatieven groen of rood gekleurd ten teken dat de berekening wel of niet succesvol is geweest. Met de knop Trace kan één geselecteerde berekening verder bekeken worden. e Nu zien we dat er ook tussentijds berekeningen rood worden gekleurd omdat ze niet tot succes leiden. 7.8

De taal is regulier. We kunnen een (deterministische) eindige automaat maken die de taal accepteert. Deze eindige automaat kan dan eenvoudig omgezet worden naar de volgende stapelautomaat met stapelstartsymbool Z:

We kunnen ook rechtstreeks een stapelautomaat met stapelstartsymbool Z maken die controleert dat er minimaal twee a's worden gelezen. De stapelautomaat blijft dan in de begintoestand zolang a's worden gelezen:

7.9

a In het begin staat Z op de stapel. De automaat kan de lege string lezen of een a lezen. Met de lege string komt de automaat in eindtoestand q1. De lege string wordt dus geaccepteerd door de stapelautomaat. Met een a wordt de top van de stapel vervangen door 11Z en blijft de stapelautomaat in toestand q0. De stapelautomaat kan nu in deze toestand nog meer a’s lezen, waarbij steeds 11 bovenop de stapel wordt toegevoegd. Het is dan niet meer mogelijk om in een eindtoestand te komen, omdat de enige overgang naar de eindtoestand verwacht dat Z bovenop de stapel staat. De enige string die door de stapelautomaat wordt geaccepteerd, is dus de lege string. b Net zoals in opdracht a accepteert de stapelautomaat de lege string. Vanuit de begintoestand kan de stapelautomaat ook symbool a lezen en dan in toestand q1 komen. Omdat de stapel bij de start stapelsymbool Z bevat, wordt door die stap Z vervangen door 11Z. In toestand q1 moet de stapelautomaat dan twee b's lezen om de stapel terug te brengen tot Z. Dan kan de lege string worden gelezen om in eindtoestand qf te komen. Dit betekent dat string abb wordt geaccepteerd. Andere strings kunnen niet geaccepteerd worden omdat er maar één a gelezen kan worden (de overgang van q0 naar q1 met label a, 1; 111 kan nooit gebruikt worden). De stapelautomaat accepteert de lege string en de string abb.

7.10

De stapelautomaat moet in toestand q0 blijven zolang a's worden gelezen en naar toestand q1 gaan bij de eerst gelezen b. Hierbij worden voor elke a twee stapelsymbolen aan de stapel toegevoegd en voor elke b wordt één stapelsymbool weggehaald. We krijgen dan de volgende stapelautomaat met stapelstartsymbool Z:

Merk op dat we na het lezen van de a’s niet meteen naar de eindtoestand mogen gaan, omdat er dan een 1 bovenop de stapel staat in plaats van een Z. 7.11

We tellen elke a met een stapelsymbool 0. Daarna tellen we de b's. Eerst halen we voor elke b een stapelsymbool 0 weg. Zolang de top van de stapel een 0 bevat, zijn er meer a's dan b's gelezen. Komen we bij het stapelstartsymbool, dan zijn er evenveel a's als b's gelezen. Nu plaatsen we voor elke b een stapelsymbool 1 op de stapel. Als de top van de stapel een stapelsymbool 1 bevat, zijn er meer b's dan a's gelezen. De stapelautomaat kan alleen in de eindtoestand komen als de top van de stapel een 0 of een 1 bevat. Dit vertalen we naar de volgende stapelautomaat met stapelstartsymbool Z:

7.12

Ja, want de gegeven pda gebruikt zijn stapel niet, en is dus equivalent met de dfa met overgangen (q0, a) = q1 (q0, b) = q0 (q1, a) = q1 (q1, b) = q0

7.13

De gegeven stapelautomaat zetten we om naar een graaf:

Deze stapelautomaat accepteert de reguliere taal L(abb*a + a). 7.14

We mogen nu niet alle toestanden gebruiken die nodig zijn om de overstap van a’s naar b en weer naar a’s bij te houden. In plaats daarvan zetten we een symbool op de stapel dat onthoudt waar we zijn. Een mogelijke uitwerking is

Dus zolang er Z of een 1 op de stapel staat, zitten we in de eerste groep a’s, als we een b lezen terwijl er een 1 op de stapel staat, gaan we over naar de tweede groep a’s, met een 2 op de stapel. We kunnen dan alleen nog a’s lezen, en stoppen. 7.15

a Voor de methode in Linz moeten we eerst de grammatica omzetten naar Greibach-normaalvorm: S  aABBaAA A  aBBa B  bBBaBBa We krijgen dan de volgende stapelautomaat met stapelstartsymbool Z:

Met de alternatieve methode uit het werkboek krijgen we de volgende stapelautomaat met stapelstartsymbool Z:

b In beide grafen moet de volgende overgang worden toegevoegd om de lege string te accepteren: (q0, , Z) = {(q2, Z)}. 7.16

a Met de alternatieve methode uit het werkboek krijgen we de volgende stapelautomaat:

b De strings a, bb, abb, baab, abababab worden geaccepteerd. Bij sommige strings vraagt JFLAP tijdens de berekening of er verder moet worden geprobeerd. Dat is zeker het geval voor de laatste string. Daar moeten meer dan 4000 configuraties worden uitgeprobeerd voordat het antwoord verschijnt.

7.17

c

Op de string a na, worden geen strings geaccepteerd die eindigen op een a.

a

De stapelautomaat voor de transformatie ziet er als volgt uit:

Na transformatie krijgen we de volgende stapelautomaat:

b De stapelautomaat voldoet aan conditie 1 (stap 1) vanwege de vierde overgang: (q1, , z) = {(q2, )}. Er is maar één eindtoestand. De enige manier om in de eindtoestand te komen is met een lege stapel, en de enige keer dat een lege stapel voorkomt is het moment waarop de eindtoestand bereikt wordt. De stapelautomaat voldoet niet aan conditie 2 (stap 2) vanwege de tweede overgang (q0, a, A) = {(q0, A)}. Deze overgang heeft niet de vereiste vorm omdat hiermee de stapel niet wordt gewijzigd met één stapelsymbool meer of minder. c Dit is goed te zien in de overgangsgrafen van uitwerking a. Het lezen van a gebeurt nu ook in twee stappen: vóór transformatie werd in één stap a en A gelezen, na transformatie wordt eerst in één stap a gelezen en A weggehaald, en in de volgende stap  gelezen en A teruggezet. Hiervoor wordt een extra toestand q3 geïntroduceerd. Als symbool a wordt gelezen met stapelsymbool A, dan wordt eerst het stapelsymbool weggehaald waardoor alleen Z op de stapel overblijft en vervolgens wordt een stapelsymbool A op de stapel geplaatst. Eén stapelsymbool erbij en één stapelsymbool eraf komt op hetzelfde neer als geen wijziging van de afmetingen van de stapel. d De eerste twee overgangen (van het tweede rijtje overgangen op pagina 198 van Linz) zijn van de vorm (7.6) en de andere drie zijn van de vorm (7.5). e Er zijn drie overgangen die van de vorm (7.5) zijn. Elke overgang geeft precies één productieregel (stap 3). Er zijn twee overgangen die van de vorm (7.6) zijn. Dat zijn (q0, a, Z) = {(q0, AZ)} en (q3, , Z) = {(q0, AZ)}. Om het aantal productieregels te bepalen, moeten we bepalen hoeveel waarden de variabelen i, j, k en l kunnen aannemen. De variabele i in de formule in stap 4 kan twee waarden aannemen, de waarden 0 en 3. Voor beide waarden van i kan j één waarde aannemen, in beide gevallen de waarde 0. Voor k en l zijn er voor elke waarde van i 4 mogelijkheden, namelijk voor elke toestand één. Het aantal productieregels wordt dus 3 + 2 * 4 * 4 = 35. f Ja, er zijn nog meer combinaties van toestanden aan te wijzen waartussen geen pad bestaat: van q2 naar q0, van q2 naar q1 en van q2 naar q3. Maar na de verwijdering van de productieregels voor de wel genoemde combinaties zijn er geen hulpsymbolen meer die de laatste combinaties bevatten.

7.18

We kijken naar de laatste versie van de grammatica in example 7.8, op pagina 199. Hulpsymbool (q0zq1) is nutteloos omdat het geen startsymbool is en omdat het hulpsymbool verder nergens voorkomt aan de rechterkant van de productieregels: het is dus niet bereikbaar vanuit het startsymbool.

7.19

Voor meer inzicht tekenen we eerst de transitiegraaf van de stapelautomaat:

Door goed te kijken komen we op het antwoord: S  aBa B  bB Voor de toepassing van de constructie uit section 7.2 moet de stapelautomaat de eindtoestand met een lege stapel bereiken. Hiervoor introduceren we een nieuwe eindtoestand q2 (q1 is niet langer meer eindtoestand) en de volgende overgangen: (q1, , A) = {(q1, )} (q1, , Z) = {(q2, )}

We hebben nu drie overgangen die voldoen aan stap 3 van de constructie en twee overgangen die voldoen aan stap 4. De grammatica zal daarom 21 productieregels bevatten (3 + 2 * 3 * 3). De grammatica is G = (V, T, (q0Zq2), P) met de hulpsymbolen (qiAqj)  Q = {q0, q1, q2} en A  Γ = {A, Z}, de eindsymbolen gelijk aan het alfabet T =  = {a, b} en P als volgt: (q0Zr) (q0Ar) (q1Aq1) (q1Zq2) (q0Aq1)

 a(q0As)(sZr)  b(q0As)(sAr)   a

voor alle r, s  {q0, q1, q2} voor alle r, s  {q0, q1, q2}

7.20

De automaat is niet deterministisch. Vanuit toestand q0 zijn er drie mogelijkheden als het te lezen symbool a is en als de stapel stapelsymbool 0 bevat: een optie is om niets te lezen en het stapelsymbool weg te halen en een tweede optie is om a te lezen en het stapelsymbool weg te halen. In beide gevallen gaat de stapelautomaat naar toestand q3. Als laatste optie kan a gelezen worden, kan 10 op de stapel worden geplaatst en kan de stapelautomaat naar toestand q1 gaan. De condities 1 en 2 van definition 7.3 in Linz worden beide overtreden: (q0, a, 0) = {(q1, 10), (q3, )}) (q0, , 0) ≠  en (q0, a, 0) ≠ 

(conditie 1 niet gerealiseerd) (conditie 2 niet gerealiseerd)

Dit kunt u in JFLAP controleren met de optie TestHighlight Nondeterminism. Gebruik hiervoor het bestand Jexample7.2.jff uit de JFLAP Examples van JFLAP Activities. NB

7.21

In example 7.11 wordt met een voorbeeld aannemelijk gemaakt dat er contextvrije talen zijn die niet deterministisch context-vrij zijn. Hiervoor wordt een bewijs uit het ongerijmde gebruikt. De taal L is context-vrij. We nemen aan dat L wel deterministisch context-vrij is. Dan bestaat er een dpda M die L accepteert. Met deze stapelautomaat kunnen we een npda construeren die de taal Lˆ accepteert. Lˆ is dus een context-vrije taal. Deze conclusie is echter niet juist (het bewijs wordt in example 8.1 in chapter 8 van Linz gegeven). Omdat de conclusie niet juist is, is de aanname ook niet juist.

7.22

Ja, L is deterministisch. Dit kan men zien doordat er telkens op basis van het invoersymbool de juiste keus kan worden gemaakt. We kunnen voor L de volgende deterministische stapelautomaat geven:

7.23

De stapelautomaat is niet deterministisch want in toestand q0 met z op de stapel zijn er twee keuzes. De stapelautomaat kan een symbool lezen en in toestand q0 blijven of niets () lezen en naar q1 gaan. Dit is in strijd met conditie 2 van definition 7.3 in Linz.

De taal L is deterministisch context-vrij omdat het mogelijk is om een deterministische stapelautomaat te maken die L accepteert:

De lege string wordt geaccepteerd door deze automaat: q0 is de eindtoestand. Begint de string met een a, dan wordt de bovenkant van de stapelautomaat gebruikt. Met het lezen van de eerste a wordt een 0 bovenop de stapel gezet. Voor elk volgende a komt een 1 op de stapel. Bij het lezen van een b wordt één stapelsymbool 1 van de stapel gehaald. Wordt een b gelezen en staat 0 op de stapel, dan zijn evenveel a's als b's gelezen. De stapelautomaat gaat dan naar het eindtoestand q0. De onderkant is identiek aan de bovenkant, maar voor het geval de string met een b begint. NB

7.24

zie ook example 1.13.

Ja, L is deterministisch. Na het lezen van twee b's minder dan a's wordt de string geaccepteerd (situatie n = m + 2). Volgt er nu nog een b, dan moet alleen nog gecontroleerd worden of de string evenveel b's als a's heeft (situatie n = m). We kunnen de volgende deterministische stapelautomaat geven die de taal L accepteert:

7.25

In de transitiegraaf van figure 7.4 zijn de transitiegrafen voor het lezen van het tweede deel bn en voor het lezen van cn gelijk (alleen waar beneden b staat, staat boven c). Stel anbnck wordt geaccepteerd voor k ≠ n. Dan bestaat er een eindtoestand in het bovenste deel van de transitiegraaf van figure 7.4 na het lezen van ck. Door de -overgang niet te volgen, bestaat er ook een eindtoestand in het onderste deel voor het lezen van bk. Dan wordt anbn+k geaccepteerd door de stapelautomaat. Dit laatste klopt niet, dus onze aanname klopt niet. 2

1

a

Uitwerking van de zelftoets Een stapelautomaat voor L is M = (Q, , Γ, , q0, Z, q1) met

Q = {q0, q1}  = {a, b} Γ = {A, Z} en met als overgangsfunctie: (q0, a, Z) = {(q0, AZ)} (q0, b, A) = {(q0, )} (q0, b, Z) = {(q1, Z)}. De transitiegraaf is dan:

b De stapelautomaat is deterministisch. Voor de combinatie van een gegeven invoersymbool en een gegeven stapelsymbool is hooguit één keuzemogelijkheid (en er zijn geen -overgangen die roet in het eten kunnen gooien). c

De berekening is als volgt:

(q0, abb, Z) ⊢ (q0, bb, AZ) ⊢ (q0, b, Z) ⊢ (q1, , Z) 2

Eerst zien we dat de stapelautomaat de lege string accepteert. Verder zien we dat de stapelautomaat verschillende voorkomens van symbool a kan lezen. De stapelautomaat houdt bij hoeveel a's worden gelezen. Na het lezen van de a's gaat de stapelautomaat over naar toestand q2 bij het lezen van een b. Voor alle gelezen b's wordt één stapelsymbool weggehaald. Na het lezen van evenveel b's als a's (anbn) komt de stapelautomaat weer in toestand q0. Dan kan de stapelautomaat stoppen of opnieuw ambm lezen. De stapelautomaat accepteert de taal L = {anbn : n  1}*.

3

We passen de alternatieve constructie uit het werkboek toe. M = ({q0, q1, q2}, {a, b}, {a, b, S, z}, , q0, z, {q2}) met (q0, , z) = {(q1, Sz)} (q1, , z) = {(q2, )} (q1, , S) = {(q1, aSbb), (q1, ab)} (q1, a, a) = {(q1, )} (q1, b, b) = {(q1, )}

4

a De stapelautomaat is niet deterministisch. Er zijn twee mogelijke overgangen als de stapelautomaat in toestand q0 is met a als invoer en Z op de stapel. De twee mogelijkheden zijn a wel of niet lezen. b De overgangsfunctie luidt: (q0, a, Z) = {(q0, AZ)} (q0, b, A) = {(q0, )} (q0, , Z) = {(q1, )} De stapelautomaat heeft de vereiste eindtoestand en alle overgangen zijn van de vorm (7.5) of (7.6). We krijgen door de constructie toe te passen de volgende grammatica met startsymbool (q0Zq1): (q0Aq0) (q0Zq1) (q0Zq0) (q0Zq1)

5

b   a(q0Aq0)(q0Zq0)a(q0Aq1)(q1Zq0)  a(q0Aq0)(q0Zq1)a(q0Aq1)(q1Zq1)

Zie de terugkoppeling in Linz op pagina 425.

Properties of context-free languages Introductie Leerkern 1 2

175 175

Two pumping lemmas 175 Closure properties and decision algorithms for context-free languages 178

Zelftoets

179

Terugkoppeling 1 2

180

Uitwerking van de opgaven 180 Uitwerking van de zelftoets 186

Leereenheid 8

Properties of context-free languages

INTRODUCTIE

We beginnen deze leereenheid met een manier om aan te tonen dat een gegeven taal niet context-vrij is: het pomplemma voor context-vrije talen. Vervolgens laten we zien dat de familie van context-vrije talen gesloten is onder vereniging, concatenatie en Kleene-afsluiting, net als de familie van reguliere talen. De familie van context-vrije talen blijkt echter niet gesloten te zijn onder doorsnede en complement, en verschilt hierin dus van de familie van reguliere talen. Ten slotte bespreken we twee beslissingsproblemen voor context-vrije talen (leegheid en oneindigheid), en merken op dat andere beslissingsproblemen niet zo eenvoudig of zelfs onmogelijk op te lossen zijn. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – het pomplemma voor context-vrije talen kunt toepassen – de constructies voor vereniging, concatenatie en Kleene-afsluiting van twee context-vrije talen kent, en deze kunt uitvoeren in een concrete situatie – kunt uitleggen waarom de familie van context-vrije talen niet gesloten is onder doorsnede en complement – een algoritme kunt geven om te bepalen of een context-vrije taal leeg is – een algoritme kunt geven om te bepalen of een context-vrije taal oneindig is. Studeeraanwijzingen Bij deze leereenheid hoort chapter 8 van het tekstboek. Beide sections daarvan zijn verplicht, maar van section 8.1 behandelen we alleen subsection A pumping lemma for context-free languages. De studielast van deze leereenheid bedraagt circa 7 uur.

LEERKERN 1 Studeeraanwijzing

Two pumping lemmas

Lees de introduction van chapter 8 van Linz. In de introduction wordt gesproken over een hiërarchie van formele talen. Een stukje van die hiërarchie hebben we al gezien: iedere eindige taal is ook een reguliere taal, en iedere reguliere taal is ook een context-vrije taal. Anders gezegd: de familie van eindige talen is een deelverzameling van de familie van reguliere talen, die weer een

deelverzameling is van de familie van context-vrije talen (de families van lineaire en deterministisch context-vrije talen zitten hier ook nog tussen, maar die laten we nu even buiten beschouwing). Bovendien weten we dat het in beide gevallen om een echte deelverzameling gaat: er bestaan reguliere talen die niet eindig zijn, en er bestaan context-vrije talen die niet regulier zijn. Maar bestaan er ook talen die niet context-vrij zijn? En hoe ver kun je zo doorgaan? Een uitgebreider antwoord daarop geven we in leereenheid 9. We houden ons nu eerst bezig met de vraag hoe we kunnen bewijzen dat een taal niet context-vrij is, en hoe we context-vrije talen van andere talen kunnen onderscheiden. Studeeraanwijzing

Bestudeer subsection A pumping lemma for context-free languages van section 8.1 in Linz.

Erratum bij figure 8.2

In het vierde deel van de string, in bm, begint de substring niet met twee a's maar met twee b's.

Erratum bij example 8.3

In example 8.3 staat een tikfoutje: de laatste formule moet luiden m! – (k + l) > (m – 1)!.

(Context-vrij) pompbaar

Met ‘het pomplemma voor context-vrije talen’ duiden we theorem 8.1 aan, en met ‘context-vrij pompbaar’ bedoelen we pompbaar zoals aangegeven in theorem 8.1. Als er geen verwarring kan ontstaan met het pomplemma voor reguliere talen en het begrip regulier pompbaar, gebruiken we in deze leereenheid gewoon ‘het pomplemma’ en ‘pompbaar’.

OPGAVE 8.1

Gegeven is de context-vrije taal L = {anbn : n > 0}. In het eerste deel van deze opgave kijken we alleen naar de uitspraak in theorem 8.1; u hebt het bewijs van die stelling hier nog niet nodig. a Neem m = 2, en zoek voor de string w = a2b2  L een decompositie in u, v, x, y en z met eigenschappen (8.1) tot en met (8.4) van theorem 8.1. b Neem weer m = 2, maar zoek nu voor de string w = ab  L een decompositie in u, v, x, y en z met eigenschappen (8.1) tot en met (8.4) van theorem 8.1. c Doe nu hetzelfde voor m = 4 en w = a2b2. d Wat is de kleinste waarde van m waarvoor het altijd werkt? Laat zien dat voor die waarde van m inderdaad iedere string uit L die lang genoeg is, te pompen is. U hoeft (nog) niet uit te leggen waarom dit zo is, u hoeft alleen te laten zien dat het zo is. We betrekken nu het bewijs van theorem 8.1 erbij. e Neem m gelijk aan de waarde van onderdeel d. Kunt u uitleggen waarom alle strings uit L die minstens lengte m hebben, pompbaar zijn voor alle i? Net als bij het pomplemma voor reguliere talen (leereenheid 4) kan het pomplemma voor context-vrije talen alleen gebruikt worden om aan te tonen dat een gegeven taal niet context-vrij is. Het pomplemma zegt immers “als een taal context-vrij is, dan is die taal ook pompbaar”. Deze uitspraak zegt niets over het geval van een niet-context-vrije taal: die taal kan dan wel of niet pompbaar zijn. Maar volgens de regels van de logica (contrapositie) is de uitspraak “als een taal context-vrij is, dan is die taal

ook pompbaar” equivalent met de uitspraak “als een taal niet pompbaar is, dan is die taal niet context-vrij”, en dat is precies hoe het pomplemma gebruikt wordt: we tonen aan dat een taal niet pompbaar is, en concluderen daaruit dat die taal niet context-vrij kan zijn.

Afspraak

Merk op dat, om te bewijzen dat een taal niet pompbaar is, we eerst een geschikte voldoende lange string w uit die taal moeten kiezen (minstens lengte m), en dan voor alle mogelijke decomposities uvxyz van w metvxy m envy≥ 1 moeten laten zien dat die string de taal uit gepompt kan worden. In example 8.1 en 8.2 lijkt het misschien alsof het genoeg is een voorbeeld van mislukt pompen te geven, maar in beide examples wordt wel degelijk opgemerkt dat alle mogelijke decomposities stranden: in example 8.1 staat “In fact, the only way the adversary could stop us from winning is …But this is not possible …”, en in example 8.2 staat “There are many ways … but for all of them we have a winning countermove.” Voor example 8.4 geldt hetzelfde, en zelfs nog iets subtieler: “The adversary now has several choices. The only one that requires much thought is …”. Als u gevraagd wordt het pomplemma toe te passen, dan is het de bedoeling dat u alle mogelijke decomposities bespreekt. Dat kan bijvoorbeeld met behulp van een overzichtelijk plaatje zoals in figure 8.2, of gewoon door in tekst aan te duiden waar vxy zich kan bevinden. Door goed te kijken kan de hoeveelheid werk soms beperkt worden door een aantal gevallen samen te nemen. Example 8.4 is een leuke herinnering aan het feit dat het tekstboek eigenlijk over “the theory of computation” gaat: de taal die daar gegeven is, L = {anbj : n = j2}, ‘berekent’ bij ieder geheel getal het kwadraat van dat gehele getal (of dat is in ieder geval één van de manieren waarop je er tegenaan kunt kijken). We hebben al eerder andere voorbeelden gezien van ‘computation’ met talen; zo somt de taal van example 8.3 alle faculteiten op. En blijkbaar zijn context-vrije talen daarvoor niet genoeg! In leereenheid 9 onderzoeken we wat we dan wel nodig hebben om alles te kunnen berekenen wat we zouden willen berekenen.

OPGAVE 8.2

Bekijk de volgende redenering, en geef aan welke fouten er zijn gemaakt bij het toepassen van het pomplemma: Gegeven is de taal L = {ssR : s  {0, 1}*}. Deze taal is context-vrij pompbaar en dus context-vrij. Laat namelijk m de constante uit het pomplemma zijn, en kies w = 0m110m  L. Dan geldt w≥ m, en dus moet w te pompen zijn. Dat is ook zo: neem u = 0m, v = 1, x = , y = 1 en z = 0m. Dan geldt uvnxynz = 0m1n1n0m, en die string zit in L voor alle n ≥ 0. OPGAVE 8.3

Laat zien dat de taal L = { wwRw : w  {a, b}* } niet context-vrij is. OPGAVE 8.4

Laat zien dat L = { w  {a, b, c}* : na(w) < nb(w) < nc(w) } niet context-vrij is. OPGAVE 8.5

Maak exercise 8abc van section 8.1 van Linz.

OPGAVE 8.6

Geef van ieder van de twee onderstaande uitspraken aan of die waar of onwaar is. Motiveer uw antwoord. a Iedere reguliere taal is context-vrij pompbaar. b Iedere context-vrije taal is regulier pompbaar. 2

Studeeraanwijzing

Closure properties and decision algorithms for context-free languages

Bestudeer section 8.2 van Linz.

OPGAVE 8.7

In deze opgave willen we bewijzen dat het complement van de taal L van example 8.8 wel context-vrij is (zie exercise 1 van section 8.2 van Linz). a L = {w  {a, b, c}* : na(w)  nb(w)  na(w)  nc(w)}. Waarom is de gegeven voorwaarde voldoende om L te beschrijven? b Laat zien dat L context-vrij is. Kies zelf of u dat via een npda of via een context-vrije grammatica wilt doen. In het laatste geval is het handig om onderscheid te maken tussen de vier gevallen na(w) > nb(w), na(w) < nb(w), na(w) > nc(w) en na(w) < nc(w). OPGAVE 8.8

Maak exercise 3 van section 8.2 van Linz. OPGAVE 8.9

Maak exercise 5 van section 8.2 van Linz. OPGAVE 8.10

Maak exercise 7 van section 8.2 van Linz. OPGAVE 8.11

Maak exercise 8 van section 8.2 van Linz. OPGAVE 8.12

Maak exercise 11 van section 8.2 van Linz. OPGAVE 8.13

Maak exercise 18 van section 8.2 van Linz. OPGAVE 8.14

Bekijk de volgende redenering. Geef aan welke fout er is gemaakt bij het toepassen van de afsluitingseigenschappen. Is de conclusie wel juist? Motiveer uw antwoord. Gegeven is de taal L = {x  {0, 1}* : x bevat een prefix van de vorm ww, met w  {0, 1}*}. Deze taal is niet context-vrij: L is namelijk gelijk aan {ww : w  {0, 1}*}{0, 1}*. De taal {0, 1}* is regulier en dus context-vrij, en context-vrije talen zijn gesloten onder concatenatie. Als L context-vrij zou zijn, dan zou {ww : w  {0, 1}*} dat dus ook zijn. We weten echter uit example 8.2 dat dat niet het geval is, dus kan L ook niet context-vrij zijn.

OPGAVE 8.15

Bekijk de volgende redenering. Geef aan welke fout er is gemaakt bij het toepassen van de afsluitingseigenschappen. Motiveer uw antwoord. Denkt u dat de conclusie wel juist is? Gegeven is de taal L = {1p : p is een priemgetal}. We kunnen deze taal schrijven als een vereniging: L = {12}  {13}  {15}  {17}  {111}  … Elk van deze talen is zelf context-vrij. Context-vrije talen zijn gesloten onder vereniging, dus is de vereniging L context-vrij. OPGAVE 8.16

Bekijk de volgende redenering. Geef aan welke fout er is gemaakt bij het toepassen van de afsluitingseigenschappen. Is de conclusie wel juist? Motiveer uw antwoord.  Gegeven is de taal L = {x {0, 1}* : er is geen w  {0, 1}* met x = wwR}. Deze taal is het complement van de context-vrije taal {wwR : w  {0, 1}*}. Volgens theorem 8.4 is het complement van een context-vrije taal niet context-vrij, dus L is niet context-vrij. OPGAVE 8.17

Maak exercise 21 van section 8.2 van Linz. OPGAVE 8.18

Maak exercise 22 van section 8.2 van Linz. OPGAVE 8.19

Maak exercise 24 van section 8.2 van Linz.

ZELFTOETS

1

Bepaal of de taal L = {apbqcr : p, q ≥ 1 en r = min(p, q)} context-vrij is, waarbij min(p, q) het minimum van p en q is. Motiveer uw antwoord.

2

Bepaal of de taal L = {apbqarbs : p + r = q + s, en p, q, r, s ≥ 0} context-vrij is. Motiveer uw antwoord.

3

Laat zien dat de familie van ondubbelzinnig context-vrije talen niet gesloten is onder vereniging (exercise 14 van section 8.2 van Linz).

4

Hieronder ziet u vier uitspraken. Geef van iedere uitspraak aan of deze waar of onwaar is, en motiveer uw antwoord. a Het is mogelijk dat de doorsnede van twee context-vrije talen niet context-vrij is. b Het is mogelijk dat een oneindige vereniging van context-vrije talen context-vrij is. c Het is mogelijk dat de positieve afsluiting van een context-vrije taal niet context-vrij is. d Het is mogelijk dat het verschil van twee context-vrije talen contextvrij is.

TERUGKOPPELING 1

8.1

Uitwerking van de opgaven

a De lengte van w is groter dan m, dus volgens theorem 8.1 moeten er u, v, x, y, z zijn met w = uvxyz,vxy m envy≥ 1, zodat voor alle i ≥ 0 geldt dat uvixyiz  L. Die u, v, x, y, z zijn er inderdaad: neem v gelijk aan de tweede a en neem y gelijk aan de eerste b, ofwel: u = a, v = a, x = , y = b en z = b. Dan geldt w = uvxyz = aabb,vxy=ab= 2  m envy=ab= 2 ≥ 1. En inderdaad geldt voor alle i ≥ 0 dat uvixyiz  L: uv0xy0z = ab  L, uv1xy1z = w  L, uv2xy2z = aa2b2b = a3b3  L, uv3xy3z = aa3b3b = a4b4  L enzovoort. NB Dit is de enige decompositie die voor deze m en w voldoet aan (8.1) tot en met (8.4) – ga dat na! – maar dat is genoeg. b Voor de combinatie m = 2 met w = ab werkt het niet: omdatvy≥ 1 moet gelden, moet óf v óf y ongelijk aan  zijn, en omdat we uiteindelijk v en y even hard gaan pompen en het aantal a’s gelijk moet blijven aan het aantal b’s, moet dan wel v = a en y = b, en dus u = x = z = . Maar dan gaat het pompen mis voor i = 0: uv0xy0z = v0y0 = a0b0 = , maar   L. Blijkbaar is m = 2 dus geen goede keuze. c Voor m = 4 en w = a2b2 kunnen we dezelfde v en y kiezen als in onderdeel a: v is de laatste a en y de eerste b. De substring u bevat dan de rest van de a’s, de substring z de rest van de b’s, en x is leeg. Pompen zorgt er dan in alle gevallen voor dat het aantal a’s gelijk blijft aan het aantal b’s, en in tegenstelling tot onderdeel b levert omlaag pompen nu niet een string buiten L op. NB Er zijn in dit geval meer keuzes mogelijk voor v en y: v moet precies één van de twee a’s bevatten en y precies één van de twee b’s. Omdat m = w= 4 komen v en y dan nooit te ver uit elkaar te liggen, en het resultaat van pompen is voor alle i ≥ 0 een string in L. d De kleinste waarde van m waarvoor er voor iedere w  L een decompositie bestaat die voldoet aan (8.1) tot en met (8.4) is m = 3 (we hebben al gezien dat het voor m = 2 niet goed gaat). We kunnen dan namelijk altijd de strategie van v is de laatste a, x is leeg en y is de eerste b volgen. Neem immers w = apbp metw≥ m = 3. In de praktijk komt dat voor deze taal neer op p ≥ 2 (er bestaan geen strings in L van lengte 3). Dat betekent dat x = ap–1 en z = bp-1 met p – 1 > 0, zodat omlaag pompen (met i = 0) nooit  op kan leveren. Omhoog pompen (met i > 1) geeft natuurlijk altijd een string in L, omdat v en y even lang zijn en verschillende soorten letters bevatten. e We illustreren de redenering van het bewijs van theorem 8.1 met een specifieke grammatica; de redenering in het bewijs zelf is algemener opgesteld, omdat het moet gelden voor iedere context-vrije taal en iedere grammatica voor die taal. Neem m = 3, en bekijk de grammatica S  aSbab die L genereert. Het is niet mogelijk om met deze producties een string van lengte minstens 3 te genereren in maar één afleidingsstap: er zijn er minstens twee nodig. De afleiding van de kleinste string met lengte

minstens 3 is S  aSb  aabb. De laatste sentential form in deze afleiding bestaat alleen uit eindsymbolen, maar de andere twee bevatten ieder (minstens) een hulpsymbool. Omdat deze grammatica maar één hulpsymbool bevat (S), komt dat hulpsymbool dus in twee verschillende sentential forms voor. Vanwege de karakteristieke eigenschap van context-vrije grammatica’s (waar je ook een S tegenkomt, je mag hem altijd vervangen door de rechterkant van een productie voor S) kunnen we dit stukje afleiding nu wegsnijden (i = 0) of herhalen zo vaak we willen (i > 0); we krijgen dan altijd een geldige afleiding van een string in L. 8.2

De gegeven redenering bevat een aantal fouten: Ten eerste mogen we uit het feit dat L context-vrij pompbaar is niet concluderen dat L context-vrij is, want we mogen het pomplemma niet omdraaien. Ten tweede klopt de redenering die aan moet tonen dat L pompbaar is niet: het begin van de gegeven redenering hoort bij een bewijs dat L niet pompbaar is. Als we willen aantonen dat een taal pompbaar is moeten we een waarde voor m geven waarmee we kunnen laten zien dat alle strings uit L die minstens lengte m hebben pompbaar zijn. NB De conclusie van de redenering is overigens wel juist: L is wel context-vrij pompbaar en ook context-vrij.

8.3

De kunst is ten eerste om een goede string s te kiezen waarvan we kunnen aantonen dat hij niet te pompen is, en ten tweede om met zo weinig mogelijk werk toch alle mogelijke decomposities van s te controleren. We beginnen met het kiezen van s (we noemen hem s omdat w al in de definitie van de taal voorkomt). Een eerste poging zou s = amamam kunnen zijn, met m de constante uit het pomplemma. Helaas kunnen we dan decomposities vinden die altijd gepompt kunnen worden, namelijk alle decomposities met v = a3k en y = a3l. Er geldt namelijk dat dan door pompen weer een drievoud aan a’s ontstaat, en voor iedere string van de vorm a3n geldt dat hij ook van de vorm wwRw is, met w = an. We moeten dus een andere s kiezen en komen dan al gauw uit op iets met ambm. Specifieker: we kiezen w = ambm en dus s = ambmbmamambm, met m de constante uit het pomplemma. We kunnen nu een opsomming maken van alle mogelijke decomposities: v en y kunnen beide in het eerste groepje a’s zitten, of beide in het eerste groepje b’s, of beide in het tweede groepje b’s, enz., of v kan in het eerste groepje a’s en y in het eerste groepje b’s zitten, enz. Dat is alleen wel veel en vervelend werk, en gelukkig kan het hier ook iets handiger: als we s weer even zien als wwRw, dan kan vxy zich bevinden: (1) in de eerste w, of (2) in wwR, of (3) in wR, of (4) in wRw, of (5) in de tweede w. Merk op dat een paar van deze gevallen elkaar zelfs nog overlappen. In alle gevallen zit één van de twee w’s niet in het gepompte stuk, en die w zal dus na pompen niet meer kloppen met de rest.

8.4

We kiezen w = ambm+1cm+2, met m de constante uit het pomplemma. Het is duidelijk dat w  L en datw≥ m. We moeten nu van alle mogelijke decomposities w = uvxyz metvxy  m envy≥ 1 nagaan dat er een i ≥ 0 bestaat waarvoor uvixyiz  L. Welnu, vy kan bestaan uit (1) alleen a’s, (2) alleen b’s, (3) alleen c’s, (4) een combinatie van a’s en b’s, en (5) een combinatie van b’s en c’s. In de eerste twee gevallen levert omhoog pompen (i > 1) een string op die niet in L zit (te veel a’s of b’s). In het derde geval levert omlaag pompen (i = 0) een string op die niet in L zit. In het vierde geval levert omhoog pompen te weinig c’s, en in het vijfde geval levert omlaag pompen teveel a’s. Kortom, voor iedere denkbare decompositie is er wel een i te bedenken waarvoor de gepompte string niet meer van de vorm ajbkcl is met j < k < l, en dus ook niet van de vorm s  {a, b, c}* met na(s) < nb(s) < nc(s). Let op die laatste toevoeging: het is bij deze keuze van w heel gemakkelijk om onderweg te vergeten dat de a’s, b’s en c’s ook door elkaar mogen staan!

8.5

a De taal L = {anwwRbn : n ≥ 0, w  {a, b}*} is context-vrij, en wordt gegenereerd door S  aSbT T  aTabTb b De taal L = {anbjanbj : n ≥ 0, j ≥ 0} is niet context-vrij, omdat ze niet pompbaar is. Dat laten we zien door een w  L te kiezen metw≥ m, met m de constante uit het pomplemma: w = ambmambm. Voor deze w geldt dat er voor iedere decompositie w = uvxyz metvxy m envy≥ 1 een i ≥ 0 te vinden is zodat uvixyiz  L. De mogelijke posities van vxy zijn namelijk: (1) binnen de eerste groep a’s, (2) binnen het eerste stuk ambm, (3) binnen de eerste groep b’s, (4) binnen bmam, (5) binnen de tweede groep a’s, (6) binnen het tweede stuk ambm, en (7) binnen de laatste groep b’s. Het is duidelijk dat bij pompen in geval (1) de lengte van het tweede groepje a’s niet meer klopt met de lengte van het eerste groepje a’s, en dat voor de gevallen (3), (5) en (7) net zoiets geldt. In geval (2) geldt dat bij pompen het eerste stuk ambm verandert maar het tweede stuk ambm niet, en dan zijn ze dus niet meer gelijk. In geval (6) geldt het omgekeerde. In geval (4) zullen na pompen de twee groepen a’s niet meer even lang zijn en/of de twee groepen b’s niet meer even lang zijn. NB Eigenlijk zijn de gevallen (1) en (3) niet nodig, want die vallen onder geval (2), met dezelfde uitleg als nu gegeven bij geval (2). Evenzo zijn geval (5) en (7) eigenlijk overbodig, omdat ze onder geval (6) vallen. Dat geeft niets: zolang op de een of andere manier alle gevallen maar benoemd en besproken worden, is het goed.

Eigenlijk bevat het huidige geval (2) zelfs nog een heleboel subgevallen, zoals (2a) v = ap en x = bp, (2b) v = ap en x = bq met p  q, (2c) v = apbq en x = br enzovoort. Gelukkig voldoet in al die subgevallen de huidige uitleg dat pompen in geval (2) problemen oplevert. Zoals altijd is het dus de kunst om wel volledig te zijn, maar te proberen toch zo weinig mogelijk werk te doen. c De taal L = {anbjajbn : n ≥ 0, j ≥ 0} is context-vrij, en wordt gegenereerd door S  aSbT T  bTa 8.6

a De uitspraak is waar. Iedere reguliere taal is context-vrij (want een reguliere grammatica is een context-vrije grammatica met een speciale vorm), en volgens het pomplemma is iedere context-vrije taal context-vrij pompbaar. b De uitspraak is niet waar. Niet iedere context-vrije taal is regulier pompbaar: neem bijvoorbeeld L = {anbn : n ≥ 0}. L is context-vrij, maar niet regulier pompbaar (zie opgave 4.25).

8.7

a Hier is wel enig nadenken voor nodig: als niet mag gelden dat na(w) = nb(w) = nc(w), dan moet na(w)  nb(w) of na(w)  nc(w) of nb(w)  nc(w), of een combinatie van die drie ongelijkheden gelden. Maar als nb(w)  nc(w) geldt, dan kan niet tegelijkertijd na(w) = nb(w) en na(w) = nc(w) gelden, want dan zouden nb(w) en nc(w) toch gelijk zijn aan elkaar (want allebei gelijk aan na(w)). Dus als nb(w)  nc(w) geldt, dan geldt ook na(w)  nb(w)  na(w)  nc(w). De voorwaarde na(w)  nb(w)  na(w)  nc(w) is dus sterker dan de voorwaarde nb(w)  nc(w), en we hoeven het geval nb(w)  nc(w) niet apart te bekijken. b We kunnen bewijzen dat L context-vrij is door een niet-deterministische stapelautomaat te construeren die L herkent, of door een context-vrije grammatica voor L te maken. Het eerste is waarschijnlijk het makkelijkst: maak een npda die óf controleert dat na(w)  nb(w) óf controleert dat na(w)  nc(w). Beide controles zijn eenvoudig: gebruik het idee van example 7.4, maar laat de npda alleen accepteren als er nog iets anders dan het stapelstartsymbool op de stapel staat. Voor de volledigheid geven we ook een context-vrije grammatica voor L . Daarvoor gebruiken we het idee van example 1.13, met een kleine aanpassing omdat we nu juist niet gelijke aantallen moeten hebben. S  AaABbBCaCDcD A  aAAaaAbbAacAAcAA B  bBBbaBbbBacBBcBB C  aCCaaCccCabCCbCC D  cDDcaDccDabDDbDD

na(w) > nb(w) na(w) < nb(w) na(w) > nc(w) na(w) < nc(w)

8.8

Laat h een homomorfisme zijn, en G een context-vrije grammatica. Vervang in iedere productie van de grammatica iedere a  T door h(a). Het moge duidelijk zijn dat de nieuwe grammatica dan precies h(L(G)) genereert.

8.9

Uit een gegeven context-vrije grammatica G construeren we een contextvrije grammatica G’ door iedere productie A  x van G te vervangen door A  xR in G’. We kunnen dan vrij eenvoudig laten zien (met inductie naar het aantal afleidingsstappen) dat w kan worden afgeleid door G dan en slechts dan als wR kan worden afgeleid door G’.

8.10

Om te laten zien dat de familie van context-vrije talen niet gesloten is onder verschil, zouden we twee concrete context-vrije talen kunnen zoeken waarvan het verschil niet context-vrij is. In dit geval hoeft dat niet, want we kunnen gebruik maken van het feit dat het complement van een taal uitgedrukt kan worden als een verschil, en van het feit dat de familie van context-vrije talen niet gesloten is onder complement. Het complement van een taal over  is gelijk aan * – L, en * is regulier, dus ook context-vrij. Als de familie van context-vrije talen gesloten zou zijn onder verschil, dan zou voor iedere context-vrije taal L gelden dat * – L, dus het complement van L, ook context-vrij is. We hebben echter al een context-vrije taal gezien waarvan het complement niet context-vrij is: zie opgave 8.7 en example 8.8. Er werd ook gevraagd om te laten zien dat de familie van context-vrije talen wel gesloten is onder verschil met een reguliere taal, dat wil zeggen: als L1 context-vrij is en L2 regulier, dan is L1 – L2 context-vrij. Als we ons realiseren dat L1 – L2 gelijk is aan L1  L 2 , dan kunnen we eenvoudig de constructie uit het bewijs van theorem 8.5 aanpassen (zie ook opgave 4.6). Het enige dat we aan die constructie moeten veranderen, is de verzameling eindtoestanden van de nieuwe npda: die wordt F1  (P – F2).

8.11

We kunnen hiervoor dezelfde constructie gebruiken als voor het verschil van een niet-deterministisch context-vrije taal met een reguliere taal (opgave 8.10): als we starten met een dpda in plaats van een npda, dan kan door de constructie uit het bewijs van theorem 8.5 immers geen nietdeterminisme worden gecreëerd.

8.12

De familie van deterministisch context-vrije talen is niet gesloten onder vereniging, want we kennen twee deterministisch context-vrije talen waarvan we weten dat de vereniging niet deterministisch context-vrij is. De taal L1 = {anbn : n ≥ 0} is deterministisch context-vrij volgens example 7.10, en het is eenvoudig in te zien dat de taal L2 = {anb2n : n ≥ 0} dan ook deterministisch context-vrij is. De vereniging L1  L2 = {anbm : n ≥ 0 en (m = n  m = 2n)} is echter niet deterministisch context-vrij (example 7.11). De familie van deterministisch context-vrije talen is ook niet gesloten onder doorsnede, want we kunnen twee deterministisch context-vrije talen geven waarvan de doorsnede niet deterministisch context-vrij is. We halen die twee talen uit het bewijs van theorem 8.4: L3 = {anbncm : n ≥ 0, m ≥ 0} en L4 = {anbmcm : n ≥ 0, m ≥ 0}. Van beide talen is eenvoudig aan te tonen dat ze deterministisch context-vrij zijn. Hun doorsnede L3  L4 = {anbncn : n ≥ 0} is echter niet eens context-vrij, laat staan deterministisch context-vrij.

8.13

De taal L = {w  {a, b}* : na(w) = nb(w)  w bevat geen substring aab} is gelijk aan de taal L1  L2, met L1 = {w  {a, b}* : na(w) = nb(w)} en L2 = {w  {a, b}* : w bevat geen substring aab}. De eerste is context-vrij (volgens example 1.13), en de tweede is regulier (volgens een constructie analoog aan die in de uitwerking van opgave 2.4b). Omdat de familie van context-vrije talen gesloten is onder reguliere doorsnede (theorem 8.5), is L dus context-vrij.

8.14

Theorem 8.3 zegt (onder andere) dat de concatenatie van twee contextvrije talen altijd weer context-vrij is. De gegeven redenering gebruikt een ander argument: als L1 niet context-vrij is en L2 wel, dan is L1L2 niet context-vrij. Dit is dus een heel ander argument dan dat van theorem 8.3. Dat geeft op zich niet, als het maar een geldig argument is. Dat is echter niet het geval, en dat kunnen we laten zien aan de hand van L zelf: neem L1 = {ww : w  {0, 1}*} en L2 = {0, 1}*. Dan geldt L1  L2 = {0, 1}*. Er geldt namelijk: – L1  L2  {0, 1}*, want we gebruiken alleen 0’en en 1’en, en – {0, 1}*  L1 L2, want we kunnen iedere string x  {0, 1}* schrijven als x, met   L1 en x  L2. Dus geldt dat L = L1 L2 = {0, 1}*, en die laatste taal is natuurlijk contextvrij.

8.15

Het probleem in de gegeven redenering is de vereniging: dat is namelijk een oneindige vereniging (van eindige – dus context-vrije – talen). In theorem 8.3 gaat het echter (impliciet, maar toch) om een eindige vereniging (van mogelijk oneindige talen). De daar gegeven constructie werkt ook helemaal niet voor onze oneindige vereniging, omdat we dan een oneindig aantal productieregels zouden krijgen, en dat mag niet volgens definition 1.1 (die de basis is van definition 5.1). De conclusie dat L context-vrij is, is ook niet juist. Dat kunnen we bewijzen met het pomplemma en enige wiskundige creativiteit; vanwege dat laatste vragen we u niet om het bewijs.

8.16

Theorem 8.4 zegt dat het complement van een context-vrije taal niet in alle gevallen context-vrij is (en dat we dus niet kunnen zeggen dat de familie van context-vrije talen gesloten is onder complement). Dat is iets heel anders dan de bewering dat het complement van een context-vrije taal nooit context-vrij is. Die laatste bewering kunnen we met een heel eenvoudig voorbeeld weerleggen: de taal {0, 1}* is context-vrij, en het complement daarvan (ten opzichte van {0, 1}*) is  en dus ook contextvrij. De conclusie dat L niet context-vrij is klopt niet. Om dat aan te tonen geven we een context-vrije grammatica voor L, dus voor de verzameling van alle strings over {0, 1} die niet van de vorm wwR zijn. Hoe komen we erachter dat een string niet van die vorm is? Neem als voorbeeld de string 01001110. We beginnen met van buiten naar binnen naar een afwijking van de vorm wwR te zoeken. De twee buitenste symbolen (dus het eerste en het laatste symbool) zijn beide 0. De volgende twee (het tweede en het op één na laatste symbool) zijn beide 1. De daarop volgende twee zijn verschillend: links 0, rechts 1. De string zit dus in L, en de symbolen die nog verder naar binnen staan zijn niet meer van belang.

Het is ook mogelijk dat we geen verschil vinden, maar dat de string toch niet van de vorm wwR is: als we in het midden één symbool overhouden. De string is dan van oneven lengte. Sterker nog: iedere string van oneven lengte kan in L zitten. Samenvattend heeft iedere string in L dus de vorm wxwR, waarbij w  {0, 1}* en x van de vorm 0y1 of 1y0 of 0 of 1 is, met y  {0, 1}*. Deze analyse leidt tot de volgende context-vrije grammatica voor L: S  0S01S1T T  1R00R101 R  0R1R 8.17

(Zie de uitwerking op pagina 428 in Linz.) Er geldt dat   L(G) dan en slechts dan als S nullable (verdwijnend) is. We hebben al een algoritme om te bepalen welke hulpsymbolen verdwijnend zijn: dat staat in het bewijs van theorem 6.3. Als we dat algoritme hebben uitgevoerd, hoeven we dus alleen nog te kijken of S in de daar geconstrueerde verzameling zit.

8.18

Breng de grammatica in Greibach-normaalvorm (wij hebben dat in leereenheid 6 alleen ‘uit de losse pols’ gedaan, maar zoals we daar al opmerkten bestaat er ook een algoritme voor). Som dan alle partiële afleidingen van lengte n of korter op, en kijk of een van die afleidingen een string uit de taal oplevert (dat wil zeggen aan het eind van de afleiding geen hulpsymbolen meer bevat).

8.19

L1 en L2 hebben een gemeenschappelijk element dan en slechts dan als hun doorsnede L1  L2 niet leeg is. De doorsnede van een context-vrije taal L1 en een reguliere taal L2 is context-vrij (theorem 8.5), en er bestaat een algoritme om na te gaan of een context-vrije taal leeg is (theorem 8.6). 2

1

Uitwerking van de zelftoets

De taal L = {apbqcr : p, q ≥ 1 en r = min(p, q)} is niet context-vrij. Laat m de constante uit het pomplemma zijn, en kies w = ambmcm. Dan geldt w  L enw≥ m. Er is nu een aantal mogelijkheden voor een opdeling uvxyz van w zodatvxy m envy≥ 1: (1) vxy bevat alleen a’s en/of b’s, (2) vxy bevat zowel b’s als c’s, en (3) vxy bevat alleen c’s. In geval (1) zorgt omlaag pompen (met i = 0) ervoor dat het aantal c’s niet meer gelijk is aan het minimum van het aantal a’s en het aantal b’s, want dat minimum is lager geworden maar het aantal c’s niet. In geval (3) zorgt pompen met iedere i  1 ervoor dat het aantal c’s wordt aangepast maar het minimum niet. Geval (2) valt uiteen in drie subgevallen: (2a) v bevat alleen b’s en y bevat alleen c’s, (2b) v bevat zowel b’s als c’s, en (2c) y bevat zowel b’s als c’s.

In geval (2a) zorgt pompen met i > 1 ervoor dat het minimum gelijk blijft maar het aantal c’s niet. In geval (2b) en (2c) levert pompen met i > 1 een string op die niet van de vorm apbqcr is, voor willekeurige p, q en r. We hebben nu aangetoond dat niet iedere string uit L te pompen is, dus volgens het pomplemma kan L niet context-vrij zijn. 2

De taal L = {apbqarbs : p + r = q + s, en p, q, r, s ≥ 0} is context-vrij. Een manier om dat aan te tonen is om een stapelautomaat voor L te maken. Als we bedenken dat p + r = q + s eigenlijk gewoon betekent dat het aantal a’s gelijk moet zijn aan het aantal b’s, kunnen we hiervoor het idee van example 7.4 gebruiken, en krijgen dan de volgende automaat:

Merk op dat het verschil met de npda van example 7.4 is dat we hier ook nog moeten nagaan dat iedere geaccepteerde string van de vorm a*b*a*b* is. Een alternatief is om te gebruiken dat L = L1  L(a*b*a*b*), met L1 = {w  {a, b}* : na(w) = nb(w)}. We weten dat L1 context-vrij is (example 7.4) en L(a*b*a*b*) is natuurlijk regulier. Volgens theorem 8.5 is de doorsnede van een context-vrije taal met een reguliere taal weer context-vrij. 3

We kunnen de vraag herformuleren tot: zoek twee niet-dubbelzinnige context-vrije talen waarvan de vereniging wel (inherent) dubbelzinnig is. We vinden zo’n voorbeeld in example 5.13: L1 = {anbncm : n, m ≥ 0} en L2 = {anbmcm : n, m ≥ 0} zijn beide context-vrij en ondubbelzinnig, maar hun vereniging is inherent dubbelzinnig (het bewijs daarvan wordt in example 5.13 niet gegeven, maar wel genoemd).

4

a De uitspraak is waar. De familie van context-vrije talen is niet gesloten onder doorsnede (theorem 8.4), dus het is inderdaad mogelijk dat de doorsnede van twee context-vrije talen niet context-vrij is. De context-vrije talen L1 = {anbncm : n, m ≥ 0} en L2 = {anbmcm : n, m ≥ 0}, waarvoor L1  L2 = {anbncn : n ≥ 0}, zijn daar een voorbeeld van, aangezien het resultaat {anbncn : n ≥ 0} het standaardvoorbeeld van een niet context-vrije taal is.

b De uitspraak is waar. Neem bijvoorbeeld de oneindige vereniging L = {}  {a}  {aa}  {aaa}  … Daarvoor geldt dat L = {a}*, en die taal is natuurlijk context-vrij: S  aS c De uitspraak is niet waar. De constructie uit het bewijs van theorem 8.3 waarmee wordt aangetoond dat de familie van context-vrije talen gesloten is onder Kleene-afsluiting (star-closure) kan eenvoudig aangepast worden tot een constructie waarmee bewezen wordt dat de familie van context-vrije talen ook gesloten is onder positieve afsluiting (positive closure): we hoeven alleen de expliciet geconstrueerde  weg te werken (let op: dat is niet hetzelfde als ‘weghalen’). Gegeven een context-vrije grammatica G met startsymbool S construeren we een context-vrije grammatica voor (L(G))+ door alle producties van G over te nemen en daaraan de producties S’  SS’S toe te voegen. Hierbij is S’ een nieuw hulpsymbool en het startsymbool van de nieuwe grammatica. d De uitspraak is waar. Het mag dan zo zijn dat de familie van context-vrije talen niet gesloten is onder verschil (opgave 8.10), dat wil nog niet zeggen dat elk verschil van twee context-vrije talen niet context-vrij is. Om maar weer eens een heel eenvoudig voorbeeld te geven: neem L1 = {a, b}* en L2 = . Beide talen zijn duidelijk context-vrij (hoe ziet een grammatica voor  eruit?), en het verschil L1 – L2 = {a, b}* is dat ook. NB Het geeft niet dat het een triviaal (‘flauw’) voorbeeld is: die tellen ook mee!

Blok 4

Turingmachines en beslisbaarheid

Turing machines Introductie Leerkern 1

2 3 4

191 192

The standard Turing machine 192 1.1 Definitie van een turingmachine 192 1.2 Turingmachines voor het herkennen van talen 196 1.3 Turingmachines als transducers 197 Combining Turing machines for complicated tasks 198 Turing’s thesis 198 De Chomsky-hiërarchie 198 4.1 Context-gevoelige talen 198 4.2 Recursief opsombare talen 200 4.3 De Chomsky-hiërarchie 200

Zelftoets

202

Terugkoppeling 1 2

203

Uitwerking van de opgaven 203 Uitwerking van de zelftoets 210

Leereenheid 9

Turing machines

INTRODUCTIE

De definitie van de turingmachine dateert uit de jaren dertig van de twintigste eeuw. In die tijd vroegen verschillende mensen zich af wat nu eigenlijk een algoritme is. Er bestond wel een intuïtief begrip van een systematische methode om iets te berekenen, maar dat was niet geformaliseerd. De Engelse wiskundige Alan Turing definieerde in 1936 zijn turingmachine als formalisering van het begrip algoritme. Een turingmachine heeft in- en uitvoer, en kan dus beschouwd worden als een soort abstracte computer, met een klein aantal heel eenvoudige instructies. Het voorstel van Turing om de turingmachine als formalisering van het begrip algoritme te gebruiken is algemeen geaccepteerd. Belangrijk daarbij was dat er ook andere redelijke definities van het begrip algoritme bedacht zijn, en dat die allemaal equivalent bleken te zijn met die van Turing. Mede daarom is men het er in het algemeen over eens dat Turings definitie het intuïtieve begrip dekt, en dat een probleem dat niet opgelost kan worden met een turingmachine op geen enkele andere manier systematisch is op te lossen. Een turingmachine is een automaat, en wel een automaat die blijkbaar aan de top van een of andere hiërarchie staat. De automaten die we eerder zijn tegengekomen – eindige automaten en stapelautomaten – passen ook in deze hiërarchie: de eindige automaten onderaan, en de stapelautomaat ergens tussenin. In deze leereenheid bespreken we deze hiërarchie, de Chomsky-hiërarchie, en maken haar compleet: er zit in de oorspronkelijke Chomsky-hiërarchie namelijk nog een laag tussen stapelautomaten en turingmachines. We betrekken ook de bijbehorende grammatica’s erbij, en bekijken een paar extra ‘tussenlagen’ die we eerder zijn tegengekomen. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – de definitie van een turingmachine kunt geven – het verschil tussen een accepter en een transducer kunt uitleggen – voor een gegeven taal een turingmachine kunt construeren die deze taal accepteert – voor een gegeven functie een turingmachine kunt construeren die deze functie implementeert – kunt bepalen wat een gegeven turingmachine doet – weet waarvoor turingmachines bedacht zijn – Turing’s thesis kunt uitleggen – de definitie van een algoritme kunt geven – de Chomsky-hiërarchie kunt beschrijven.

Studeeraanwijzingen Bij deze leereenheid hoort chapter 9 van het tekstboek. Sections 9.1 en 9.3 zijn verplicht, section 9.2 is facultatief. Deze leereenheid bevat een extra paragraaf over de Chomsky-hiërarchie, die ook verplicht is. Hierbij horen de introduction en section 11.4 van chapter 11 uit Linz. De studielast van deze leereenheid bedraagt circa 9 uur.

LEERKERN 1 Studeeraanwijzing

The standard Turing machine

Lees de introduction van chapter 9 in Linz. In de introductie wordt impliciet al verwezen naar de Chomskyhiërarchie, die we in paragraaf 4 bespreken: de familie van reguliere talen is bevat in de familie van context-vrije talen, en er is blijkbaar nog iets ‘boven’ context-vrij, vanwege onder andere {anbncn} en {ww}. Omdat chapter 9 over een speciale soort automaten gaat – de turingmachines – onderzoeken we hier alleen een uitbreiding van de automaten die we al kennen. We zouden echter net zo goed kunnen proberen de grammatica’s die we al kennen uit te breiden, om zo ‘meer’ dan context-vrij aan te kunnen; ook dat komt aan de orde in paragraaf 4. 1.1

Turingmachine

Voorbeeld 9.1

DEFINITIE VAN EEN TURINGMACHINE

We geven een korte, informele beschrijving van een standaard turingmachine. Een turingmachine is niets anders dan een uitbreiding van de eindige automaat. Een turingmachine heeft dus een invoerband en een eindig aantal toestanden; verder zijn er drie verschillen met de eindige automaat. Ten eerste kan op de invoerband van een turingmachine ook geschreven worden, en wel door het symbool onder de kop te vervangen door één ander symbool. Ten tweede kan die kop niet alleen steeds één cel naar rechts bewegen, maar ook terug naar links (ook met één cel tegelijk). De band fungeert bij deze automaat dus als een geheugen waar zowel informatie uit gelezen als in geschreven kan worden, en waar de automaat vrij toegang toe heeft. Ten derde is de invoerband oneindig: de automaat kan er zoveel op schrijven als hij wil. In het begin kan er een invoerstring op staan, en zowel links als rechts daarvan zijn dan oneindig veel lege cellen. Zie figure 9.1 in Linz voor een schematische weergave van een turingmachine. We geven een voorbeeld van een algoritme voor een concrete turingmachine, om u een idee te geven van de werking van turingmachines en van de kracht van deze eenvoudige verzameling instructies. Neem aan dat aan het begin de invoerband een aaneengesloten rij van n enen bevat, die een unaire representatie van het getal n voorstelt. De bedoeling is de volgende functie te berekenen, voor n > 0: f(n) = n/2 f(n) = (n +1)/2

als n even is, en als n oneven is

Dus als we starten met de string 111111 op de band moet het resultaat 111 zijn, en als we starten met 1111111 moet het resultaat 1111 zijn. Hoe zou u dit aanpakken, met alleen de toegestane instructies van een turingmachine? De turingmachine begint met uit te zoeken of het aantal enen even of oneven is, analoog aan hoe een eindige automaat dat aan zou pakken: met behulp van twee toestanden q0 en q1. Hij begint in toestand q0 met de kop op de meest linkse 1. Hij loopt symbool voor symbool door de string naar rechts en alterneert onderweg tussen q1 en q0, tot hij een lege cel tegenkomt. Die lege cel laat hij dan zo, hij doet een stapje terug, en bepaalt op basis van de toestand waarin hij zich bevindt wat er nu moet gebeuren. Als het aantal enen oneven is (hij zat in q1) laat hij de meest rechtse 1 (waar hij nu op staat) ongemoeid, doet een stapje naar links en markeert de voorlaatste 1, bijvoorbeeld door er een X voor in de plaats te zetten. Dan loopt hij (stapje voor stapje) helemaal naar links en haalt de meest linkse 1 weg. Vervolgens loopt hij weer naar rechts tot hij de X ziet, vervangt die weer door een 1, en vervangt de 1 er net vóór nu door een X, loopt weer naar links om de eerste 1 weg te halen enzovoort. Hierbij gebruikt hij natuurlijk allerlei toestanden om bij te houden in welke fase hij zit. Op een gegeven moment is er geen 1 meer om te vervangen door een X. Dan doet hij een stapje naar rechts, en blijft op de eerste 1 staan om aan te geven dat daar het antwoord begint. Als het aantal enen even is (hij zat in q0) hoeft hij alleen maar de stap met de meest rechtse 1 hierboven over te slaan. We plaatsen drie opmerkingen bij dit voorbeeld: – Een turingmachineprogramma heeft wel wat weg van een programma in machinecode voor een ‘echte’ computer. Dat is niet zo gek, want de turingmachine doet onder andere dienst als theoretisch model van een ‘echte’ computer. – Het bedenken van het algoritme dat de functie implementeert is niet zo moeilijk – als we tenminste gewend zijn aan de instructies die een turingmachine kan uitvoeren – maar het opschrijven ervan in natuurlijke taal kost al gauw aardig wat tijd. U zult later merken dat het vertalen van dit programma naar echte turingmachine-instructies nog wat lastiger en tijdrovender is (ook dat is overigens vergelijkbaar met het schrijven van programma’s in machinecode). – We hebben nu laten zien hoe een turingmachine een functie kan berekenen, maar turingmachines kunnen ook talen (of andere verzamelingen) herkennen, net als de andere automaten die we gezien hebben. Het herkennen dat een string element is van een taal is namelijk niets anders dan die string lezen, en positief antwoorden als de string tot de taal behoort en negatief als dat niet zo is. We geven nu een formele definitie van een turingmachine. Die definitie is tot stand gekomen door definition 9.1, definition 9.3 en allerlei extra informatie in de tekst van section 9.1 te combineren. U hebt zo alle relevante informatie overzichtelijk bij elkaar. Straks vragen we u section 9.1 te bestuderen, waarin deze informatie nog eens stap voor stap wordt aangeboden, met uitleg en voorbeelden.

Definitie turingmachine

Een turingmachine M wordt gedefinieerd door M = (Q, , , , q0, □, F) met Q    – {□}   : (Q – F)    Q    {L, R} q0  Q □ FQ

Momentane beschrijving Configuratie

Stoppen

Afspraak

Berekening

Voorbeeld 9.2

een eindige verzameling toestanden, het (eindige) invoeralfabet, het (eindige) bandalfabet, de (eventueel partiële) overgangsfunctie, de begintoestand, een speciaal symbool (blanco), de (eindige) verzameling eindtoestanden.

Verder geldt: – De band is tweezijdig oneindig, dat wil zeggen dat zowel links als rechts van de invoerstring oneindig veel lege cellen (blanco’s) staan. – De invoerstring staat op een aaneengesloten stuk van de band (er staan geen blanco’s of andere niet-invoersymbolen tussen). – Een toestandsovergang (p, a) = (q, b, L) betekent: als de turingmachine in toestand p is en een a op de band leest, gaat hij naar toestand q, vervangt de a door een b en verplaatst de kop één cel naar links. De uitleg voor (p, a) = (q, b, R) is analoog. – Net als bij stapelautomaten gebruiken we de momentane beschrijving (of configuratie) om de werking van een turingmachine te beschrijven. Een momentane beschrijving is een element van *Q* dat niet met □ begint of eindigt (we laten de oneindige rijen lege cellen links en rechts dus weg). De momentane beschrijving upav geeft aan dat de inhoud van de band uav is, en dat de turingmachine in toestand p is met de kop op a. Als de kop op een blanco net vóór of net achter de inhoud van de band staat, schrijven we toch een □ in de configuratie: p□u of up□. – Net als bij stapelautomaten gebruiken we ⊢ als stapteken. We schrijven ucpav ⊢ uqcbv als (p, a) = (q, b, L), en ucpav ⊢ ucbqv als (p, a) = (q, b, R). We gebruiken * voor een opeenvolging van nul of meer stappen. – We zeggen dat een turingmachine stopt in een configuratie upav als er geen overgang mogelijk is in die configuratie, met andere woorden als (p, a) ongedefinieerd is. Toestand p heet dan een stoptoestand (voor die configuratie; er kunnen ook configuraties zijn waarvoor de turingmachine niet stopt in p). – We zorgen ervoor dat de turingmachine stopt zodra hij in een eindtoestand komt, dat wil zeggen we zorgen ervoor dat voor iedere f  F geldt dat (f, a) ongedefinieerd is voor alle a   (ofwel: een eindtoestand heeft nooit uitgaande pijlen). Dit is de reden dat we in onze uitgebreide versie van definition 9.1 de definitie van  hebben aangepast. – Als de turingmachine stopt in de laatste van een rij opeenvolgende configuraties noemen we die rij een berekening. – Onze turingmachine is deterministisch. Er zijn verschillende andere definities mogelijk, die allemaal equivalent zijn (niet-deterministisch, met meer dan één band enzovoort; liefhebbers verwijzen we naar chapter 10 voor een overzicht). We hebben het programma van voorbeeld 9.1 geïmplementeerd als concrete turingmachine. Hieronder ziet u het resultaat: een turingmachine M = (Q, , , , q0, □, F) met toestandsverzameling {q0, q1, …, q8}, invoeralfabet  = {1}, bandalfabet  = {1, X, □} en verzameling eindtoestanden F = {q8}. In de eindtoestand q8 stopt de turingmachine, zoals afgesproken.

U ziet dat we van een turingmachine een plaatje kunnen tekenen dat erg lijkt op een stapelautomaat (alleen zijn de , en de ; in de labels bij de pijlen omgedraaid). De pijl met label 1;X,L van q3 naar q4 staat voor de overgang (q3, 1) = (q4, X, L), ofwel: als de turingmachine in toestand q3 is, met de kop op een 1, dan gaat hij over in toestand q4, vervangt de 1 door een X en gaat één cel naar links. Een berekening van f(3) = (3+1)/2 = 2 ziet er als volgt uit: q0111 ⊢ 1q111 ⊢ 11q01 ⊢ 111q1□ ⊢ 11q21 ⊢ 1q311 ⊢ q41X1 ⊢ q4□1X1 ⊢ q51X1 ⊢ q6X1 ⊢ q7□11 ⊢ q811 OPGAVE 9.1

Geef de berekening van M als hij start met 11 op de band. Geeft hij het verwachte antwoord? JFLAP

U kunt de werking van deze turingmachine met de hand nagaan voor een aantal strings, of met behulp van de bouwsteen voorbeeld09.2.jff en de optie Input Step… in JFLAP. U kunt die optie voor twee doeleinden gebruiken: om het algoritme van voorbeeld 9.1 in werking te zien, dus zonder op implementatiedetails te letten, of om juist wel op implementatiedetails te letten en de turingmachine te debuggen. Om de werking van het algoritme van voorbeeld 9.1 te volgen, hoeft u alleen naar de configuratie onderin te kijken en steeds op de knop Step te klikken. U ziet dan de kop heen en weer bewegen en de inhoud van de band veranderen. Voor het debuggen of doorgronden van de implementatie van een turingmachine raden we u aan om bij iedere configuratie eerst zelf te bepalen welke overgang toegepast zal gaan worden, dan op de knop Step te klikken en te kijken of het klopt.

OPDRACHT 9.2 Gebruik JFLAP

om te kijken hoe het algoritme van voorbeeld 9.1 werkt, en om de berekening van de turingmachine van voorbeeld 9.2 voor een paar (kleine) invoerstrings te volgen. U vindt deze turingmachine in de bouwsteen voorbeeld09.2.jff. Studeeraanwijzing

Bestudeer de introductie en subsection Definition of a Turing machine van section 9.1 in Linz. Merk op dat we nu weten wat een turingmachine allemaal mag, maar nog niet wat de bedoeling van het geheel is. Moet hij de invoerstring lezen en dan wel of niet accepteren? Moet hij de invoerstring omzetten

Accepter

Transducer

in iets anders? Moet hij stoppen als de invoerstring ‘goed’ is en anders niet? Moet hij überhaupt iets met de invoerstring doen? Het antwoord is: dat ligt eraan waar je de turingmachine voor wilt gebruiken. We noemen drie mogelijkheden: – We kunnen een turingmachine beschouwen als een automaat die een taal herkent (accepter). De taal die door M wordt geaccepteerd is L(M) = {w  + : q0w * x1qfx2 voor een qf F en x1, x2  *}. We bespreken accepters in paragraaf 1.2. – We kunnen een turingmachine beschouwen als een automaat die een functie f implementeert (transducer): gegeven een bepaalde invoer w op de band zal de turingmachine op een gegeven moment stoppen met f(w) op de band en de kop op het eerste symbool van f(w): q0w * qf f(w) voor een qf  F. We hebben al een transducer gezien in voorbeeld 9.2; in paragraaf 1.3 zien we er meer. – We kunnen een turingmachine beschouwen als een automaat die alle elementen van een taal opsomt. In dat geval kan het dus zijn dat de turingmachine nooit stopt! Neem bijvoorbeeld de verzameling van natuurlijke getallen {1, 2, 3, 4, …}. Een turingmachine kan die (oneindige) verzameling opsommen (in unaire representatie) door te starten met een lege band, dan één 1 op de band te schrijven, daar vervolgens één 1 achter te zetten, en daar weer één 1 achter, en zo verder. We moeten dan een speciale toestand aanwijzen zodat we weten dat steeds als de turingmachine in die toestand komt we een nieuw element kunnen aflezen. Deze laatste soort turingmachines komt overigens niet aan de orde in deze leereenheid (op één vermelding in paragraaf 4.2 na); we noemen hem hier als zinvol voorbeeld van een turingmachine die nooit stopt – dat wil zeggen het niet-stoppen is hier de bedoeling. 1.2

TURINGMACHINES VOOR HET HERKENNEN VAN TALEN

Studeeraanwijzing

Bestudeer subsection Turing machines as language accepters van section 9.1 van Linz.

Afspraak

Als we u vragen een turingmachine te construeren, verwachten we altijd ook een beschrijving in natuurlijke taal of pseudocode van het idee achter uw algoritme (Linz noemt deze fase ‘design’, en vraagt daar niet altijd expliciet om). Dat hoeft niet tot in het kleinste detail, zolang er maar uit blijkt dat het wel zal werken. In uw implementatie (de verzameling toestandsovergangen) werkt u dan – indien gevraagd – de details uit (‘construct’).

JFLAP

Bij alle opgaven waar u gevraagd wordt een turingmachine te construeren, raden we u aan eerst uw uitwerking op papier te maken en die daarna te controleren in JFLAP, met een geschikte (weldoordachte) verzameling teststrings. Kies daarvoor zowel strings die geaccepteerd moeten worden als strings die afgewezen moeten worden (in het geval van een accepter)! Als het goed is bent u intussen zo bedreven in het gebruik van JFLAP dat het invoeren van een turingmachine en een aantal teststrings u hooguit een paar minuten kost. Merk op dat JFLAP naast de aanduidingen L en R voor de beweegrichting van de kop een optie S heeft, voor ‘sta stil’ (stay put), die wij niet gebruiken. Het testen gaat het snelst met de optie Input Multiple Run voor accepters en Input Multiple Run (Transducer) voor transducers; bij deze laatste hoeft u de gewenste uitkomsten niet zelf in te vullen!

Let op

Wees u er wel van bewust dat u het testen tijdens het tentamen met de hand zult moeten doen...

OPGAVE 9.3

Maak exercise 3 van section 9.1 in Linz. OPGAVE 9.4

Maak exercise 4 van section 9.1 in Linz. U kunt uw antwoord controleren in JFLAP; de turingmachine van example 9.7 is te vinden in JFLAP Activities JFLAP FilesJFLAP Examples onder de naam Jexample9.7.jff. OPGAVE 9.5

a De band van een turingmachine loopt oneindig ver door naar links en naar rechts. Betekent dit dat er tijdens een berekening oneindig veel nietblanco’s op kunnen staan? b Is er een invoerstring waarvoor de turingmachine van example 9.7 in een oneindige lus komt? (exercise 5 van section 9.1 in Linz) OPGAVE 9.6

Maak exercise 6 van section 9.1 in Linz. Doe dit eerst met de hand, en controleer daarna in JFLAP. OPGAVE 9.7

Construeer turingmachines die de volgende talen over {a. b} accepteren: a L = L(aba*b) b L = { w : wis een drievoud } c L = { anbm : n  1, n  m } d L = { w : na(w) = nb(w) } OPGAVE 9.8

Maak exercise 9 van section 9.1 in Linz. Het is voldoende als u een algoritme beschrijft; als u wilt kunt u de turingmachine ook nog implementeren, maar dat zal vrij veel werk zijn. OPGAVE 9.9

Maak exercise 20 van section 9.1 in Linz. 1.3 Studeeraanwijzing

TURINGMACHINES ALS TRANSDUCERS

Bestudeer subsection Turing machines as transducers van section 9.1 van Linz.

OPGAVE 9.10

Maak exercise 10 van section 9.1 in Linz. Denk er bij het testen van transducers aan (als u daarvoor JFLAP gebruikt tenminste) dat u voor InputMultiple Run (Transducer) kiest! OPGAVE 9.11

Construeer en test turingmachines die de volgende functies berekenen. Hierin zijn x en y positieve integers, unair gerepresenteerd. a f(x) = 3x b f(x) = x mod 5 OPGAVE 9.12

Maak exercise 14 van section 9.1 in Linz.

2 Studeeraanwijzing

Lees section 9.2 van Linz. Deze section laat zien hoe met de eenvoudige instructies van een turingmachine steeds ingewikkeldere programma’s geschreven kunnen worden. Deze section is facultatief. 3

Studeeraanwijzing

Combining Turing machines for complicated tasks

Turing’s thesis

Bestudeer section 9.3 van Linz.

OPGAVE 9.13

Maak exercise 1 bij section 9.3 van Linz. 4

De Chomsky-hiërarchie

In paragraaf 1 hebben we beloofd dat we hier zouden laten zien hoe we de vraag “Wat komt er na de context-vrije talen?” kunnen beantwoorden met behulp van grammatica’s in plaats van met automaten. We gaan daarvoor terug naar definition 1.1, die we hier herhalen, uitgebreid met wat extra informatie uit de begeleidende tekst op pagina 21-22 in Linz. Definition 1.1 revisited

Een grammatica G is een viertupel G = (V, T, S, P), met V T SV P  (V  T)+  (V  T)*

een eindige verzameling hulpsymbolen, een eindige verzameling eindsymbolen, het startsymbool, en een eindige verzameling producties.

Hierbij geldt dat V en T beide niet-leeg zijn, en dat V  T = . Producties worden opgeschreven als x  y, met x  (V  T)+ en y  (V  T)*. Als x een enkel hulpsymbool is en y een enkel eindsymbool of een eindsymbool gevolgd door een hulpsymbool (x  V en y  TV), dan heet de grammatica rechtslineair (en regulier). Als x een enkel hulpsymbool is (x  V) en aan y worden geen extra eisen gesteld, dan heet de grammatica context-vrij. 4.1

Monotone grammatica Context-gevoelige taal

CONTEXT-GEVOELIGE TALEN

We noemen nu een derde beperking aan de vorm van producties, die nog niet eerder aan de orde is geweest. Als voor alle producties x  y geldt dat x  y, dan noemen we de grammatica monotoon. De familie van talen die door monotone grammatica’s worden gegenereerd heet de familie van monotone of context-gevoelige talen. Die laatste naam is afkomstig van een normaalvorm voor context-gevoelige grammatica’s, waarin iedere productie van de vorm uAv  uyv is, met y ongelijk aan . Je zou kunnen zeggen dat zo’n productie specificeert in welke context de productie A  y toegepast mag worden. Dit is in scherp contrast met context-vrije grammatica’s, waarin een productie A  y juist ongeacht de context van A toegepast mag worden: iedere A in een zinsvorm (sentential form) mag worden vervangen door y. We gaan weer even terug naar de ‘gewone’ monotone grammatica, dus met producties van de vorm x  y met x  y. Merk op dat hieruit volgt dat een monotone grammatica geen -producties heeft en dus niet

Lidmaatschapsprobleem voor context-gevoelige talen

 kan genereren! Hier is dus weer een reden om  als een apart geval te behandelen. Merk op dat dit niet uitmaakt; zie de opmerking over wel/geen  op pagina 156-157 in Linz. Verder geldt dat de zinsvormen uit een afleiding in een monotone grammatica nooit kleiner worden: een linkerkant van een productie wordt immers altijd vervangen door een rechterkant die minstens zo lang is. Dat is een prettige eigenschap, omdat we de grammatica daardoor in een normaalvorm kunnen brengen die toestaat te schatten hoe lang een afleiding kan zijn van een string met een gegeven lengte (vergelijk de Chomsky- en Greibach-normaalvormen voor context-vrije grammatica’s, die ook zulke schattingen opleveren). Die schatting wordt dan weer gebruikt om vast te stellen of een gegeven monotone grammatica een gegeven string al dan niet voortbrengt. Met andere woorden: het lidmaatschapsprobleem voor context-gevoelige talen is beslisbaar. We geven een voorbeeld van een grammatica die niet regulier is en niet context-vrij, maar wel monotoon: S  aSBCaBC CB  BC aB  ab bB  bb bC  bc cC  cc Deze grammatica genereert de taal {anbncn : n > 0}, waarvan we weten dat die niet context-vrij is (example 8.1).

OPGAVE 9.14

Ga na dat de monotone grammatica die hierboven gegeven is inderdaad {anbncn : n > 0} genereert. U hoeft alleen uzelf te overtuigen, en dus geen formeel bewijs te geven.

Generatieve kracht

Iedere -vrije context-vrije grammatica is monotoon; de linkerkant bestaat immers altijd uit één hulpsymbool, terwijl de rechterkant niet leeg mag zijn. De familie van context-vrije talen is dus een echte deelverzameling van de familie van context-gevoelige talen. Het is interessant om te vermelden dat, met uitzondering van het lidmaatschapsprobleem, alle voor de hand liggende vragen (dus de vragen die we eerder stelden over reguliere en context-vrije talen) voor context-gevoelige talen onbeslisbaar zijn. Monotone grammatica’s hebben een grotere generatieve kracht dan context-vrije talen (‘ze kunnen meer talen genereren’), maar dat heeft dus wel een prijs: veel eigenschappen van de gegenereerde talen kunnen niet meer op een systematische manier onderzocht worden. Niet alle talen zijn context-gevoelig. Niet-context-gevoelige talen zijn echter geen gemakkelijk te definiëren verzamelingen zoals {anbncn : n > 0} of {anbncndn: n > 0} (die laatste is namelijk ook context-gevoelig!). Als u graag een voorbeeld ziet van een taal die niet context-gevoelig is, kunt u in het bewijs van theorem 11.11 kijken. Maar u kunt ook gewoon van ons aannemen dat er niet-context-gevoelige talen bestaan, en dat de meeste talen die iemand zou willen kunnen genereren door een monotone grammatica gegenereerd kunnen worden.

Lineair begrensde automaat

Ook voor de context-gevoelige talen bestaat een automaat die precies alle talen uit die familie accepteert. Deze heet de lineair begrensde automaat (linear bounded automaton) en heeft dezelfde eigenschappen als een turingmachine plus één beperking, die de automaat zijn naam heeft gegeven: de automaat mag alleen het deel van de band gebruiken waar de invoer op staat. Als de invoer uit twee symbolen bestaat, dan kan het geheugen (de invoerband dus) gedurende die berekening nooit meer dan twee symbolen bevatten. Als de invoer uit tienduizend symbolen bestaat, dan is gedurende die berekening het geheugen tienduizend symbolen groot. 4.2

Type-0-grammatica Onbeperkte grammatica

Recursief opsombare talen

RECURSIEF OPSOMBARE TALEN

We hebben nog een voor de hand liggende soort grammatica’s over, namelijk de grammatica’s die – zonder verdere beperkingen – voldoen aan definition 1.1. Deze grammatica’s staan bekend onder verschillende namen, waarvan we er twee noemen: type-0-grammatica en onbeperkte grammatica (unrestricted grammar). Door het laten vallen van de beperking x  y wordt de generatieve kracht van de grammatica opnieuw vergroot. Er bestaan dus talen die niet context-gevoelig zijn, maar wel worden gegenereerd door een type-0-grammatica. De familie van alle talen die worden gegenereerd door type-0-grammatica’s heet de familie van recursief opsombare talen (recursively enumerable languages). Voor deze familie van talen is zelfs het lidmaatschapsprobleem onbeslisbaar. Het zal u niet verbazen dat de automaat die bij de familie van recursief opsombare talen hoort de turingmachine is. Iedere recursief opsombare taal wordt geaccepteerd door een turingmachine, en iedere taal die door een turingmachine wordt geaccepteerd is recursief opsombaar. De naam recursief opsombaar komt van de derde kijk op turingmachines die we in paragraaf 1 noemden: de turingmachine als automaat die alle elementen van een taal (verzameling) opsomt. Er bestaan ook talen die niet recursief opsombaar zijn, maar die zijn erg moeilijk te vinden en lastig te begrijpen. Linz zegt hierover (pagina 289, net voor theorem 11.3): “Since every language that can be described in a direct algorithmic fashion can be accepted by a Turing machine and hence is recursively enumerable, the description of a language that is not recursively enumerable must be indirect. Nevertheless, it is possible. The argument involves a variation on the diagonalization theme”. 4.3

Chomskyhiërarchie

DE CHOMSKY-HIËRARCHIE

We kunnen de door grammatica’s gegenereerde talen dus indelen in vier families: de reguliere talen, de context-vrije talen, de context-gevoelige talen en de recursief opsombare talen. Iedere familie is een echte deelverzameling van haar opvolger. Verder hoort bij iedere familie een soort automaat die precies alle talen uit die familie accepteert. Deze vier families vormen samen de zogeheten Chomsky-hiërarchie. Deze hiërarchie is vernoemd naar de taalkundige Noam Chomsky, die de vier families en de bijbehorende grammatica’s definieerde tijdens zijn onderzoek naar mogelijke grammatica’s voor natuurlijke talen; hij noemde ze

respectievelijk type-3, type-2, type-1 en type-0. In de literatuur komen we ook vaak de iets makkelijker te onthouden afkortingen REG, CF, CS en RE voor de vier families tegen. In tabel 9.1 geven we een overzicht van de Chomsky-hiërarchie. In de kolom ‘structuur’ proberen we kort aan te geven wat de karakteristieke verschillen tussen de families zijn; een taal als {anbn : n ≥ 0} is natuurlijk niet de enige taal die wel in CF zit maar niet in REG. De opmerking ‘bijna alles’ in de kolom ‘structuur’ bij RE is met opzet vaag gehouden; we bedoelen daarmee dat als iemand een idee voor een turingmachineprogramma in zijn hoofd heeft, dat programma altijd daadwerkelijk te implementeren is op een turingmachine. We bedoelen er niet mee dat bijna alle talen recursief opsombaar zijn; er zijn namelijk heel veel talen die niet recursief opsombaar zijn! De Chomsky-hiërarchie

TABEL 9.1 type

familie

‘structuur’

automaat

grammatica

3

REG

{(ab)n : n ≥ 0}, {anbm : n, m ≥ 0}

eindige automaat

reguliere grammatica

2

CF

{anbn : n ≥ 0}

stapelautomaat

context-vrije grammatica

1

CS

{anbncn: n > 0}

lineair begrensde automaat

contextgevoelige grammatica

0

RE

‘bijna alles’

turingmachine

onbeperkte grammatica

Tabel 9.1 toont de originele Chomsky-hiërachie, maar er zijn nog meer talenfamilies te onderscheiden:  de familie FIN van eindige talen. We weten dat hiervoor geldt FIN  REG (opgave 1 van de zelftoets van leereenheid 2).  de familie LIN van lineaire talen. We weten dat REG  LIN  CF. (De eerste inclusie wordt beschreven in example 3.14, de tweede in paragraaf 1 van leereenheid 5).  de familie DCF van deterministisch context-vrije talen. Daarvan weten we dat DCF  CF (definition 7.3 en example 7.11). Merk op dat dus zowel LIN  CF als DCF  CF, maar dat we nog niets weten over de relatie tussen LIN en DCF. Uitsluitsel daarover vindt u in example 11.3 van section 11.4.  de familie REC van recursieve talen; die hebben we nog niet gezien. Een taal heet recursief als zij wordt geaccepteerd door een turingmachine die stopt voor iedere invoer. Er geldt dat CS  REC  RE (we gaan hier verder niet op in; als u wilt kunt u dit nalezen in chapter 11). Studeeraanwijzing

Lees, bij wijze van samenvatting, de introduction van chapter 11, en section 11.4 uit Linz. Als u wilt, kunt u de rest van chapter 11 ook eens doorbladeren. Tot slot: in opgave 1.2 van leereenheid 1 hebben we het gehad over de programmeertaal Java. Daarvan hebben we toen gezegd dat de verzameling keywords en de verzameling identifiers reguliere talen zijn, en dat de verzameling welgevormde programma’s een context-vrije taal is.

Daar kunnen we nu nog iets aan toevoegen: Java heeft turingkracht, dat wil zeggen: alle algoritmen die we met een turingmachine kunnen uitvoeren kunnen we ook in Java implementeren. Dat is prettig (om niet te zeggen noodzakelijk voor een general purpose language als Java), want anders zou het zomaar kunnen gebeuren dat we een of ander algoritme willen implementeren maar dat dat met Java nu net niet kan! Het is voor computertalen overigens niet noodzakelijk om turingkracht te hebben; er bestaan ook heel bruikbare talen zonder turingkracht, zoals de databasetaal SQL.

Turingkracht

ZELFTOETS

1

Construeer een turingmachine met bandalfabet  = {a, b, □} die de taal L = {anbn : n > 0} accepteert (alternatief voor de oplossing in example 9.7).

2

Construeer een turingmachine die de functie f gedefinieerd door f(x, y) = x – y f(x, y) = 0

als x > y, en als x  y

berekent. 3

a Welke taal genereert onderstaande onbeperkte grammatica? (exercise 1 van section 11.2) S  S1B S1  aS1b bB  bbbB aS1b  aa B  b Als we de laatste productieregel B   verwijderen, krijgen we dan een context-gevoelige grammatica?

TERUGKOPPELING 1

Uitwerking van de opgaven

9.1

Als de turingmachine start met 11 op de band, dan is hij in q0 met de kop op de voorste 1. De berekening wordt dan q011 ⊢ 1q11 ⊢ 11q0□ ⊢ 1q31 ⊢ q41X ⊢ q4□1X ⊢ q51X ⊢ q6X ⊢ q7□1 ⊢ q81. Omdat f(2) = 2/2 = 1 is dit precies het gewenste antwoord.

9.2

Geen uitwerking.

9.3

Een oplossing met drie toestanden is

Er zijn nog veel meer mogelijkheden; het maakt bij een accepter namelijk totaal niet uit hoe de band er na afloop uitziet. Het enige wat van belang is, is dat de turingmachine stopt in een eindtoestand. In het bijzonder is (q1, b) = (q1, b, R) natuurlijk ook goed; de turingmachine doet dan echt alleen de voor de hand liggende eindige automaat na. Het is ook mogelijk met slechts twee toestanden: de turingmachine M = ({q0, q1}, {a, b}, {a, b, □}, , q0, □, {q1}) met (q0, a) = (q1, a, R) kijkt alleen maar of het eerste symbool een a is; wat daarna nog komt maakt niet uit (vanwege het invoeralfabet weten we dat het alleen a’s en/of b’s kunnen zijn). Een turingmachine hoeft niet de hele string uit te lezen voor hij kan stoppen en accepteren. 9.4

Als de turingmachine wordt gestart met aba op de band voert hij de volgende berekening uit: q0aba ⊢ xq1ba ⊢ q2xya ⊢ xq0ya ⊢ xyq3a ⊢ ?? (loopt vast). Hij stopt dus in toestand q3 met een a onder de kop. Omdat q3 geen eindtoestand is wordt aba niet geaccepteerd. Als de turingmachine wordt gestart met aaabbbb op de band gebeurt het volgende: q0aaabbbb ⊢ xq1aabbbb * xaaq1bbbb ⊢ xaq2aybbb ⊢ xq2aaybbb ⊢ q2xaaybbb ⊢ xq0aaybbb ⊢ xxq1aybbb ⊢ xxaq1ybbb ⊢ xxayq1bbb ⊢ xxaq2yybb ⊢ xxq2ayybb ⊢ xq2xayybb ⊢ xxq0ayybb ⊢ xxxq1yybb ⊢ xxxyq1ybb ⊢ xxxyyq1bb ⊢ xxxyq2yyb ⊢ xxxq2yyyb ⊢ xxq2xyyyb ⊢ xxxq0yyyb * xxxyyyq3b ⊢ ?? (loopt vast). Ook voor aaabbbb stopt hij dus in een niet-eindtoestand.

9.5

a Nee, er kunnen op ieder moment maar eindig veel niet-blanco’s op de band staan. Dat komt omdat we altijd starten met een string uit +, en die is per definitie eindig. Het aantal niet-blanco’s per configuratie is wel willekeurig groot: er is in het algemeen voor een gegeven turingmachine M geen natuurlijk getal n aan te geven zodat we kunnen garanderen dat er nooit meer dan n symbolen op de band zullen staan tijdens een berekening van M. Maar ‘willekeurig veel’ is iets anders dan ‘oneindig veel’.

b Nee, de turingmachine van example 9.7 kan niet in een oneindige lus raken. Dit is het makkelijkst in te zien met het plaatje erbij; u kunt dat met de hand tekenen of Jexample9.7.jff erbij pakken. We zien dat er lusjes zijn bij q1, q2 en q3, en een cykel q0 q1 q2 q0. In het plaatje is duidelijk te zien dat de twee lusjes bij q1 over a’s en y’s heen naar rechts lopen. Omdat de string waarmee we begonnen eindig was, en deze lusjes geen extra symbolen toevoegen, houden de a’s en y’s een keer op, en kan de turingmachine hier dus niet in een oneindige lus komen. Hetzelfde geldt voor de twee lusjes bij q2 en het lusje bij q3. Voor de cykel geldt dat er vooral veel heen en weer wordt gelopen, maar dat het enige wat verandert is dat er a’s in x’en worden veranderd en b’s in y’s. Op een gegeven moment zijn de a’s of de b’s op, dus ook hier kan de turingmachine niet in een oneindige lus komen. 9.6

Deze vraag is een instinker: het pad van q0 via q1 naar q3 controleert op het bereiken van het einde van de invoerstring (□). Via dit pad wordt dus alleen ab* geaccepteerd (de turingmachine doet gewoon een eindige automaat na). Het pad van q0 via q2 naar q3 echter controleert niet op het bereiken van het einde van de invoerstring; dit deel gaat dus alleen na dat de invoerstring begint met bb*a. Daarna kunnen nog willekeurig veel a’s en of b’s komen (een controle met JFLAP bevestigt dit alles). De turingmachine accepteert dus de taal L(ab* + bb*a(a + b)*).

9.7

a De taal L(aba*b) is regulier, dus we hoeven alleen de voor de hand liggende eindige automaat te simuleren, én te checken dat we de hele invoerstring gelezen hebben (dit laatste is in dit geval echt nodig; gaat u maar na met JFLAP). We kunnen de gevraagde turingmachine dus meteen geven (bouwsteen opgave09.7a.jff):

Natuurlijk zijn er ook andere mogelijkheden, waarbij gelezen letters worden gemarkeerd of verwijderd. Dat is echter niet nodig, en we hebben ervoor gekozen zo dicht mogelijk bij de eindige automaat te blijven. b Ook de taal L = {w  {a, b}* : w is een drievoud} is regulier. Ook hier kiezen we een turingmachine die de voor de hand liggende eindige automaat zo precies mogelijk simuleert (bouwsteen opgave09.7b.jff):

c De taal L = {anbm : n ≥ 1, n  m} is niet regulier, maar context-vrij. We kiezen voor de volgende aanpak: we verwijderen de eerste a en lopen dan helemaal naar rechts om de laatste b te verwijderen. Dan weer helemaal naar links, de eerste a verwijderen, en helemaal naar rechts om de laatste b te verwijderen enzovoort. Op een gegeven moment zijn, als het goed is, de a’s op maar de b’s nog niet (dat wil zeggen, we zien een b terwijl we een a verwachten), of de b’s op maar de a’s nog niet (we kunnen geen b vinden). Een implementatie is de volgende (bouwsteen opgave09.7c.jff):

Let op de overgang (q2, □) = (q4, □, R); die volgt niet rechtstreeks uit de beschrijving, maar is nodig voor het geval dat n = m + 1 (ga dat na). d De taal L = {w  {a, b}* : na(w) = nb(w)} is context-vrij. Voor een turingmachine voor L kiezen we de volgende aanpak. We lopen van links naar rechts door de string. Als het eerste ongemarkeerde symbool een a is markeren we die a (we vervangen hem bijvoorbeeld door x) en gaan op zoek naar een b; als die gevonden is markeren we die (ook maar met een x). Als het eerste ongemarkeerde symbool een b is gaan we natuurlijk op zoek naar een a. Vervolgens lopen we terug naar het begin en kijken wat het eerste ongemarkeerde symbool is. Als we geen ongemarkeerde symbolen meer kunnen vinden zijn we klaar.

We krijgen dan de volgende turingmachine (bouwsteen opgave09.7d.jff):

Deze turingmachine kunnen we optimaliseren door de overgang δ(q0, x) = (q0, x, R) te vervangen door δ(q0, x) = (q0, □, R). De markeringen aan de linkerkant van de invoer kunnen gewist worden om hiermee de zoekruimte voor de volgende cykel te verkleinen. NB

9.8

De taal L = {ww : w  {a, b}+} is niet context-vrij. Voor een turingmachine is dat echter geen probleem: we gaan eerst op zoek naar het midden van de invoerstring. Dat kan bijvoorbeeld door heen en weer te lopen door de string en de eerste en laatste letter te markeren, de tweede en de voorlaatste enzovoort. We moeten wel even nadenken over die markeringen: we moeten straks nog na kunnen gaan dat de string bestaat uit twee gelijke substrings, dus we moeten nog wel kunnen zien waar de a’s en waar de b’s zitten. Bovendien moeten we – als we tenminste geen nieuw symbool tussen de twee substrings zetten, en dat zijn we in deze oplossing inderdaad niet van plan – nog onderscheid kunnen maken tussen de eerste en de tweede helft. Dat leidt tot een markering zoals de volgende: de a’s en b’s in de eerste substring worden respectievelijk A’s en B’s, de a’s en b’s in de tweede substring worden respectievelijk X’en en Y’en. We hebben het midden gevonden dan en slechts dan als alle symbolen gemarkeerd zijn. Dan begint de volgende fase: nagaan dat de eerste helft gelijk was aan de tweede helft. Dat gaat eenvoudig door heen en weer te lopen en A’s met X’en te matchen, en B’s met Y’s. Voor de volledigheid geven we een implementatie (bouwsteen opgave09.8.jff):

Hieruit blijkt dus (net als uit example 9.8) dat turingmachines krachtiger zijn dan stapelautomaten. 9.9

Als de verzameling eindtoestanden meer dan één element bevat, maak dan een nieuwe eindtoestand qf, en voeg overgangen (q, a) = (qf, a, R) toe voor alle q  F (de ‘oude’ F) en a  . De oude eindtoestanden blijven dus als (gewone) toestanden bestaan. Op het moment dat de turingmachine in zo’n oude eindtoestand aankomt is duidelijk dat de invoerstring geaccepteerd moet worden, ongeacht wat er nu nog op de invoerband staat. Omdat we natuurlijk geen idee hebben wat er op dat moment onder de kop staat, kunnen we alleen maar zeggen dat, wat er ook staat, we naar de nieuwe eindtoestand moeten.

9.10

We bedenken eerst een algoritme om de gevraagde functie f(w) = wR met w  {0, 1}+ met een turingmachine te berekenen: zet een nieuw symbool (zeg 2) net vóór w op de band. Markeer dan het eerste symbool na de 2 (bijvoorbeeld door er een 3 van te maken – een 2 kan ook, maar met een ander symbool wordt het hopelijk iets overzichtelijker), loop terug en zet dat symbool helemaal vooraan. Loop dan weer naar het eerste symbool na de 3, vervang dat door 3, loop weer helemaal naar voren en zet het symbool daar neer. Als er na de laatste 3 geen 0’en of 1’en meer komen halen we alle 3’en en de 2 weg, lopen terug naar het meest linkse symbool en stoppen daar. Merk op dat we bij een transducer dus vaak nog wat opruimwerk te doen hebben aan het eind! De band moet na afloop immers alleen het antwoord bevatten, met de kop op het eerste symbool daarvan.

Een implementatie is de volgende (bouwsteen opgave09.10.jff):

9.11

a Gevraagd wordt een transducer voor f(x) = 3x, met x unair gerepresenteerd. De bedoeling is duidelijk: we moeten tweemaal de invoerstring achter de invoerstring zetten. Een snelle manier om dat te doen, is voor iedere 1 in de invoerstring twee 1’en aan het eind te zetten. We gebruiken een x om de 1 waar we mee bezig zijn te markeren, en in plaats van 1’en zetten we y’s aan het eind van de string, anders kunnen we de verschillende 1’en niet van elkaar onderscheiden. Een implementatie is de volgende (bouwsteen opgave09.11a.jff):

b Gevraagd wordt een transducer voor f(x) = x mod 5, met x unair gerepresenteerd, ofwel: we moeten uitzoeken wat er van x overblijft als we er zo vaak mogelijk 5 vanaf trekken. In unaire representatie betekent dat: zo vaak mogelijk 11111 weghalen, en de rest die dan overblijft is het antwoord. We moeten hier wel voorzichtig zijn: als we enthousiast beginnen vijf 1’en te wissen, kan het gebeuren dat er geen vijf 1’en beschikbaar zijn, en dat we het wissen dus moeten terugdraaien! Daarom gaan we niet meteen wissen maar – u raadt het al – markeren. Zodra we een vijftal 1’en hebben gemarkeerd lopen we even terug om ze echt te wissen. Een implementatie is de volgende (bouwsteen opgave09.11b.jff):

Merk op dat we bij deze implementatie als vanzelf op de goede plek eindigen, namelijk met de kop op het eerste symbool van het antwoord. NB De enige reden dat we twee eindtoestanden hebben is dat het plaatje dan overzichtelijker is. 9.12

We kunnen niet zomaar naar links of naar rechts lopen om de 0 te zoeken, omdat we van tevoren niet kunnen weten hoever we dan moeten doorlopen. Daarom moeten we het oplossen met heen en weer lopen: we gaan als het ware met een steeds bredere zigzagbeweging om/over het startpunt heen, en alleen als we een 0 zien stoppen we. We gebruiken links en rechts een markering om aan te geven tot hoever we de ‘vorige keer’ gekomen waren.

9.13

We hebben over het hoofd gezien dat een turingmachine, zoals we die nu gedefinieerd hebben, deterministisch is, terwijl een pda nietdeterministisch kan zijn. Daarom kunnen we nog niet zeggen dat turingmachines krachtiger zijn dan stapelautomaten. Merk op dat het hier echt alleen gaat om het argument dat je op de band van een turingmachine naast de invoer een stapel kunt simuleren, en dat een turingmachine ‘dus’ in ieder geval alles kan wat een stapelautomaat ook kan. Alleen op basis hiervan mogen we dat niet concluderen, omdat

stapelautomaten niet-deterministisch kunnen zijn en turingmachines (nog) niet. We hebben natuurlijk allang een taal gezien die niet met een stapelautomaat herkend kan worden maar wel met een turingmachine, dus we weten al dat turingmachines krachtiger zijn dan stapelautomaten, maar het ging nu even om dat ene argument. 9.14

De producties S  aSBC en S  aBC zorgen dat, steeds als er een a wordt gegenereerd, ‘onthouden’ wordt (door middel van de B en de C) dat daarvoor later ook nog een b en een c gegenereerd moeten worden. Het enige wat je met een B kunt doen is namelijk hem vervangen door een b (aB  ab en bB  bb), en evenzo voor C en c (bC  bc en cC  cc). Hierbij is S  aBC de bodem van de recursie, terwijl S  aSBC ervoor zorgt dat er een willekeurig aantal a’s gegenereerd kan worden. De aantallen a’s, b’s en c’s kloppen dus; nu de volgorde nog. Het is namelijk zo dat S bijvoorbeeld de zinsvorm aaaBCBCBC kan afleiden. Hiervoor is de * wisseltruc CB  BC ingebouwd: S  aaaBCBCBC  aaaBBCCBC  aaaBBCBCC  aaaBBBCCC (we hebben het voorkomen van CB dat herschreven wordt onderstreept). U kunt zich nog afvragen of het niet mogelijk is om de B’s en C’s in aaaBCBCBC met behulp van de laatste vier producties toch nog in de verkeerde volgorde naar b’s en c’s te vertalen. Ook daar is aan gedacht, en wel door de context aan te geven waarin een B door b vervangen mag worden: dat mag alleen als net vóór die B een a (het gaat om de eerste b) of een b (het gaat om een b een eindje verderop) staat. Voor het vervangen van C’s door c’s geldt net zoiets. 2

Uitwerking van de zelftoets

1

Vanwege de beperking van het bandalfabet kunnen we niet markeren zoals in example 9.7. Dat is geen probleem: we kunnen in plaats daarvan steeds de eerste a en de laatste b wissen, net zolang tot er niets over is. Een implementatie is de volgende (bouwsteen zelftoets09.1.jff):

2

We willen een turingmachine construeren die de berekening q0w(x)0w(y) * qf w(x – y) uitvoert als x > y, en de berekening q0w(x)0w(y) * qf 0 anders. Hierbij stelt w(x)  {1}+ de unaire representatie van x voor, en analoog voor y. Een mogelijke aanpak is om de laatste 1 van w(y) te

wissen en vervolgens de eerste 1 van w(x) te wissen, en dit te herhalen totdat w(y) op is. Dan de 0 nog weghalen en op de eerste 1 van de rest van w(x) gaan staan. Als w(x) eerder op is dan w(y) halen we alles behalve de 0 weg en gaan op de 0 staan. Een implementatie is de volgende (bouwsteen zelftoets09.2.jff):

3

a

We bekijken de volgende typische afleiding:

* * S aS1bB  aaS1bbB  anS1bnB  an+1bn-1B  an+1bn+1B ...

Op basis hiervan vermoeden we dat de grammatica de volgende taal genereert: L = {an+1bn+k: n  1, k = -1, 1, 3, ...} Enig uitproberen bevestigt dit vermoeden (u hoeft het niet formeel te bewijzen). b Nee, de grammatica die dan ontstaat is niet context-gevoelig. De productieregel aS1b  aa voldoet niet aan de vorm voor contextgevoelige grammatica's: de lengte van de rechterkant is groter dan de lengte van de linkerkant, en de grammatica is dus niet monotoon.

Limits of algorithmic computation Introductie Leerkern 1

213 214

Some problems that cannot be solved by Turing machines 1.1 Computability and decidability 214 1.2 The Turing machine halting problem 214 1.3 Reducing one undecidable problem to another 217

Zelftoets

222

Terugkoppeling 1 2

223

Uitwerking van de opgaven 223 Uitwerking van de zelftoets 226

214

Leereenheid 10

Limits of algorithmic computation

INTRODUCTIE

In deze cursus hebt u kennis gemaakt met verschillende families van formele talen en hebt u drie soorten automaten leren hanteren die in relatie staan met formele talen. Onder die automaten zijn de turingmachines de krachtigste. De stelling van Church-Turing (zie Linz chapter 9 en chapter 13) zegt dat een turingmachine kan worden ontworpen voor elk beslissingsprobleem dat opgelost kan worden met een algoritme. Een oplossing vinden voor een probleem hangt sterk af van de aard van het probleem zelf, en er bestaan problemen waarvoor geen algoritme gevonden kan worden. Zulke problemen heten onbeslisbaar. Voor informatici in het algemeen, en programmeurs in het bijzonder, is het goed om te weten dat er (algoritmische) problemen bestaan waarvoor geen oplossing is. In deze leereenheid maakt u kennis met een aantal problemen die in de categorie onbeslisbaar vallen. Het meest bekende daarvan is het stopprobleem voor turingmachines. Alan Turing heeft in 1936 bewezen dat het stopprobleem onbeslisbaar is. Verder leert u in deze leereenheid de techniek van reductie, die kan helpen om aan te tonen dat een probleem onbeslisbaar is. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – kunt uitleggen wat de concepten beslisbaarheid en berekenbaarheid inhouden – kunt uitleggen hoe reductie gebruikt kan worden bij beslisbaarheidsproblemen – het stopprobleem voor turingmachines, het state-entry-probleem en het blank-tape-stopprobleem kunt omschrijven – kunt aangeven hoe aangetoond kan worden dat het state-entryprobleem en het blank-tape-stopprobleem onbeslisbaar zijn – voor een gegeven eenvoudig probleem met het gebruik van reductie kunt aantonen dat het probleem onbeslisbaar is. Studeeraanwijzingen Bij deze leereenheid hoort chapter 12 van het tekstboek. Alleen section 12.1 behoort tot de leerstof. Aan het begin van de leereenheid maken we tevens gebruik van theorie uit de chapters 10 en 11 van Linz. Deze leereenheid gaat over een moeilijk onderwerp; het kan helpen om de stof twee of drie keer te bestuderen, met tussenpozen van enkele dagen om het te laten bezinken. De studielast van deze leereenheid bedraagt circa 6 uur.

LEERKERN

In de chapters 10 en 11 van Linz (die we niet bestudeerd hebben) staan een paar belangrijke onderdelen die we hier ter introductie op deze leereenheid willen presenteren. Universele turingmachine

Bestudeer de beschrijving van de universele turingmachine in section 10.4 op de pagina's 276 en 277 tot en met de zin “Finally, tapes 2 and 3 will be modified to reflect the result of the move”. Belangrijk is dat u inziet dat een turingmachine een andere vorm kan aannemen dan u in leereenheid 9 hebt geleerd. Belangrijk is ook dat u begrijpt hoe de werking van een turingmachine met behulp van nullen en enen beschreven kan worden.

Recursief opsombaar

Een taal L over een alfabet  is recursief opsombaar als er een turingmachine bestaat die de taal accepteert; zie hiervoor leereenheid 9 en definition 11.1 in Linz. Elke string uit L brengt de turingmachine in een eindtoestand. Voor strings die niet tot L behoren is niets gespecificeerd.

Recursieve taal

Een taal L over een alfabet  is recursief als er een turingmachine bestaat die L accepteert en die stopt voor elke string uit +; zie hiervoor definition 11.2 in Linz. Zo’n turingmachine kan dus voor iedere string beslissen of die string wel of niet tot de taal behoort.

Studeeraanwijzing

Lees nu de introductie op chapter 12 in het tekstboek van Linz.

Studeeraanwijzing

1

Some problems that cannot be solved by Turing machines

1.1

COMPUTABILITY AND DECIDABILITY

Bestudeer in section 12.1 van het tekstboek van Linz de introductie en subsection Computability and decidability.

OPGAVE 10.1

a Leg uit wat precies bedoeld wordt met “By a problem we will understand a set of related statements”. b Wanneer is een probleem beslisbaar? c Gegeven is de bewering “Het is een onbeslisbaar probleem om te bepalen of een context-vrije grammatica wel of niet dubbelzinnig is”. Toch hebben we in opgave 5.19 bewezen dat de context-vrije grammatica van example 5.4 dubbelzinnig is. Is de uitwerking van opgave 5.19 dan niet tegenstrijdig met de gegeven bewering? 1.2 Studeeraanwijzing

THE TURING MACHINE HALTING PROBLEM

In deze subsection wordt een speciaal probleem behandeld. Het gaat namelijk over het oplossen door een turingmachine van een probleem betreffende turingmachines. In het algemeen hoeven er helemaal geen turingmachines aan te pas te komen bij beslisbaarheids- en berekenbaarheidsvraagstukken. Met andere woorden, in het algemeen hoeft het domein van het probleem geen turingmachines te bevatten, en het algoritme dat het probleem oplost hoeft niet als turingmachine beschreven te zijn. Bestudeer nu in section 12.1 van het tekstboek van Linz de subsection The Turing machine halting problem, tot en met definition 12.1.

OPGAVE 10.2

a Geef in eigen woorden een beschrijving van het stopprobleem voor turingmachines. b Beschrijf in eigen woorden de essentie van definition 12.1. Studeeraanwijzing theorem 12.1

We zijn nu toe aan het bestuderen van theorem 12.1, waarin gesteld wordt dat er geen turingmachine bestaat die het stopprobleem voor turingmachines oplost. Het bewijs van theorem 12.1 hoeft u maar door te lezen en proberen te begrijpen, bijvoorbeeld aan de hand van de extra uitleg in het werkboek. Er wordt niet van u verwacht dat u het bewijs kunt reproduceren. Het idee achter het bewijs van theorem 12.1 is het volgende: we beginnen met aan te nemen dat er wel een turingmachine H bestaat die het stopprobleem oplost. Vervolgens transformeren we H tot een turingmachine H’, en daarna transformeren we H’ nog eens tot Hˆ . Tot slot passen we Hˆ op zichzelf toe, en dan beginnen de problemen: Hˆ spreekt zichzelf namelijk tegen. Het is belangrijk dat u inziet dat die problemen niet veroorzaakt worden door de uitgevoerde transformaties: die transformaties zijn niets anders dan volkomen legale aanpassingen aan een gegeven turingmachine, die leiden tot een nieuwe turingmachine (zie ook opgave 10.4). We illustreren het bewijs van theorem 12.1 met een viertal schema's, in figuur 10.1 tot en met 10.4. De turingmachine H, waarvan we aannemen dat het een implementatie is van het algoritme dat het stopprobleem voor turingmachines oplost, is getekend in figuur 10.1:

FIGUUR 10.1

De turingmachine H

H is dus een turingmachine die bepaalt of een gegeven turingmachine al dan niet stopt met een gegeven invoer, en M is een turingmachine waarvan we willen weten of die stopt op invoer w. De invoer van H bestaat dus uit M en w, samen gecodeerd in de string wMw. Merk op dat H altijd stopt, ook als M niet stopt. We gaan nu H transformeren naar H’, en wel zo dat H’ in een oneindige lus komt dan en slechts dan als H stopt in qy (zie figuur 10.2). De transformatie bestaat uit het toevoegen van een paar nieuwe toestanden en overgangen aan ‘het eind van’ H (zie figure 12.1 en 12.2 in Linz).

FIGUUR 10.2

De turingmachine H’

H’ doet dus iets anders dan H, maar dat geeft niet: het gaat erom dat duidelijk is dat “als H bestaat, dan bestaat H’ ook”. Vervolgens transformeren we H’ naar Hˆ , deze keer door aan ‘het begin van’ H’ nieuwe toestanden en overgangen toe te voegen die ervoor zorgen dat Hˆ eerst zijn invoer kopieert. Zodra dat gebeurd is laten we Hˆ verdergaan zoals H’ dat zou doen. We laten Mˆ werken met een ander soort invoer dan H’: we geven alleen de beschrijving wM van M aan Hˆ , en laten w dus weg. In figuur 10.3 geven we een schematische weergave van Hˆ met wM als invoer:

FIGUUR 10.3

De turingmachine Hˆ met invoer wM

Weer geldt: Hˆ doet iets heel anders dan H’, maar dat maakt niet uit. Het doel was alleen maar te laten zien dat Hˆ bestaat (onder de aanname dat H en dus H’ bestaat), zodat we in de volgende stap een tegenspraak kunnen creëren. We zijn nu klaar met transformeren. Aangezien Hˆ een turingmachine is, bestaat er een codering wˆ van Hˆ , die we als invoer aan Hˆ kunnen aanbieden. Die situatie is getekend in figuur 10.4:

FIGUUR 10.4

De turingmachine Hˆ met invoer wˆ

We hebben nu een situatie gecreëerd waarin Hˆ zichzelf tegenspreekt: als Hˆ stopt met invoer wˆ dan komt Hˆ in een oneindige lus, en als Hˆ niet stopt met invoer wˆ dan stopt Hˆ in een afwijzende toestand. Dit is een situatie die nooit voor kan komen, en omdat al onze redeneerstappen (de transformaties van H naar H’ naar Hˆ en het toepassen van Hˆ op een beschrijving wˆ van zichzelf) gezond waren, moet de oorzaak van deze onmogelijke situatie wel in onze aanname liggen. Die aanname – dat er een turingmachine H bestaat die het stopprobleem voor turingmachines oplost – is dus niet waar. OPGAVE 10.3

Beschrijf waarom aan het einde van het bewijs geconcludeerd wordt: “This is clearly nonsense”.

OPGAVE 10.4

Maak exercise 1 uit section 12.1 van Linz. Studeeraanwijzing

Bestudeer nu de rest van subsection The Turing machine halting problem. 1.3

Reductie

REDUCING ONE UNDECIDABLE PROBLEM TO ANOTHER

In het geval van het stopprobleem hebben we in theorem 12.1 bewezen dat dit onbeslisbaar is door alleen (een hypothetisch algoritme voor) het stopprobleem zelf te gebruiken, en daarop een – weliswaar gekunstelde en vergezochte, maar in ieder geval geldige – logische redenering toe te passen: ‘als we dat algoritme een beetje aanpassen, krijgen we iets dat niet kan’. Als we eenmaal een probleem hebben waarvan we weten dat het onbeslisbaar is, kunnen we dat gebruiken om van andere problemen aan te tonen dat ze onbeslisbaar zijn. We hoeven dan niet meer per probleem een slimme truc zoals in het bewijs van theorem 12.1 te bedenken, maar kunnen een algemeen toepasbare methode gebruiken: reductie (overigens moet er dan nog steeds wel per probleem een ‘trucje’ verzonnen worden). Op Wikipedia staat op de pagina over berekenbaarheid een heel duidelijke omschrijving van het begrip reductie: Een belangrijk hulpmiddel bij het bepalen of een gegeven probleem berekenbaar (of beslisbaar) is of niet, is de techniek van reductie. Een reductie van een probleem A naar een ander probleem B is een formele manier om A uit te drukken in termen van B (transformeren naar B), zodanig dat een oplossing voor B ook gebruikt kan worden om A op te lossen. Enerzijds kun je zo'n reductie gebruiken om A op te lossen indien voor B al een oplossing bekend is. Anderzijds, indien al bekend is dat A een onberekenbaar (of onbeslisbaar) probleem is, kun je met een reductie aantonen dat B ook onberekenbaar (of onbeslisbaar) is; zie figuur 10.5.

FIGUUR 10.5

Reductie van A naar B

Anders geformuleerd in termen van beslisbaarheid: als “probleem A wordt gereduceerd tot probleem B” dan geldt: “als B beslisbaar is, dan is A ook beslisbaar” ofwel “als A onbeslisbaar is, dan is B ook onbeslisbaar”.

In de examples 12.1 en 12.2 – in de subsection die u zo dadelijk gaat bestuderen – wordt steeds als probleem A het stopprobleem voor turingmachines gebruikt. Om te bewijzen dat een ander probleem onbeslisbaar is, wordt het stopprobleem gereduceerd tot dat andere probleem. NB

– Een veel gemaakte fout bij reduceren is ‘de verkeerde kant op reduceren’. Om te bewijzen dat B onbeslisbaar is, moet een probleem A, waarvan bekend is dat het onbeslisbaar is, gereduceerd worden tot B. Dus niet andersom! – Om de techniek van reductie te kunnen gebruiken moet een transformatie van A naar B bestaan. Zonder bruikbare transformatie, geen reductie! Wat we eerder over beslisbaarheids- en berekenbaarheidsvraagstukken zeiden, geldt ook voor reducties: in subsection 12.1 komen daar steeds turingmachines in voor, omdat we nu eenmaal bezig zijn de grenzen van turingmachines te verkennen. In het algemeen kunnen reducties ook gebruikt worden voor problemen die niets met turingmachines te maken hebben. Terminologie

In het vervolg zullen we de volgende terminologie zo consequent mogelijk hanteren: we gebruiken de term algoritme voor de turingmachine die een bepaald probleem oplost. De turingmachine die als invoer van het algoritme dient blijven we een turingmachine noemen (dit geldt natuurlijk alleen als het probleem ook daadwerkelijk over turingmachines gaat). Natuurlijk is het zo dat een algoritme en een turingmachine equivalent zijn, maar we willen de twee rollen uit elkaar houden door verschillende namen te gebruiken.

Studeeraanwijzing

Bestudeer in section 12.1 van het tekstboek van Linz de subsection Reducing one undecidable problem to another, tot en met example 12.1.

Example 12.1

We gaan hier verder in op example 12.1 door het bewijs te illustreren met een aantal schema's. Gegeven een willekeurige turingmachine M, een willekeurige toestand q van M en een willekeurige invoerstring w van M. Het state-entry-probleem is de vraag of M met w als invoer ooit in toestand q komt. Er wordt gesteld dat het probleem onbeslisbaar is. Het bewijs wordt gegeven door het stopprobleem te reduceren tot het state-entry-probleem, ofwel door te laten zien dat een algoritme voor het state-entry-probleem eenvoudig zou kunnen worden omgevormd tot een algoritme voor het stopprobleem.

State-entryprobleem

Stel we hebben een algoritme (geïmplementeerd met een turingmachine) voor het state-entry-probleem; zie figuur 10.6:

FIGUUR 10.6

Een algoritme voor het state-entry-probleem

Dit algoritme heeft dus als invoer een turingmachine, een invoerstring en een toestand van die turingmachine, en geeft als uitvoer het antwoord ‘JA’ of ‘NEE’. We willen een algoritme maken voor het stopprobleem; zie figuur 10.7:

FIGUUR 10.7

Een algoritme voor het stopprobleem

Voor dit laatste algoritme willen we gebruikmaken van het algoritme voor het state-entry-probleem. We willen dus het stopprobleem reduceren tot het state-entry-probleem; zie figuur 10.8:

FIGUUR 10.8

Reductie tot het state-entry-probleem (onvolledig)

Welke transformatie moet er plaatsvinden? Ofwel: hoe moeten we de invoer van het stopprobleem transformeren tot invoer voor het stateentry-probleem, zo dat een JA-antwoord van het state-entry-probleem een JA-antwoord van het stopprobleem betekent, en een NEE-antwoord van het state-entry-probleem een NEE-antwoord van het stopprobleem? Figuur 10.9 toont een schematische weergave van deze vraag.

FIGUUR 10.9

Welke transformatie moet er plaatsvinden?

De invoer (M, w) voor het stopprobleem moet worden getransformeerd tot invoer ( Mˆ , q, w) voor het state-entry-probleem, en wel zo dat M stopt op invoer w dan en slechts dan als Mˆ komt in toestand q op invoer w.

Dit kunnen we bereiken door een nieuwe toestand q toe te voegen aan M en door, voor elke stoptoestand van M, een overgang naar q toe te voegen. Het maakt niet zoveel uit wat er dan in toestand q gebeurt: het gaat er alleen maar om dat Mˆ in toestand q komt dan en slechts dan als M stopt. In example 12.1 is ervoor gekozen om q een eindtoestand te maken. Figuur 10.10 toont de turingmachine Mˆ .

FIGUUR 10.10

De turingmachine Mˆ met eindtoestand q

Gebruiken we deze transformatie in figuur 10.9, dan krijgen we een algoritme voor het stopprobleem; zie figuur 10.11 (zie ook figuur 10.7).

FIGUUR 10.11

Een algoritme voor het stopprobleem dat gebruikmaakt van een algoritme voor het state-entry-probleem

Omdat het stopprobleem onbeslisbaar is, is het niet mogelijk om er een algoritme voor te construeren. Toch hebben we dat gedaan in figuur 10.11, dankzij onze aanname dat er een algoritme bestaat voor het stateentry-probleem. Die aanname is dus niet juist: het state-entry-probleem is onbeslisbaar. OPGAVE 10.5

In example 12.1 wordt een manier beschreven waarop je aan de overgangsfunctie van een turingmachine M kunt zien of M stopt: “If M halts, it does so because some (qi, a) is undefined”. Waarom kunnen we deze observatie niet gebruiken om het stopprobleem op te lossen? Tot zover example 12.1; we bekijken nu example 12.2. Studeeraanwijzing Example 12.2

Bestudeer example 12.2 in de subsection Reducing one undecidable problem to another van section 12.1 van het tekstboek van Linz.

Blank-tapestopprobleem

Het blank-tape-stopprobleem is de vraag of, gegeven een willekeurige turingmachine, deze turingmachine stopt met een lege tape, dat wil zeggen met invoer . Dit probleem is onbeslisbaar, en dat kunnen we bewijzen met behulp van de techniek van reductie. De volgende twee vragen moeten dan worden beantwoord: – Welke reductie kunnen we toepassen om te bewijzen dat het blanktape-stopprobleem onbeslisbaar is? – Welke transformatie hebben we daarvoor nodig? Een antwoord op de eerste vraag is: we kunnen proberen of we het stopprobleem (probleem A in figuur 10.5) kunnen reduceren tot het blank-tape-stopprobleem (probleem B in dezelfde figuur). Als dat kan (als we een geschikte transformatie kunnen vinden), dan zou uit een algoritme voor het blank-tape-stopprobleem een algoritme voor het stopprobleem geconstrueerd kunnen worden (zie figure 12.3 in Linz). We weten echter dat dit laatste algoritme niet kan bestaan, en kunnen dan dus concluderen dat er ook geen algoritme voor het blank-tapestopprobleem kan bestaan. Voor de transformatie (het antwoord op de tweede vraag) moeten we de invoer voor het algoritme A voor het stopprobleem transformeren naar invoer voor het algoritme B voor het blank-tape-stopprobleem, en wel zo dat een JA-antwoord van B een JA-antwoord van A betekent, en een NEE-antwoord van B een NEE-antwoord van A. Met andere woorden, we moeten een turingmachine M, waarvan we willen weten of die stopt op invoer w, transformeren naar een turingmachine Mw, en wel zo dat M stopt op w

dan en slechts dan als

Mw stopt op 

Hoewel Mw dus start met een lege tape, moet hij toch na kunnen gaan of M zal stoppen met invoer w. Dat kunnen we voor elkaar krijgen door Mw als volgt uit M en w te construeren: Mw bevat alle toestanden en overgangen van M, plus een aantal nieuwe. Die nieuwe toestanden en overgangen gebruikt Mw om eerst de invoerstring w op zijn lege tape te schrijven (de string w is immers bekend, en het is eenvoudig om een stukje ‘code’ voor een turingmachine te schrijven waarmee een gegeven string op een lege band wordt gezet). Vervolgens zet Mw de leeskop op het meest linkse symbool van w en gaat in de begintoestand q0 van M. Hierna doet Mw precies wat M zou doen vanuit de configuratie q0w. Nu geldt dus dat de turingmachine M stopt met invoer w dan en slechts dan als de turingmachine Mw stopt met  als invoer. Samengevat: we kunnen vanuit iedere turingmachine M en iedere invoerstring w een turingmachine Mw construeren die werkt zoals hierboven beschreven. Als we een algoritme hebben dat het blanktape-stopprobleem oplost kunnen we dat algoritme loslaten op Mw, en dan krijgen we daarmee een antwoord op de vraag of M stopt op w. We weten echter dat die laatste vraag in het algemeen niet te beantwoorden is, dus kunnen we concluderen dat dat algoritme voor het blank-tape-stopprobleem niet kan bestaan.

OPGAVE 10.6

Beschrijf de transformatie van (M, w) naar Mw uit example 12.2. Bestudeer nu de rest van de subsection Reducing one undecidable problem to another van section 12.1 van Linz.

Studeeraanwijzing

OPGAVE 10.7

Deze opgave gaat over example 12.3 in Linz. a In figure 12.4 wordt gebruik gemaakt van een turingmachine met de naam Mu. Geef een omschrijving van Mu. b Verklaar de titel van figure 12.4. OPGAVE 10.8

Deze opgave gaat ook over example 12.3 in Linz. Maak exercise 11 uit section 12.1 van Linz. OPGAVE 10.9

Maak exercise 3 uit section 12.1 van Linz. OPGAVE 10.10

Maak exercise 9 uit section 12.1 van Linz.

ZELFTOETS

1

Gegeven een probleem P dat gereduceerd kan worden tot twee deelproblemen Q1 en Q2, volgens de volgende figuur. Q1 is algoritmisch.

Welke van de volgende vier beweringen over P en Q2 is in ieder geval niet waar: – P en Q2 zijn beide algoritmisch. – P is algoritmisch, maar Q2 is dat niet. – P is niet algoritmisch, maar Q2 is dat wel. – P en Q2 zijn geen van beide algoritmisch. 2

a We definiëren probleem P als volgt: gegeven een string l over een zeker alfabet, bepaal of l even lengte heeft. Geef aan hoe u invoer voor probleem P kunt transformeren tot invoer voor het blank-tape-stopprobleem. b Waarom is het incorrect om de reductie die in onderdeel a gesuggereerd wordt te gebruiken om te concluderen dat P onbeslisbaar is?

TERUGKOPPELING 1

10.1

Uitwerking van de opgaven

a Een probleem kunnen we beschouwen als een verzameling uitspraken die alleen verschillen in het element van het domein dat erin is ingevuld. In het voorbeeld dat Linz geeft, de bewering “For a context-free grammar G, the language L(G) is ambiguous”, kun je deze ene bewering ook opvatten als een hele verzameling beweringen of uitspraken, namelijk één uitspraak voor iedere concrete context-vrije grammatica die we kunnen verzinnen. Het ‘probleem’ is dus de algemene versie van die uitspraken. b Een probleem is beslisbaar als er een turingmachine bestaat die voor ieder element in het domein van het probleem het correcte antwoord geeft (ja of nee) op de specifieke uitspraak die bij dat element hoort. c De uitwerking van opgave 5.19 is niet tegenstrijdig met de bewering “Het is een onbeslisbaar probleem om te bepalen of een context-vrije grammatica wel of niet dubbelzinnig is”. De uitwerking geeft een antwoord op het al dan niet dubbelzinnig zijn van één gegeven context-vrije grammatica. Om het antwoord te geven maakt de uitwerking gebruik van de eigenschappen van de gegeven grammatica. Het domein van de bewering is de verzameling van alle context-vrije grammatica’s. De bewering gaat over een algoritme dat voor elke willekeurige context-vrije grammatica G een antwoord zou moeten geven op de vraag of G wel of niet dubbelzinnig is. Zo’n algoritme kan niet uitgaan van de eigenschappen van G, omdat over G niets anders bekend is dan dat het een context-vrije grammatica is. Daarom is zo’n algoritme niet te vinden.

10.2

a Het stopprobleem voor turingmachines is de vraag of, gegeven een willekeurige turingmachine M en een willekeurige string w, de turingmachine wel of niet zal stoppen met de string w als invoer. b Definition 12.1 zegt dat een oplossing voor het stopprobleem voor turingmachines een turingmachine H is die, als hij de beschrijving wM van een turingmachine M en een invoerstring w voor M krijgt aangeboden, stopt in één van de twee eindtoestanden qy (accepterende eindtoestand) en qn (afwijzende eindtoestand). H stopt in eindtoestand qy dan en slechts dan als M stopt op de invoer w en H stopt in eindtoestand qn dan en slechts dan als M niet stopt op de invoer w.

10.3

In plaats van de beschrijving van M wordt de beschrijving wˆ van Hˆ aan Hˆ aangeboden. Er zijn dan hoogstens twee mogelijkheden: óf Hˆ stopt op invoer wˆ , óf Hˆ stopt niet op invoer wˆ . Beide mogelijkheden leiden tot een tegenspraak, want de een zegt “als Hˆ stopt op invoer wˆ , dan stopt Hˆ niet op invoer wˆ ”, en de ander zegt “als Hˆ niet stopt op invoer wˆ , dan stopt Hˆ op invoer wˆ ”. Er blijven dus geen mogelijkheden over, en dat is de reden waarom Linz zegt “This is clearly nonsense”.

10.4

We kunnen de overgangsfunctie als volgt uitbreiden voor alle a  Γ: δ(qy, a) = (qa, a, R) δ(qa, a) = (qb, a, R) δ(qb, a) = (qa, a, L) Q is nu uitgebreid met qa en qb. F is ook gewijzigd: qy is geen eindtoestand meer.

10.5

Je kunt inderdaad aan iedere turingmachine zien of hij ongedefinieerde toestandsovergangen heeft (combinaties van een toestand q en een tapesymbool a waarvoor niet is vastgelegd wat er moet gebeuren). Als de turingmachine in zo’n situatie belandt, zal hij ook inderdaad stoppen. Het probleem is dat we niet kunnen bepalen of hij in zo’n situatie terecht zal komen, en in het geval van het stopprobleem is dat nu juist precies wat we moeten weten. In het geval van example 12.1 hoeven we niet te weten of M stopt, we willen er alleen voor zorgen dat als M stopt hij automatisch overgaat in een speciale toestand. En dat kan wel, op de manier die in example 12.1 beschreven is.

10.6

Laat w = w1w2…wn voor wi in het invoeralfabet van M en n  1 (als n = 0 is Mw gewoon gelijk aan M). Mw heeft alle toestanden en overgangen van M, alleen is de begintoestand q0 van M in Mw een gewone toestand. Mw heeft verder een nieuwe begintoestand qin en nieuwe gewone toestanden q1, …, qn, en overgangen (qin, □) = (q1, w1, R), (q1, □) = (q2, w2, R), …, (qn–1, □) = (qn, wn, L), waarmee w op de band gezet wordt. Daarna heeft Mw voor iedere wi een overgang (qn, wi) = (qn, wi, L), waarmee de kop weer naar het begin van w loopt. Vervolgens doet Mw de stap (qn, □) = (q0, □, R): hij gaat in de begintoestand van M met de kop op het eerste symbool van w. Daarna simuleert hij M.

10.7

a Mu is een universele turingmachine die de beschrijving van Mˆ als invoer krijgt. Mu beslist of M stopt in m (of minder) stappen. b De turingmachine die in figure 12.4 wordt geconstrueerd kan gebruikt worden om te beslissen of M wel of niet stopt met een lege tape als invoer. Dit is een turingmachine voor het blank-tape-stopprobleem. In deze turingmachine is aangenomen dat F, een turingmachine voor het berekenen van f(n), bestaat. Deze aanname is onjuist.

10.8

Hoewel de functie niet berekenbaar is, is het mogelijk om in sommige gevallen de waarde van f te berekenen. Heeft een turingmachine één toestand, dan geldt f(1) = 0. Zou (q0, □) gedefinieerd zijn, dan zou de turingmachine nooit stoppen. Ongeacht welk tapesymbool op de tape wordt geschreven, gaat in dat geval de turingmachine steeds verder naar rechts (of naar links) en komt steeds een blanco tegen. Heeft een turingmachine twee toestanden, dan geldt f(2) = 5. De overgang (q0, □) moet nu gedefinieerd zijn om in q1 te kunnen komen. Is q1 een eindtoestand dan stopt de turingmachine na één stap. Maar q1 hoeft

geen eindtoestand te zijn omdat de turingmachine kan stoppen door een ongedefinieerde overgang. We kunnen alle mogelijke overgangen opsommen die geen lus inhouden. Kijk bijvoorbeeld naar de volgende turingmachine:

Eén van de zes overgangen moet ongedefinieerd zijn, wil de turingmachine stoppen. Dit is bijvoorbeeld het geval als (q0, □), (q1, □), (q0, 1), (q1, 0) en (q0, 0) gedefinieerd zijn en (q1, 1) ongedefinieerd is. In dat geval kunnen we maximaal 5 bewegingen maken. Andere waarden van de overgangen en een andere keus voor een ongedefinieerde overgang leiden tot vergelijkbare resultaten en we kunnen concluderen dat f(2) = 5. 10.9

Stelling: Er bestaat geen algoritme A dat kan bepalen of een tapesymbool a   ooit op de tape geschreven wordt gedurende het toepassen van M op w  + Om te bewijzen dat een probleem B onbeslisbaar is, kunnen we proberen of we een probleem A waarvan we al weten dat het onbeslisbaar is, kunnen reduceren tot B (figuur 10.5). In de praktijk kan het even zoeken zijn naar een geschikte kandidaat voor A, maar aangezien we bezig zijn met een introductie op dit gebied en aangezien we tot nu toe alleen twee voorbeelden hebben gezien waarin de rol van A wordt vervuld door het stopprobleem, lijkt het een veilige keuze om nu ook voor het stopprobleem te kiezen. We krijgen dan de volgende situatie:

Deze reductie werkt alleen als we een geschikte transformatie kunnen vinden. Als we bedenken dat bovenstaand plaatje wel erg veel lijkt op figuur 10.11 (reductie van het stopprobleem naar het state-entryprobleem) is het vinden van die transformatie niet zo moeilijk meer: we gebruiken gewoon dezelfde ‘truc’ als in example 12.1, maar in plaats van M zo aan te passen dat hij in een speciale toestand gaat dan en slechts dan als hij stopt, zorgen we ervoor dat hij (ook) een speciaal symbool schrijft dan en slechts dan als hij stopt. Iets concreter: We construeren M’ uit M door het toevoegen van een speciaal tapesymbool #, een nieuwe accepterende toestand q# en nieuwe overgangen (q, y) = (q#, #, R) voor elke q  Q en y   waarvoor  nog niet gedefinieerd is.

We hebben er dan voor gezorgd dat  als M stopt op w, dan schrijft M’ een #, en  als M’ een # schrijft, dan stopt M op w. Kortom, de transformatie is rond, en als er een algoritme bestaat dat bepaalt of een turingmachine tijdens zijn berekening een # schrijft, dan kunnen we dat algoritme dus gebruiken om het stopprobleem op te lossen. We weten echter dat het stopprobleem niet opgelost kan worden, dus ‘ons’ algoritme bestaat ook niet. (Een kortere versie van deze uitwerking staat op pagina 426 in Linz.) 10.10

Ja, het stopprobleem is beslisbaar voor deterministische stapelautomaten (dpda’s). Deterministische stapelautomaten kennen regels van de vorm: (q1, a, b) =  (q1, a, b) = {(q2, c)} (q1, , b) =  (q1, , b) = {(q2, c)}

a   en b, c  * als (q1, d, b) =  voor alle d  

Aangezien de strings eindig zijn, kan de automaat niet oneindig doorgaan met het lezen van letters. Wij zijn dus alleen geïnteresseerd in de regels van de vorm (q1, , b) = {(q2, c)}. Beter gezegd, wij zijn alleen geïnteresseerd in automaten waar het mogelijk is een oneindig aantal ’s te ‘lezen’. Ofwel waar ’(qi, , s) = {(qi, sx)} met s   en x  * mogelijk is zodanig dat de stapel ofwel groeit, ofwel gelijk blijft. Aangezien de stapelautomaat deterministisch is, kan altijd worden voorzien of een dpda voor een string w twee maal in zo’n qi met een gelijke of grotere stapel terecht komt. Als dat het geval is zal de deterministische stapelautomaat niet stoppen. Formeler: wij kunnen een turingmachine maken die gegeven een dpda M en een string w voor elke -stap registreert uit welke toestand (qi, sx), met qi  Q, s   en x  *, machine M komt. Bij het uitvoeren van een niet--stap wordt dit register geschoond. Na elke -stap kan gecontroleerd worden of de automaat in een toestand (qi, sy) is beland, y  *, met y  x. Zo ja, dan beslist de turingmachine dat automaat M toegepast op w niet stopt. 2

1

Uitwerking van de zelftoets

Het derde alternatief is in elk geval niet waar. Als Q2 algoritmisch is, dan volgt uit de gegeven reductie dat P ook algoritmisch is. Het eerste alternatief kan dus waar zijn, maar het derde niet. Is Q2 niet-algoritmisch, dan weten we nog niets over P. De reductie die hier getoond is, is in dat geval zeker geen algoritme voor P. Maar er kan een andere reductie bestaan, die wel geheel uit algoritmische stappen bestaat. Zowel het tweede als het vierde alternatief zijn dus mogelijk.

2

a Als we de situatie die door de genoemde transformatie wordt gesuggereerd tekenen zoals in figuur 10.5, dan krijgen we het volgende plaatje:

We moeten dus, uitgaande van alleen een string l, een turingmachine M construeren die start met een lege tape én die stopt dan en slechts dan als |l| even is. Die turingmachine M kan er dan als volgt uitzien: Start met een lege tape. Zet l op de tape (vervang steeds □ door een symbool van l). Ga m.b.v. twee toestanden qeven en qoneven na of l even is. Zo ja: ga vanuit qeven naar een eindtoestand. Zo nee: ga vanuit qoneven in een oneindige lus. Het moge duidelijk zijn dat nu geldt l is even dan en slechts dan als M stopt met invoer . b Het enige dat we weten is dat het blank-tape-stopprobleem onbeslisbaar is. Dat betekent dat we geen algoritme voor P kunnen construeren uit een algoritme voor het blank-tape-stopprobleem (want dat laatste bestaat niet), maar het betekent niet per se dat er geen ander algoritme kan zijn dat we kunnen ombouwen tot een algoritme voor P. Anders gezegd: een reductie van P tot het blank-tape-stopprobleem kan alleen gebruikt worden voor de volgende twee (equivalente) uitspraken: – “Als P onbeslisbaar is, dan is het blank-tape-stopprobleem ook onbeslisbaar”. – “Als het blank-tape-stopprobleem beslisbaar is, dan is P beslisbaar”. De uitspraak “Als het blank-tape-stopprobleem onbeslisbaar is, dan is P onbeslisbaar” is niet equivalent met bovenstaande twee uitspraken, en daarom kan de gesuggereerde reductie niet gebruikt worden om aan te tonen dat P onbeslisbaar is. En dat is maar goed ook, want P is wel beslisbaar: het is gewoon een instantie van het lidmaatschapsprobleem voor reguliere talen (“zit l in de reguliere taal L = { w : w is even}?”). NB

An overview of computational complexity Introductie Leerkern 1 2 3 4 5 6 7 8

229 230

Efficiency of computation 231 Turing machine models and complexity 231 Language families and complexity classes 231 The complexity classes P and NP 232 Some NP problems 233 Polynomial-time reduction 234 NP-completeness and an open question 234 Afsluitende opmerking 235

Zelftoets

235

Terugkoppeling 1 2

236

Uitwerking van de opgaven 236 Uitwerking van de zelftoets 237

Leereenheid 11

An overview of computational complexity

INTRODUCTIE

In deze cursus hebben we ons beziggehouden met de structuur van verzamelingen strings, en met onder andere een serie steeds krachtiger wordende automaten om die structuren te beschrijven: de eindige automaat, de stapelautomaat, de lineair begrensde automaat (maar die hebben we alleen even genoemd) en de turingmachine. Die laatste automaat is zo krachtig dat hij geaccepteerd is als model van een computer: als voor een probleem bewezen kan worden dat er geen algoritme kan bestaan dat dat probleem oplost op een turingmachine, dan is dat probleem onoplosbaar – ook op een ‘echte’ computer. We hebben tot nu toe nog nauwelijks gekeken naar de praktische uitvoerbaarheid van de oplossingen. Eigenlijk alleen bij het parsen van strings van een context-vrije taal hebben we daarover iets opgemerkt (pagina’s 143-144 en 180 in Linz). In deze leereenheid verkennen we die praktische uitvoerbaarheid van algoritmen een beetje: wat bedoelen we er precies mee, waarom is het ene algoritme wel en het andere niet praktisch uitvoerbaar, hoe komen we erachter of een probleem een praktisch uitvoerbare oplossing heeft? Dit vakgebied heet complexiteitstheorie. Het gaat ons in deze leereenheid vooral om het grote geheel en de grote lijnen, en het verband met formele-talentheorie. We gaan dus niet van concrete algoritmen in detail de complexiteit bepalen; dat laten we over aan de cursus Datastructuren en algoritmen. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – kunt uitleggen wat de verzamelingen P en NP inhouden, en wat het verband ertussen is – enkele voorbeelden kunt noemen van problemen in P en van problemen in NP – kunt uitleggen hoe reductie gebruikt kan worden bij complexiteitsproblemen – kunt uitleggen wanneer een probleem NP-compleet heet, en wat het belang is van NP-complete problemen. Studeeraanwijzingen Bij deze leereenheid horen section 12.5 en chapter 14 van het tekstboek. Ook deze leereenheid gaat over een moeilijk onderwerp; we gaan er wat minder diep op in dan op het onderwerp van de vorige leereenheid, maar toch geven we ook hier het advies om de stof meer dan één keer te bestuderen en het tussendoor even te laten rusten. De studielast van deze leereenheid bedraagt circa 4 uur.

LEERKERN

U komt in de delen van Linz die bij deze leereenheid horen hier en daar de orde-van-grootte-notaties O,  en  tegen, die u zich misschien nog herinnert uit section 1.1 van chapter 1. Als u wilt kunt u die (sub)paragraaf nog eens bekijken, maar u kunt het ook doen met de nu volgende zeer informele benadering. In een uitdrukking als 4n3 + 7n2 – 5n + 23 is, in het kader van complexiteit, alles behalve n3 onbelangrijk. Dat is namelijk het deel dat het hardst groeit als n groeit, en daar gaat het om – als we tenminste hebben afgesproken dat n staat voor de ‘grootte van de invoer’, wat dat dan ook precies mag betekenen. In plaats van het heel specifieke 4n3 + 7n2 – 5n + 23 gebruiken we daarom liever O(n3) (of  (n3) of  (n3) als dat van toepassing is, maar wij kijken nu niet naar de verschillen tussen de drie notaties). Een algoritme met (tijd)complexiteit O(n3) is in de meeste gevallen langzamer dan een algoritme met (tijd)complexiteit O(n2). In het algemeen geldt: hoe hoger de exponent, hoe langzamer.

Polynomiaal

Exponentieel

Zolang de n in het grondtal van de belangrijkste term blijft (n, n2, n3, n4,…) heet de complexiteit polynomiaal, en dan is het meestal praktisch gezien nog ‘te doen’ (tractable of ‘handelbaar’). Maar zodra de n in de belangrijkste term niet meer het grondtal is, maar naar de exponent verhuist (2n, 3n, 4n,…), wordt de complexiteit exponentieel, wat in de praktijk meestal neerkomt op ‘ondoenlijk’ (intractable of ‘onhandelbaar’).

Datastructuren en algoritmen

In de cursus Datastructuren en algoritmen leert u hoe u de complexiteit van concrete algoritmen precies kunt berekenen, en hoe u die kunt beschrijven met behulp van de verschillende orde-van-grootte-notaties. In deze leereenheid nemen we gewoon aan dat de tijdscomplexiteit die bij een algoritme genoemd wordt klopt; vaak staat er een (summiere) uitleg bij van hoe ze daaraan gekomen zijn, maar u hoeft geen tijd te steken in het begrijpen daarvan. U hoeft ook niet zelf de complexiteit van een algoritme te kunnen berekenen.

Studeeraanwijzing Erratum in Linz

Lees section 12.5 en de introductie op chapter 14 in Linz. In die introductie wordt verwezen naar chapter 11, maar daarmee wordt section 12.5 bedoeld. De kern van het verhaal is dat, hoewel deze cursus en het tekstboek in de titel de term ‘formele talen en automaten’ hebben, we toch eigenlijk steeds bezig zijn geweest met berekenbaarheid, ‘the theory of computation’. En daarbij hebben we inderdaad tot nu toe een voor de praktijk heel belangrijk aspect onderbelicht gelaten: is het ook allemaal praktisch uitvoerbaar? Verder hebben we het al een aantal keren over determinisme en nietdeterminisme gehad: voor reguliere talen maakt dat niet uit, maar voor context-vrije talen wel, bijvoorbeeld. We zullen nu zien dat het voor complexiteit ook wel degelijk verschil lijkt te maken.

We kijken in deze leereenheid vooral naar het grote plaatje, dus naar complexiteit in het algemeen, en (nog steeds) naar families van talen. De stof in deze leereenheid is dan ook vrij abstract, en we laten veel details zitten. Het gaat er alleen maar om dat u een indruk krijgt van dit deel van berekenbaarheid, en dat u een idee krijgt van een van de grote nog openstaande vragen uit de informatica. 1 Studeeraanwijzing

Efficiency of computation

Lees section 14.1 van Linz. De belangrijkste afspraak uit deze section is dat we de (tijd)complexiteit T(n) van een probleem definiëren als het maximale aantal stappen dat een (nog nader te bepalen) turingmachine nodig heeft om een instantie van grootte n van dit probleem op te lossen. 2

Studeeraanwijzing

Turing machine models and complexity

Lees section 14.2 van Linz. U mag de bewijzen van theorems 14.1 en 14.2 overslaan. De laatste twee alinea’s van section 14.2 zijn wel heel belangrijk.

OPGAVE 11.1

Bij complexiteitsvraagstukken kijken we in het algemeen naar de worst case: de gevallen die het meeste tijd kosten. Bekijk nu de laatste alinea van example 14.2 nogmaals. Daar staat ’Again, the nondeterministic alternative simplifies matters. If e is satisfiable, we guess the value of each xi and then evaluate e’. We gaan er dus blijkbaar van uit dat we in één keer goed gokken. Dat is toch niet de worst case? De belangrijkste punten uit deze section op een rijtje: – de beschrijving van SAT, het satisfiability problem – nogmaals de constatering dat voor berekenbaarheid het verschil tussen determinisme en niet-determinisme niet uitmaakt, maar voor complexiteit hoogstwaarschijnlijk wel – de (nu definitieve) afspraak dat we multitape turingmachines gaan gebruiken om complexiteit te bestuderen. 3

Language families and complexity classes

In deze section doet Linz een poging om de families van talen die we kennen uit de rest van de cursus te koppelen aan complexiteitsklassen. Die complexiteitsklassen kunnen we inmiddels een beetje voelen aankomen: een paar voorbeelden zijn de verzameling van alle problemen die door een deterministische multitape turingmachine in O(n3) stappen kunnen worden opgelost, en de verzameling van alle problemen die door een niet-deterministische multitape turingmachine in O(n) stappen kunnen worden opgelost. Een eerste vraag is dan wel: wat heeft een taal te maken met complexiteit? Complexiteit hoort immers bij problemen, niet bij talen. In definition 14.1 geeft Linz daar een antwoord op: we stellen een taal als het ware gelijk aan het lidmaatschapsprobleem voor die taal.

Studeeraanwijzing

Lees section 14.3 tot en met definition 14.1. In definition 14.2 definieert Linz vervolgens complexiteitsklassen voor talen, in twee varianten: deterministisch en niet-deterministisch. In het stuk daarna legt hij wat voor de hand liggende verbanden tussen sommige van die complexiteitsklassen, en zien we een soort hiërarchie ontstaan, die echter geen direct verband lijkt te houden met de Chomsky-hiërarchie (de alinea’s net voor en net na theorem 14.4). Ook laat hij zien dat er geen bovengrens aan deze hiërachie zit: er is geen functie waarvoor geldt dat alle beslisbare problemen maximaal die complexiteit hebben (theorem 14.4). In de voorbeelden aan het eind van section 14.3 kijkt hij naar de relaties tussen de families van reguliere, context-vrije en context-gevoelige talen enerzijds en complexiteitsklassen anderzijds. Helaas moet de conclusie zijn dat het allemaal niet erg opschiet op deze manier: er zijn wel wat verbanden te leggen, maar er blijven veel te veel vragen over.

Studeeraanwijzing Erratum in Linz

Lees de rest van section 14.3. U mag het bewijs van theorem 14.4 overslaan. In example 14.3 wordt verwezen naar example 13.7; dat moet 12.7 zijn. 4

Studeeraanwijzing

The complexity classes P and NP

Lees section 14.4 van Linz. Linz maakt voor de definities van P en NP gebruik van de eerder gedefinieerde complexiteitsklassen DTIME(ni) en NTIME(ni). In de literatuur zien we ook vaak definities van P en NP in woorden: – P is de klasse van talen die in polynomiale tijd geaccepteerd worden door een deterministische turingmachine. – NP is de klasse van talen die in polynomiale tijd geaccepteerd worden door een niet-deterministische turingmachine.

Let op

Laat u niet op het verkeerde been zetten door de namen P en NP: zoals ook duidelijk blijkt uit de formele definities van P en NP in Linz, is P een afkorting voor ‘deterministic polynomial-time’ en NP voor ‘non-deterministic polynomial-time’ (dus niet voor ‘niet-polynomiaal’). In de literatuur wordt het onderscheid tussen talen in P en talen in NP ook nog anders omschreven: – P bevat alle problemen waarvoor efficiënt (dus in polynomiale tijd) een oplossing gevonden kan worden – NP bevat alle problemen waarvoor een mogelijke oplossing efficiënt (dus in polynomiale tijd) geverifieerd kan worden.

OPGAVE 11.2

Relateer de beide definities van NP die hierboven gegeven zijn aan elkaar: wat heeft ‘in polynomiale tijd geaccepteerd worden door een niet-deterministische turingmachine’ te maken met ‘een mogelijke oplossing kan efficiënt (dus in polynomiale tijd) geverifieerd worden’?

OPGAVE 11.3

In de laatste alinea van example 14.2 staat een voorbeeld van het verifiëren van een kandidaatoplossing in (deterministiche) polynomiale tijd genoemd. Leg kort in woorden uit dat dat inderdaad deterministisch in polynomiale tijd kan. In de laatste alinea van section 14.4 geeft Linz nog aan dat niet alle problemen in P een praktisch uitvoerbare oplossing hebben. Het is wel zo dat de meeste problemen in P een praktisch uitvoerbare oplossing hebben. Het is overigens ook niet zo dat we per definitie helemaal niets kunnen met problemen in NP. Er zijn veel problemen in NP die met bijvoorbeeld een zogenaamde SAT solver in de praktijk in de meeste gevallen efficiënt opgelost kunnen worden. Een van de belangrijkste open vragen van de informatica is: geldt P = NP? Ofwel: kunnen we voor alle problemen waarvoor we nu nog geen efficiënt deterministisch algoritme kennen wel ooit zo’n algoritme vinden? Of zijn er talen in NP die niet in P zitten? De heersende opinie is dat er inderdaad zulke talen zijn; er is echter nog geen voorbeeld gevonden (en bewezen). OPGAVE 11.4

Wat heeft de eerste alinea van pagina 361 in Linz te maken met de vraag of P = NP? 5

Some NP problems

Section 14.5 geeft een aantal voorbeelden van problemen die in ieder geval in NP zitten, maar waarvan nog niet bekend is of ze ook in P zitten. Dit zijn heel ‘normale’ problemen, waarvan je je kunt voorstellen dat het handig zou zijn als er een efficiënt deterministisch algoritme voor bekend is. En voor al deze problemen is niet alleen nog geen efficiënt deterministisch algoritme bekend, het is zelfs niet eens bekend of er wel zo’n algoritme kan bestaan (maar dat tot nu toe niemand slim genoeg is geweest om het op te kunnen schrijven). Studeeraanwijzing

Lees section 14.5 van Linz. Linz gebruikt de voorbeelden in deze paragraaf ook om de vertaling van probleem naar taal te maken. In de volgende opgave onderzoeken we hoe dat nu precies zit. Daartoe moeten we ons eerst realiseren dat ‘het satisfiability probleem’ heel algemeen geformuleerd is. De omschrijving van het probleem is ’gegeven een satisfiable expressie e in CNF, vind een toekenning aan de variabelen die e waar maakt’. In dit algemene geval ligt de expressie dus niet vast, en de vraag is eigenlijk: kunnen we een (één!) algoritme geven dat voor iedere mogelijke satisfiable CNF-expressie een juiste toekenning aan de variabelen vindt?

In example 14.6 lijkt het probleem niet zo algemeen aangepakt te worden: er wordt een turingmachine beschreven die voor één specifieke satisfiable CNF-expressie een toekenning kan gokken en verifiëren. Ofwel: het lijkt erop dat er voor iedere expressie een eigen turingmachine en dus een eigen algoritme nodig is. Gelukkig is dat bij nader inzien toch niet zo: die specifieke expressie wordt als het ware ‘ingebakken’ in de specifieke turingmachine, en het algemene algoritme is als volgt: gegeven een satisfiable expressie e in CNF, construeer de bijbehorende ‘voorgebakken’ turingmachine en zet die dan aan het werk. Dit laatste, cursieve stukje is wat in example 14.6 wordt beschreven. Nu we eenmaal gezien hebben dat er wel degelijk aan het algemene gedeelte is gedacht, kunnen we ons in de volgende opgave concentreren op de vertaling van probleem naar taal, voor een specifieke expressie e. OPGAVE 11.5

Formuleer het satisfiability probleem tweemaal in eigen woorden: a met de focus op ‘probleem’ en ‘oplossing’ b met de focus op ‘taal’ en ‘acceptatie’ Ga ervan uit dat er een turingmachine wordt gebruikt voor het ‘oplossen’ dan wel ‘accepteren’. 6

Polynomial-time reduction

Uit leereenheid 10 kennen we de techniek van reducties: als we probleem A kunnen ‘omschrijven’ naar een probleem B waarvan we weten dat het berekenbaar is, dan hebben we daarmee bewezen dat A ook berekenbaar is. Deze techniek blijkt ook bruikbaar te zijn bij complexiteitsvraagstukken: als bekend is dat voor probleem B een deterministisch algoritme in polynomiale tijd bestaat, en als we probleem A in polynomiale tijd via een deterministische transformatie kunnen omschrijven naar probleem B, dan weten we dat er voor A ook een deterministisch algoritme bestaat dat gegarandeerd in polynomiale tijd een correct antwoord geeft. Studeeraanwijzing

Lees section 14.6 van Linz. Blijf niet te lang hangen in details van de specifieke reducties; het gaat hier vooral om het idee dat reducties ook hiervoor gebruikt kunnen worden. 7

NP-completeness and an open question

Zoals gezegd is het (nog steeds) onbekend of er problemen in NP zijn die niet in P zitten. Een aanpak om zo’n probleem te vinden zou kunnen zijn om te gaan zoeken naar de ‘moeilijkste’ problemen van NP. Een probleem A wordt gezien als een van de moeilijkste in NP als het volgende geldt: als er een deterministische polynomiale-tijdoplossing is voor A, dan ook voor alle andere problemen in NP. Ofwel: A is een van de moeilijkste problemen in NP als er voor ieder probleem uit NP een deterministische transformatie in polynomiale tijd naar A bestaat. Deze moeilijkste problemen uit NP noemen we NP-compleet. Studeeraanwijzing

Lees section 14.7 van Linz.

8

Afsluitende opmerking

Voor deze leereenheid hebben we, naast Linz, dankbaar gebruikgemaakt van de volgende teksten: – John E. Hopcroft, Jeffrey D. Ullman, Introduction to automata theory, languages, and computation, Addison-Wesley, 1979 – Sara Baase, Computer algorithms: introduction to design and analysis, second edition, Addison-Wesley, 1991 – de epiloog uit een eerdere versie van de cursus Datastructuren en algoritmen (T26241).

ZELFTOETS

1

Geef van de volgende twee uitspraken aan of ze waar zijn of niet, en waarom: a NP bevat de problemen die niet in polynomiale tijd oplosbaar zijn. b NP bevat de problemen waarvan een oplossing in polynomiale tijd geverifieerd kan worden.

2

Als een probleem in NP zit, kan er dan een deterministisch algoritme met polynomiale complexiteit voor bestaan?

3

Is er een probleem in de complexiteitsklasse NP bekend waarvoor geen deterministisch algoritme met polynomiale complexiteit kan bestaan?

4

We noemen de verzameling van alle NP-complete problemen even NPC. Teken twee venndiagrammen waarin de op dit moment bekende verbanden tussen P, NP en NPC duidelijk worden: a in het geval dat P  NP b in het geval dat P = NP

TERUGKOPPELING 1

Uitwerking van de opgaven

11.1

Het antwoord zit hem in de definitie van niet-deterministisch algoritme. Dat is misschien het eenvoudigst in te zien als we nog eens naar reguliere talen kijken: een nfa accepteert een zekere invoerstring als er een route van begintoestand naar eindtoestand door die nfa is waarop precies die string staat. We hoeven niet aan te geven hoe we die route kunnen vinden, hij hoeft er alleen maar te zijn. Zo werkt het ook met een niet-deterministisch algoritme in het algemeen: het is helemaal niet zo dat je meteen goed gokt, er wordt eigenlijk helemaal niet gegokt! Er wordt alleen van uitgegaan dat er een juiste keuze bestaat, er wordt niets gezegd over hoe je dan bij die juiste keuze kunt uitkomen. In het geval van SAT staat er ’if e is satisfiable’, ofwel we gaan ervan uit dat er een toekenning van waarden aan de xi bestaat waarvoor e waar is. Zo’n toekenning nemen we, en daarvoor rekenen we de waarde van e na. En dat laatste kost niet zoveel tijd.

11.2

Die niet-deterministische turingmachine gokt een kandidaatoplossing (dat kan in polynomiale tijd), en dan hebben we dus een ‘mogelijke oplossing’. De turingmachine gaat vervolgens in polynomiale tijd na of die kandidaatoplossing voldoet aan de voorwaarden en dus een echte oplossing is: de mogelijke oplossing wordt in polynomiale tijd geverifieerd.

11.3

Gegeven een satisfiable expressie e en een voorgestelde toekenning van waarheidswaarden aan de variabelen die in e voorkomen, hoeven we alleen maar: – iedere variabele in e te vervangen door zijn waarde – alle negaties uit te rekenen (‘niet 0’ wordt 1, en ‘niet 1’ wordt 0) – in iedere term (die nu bestaat uit een aantal waarheidswaarden met steeds een  ertussen) te kijken of er een 1 in staat; zo ja, dan – in de conjunctie die je dan overhoudt controleren dat er geen 0 tussen staat. Het moge duidelijk zijn dat we hiervoor hoogstens viermaal e moeten doorlopen (waarbij e steeds kleiner wordt), en dat alles deterministisch is.

11.4

In die eerste alinea van pagina 361 staat onder andere: ’a nondeterministic computation can always be performed on a deterministic machine if we are willing to take into account an exponential increase in the time required’. Dus als we een algoritme hebben dat op een niet-deterministische turingmachine in polynomiale tijd uitgevoerd kan worden (het heeft complexiteit NTIME(ni) voor een of andere i), dan kan het op een deterministische machine in exponentiële tijd (het heeft complexiteit DTIME(kn) voor een of andere k). Ofwel: als het in NP zit, dan zit het niet noodzakelijk ook in P (want dan zou het complexiteit DTIME(ni) voor een of andere i moeten hebben). Dit lijkt op het eerste gezicht misschien een aanwijzing dat P een echte deelverzameling is van NP, maar er staat niet voor niets ‘niet noodzakelijk’: ’this conclusion comes from a particularly simple-minded simulation and one can still hope to do better’. Het zou dus best kunnen dat we gewoon nog geen handige manier hebben gevonden om niet-determinisme om te zetten in determinisme. Voorlopig blijft de vraag of P = NP dus onbeslist.

11.5

a

We zoeken een turingmachine die, als hij een satisfiable expressie e in op de invoerband krijgt, uiteindelijk stopt met een toekenning die e waar maakt op de band. Het gaat hier dus om een transducer, niet om een accepter; we zien de turingmachine als computer die een algoritme uitvoert. Er is een of andere codering nodig om e en de toekenning te kunnen noteren en uitlezen; voor e wordt zo’n codering beschreven in example 14.6, voor de toekenning mogen we aannemen dat er ook zoiets te verzinnen is. CNF

b Nu moeten we de turingmachine zien als een accepter, dus als een automaat die een bepaalde verzameling strings accepteert. Het ligt voor de hand dat die verzameling bestaat uit (coderingen van) alle toekenningen die de betreffende expressie waar maken. Uit de omschrijving van het algoritme in example 14.6 zouden we de indruk kunnen krijgen dat de taal alleen bestaat uit die ene toekenning die door het niet-deterministische algoritme gegokt en geverifieerd wordt, maar dat kan niet: dan zou dezelfde automaat bij iedere andere gok een andere taal opleveren. En de definitie van de taal die door een niet-deterministische automaat wordt geaccepteerd is: de verzameling van alle strings waarvoor een route door de automaat bestaat die goed uitpakt. In theorie kunnen alle goede toekenningen een keer gegokt worden, en van allemaal kan (deterministisch, in polynomiale tijd) worden nagegaan dat ze inderdaad goed zijn. NB

2

1

Uitwerking van de zelftoets

a Deze uitspraak is niet waar: NP bevat alle problemen die nietdeterministisch in polynomiale tijd oplosbaar zijn. De klassieke valkuil dus: de P in NP staat voor polynomiaal, en de N kan dus niet meer voor niet-polynomiaal staan, maar staat voor niet-deterministisch. b Deze uitspraak is waar; dit is de alternatieve definitie uit de literatuur, die we in paragraaf 4 genoemd hebben.

2

In principe kan dat: P  NP, dus alle problemen met een deterministische polynomiale oplossing zitten ook in NP. Maar van veel problemen uit NP kennen we op dit moment alleen een niet-deterministische polynomiale oplossing.

3

Nee, want de vraag of P = NP is nog steeds open. Ofwel: het is niet bekend of ook NP  P, en er is dus ook nog geen probleem in NP gevonden dat niet in P zit.

4

De gevraagde venndiagrammen zijn: a

NB

b

Er is ook nog een klasse NP-hard, waar we verder niet naar kijken.

Accepter 196 Alfabet 17

Substring 16 Suffix 16

Berekening 154, 194 Beweging 154 Blank-tape-stopprobleem

Taal 17 Token 113 Transducer 196 Trap state 37 Turingkracht 202 Turingmachine 192 Type-0-grammatica 200

221

Chomsky-hiërarchie 200 Compiler 113 Compiler compiler 114 Compiler generator 114 Configuratie 194 Context-gevoelige taal 198 Context-vrij pompbaar 176 Contrapositie 88 Exponentieel

230

Generatieve kracht Gesloten 84 Interpreter

199

113

Kleene-afsluiting

17

Lexicale analyse 113 Lidmaatschapsprobleem voor context-gevoelige talen 199 Lineair begrensde automaat 200 Momentane beschrijving 154, 194 Monotone grammatica 198 Normaalvorm

38

Onbeperkte grammatica Operator . 62

200

Parsen 114 Patroonherkenning 67 Polynomiaal 230 (Context-vrij) pompbaar 176 (Regulier) pompbaar 88 Positieve afsluiting 17 Prefix 16 Recursief opsombaar 214 Recursief opsombare talen 200 Recursieve taal 214 Reductie 217 Regulier pompbaar 88 State-entry-probleem Stoppen 194 String 17

218

Universele turingmachine

214