A nwendung Rekursion bei Syntaxanalyse Rekursion eignet sich hervorragend für die Syntaxanalyse. Um den Begriff Syntaxanalyse zu klären, machen wir hier einen kleinen Ausflug in die Compilertheorie. Phasen eines Compilers Im allgemeinen ist der Übersetzungsvorgang eines Compilers in verschiedene Phasen unterteilt, wobei jedoch in manchen Compilern mehrere Phasen zu einer zusammengefaßt sein können. Das nachfolgende Bild zeigt die Phasen eines Compilers in der Reihenfolge, wie sie bei der Übersetzung eines Quellprogramms durchlaufen werden: Quellprogramm Lexikalische Analyse Syntaxanalyse Semantische Analyse SymboltabelleManager Fehlerbehandlung ZwischencodeGenerator Code-Optimierer Code-Generator Zielmaschinen-Code / Assembler-Programm Die Symboltabelle ist die zentrale Datenstruktur, welche für jeden Bezeichner eines Quellprogramms einen Platz vorsieht, in dem Name, Datentyp und sonstige Attribute festgehalten werden. Neben dem Symboltabelle-Manager existiert meist noch ein weiteres zentrales Teil "Fehlerbehandlung", das von allen Phasen benutzt wird. Dieses Programmteil enthält üblicherweise die zu allen möglichen Fehlerarten gehörigen Nummern und Meldungen. Das folgende Bild soll anhand eines einfachen Beispiels (Übersetzung einer C-Anweisung neuwert = wert + x * 50) die Funktionsweise jeder einzelnen Phase verdeutlichen: neuwert = wert + x * 50 Lexikalische Analyse id1 = id2 + id3 * 50 Syntaxanalyse = id 1 + id 2 * id 3 Symboltabelle 1 neuwert 2 ........ 3 x ........ 4 Semantische Analyse = ........ wert 50 id 1 + id 2 * id 3 intodouble 50 ZwischencodeGenerator temp1 = intodouble(50) temp2 = id3 * temp1 temp3 = id2 + temp2 id1 = temp3 Code-Optimierer temp1 = id3 * 50.0 id1 = id2 + temp1 Code-Generator MOVF id3, R2 MULF #50.0, R2 MOVF id2, R1 ADDF R2, R1 MOVF R1, id1 In der Phase Lexikalische Analyse wird das vorgelegte Quellprogramm in sogenannte Token zerlegt. Ein Token ist ein Überbegriff für jeden Bestandteil eines Programms, z.B. könnte das Token BEZEICHNER für Variablennamen, das Token INTEGER-KONSTANTE für ganze Zahlen, das Token OPERATOR für die Zeichen +, -, *, /, usw. stehen. So könnte die obige Anweisung in der lexikalischen Analyse eines C-Compilers wie folgt analysiert werden: Komponente Token neuwert = wert + x * BEZEICHNER OPERATOR BEZEICHNER OPERATOR BEZEICHNER OPERATOR 50 INTEGER-KONSTANTE Die Syntaxanalyse ist dann für die Gruppierung der einzelnen Token und die Prüfung auf syntaktische Richtigkeit verantwortlich. Die Semantische Analyse muß dann z.B. erkennen, daß ein Operand zuerst in double umzuwandeln ist, bevor er mit den anderen Operanden eines Ausdrucks verknüpft werden kann. Der Zwischencode-Generator übersetzt dann das Quellprogramm in eine Zwischensprache, welche dann vom Code-Optimierer verbessert wird, indem z.B. redundante Um- und Zwischenspeicherungen erkannt und beseitigt werden. Der Code-Generator schließlich übersetzt das ihm in der Zwischensprache vorgelegte Programm in den Code der Zielmaschine oder in ein Assembler-Programm. Lexikalische Analyse Die lexikalische Analyse ist für die Zerlegung eines Textes verantwortlich, indem sie entsprechende Textteile nach vorgegebenen Regeln erkennt und klassifiziert. Für jede Klasse von Textteilen werden dabei sogenannte Token definiert. Ein Token ist der Klassenname, wie z.B. BEZEICHNER für Variablennamen, PLUS für den Operator +, SCHLÜSSELWORT für if, for,... usw. Da die lexikalische Analyse einen Eingabetext nach bestimmten Textmustern abtastet, wird sie oft auch als Scanner bezeichnet. Anhand eines Beispiels soll die lexikalische Analyse verdeutlicht werden. Dazu wird nun ein einfacher Taschenrechner entwickelt, der die folgenden Operatoren kennt: + Addition Substraktion * Multiplikation / Division () Klammerung von Ausdrücken Als Operanden sind ganze Zahlen erlaubt. Der Wert jedes in einer Zeile angegebenen arithmetischen Ausdrucks soll unmittelbar ausgegeben werden. Bei den nachfolgenden Realisierungen mit der Programmiersprache C werden folgende Token-Definitionen verwendet: #define #define #define #define #define #define #define ZAHL PLUS MINUS MULT DIV AUF ZU 256 257 258 259 260 261 262 /* /* /* /* /* /* + * / ( ) */ */ */ */ */ */ #define NICHTS 263 #define ZEILENENDE 264 #define FERTIG 265 Die lexikalische Analyse für diesen Taschenrechner könnte mit dem folgenden C-Programm lexanal.c durchgeführt werden. #include <stdio.h> #include <ctype.h> #define #define #define #define #define #define #define ZAHL PLUS MINUS MULT DIV AUF ZU 256 257 258 259 260 261 262 /* /* /* /* /* /* + * / ( ) */ */ */ */ */ */ #define NICHTS 263 #define ZEILENENDE 264 #define FERTIG 265 int tokenwert = NICHTS; /* Programmglobale Variable, welcher im Falle /* einer Zahl der Zahlenwert zugewiesen wird. */ */ int lexan( void ) /* Lexikalische Analyse */ { char zeich; while (1) { zeich = getchar(); if (zeich == ' ' || zeich == '\t') /* Leer- und Tabzeichen ueberlesen*/ ; else if (zeich == '\n') { return(ZEILENENDE); } else if (isdigit(zeich)) { ungetc(zeich, stdin); /* Zuviel gelesenes Zeichen zurueck */ /* in den Eingabepuffer */ scanf("%d", &tokenwert); return(ZAHL); } else if (zeich == EOF) { return(FERTIG); } else { switch (zeich) { case '+': return(PLUS); case '-': return(MINUS); case '*': return(MULT); case '/': return(DIV); case '(': return(AUF); case ')': return(ZU); default : return(NICHTS); } } } } int { main(void) int token; while ((token=lexan()) != FERTIG) { switch (token) { case NICHTS : printf("....Unerlaubtes Zeichen\n"); case ZAHL : printf("%d (ZAHL)\n", tokenwert); case PLUS : printf("+ (PLUS)\n"); case MINUS : printf("- (MINUS)\n"); case MULT : printf("* (MULT)\n"); case DIV : printf("/ (DIV)\n"); case AUF : printf("( (AUF)\n"); case ZU : printf(") (ZU)\n"); case ZEILENENDE : printf("-----\n"); default : printf("....Programmfehler\n"); } } return(0); break; break; break; break; break; break; break; break; break; break; } Möglicher Ablauf dieses Programms lexanal.c: 2+ 3↵ 2 (ZAHL) + (PLUS) 3 (ZAHL) ----3*(4+5)↵ 3 (ZAHL) * (MULT) ( (AUF) 4 (ZAHL) + (PLUS) 5 (ZAHL) ) (ZU) ----Strg-Z ----- Hinweis: Für die lexikalische Analyse wurde unter Unix ein eigenes Tool lex geschaffen, das die lexikalische Analyse erheblich erleichtert. Dieses Tool wird inzwischen auch unter MS- DOS angeboten. So könnte unter Verwendung von lex1 die lexikalische Analyse für diesen Taschenrechner wesentlich einfacher realisiert werden: ........ %% [ \t]+ \n [0-9]+ "+" "-" "*" "/" "(" ")" . %% ; { { { { { { { { { /* Leer- und Tabzeichen ueberlesen */ return(ZEILENENDE); } tokenwert = strtol(yytext,NULL,10); return(ZAHL); } return(PLUS); } return(MINUS); } return(MULT); } return(DIV); } return(AUF); } return(ZU); } tokenwert = yytext[0]; return(NICHTS); } Die lexikalische Analyse zerlegt zwar den Eingabetext in einzelne Token, macht aber keinerlei Prüfung, ob die Reihenfolge der Token sinnvoll ist. Auch können Vorrangsregeln wie "Punkt vor Strich" nicht bei der lexikalischen Analyse berücksichtigt werden. Zur Prüfung solcher Regeln ist die Syntaxanalyse erforderlich. Syntaxanalyse: Die für einen Eingabetext vorgegebene Syntax legt die Regeln und die Grammatik fest, die für diesen Text gelten. Um eine Syntax zu beschreiben, existieren verschiedene Darstellungsmöglichkeiten: Syntaxdiagramm: Ein Beispiel für Syntaxdiagramme zeigt das folg. Bild, das ein Syntaxdiagramm für eine C-Variable zeigt. Variablenname: Nicht-Ziffer Nicht-Ziffer Ziffer Nicht-Ziffer: Ziffer: Eine der folgenden _ (Unterstrich) a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Eine der folgenden 0 1 2 3 4 5 6 7 8 9 Backus-Naur Form Eine andere und sehr häufig benutzte Darstellungsform ist die Backus-Naur Form (BNF), die oft auch kontextfreie Grammatik genannt wird. Eine kontextfreie Grammatik hat 4 Komponenten: 1. Eine Menge von terminalen Symbolen. Terminale Symbole sind die Token. 2. Eine Menge von nichtterminalen Symbolen. Nichtterminale Symbole sind Überbegriffe für Konstruktionen, welche sich aus terminalen und/oder nichtterminalen Symbolen zusammensetzen. 3. Eine Menge von Produktionen Jede Produktion wird dabei wie folgt angegeben: linke Seite ---> rechte Seite linke Seite muß dabei ein nichtterminales Symbol sein. rechte Seite ist eine Folge von terminalen und/oder nichtterminalen Symbolen. 4. Startsymbol: Ein nichtterminales Symbol legt immer das Startsymbol fest. Die Produktion mit dem Startsymbol wird bei der Angabe von Produktionen immer zuerst angegeben. Produktionen mit dem gleichen nichtterminalen Symbol auf der linken Seite, wie z.B. op 1 ---> + Das Buch Linux-Unix Profitools; Helmut Herold; Addison Wesley stellt dieses Tool ausführlich vor. op op op ---> ---> ---> * / können zu einer Produktion zusammengefaßt werden, indem die einzelnen rechten Seiten mit | voneinander getrennt angegeben werden: op ---> + | - | * | / Oft wird eine solche Produktion auch wie folgt angegeben: op | | | ---> * / + Es gibt auch ein leeres Symbol ε. In den nachfolgenden Beispielen werden terminale Symbole fett und nichtterminale Symbole kursiv angegeben. Ein Beispiel einer kontextfreien Grammatik für einen C-Variablennamen ist: C-Variablename ---> Buchziffer ---> | | Nicht-Ziffer ---> | | | | Ziffer ---> Nicht-Ziffer Buchziffer Nicht-Ziffer Buchziffer Ziffer Buchziffer e _ A N a n | | | | B O b o | | | | C P c p | | | | D Q d q | | | | E R e r | | | | F S f s | | | | G T g t | | | | H U h u | | | | I V i v | | | | J W j w | | | | K X k x | | | | L Y l y | | | | M Z m z 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 Die Syntax für unseren Taschenrechner hätte in BNF z.B. folgendes Aussehen: zeilen ---> | ausdruck e ZEILENENDE ausdruck ---> | | ausdruck ausdruck term PLUS MINUS term ---> | | term MULT term DIV factor factor ---> | | ZAHL MINUS ZAHL AUF ausdruck zeilen term term factor factor ZU Die Herleitung einer Eingabezeile für den Taschenrechner kann dabei sehr anschaulich an einem sogenannten parse tree gezeigt werden. Zum Beispiel ergäbe sich für die Eingabezeile 4 + 5 * 2↵ der in folgenden Bild gezeigte parse tree: zeilen ausdruck ausdruck ZEILENENDE PLUS term term term factor factor ZAHL ZAHL 4 + 5 MULT factor ZAHL * 2 Ein parse tree ist ein Baum mit folgenden Eigenschaften: 1. 2. 3. 4. Die Wurzel ist das Startsymbol. Jedes Blatt ist ein Token (terminales Symbol) oder ε. Jeder innerer Knoten (nicht die Blätter) ist ein nichtterminales Symbol. Wenn A ein innerer Knoten ist, der die Kinder b1, b2, ..., bn hat, so stellt dies folg. Produktion dar: A ---> b1 b2 ... bn Aus dem parse tree im Bild wird deutlich, daß zuerst 5*2 zu berechnen ist, bevor auf dieses Ergebnis dann der Wert 4 addiert wird. Der Vorteil von kontextfreien Grammatiken ist, daß man für die nichtterminalen Symbole Funktionen schreiben kann, die dann nur noch entsprechend den vorgegebenen Regeln aufzurufen sind. Dabei wird die rekursive Definition in einer Grammatik durch einen rekursiven Funkionsaufruf realisiert. So würde z.B. für die Produktion factor ---> AUF ausdruck ZU eine Funktion factor geschrieben, die ihrerseits die für Ausdrücke zuständige Funktion ausdruck aufruft. So könnte z.B. die Syntaxanalyse für unseren Taschenrechner - wie im folgenden Programm rechner.c gezeigt - durchgeführt werden: #include <stdio.h> #include <ctype.h> /*---- Definition von Token (Konstanten) ----------------------------------*/ #define ZAHL 256 #define PLUS 257 /* + */ #define MINUS 258 /* - */ #define MULT 259 /* * */ #define DIV 260 /* / */ #define AUF 261 /* ( */ #define ZU 262 /* ) */ #define NICHTS 263 #define ZEILENENDE 264 #define FERTIG 265 /*---- Deklaration von benutzten Funktionen -------------------------------*/ int lexan(void); /* Lexikalische Analyse */ int ausdruck(int *token); int term(int *token); int factor(int *token); /*---- Globale Variablen --------------------------------------------------*/ int tokenwert = NICHTS; /* Programmglobale Variable, welcher im Falle */ /* einer Zahl der Zahlenwert zugewiesen wird. */ int fehler; /* Programmglobale Variable, die anzeigt, ob ein */ /* ein Fehler in einem Ausdruck aufgetreten ist */ /*------------------------------------------------------- lexan ----------*/ int lexan( void ) /* Lexikalische Analyse */ { char zeich; while (1) { zeich = getchar(); if (zeich == ' ' || zeich == '\t') /* Leer- und Tabzeichen ueberlesen*/ ; else if (zeich == '\n') { return(ZEILENENDE); } else if (isdigit(zeich)) { ungetc(zeich, stdin); /* Zuviel gelesenes Zeichen zurueck in den Eingabepuffer */ scanf("%d", &tokenwert); return(ZAHL); } else if (zeich == EOF) { return(FERTIG); } else { switch (zeich) { case '+': return(PLUS); case '-': return(MINUS); case '*': return(MULT); case '/': return(DIV); case '(': return(AUF); case ')': return(ZU); default : fehler=1; return( NICHTS ); } } } } /*----------------------------------------------------- ausdruck ---------*/ int ausdruck( int *token ) { int ergeb = term(token); while (1) { switch(*token) case PLUS : case MINUS: default : } } { *token=lexan(); ergeb += term(token); break; *token=lexan(); ergeb -= term(token); break; return(ergeb); } /*--------------------------------------------------------- term ---------*/ int term( int *token ) { int erg = factor(token); while (1) { switch (*token) { case MULT: *token=lexan(); erg *= factor(token); break; case DIV : *token=lexan(); erg /= factor(token); break; default : return(erg); } } } /*------------------------------------------------------- factor ---------*/ int factor( int *token ) { int erg; switch (*token) { case ZAHL : erg = tokenwert; *token=lexan(); return(erg); case MINUS: switch (*token=lexan()) { case ZAHL : erg = tokenwert; *token=lexan(); return(-erg); default : fehler=1; return(NICHTS); } case AUF : *token=lexan(); erg=ausdruck(token); if (*token != ZU) fehler=1; *token=lexan(); return(erg); default : fehler=1; return(NICHTS); } } /*---------------------------------------------------------- main --------*/ int main(void) { int token, ergeb; while ((token=lexan()) != FERTIG) { fehler=0; ergeb = ausdruck(&token); if (fehler || token!=ZEILENENDE) { if (token != ZEILENENDE) while (lexan() != ZEILENENDE) ; printf(".... Syntaxfehler im Ausdruck\n"); } else printf(".... = %d\n", ergeb); } return(0); } Möglicher Ablauf dieses Programms rechner.c: 2+3 *5↵ .... = 17 (2+3) * 5↵ .... = 25 4+-12*6+3↵ .... = -65 (6*(((2+3)*4)/2+100)+5)*3↵ .... = 1995 3+*4↵ .... Syntaxfehler im Ausdruck -7--2--3↵ .... = -2 Strg-Z Die Funktion lexan zerlegt den Eingabetext in Token. Das erste Token wird dabei immer an die Funktion ausdruck übergeben, welche dann die Syntaxanalyse für einen Ausdruck (eine Zeile) einleitet. Die Syntaxanalyse wird entsprechend der Reihenfolge der gelieferten Token durchgeführt, wobei entsprechend dem parse tree das Ergebnis des Ausdrucks berechnet wird. Hinweis: Für die Syntaxanalyse wurde unter Unix ein eigenes Tool yacc geschaffen, das die Syntaxanalyse erheblich erleichtert. Dieses Tool yacc wird inzwischen auch unter MS-DOS angeboten. Unter Verwendung von yacc kann die Syntaxanalyse für diesen Taschenrechner wesentlich einfacher und verständlicher formuliert werden, da im yacc-Programm nur die zuvor in BNF vorgestellten Produktionsregeln anzugeben sind. yacc formt diese Regeln dann in ein CProgramm um. So könnte unter Verwendung von yacc2 die Syntaxanalyse für diesen Taschenrechner wesentlich einfacher realisiert werden: ....... %start zeilen %token PLUS MINUS %% zeilen : | ; MULT DIV AUF ZU ZAHL ZEILENENDE /* leeres Symbol (Epsilon) */ ausdruck { printf("%d",$1); prompt; } NICHTS ZEILENENDE ausdruck: | | ; ausdruck PLUS term { $$ = $1 + $3; } ausdruck MINUS term { $$ = $1 - $3; } term { $$ = $1; } term : | | ; term MULT factor { $$ = $1 * $3; } term DIV factor { $$ = $1 / $3; } factor { $$ = $1; } factor : | | ; AUF ausdruck ZU { $$ = $2; } ZAHL { $$ = tokenwert; } MINUS ZAHL { $$ = -tokenwert; } zeilen %% ....... 2 Das Buch Linux-Unix Profitools; Helmut Herold; Addison Wesley stellt dieses Tool ausführlich vor. A nwendung Rekursion beim Backtracking-Verfahren Backtracking ist ein Lösungsverfahren, das nach dem Trial-and-Error-Verfahren (Versuchund-Irrtum) arbeitet. Beim Backtracking versucht man, eine Teillösung eines Problems systematisch zu einer Gesamtlösung auszubauen. Falls in einem gewissen Punkt ein weiterer Ausbau einer vorliegenden Teillösung nicht mehr möglich ist (Sackgasse), werden eine oder mehrere der letzten Teilschritte rückgängig gemacht. Die dann erhaltene reduzierte Teillösung versucht man auf einem anderen Weg wieder auszubauen. Das Zurücknehmen von Schritten und erneute Vorangehen wird solange wiederholt, bis eine Lösung des vorliegenden Problems gefunden ist oder bis man erkennt, daß das Problem keine Lösung besitzt. Die Möglichkeit in Sackgassen zu laufen und aus ihnen "rückwärts" wieder herauszufinden, zeichnet das Backtracking-Verfahren aus. Beispiel: Ein klassisches Problem, das sich mit Backtracking lösen läßt, ist eine Maus im Labyrinth, die einen Käse finden muß. Maus 1 2 3 Käse 1 2 3 Die möglichen Wege der Maus lassen sich dabei als Baum darstellen. Die Knoten des Baums sind dabei mit der Position der Maus im Labyrinth markiert. (1,1) (1,2) (2,2) (2,1) (1,3) (2,3) (3,2) Sackgasse (3,3) (3,1) Lösung Viele Probleme lassen sich mit Hilfe des Backtracking-Verfahrens lösen. Beispiel: Hier als Beispiel das Programm teilsum.c, das zunächst eine Zahl einliest und dann alle möglichen Teilsummen zu dieser Zahl ausgibt. Zur Zahl 4 wären z.B. die folgenden Teilsummen möglich: 1 1 1 1 2 2 3 4 + + + + + + + 1 1 2 3 1 2 1 + 1 + 1 = 4 + 2 = 4 + 1 = 4 = 4 + 1 = 4 = 4 = 4 = 4 Das folgende Programm teilsum.c zeigt jedoch nicht nur die erlaubten Folge, sondern auch alle seine Fehlversuche ("Sackgassen"), die es bei der Anwendung des BacktrackingVerfahrens durchläuft. #include <stdio.h> void folge(int sum, int n, int i, int level ) { int j; if (sum+i==n) { printf("%*c%d ---\n", level*4, ' ', i); } else if (sum+i>n) { printf("%*c%d ...\n", level*4, ' ', i); } else { printf("%*c%d\n", level*4, ' ', i); for (j=1 ; j<=n ; j++) { folge(sum+i, n, j, level+1); } } } int { main( void ) int zahl, i; scanf("%d", &zahl); printf("------------------------------\n"); for (i=1 ; i<=zahl ; i++) folge(0, zahl, i, 1); return(0); } Mögliche Abläufe dieses Programms teilsum.c. 1. Ablaufbeispiel: 3↵ -----------------------------1 1 1 --2 ... 3 ... 2 --3 ... 2 1 --2 ... 3 ... 3 --- Diese Ausgabe ist wie folgt zu interpretieren: 1. Alle mit "---" gekennzeichnten Zahlen zeigen eine erlaubte Teilsumme an. Mit "..." gekennzeichnete sind Fehlversuche ("Sackgassen"). 2. Zu einer erlaubten Folge gehören neben der mit "---" gekennzeichnten Zahl noch alle in den vorherigen Zeilen und Spalten stehenden Zahlen. Die Ausgabe oben ist also wie folgt zu interpretieren, wobei nachfolgend die erlaubten Teilsummenfolgen fett geschrieben sind: 1 | 1 | | | | | | | 2 | 3 2 | 1 | 2 | 3 3 --- 1 --- (1+1+1) 2 ... (1+1+2) 3 ... (1+1+3) --(1+2) ... (1+3) (2) --(2+1) ... (2+2) ... (2+3) (3) 2. Ablaufbeispiel: 5↵ -----------------------------1 1 1 1 1 --2 ... 3 ... 4 ... 5 ... 2 --3 ... 4 ... 5 ... 2 1 --2 ... 3 ... 4 ... 5 ... 3 --4 ... 5 ... 2 1 1 --2 ... 3 ... 4 ... 5 ... 2 --3 ... 4 ... 5 ... 3 1 --2 ... 3 ... 4 ... 5 ... 4 --5 ... 2 1 1 1 --2 ... 3 ... 4 ... 5 ... 2 --3 ... 4 ... 5 ... 2 1 --2 ... 3 ... 4 ... 5 ... 3 --4 ... 5 ... 3 1 1 --2 ... 3 ... 4 ... 5 ... 2 --3 ... 4 ... 5 ... 4 1 --2 ... 3 ... 4 ... 5 ... 5 --- Um aus dieser Ausgabe alle möglichen Teilsummen zu erhalten, könnte man diese Ausgabe in eine Datei umlenken, und dann in dieser Datei zunächst alle Zeilen löschen, die die Kennzeichnung "..." enthalten: 1 1 1 1 1 --2 --2 1 --3 --2 1 1 --2 --3 1 --4 --2 1 1 1 --2 --2 1 --3 --3 1 1 --2 --4 1 --5 --- Nun müßte man diese Datei noch weiter editieren, indem man alle leeren Spalten mit der weiter oben stehenden Zahl in dieser Spalte füllt, und man erhält schließlich die gesuchten Teilsummen in einer lesbaren Form: 1 1 1 1 1 1 1 1 2 2 2 2 3 3 4 5 1 1 1 1 2 2 3 4 1 1 2 3 1 2 1 1 1 2 3 1 2 1 1 2 1 1 2 1 1 1 1 1 Weiteres Beispiel: Das folgende Programm mauskaes.c baut ein Labyrinth auf, indem es zufällig Hindernisse am Bildschirm positioniert. Danach plaziert es einen Käse an einer zufälligen Stelle in diesem Labyrinth, bevor sich dann die Maus (unter Anwendung des Backtracking-Verfahrens) auf die Suche nach dem Käse begibt. #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <dos.h> #include <time.h> void labyrinth_erstellen(int *breite, int *hoehe, int *hind_zahl) { int h=0, x, y; char zeich[2]; clrscr(); textcolor(LIGHTGRAY); printf("Maus sucht einen Kaese in einem Labyrinth\n" "=========================================\n\n"); do { printf("Hoehe des Labyrinths [1,20]: "); scanf("%d", hoehe); } while (*hoehe<1 || *hoehe>20); do { printf("Breite des Labyrinths [1,70]: "); scanf("%d", breite); } while (*breite<1 || *breite>70); do { printf("Anzahl der Hindernisse [1,%d]: ", *hoehe**breite-1); scanf("%d", hind_zahl); } while (*hind_zahl<1 || *hind_zahl>*hoehe**breite-1); clrscr(); textcolor(BLUE); while (h<*hind_zahl) { x = rand()%*breite+1; y = rand()%*hoehe+1; gettext(x, y, x, y, zeich); if (zeich[0] == ' ') { gotoxy(x,y); putch('#'); h++; } } textcolor(YELLOW); while (1) { x = rand()%*breite+1; y = rand()%*hoehe+1; gettext(x, y, x, y, zeich); if (zeich[0] == ' ') { gotoxy(x, y); putch('@'); break; } } } int maus_start_suchen(int breite, int hoehe, int *xpos, int *ypos) { int i, j; char zeich[2]; for (i=1 ; i<=hoehe ; i++) for (j=1 ; j<=breite ; j++) { gettext(i, j, i, j, zeich); if (zeich[0] == ' ') { *xpos=i; *ypos=j; return(1); } } return(0); } void versuch(int xpos, int ypos, int breite, int hoehe) { char zeich[2]; gettext(xpos, ypos, xpos, ypos, zeich); if (zeich[0] == '@') { gotoxy(xpos, ypos); textcolor(YELLOW+BLINK); putch('*'); gotoxy(10,23); cprintf("....Maus hat Kaese gefunden"); getch(); exit(0); /*--- Programm verlassen ---*/ } if (zeich[0] == ' ') { delay(6000/(breite*hoehe)+40); gotoxy(xpos, ypos); textcolor(GREEN); putch('*'); /*----- Backtracking-Teil ---------------------------------*/ if (xpos+1<=breite) versuch(xpos+1, ypos, breite, hoehe); if (ypos+1<=hoehe) versuch(xpos, ypos+1, breite, hoehe); if (xpos-1>0) versuch(xpos-1, ypos, breite, hoehe); if (ypos-1>0) versuch(xpos, ypos-1, breite, hoehe); } gotoxy(xpos, ypos); textcolor(RED); putch('*'); } int { main( void ) int hoehe, breite, hind_zahl, xpos, ypos; srand(time(NULL)); labyrinth_erstellen(&breite, &hoehe, &hind_zahl); if (maus_start_suchen(breite, hoehe, &xpos, &ypos)==0) { gotoxy(5,23); textcolor(YELLOW); cprintf("....Keine freie Position fuer Maus im Labyrinth gefunden"); } else { versuch(xpos, ypos, breite, hoehe); gotoxy(5,23); textcolor(YELLOW); cprintf("...Kein Durchkommen zum Kaese..."); } getch(); return(0); } In diesem Programm mauskaes.c läuft die Maus immer nur nach links, rechts, unten oder oben. Wollte man sie auch diagonal laufen lassen, so müßte man den oben fett geschriebenen Backtracking-Teil wie folgt erweitern: ..... /*----- Backtracking-Teil ---------------------------------*/ if (xpos+1<=breite) versuch(xpos+1, ypos, breite, if (xpos+1<=breite && ypos+1<=hoehe) versuch(xpos+1, ypos+1, breite, if (ypos+1<=hoehe) versuch(xpos, ypos+1, breite, if (xpos-1>0 && ypos+1<=hoehe) versuch(xpos-1, ypos+1, breite, if (xpos-1>0) versuch(xpos-1, ypos, breite, if (xpos-1>0 && ypos-1>0) versuch(xpos-1, ypos-1, breite, if (ypos-1>0) versuch(xpos, ypos-1, breite, if (xpos+1<=breite && ypos-1>0) versuch(xpos+1, ypos-1, breite, ....... hoehe); hoehe); hoehe); hoehe); hoehe); hoehe); hoehe); hoehe); Da beim Backtracking-Verfahren jedoch die Rechenzeit exponentiell mit der Suchtiefe anwächst, eignen sich Backtracking-Verfahren für die Praxis oft nur dann, wenn man durch Zusatzbedingungen vorab möglichst viele Sackgassen (Branch-and-Bound-Verfahren, Entscheidungsbaum) ausschließen kann, worauf wir hier nicht näher eingehen.