Rekursion bei Syntaxanalyse und Backtracking

Werbung
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.
Herunterladen