Vous êtes sur la page 1sur 48

CHAPITRE 1 : EXPRESSIONS REGULIERES ET AUTOMATES

1. Les notions d’expression régulière et d’automate

1.1 La commande egrep d’Unix


La commande Unix egrep permet de chercher un motif dans un texte. Chaque ligne tapée
est
- soit recopiée par egrep, si le motif s’y trouve,
- soit ignorée.
Le motif peut être défini par la suite de ses caractères, mais il peut être défini aussi par une
expression rationnelle. Exemples :

chemillier$ egrep 'abc'


abababababababa = ignoree
abbcabbcabbc = ignoree
aaaaabbbbbc = ignoree
aaaaabccccc
aaaaabccccc = ligne recopiee car elle contient ‘abc’

chemillier$ egrep 'aaaaaa'


aaaaa = ignoree
djkhaafkjhdjkhaaaajhkjh = ignoree
dkjdhkjdhkdhjaaaaaaakjfhjkfh
dkjdhkjdhkdhjaaaaaaakjfhjkfh = recopiee car plus de 6
occurrences

Plutôt que d’indiquer les répétitions en recopiant les caractères, on peut utiliser un symbole
de répétition. Ainsi a* signifie zéro, une ou plusieurs occurrences successives de a
(attention au « zéro ») :

chemillier$ egrep 'ba*ab'


dkjdhdbbbbbaaadhjhd
dlkdjldkjdlkjbaaaabflkjflkfj
dlkdjldkjdlkjbaaaabflkjflkfj = au moins 1 a entre 2 b

chemillier$ egrep 'a*'


dlkdjlkjdkldj
dlkdjlkjdkldj = recopiee, meme si 0 a

chemillier$ egrep 'ba*b'


bbbbbbccccabbb
bbbbbbccccabbb

chemillier$ egrep 'tra(la)*lere'


tralalere
tralalere = 1 occurrence
tralalalalalalere
tralalalalalalere = 5 occurrences
tralala = ne marche pas
tralere

1
tralere = OK

On peut utiliser egrep en traitant le texte contenu dans un fichier ex1. Les lignes contenant
le motif sont recopiées, et on peut aussi afficher leur numéro (option –n) :

chemillier$ cat ex1


toto
titi
tata
tutu
tete

chemillier$ egrep -n 'toto' ex1


1:toto

chemillier$ egrep -n 'titi' ex1


2:titi

Le symbole | (Maj-Alt-L sous Mac) permet d’indiquer un choix entre plusieurs caractères
ou suites de caratères :

chemillier$ egrep -n 't(a|e|u)t(a|e|u)' ex1


3:tata
4:tutu
5:tete

chemillier$ egrep -n 't(a|e|i)t(u|o|e)' ex1


5:tete

chemillier$ egrep 't((a|e)t)*a'


tatatatata
tatatatata = OK
tetetetete = non
tatetatetateta
tatetatetateta = OK
teteteteteteteta
teteteteteteteta = OK
taaaaaaaaaaaaaaaa
taaaaaaaaaaaaaaaa = OK

Les expressions régulières sous Unix (pour egrep, ou pour la commande lex qu’on verra
plus tard) comportent également un certain nombre d'abréviations :
[abc] pour a|b|c
[a-z] pour a|b|…|z
[a-zA-Z0-9] pour toutes les lettres et chiffres
x+ pour au moins une occurrence de x, c’est-à-dire xx*
x? pour un x optionnel
Exemple : [A-Z][a-z]* pour un nom propre, c’est-à-dire commençant par une majuscule.

1.2 Expressions régulières et automates finis

2
Les expressions régulières sont un moyen de définir et de traiter des ensembles de suites de
caractères.
Ces ensembles peuvent contenir :
- des suites de caractères définies explicitement,
- des répétitions de suites de caractères,
- des choix entre plusieurs suites de caractères.

Exemple : a(b|c)*d

Une autre manière de représenter une expression régulière est de dessiner un graphe appelé
« automate fini » :

Combien de mots contient l’ensemble ainsi défini ?


une infinité
=> donc il s’agit d’une représentation finie d’un ensemble infini

Un automate peut être considéré comme une machine avec :


- des états,
- des transitions qui vont d’un état à un autre quand on lit un caractère,
- un état initial (petite flèche entrante) et des états finals (double cercle).
Le « comportement » d’un automate est l’ensemble des suites de caractères qui le font
passer de l’état initial à un état final.

On distingue deux types d’automates fini :


- AFD : automate fini déterministe
- AFN : automate fini non déterministe
Les principaux automates finis étudiés dans ce cours sont les AFD.

2. La hiérarchie de Chomsky

2.1 Langages et machines

Un programme informatique est une suite de caractères (aussi appelée « mot ») qu’une
machine doit traiter.
Un « langage » est un ensemble de suites de caractères (ou de mots), une machine au sens
abstrait est un calculateur sur des mots.
3
Le domaine des langages formels a été développé par :
- les informaticiens : élaborer des langages de programmation,
- les linguistes : décrire des langues naturelles avec l’idée de les traiter
automatiquement.
Le thème central est l’étude des modèles qui permettent une spécification ou une
représention finie des langages formels.

Plusieurs approches :
- spécifier les propriétés des mots du langage : définitions en français ou expression
régulières (Kleene) egrep
- spécifier les propriétés des mots du langage : les grammaires formelles (Chomsky)
- reconnaître qu’un mot donné est un mot du langage avec des mécanismes
automatiques réalisés par différents types de machines (Turing, McCulloch et Pitt)
- spécification logique, algébrique, etc.

Les automates finis sont les machines les plus simples. Ils interviennent dans de nombreux
domaines :
- traitement de texte, compilation, codage, modélisation des circuits, vérification,
compression de données, formatage de texte, calcul formel.
- biologie (pour certains algorithmes liés au génome),
- dans l’étude des événements discrets (automatique, musique)

2.2 Classification de Chomsky

Classes de langages Types de machines Types de grammaires


réguliers (ou rationnels) AFD ou AFN type 3 (régulière)
algébriques automate à pile déterministe type 2 (algébrique)
contextuels machine de Turing non type 1 (contextuelle)
déterministe à espace
linéairement borné
récursivement énumérables machine de Turing, type 0
automates cellulaires, etc.

Objectifs du cours :
1) Éléments théoriques pour les deux derniers niveaux : langages rationnels et algébriques
2) Applications à la compilation :
- Automates finis (AF) = base de l’analyse lexicale,
- Grammaires algébriques (voir cours de Patrice Enjalbert) = base de l’analyse
syntaxique.

3. Vocabulaire

4
3.1 Définitions générales

Un alphabet Σ un ensemble fini non vide de symboles appelés aussi lettres ou caractères.
Exemple : le langage machine est basé sur l’alphabet Σ = {0, 1}.
Un mot est une suite finie de symboles.
La longueur d’un mot u notée |u| est le nombre de symboles de ce mot. Exemple : le mot u
= 011 est de longueur |u| = 3.
Le mot vide noté ε est le seul mot de longueur nulle.
Σ* représente l’ensemble des mots sur l’alphabet Σ.
Σ+ représente l’ensemble des mots sur l’alphabet Σ de longueur ≥ 1.

1.2 Concaténation

La concaténation de deux mots u et v, notée uv, est la juxtaposition de u et de v. Pour u =


a1 a2 …ap et v = b1 b2 …bq , on a uv = a1 …ap b1 …bq .
Cette opération est associative, non commutative, et ε est l’élément neutre.
La puissance n-ième d’un mot est définie inductivement :
u0 = ε, un+ 1 = un u.
Exemple : u = aba, u3 = abaabaaba.
Un mot u ∈ Σ* est facteur du mot w ∈ Σ* s’il existe v, v' ∈ Σ* tels que w = vuv'. Si v = ε,
on dit que u est préfixe. Si v' = ε, on dit que u est suffixe. Exemple : abb est facteur de
babba, et ba est à la fois préfixe et suffixe, mais aa n’est pas facteur.

1.3 Opérations sur les langages

• opérations classiques sur les ensembles :


union ∪, intersection ∩, différence \, complémentaire,

• opérations héritées de la concaténation :


La concaténation de deux langages est définie par :
L1 L2 = {uv, u ∈ L1 et v ∈ L2 }.
Exemple : L1 = {a, ab}, L2 = {c, bc}, L1 L2 = {ac, abc, abbc}.

La puissance d’un langage L est définie inductivement :


L0 = {ε}, Ln+ 1 = Ln L.
L’étoile d’un langage L, notée L* , est :
L* = {ε} ∪ L ∪ L2 … ∪ Ln ∪ …
Ce langage contient un nombre infini de mots, qui sont les répétitions indéfinies de mots de
L.

5
Exemple :
L = {ab, b}, L* est l’ensemble de tous les mots tels que aa n’est pas facteur, et a n’est pas
suffixe.
On note également L+ le langage :
L + = L ∪ L 2 … ∪ Ln ∪ …

4. Définition des expressions rationnelles (ou régulières)

Définition. Soit Σ un alphabet. Les expressions rationnelles (ou régulières) sur Σ et les
langages correspondants sont définis récursivement :
1) • ∅ est une expression rationnelle, et représente l’ensemble vide,
• ε est une expression rationnelle, et représente l’ensemble {ε},
• pour toute lettre a de Σ, a est une expression rationnelle, et représente l’ensemble
{a}.
2) Si r et s sont des expressions rationnelles, qui représentent les langages R et S,
alors r + s, rs, et r* sont des expressions rationnelles qui représentent les
langages R ∪ S, RS et R* .

Le terme « rationnel » correspond à l’usage français, « régulier » à l’usage anglais


(regular).
Ordre de priorité sur les opérations : étoile > concaténation > union.
Exemple : 01* + 1 correspond à (0(1)*) + 1.

Quelques expressions rationnelles et leurs langages, pour l’alphabet Σ = {a, b} :

Description en français Expression rationnelle Langage correspondant


se terminant par a (a + b)*a Σ* {a}
contenant le facteur bb (a + b)*bb(a + b)* Σ* {bb}Σ*
ne contenant pas le facteur bb (ε + b)(a + ab)* {ε, b}{a, ab}*

Ces définitions permettent de donner un cadre théorique à l’étude des expressions


rationnelles telles qu’on les utilise dans la pratique avec egrep ou lex.
Il y a une correspondance directe entre les opérations ci-dessus et la notation d’Unix : la
notation r + s est équivalente à la barre de disjonction d’Unix r|s.

Expression rationelle Unix (egrep, lex)


(a + b)*a (a|b)*a
(a + b)*bb(a + b)* (a|b)*bb(a|b)*
(ε + b)(a + ab)* ([]|b)(a|ab)*

Le but principal de cette approche théorique est de montrer :


6
Expressions rationnelles = Langages définis par les AFD

7
CHAPITRE 2 : AUTOMATES FINIS DETERMINISTES (AFD)

1. Définition des automates finis déterministes (AFD)

1.1 Définition générale

Définition. Un automate fini déterministe AFD sur un alphabet Σ est la donnée d’un n-
uplet (Q, δ, i, F) où :
• Q est un ensemble fini d’états,
• δ est une fonction de transition de Q × Σ dans Q,
• i est un état particulier de Q dit initial,
• F est une partie de Q d’états dits finals.
L’automate est dit complet lorsque la fonction δ est partout définie sur Q × Σ.

Exemple :

Q = {1, 2},
i = 1, état noté avec une petite flèche entrante,
F = {2}, état noté avec deux cercles.
δ:Q×Σ→Q
(1, a) → 2
(1, b) → 1
(2, b) → 1
Cet automate n’est pas complet, car δ(2, a) n’est pas défini.

Pour décrire un automate, il est commode d’utiliser une table de transitions :

1 2
a 2
b 1 1

1.2 Prolongement de la fonction de transition

1
Le calcul de l’automate consiste à suivre des flèches, en partant d’un état initial et en
s’arrêtant dans un état final. Le mot correspondant à ce calcul est la suite des étiquettes des
flèches.

On prolonge δ par induction, en une fonction sur les mots de Q × Σ* → Q


(attention : la fonction δ est définie au départ sur Q × Σ et pas sur Q × Σ* ):
δ(q, ε) = q,
δ(q, wa) = δ(δ(q, w), a)
pour tous q ∈ Q, w ∈ Σ* , a ∈ Σ.

δ(q, w) = état atteint après lecture du mot w depuis l’état q.

Propriété d’associativité. Pour tous mots u, v ∈ Σ* , on a : δ(q, uv) = δ(δ(q, u), v).

Exemple : dans l’automate ci-dessus

a correspond au calcul 1 → 2,
aba correspond au calcul 1 → 2 → 1 → 2,
donc on écrira :
δ(1, aba) = 2.
À partir de l’état 2, on ne peut lire un mot commençant par a, mais on peut lire babb par
exemple : δ(2, babb) = 1.
La propriété d’associativité donne :
δ(1, abababb) = δ(δ(1, aba), babb) = δ(2, babb) = 1.

Le mot aba correspond à un calcul terminal car :


δ(1, aba) = 2 ∈ F terminal.
En revanche, abababb ne correspond pas à un calcul terminal de l’automate :
δ(1, abababb) = 1 non terminal.

1.3 Langage reconnu par un AFD

Définition. Le langage reconnu (ou accepté) par un automate AFD est l’ensemble des
mots qui correspondent à un calcul de l’automate partant d’un état initial et s’arrêtant
dans un état final.
2
Exemple : le langage reconnu par l’automate ci-dessus est
b*a(bb*a)*

Les automates finis sont les modèles de machine les plus simples : ils n’ont aucun support
de mémoire externe (comme la pile d’un automate à pile).
Leur mémoire est donc finie (espace constant), et correspond à leur nombre d’états. Par
exemple, dans l’automate ci-dessus, l’état 1 permet de se souvenir qu’il faut lire un a pour
sortir.

Exemples :
- distributeur de café : les pièces introduites sont les symboles de l’alphabet, l’état
terminal est atteint quand le montant est supérieur au montant demandé,
- mécanisme contrôlant le code d’accès d’une porte : les chiffres tapés sont les
symboles, l’état terminal est celui qui déclenche l’ouverture,
- rubiks cube : l’alphabet est l’ensemble des rotations des 6 faces.

2. Clôture par complément des langages reconnus par AFD

Propriété. Si L est un langage reconnu par AFD, alors son complémentaire Σ* \L l’est
aussi.

Exemple : L = {aibj, i, j ∈ N},


Σ* \L = {w ∈ Σ* , ba est facteur de w}

On complète l’automate :

Puis on intervertit les états finals :

Construction :
Si A = (Q, δ, i, F) est un AFD complet qui reconnaît L, alors

3
A = (Q, δ, i, Q\F) reconnaît Σ* \L.

4
CHAPITRE 3 : AUTOMATES FINIS NON-DETERMINISTES (AFN)

1. Définition des automates finis non-déterministes (AFN)

1.1 Définition générale

Définition. Un automate fini non-déterministe AFN sur un alphabet Σ est la donnée d’un
n-uplet (Q, δ, I, F) où :
• Q est un ensemble fini d’états,
• δ est une fonction de transition de Q × Σ dans P(Q), ensemble des parties de Q,
• I est une partie de Q d’états dits initiaux,
• F est une partie de Q d’états dits finals.

1.2 Fonctionnement

Un automate fini non-déterministe est un automate tel que dans un état donné, il peut y avoir
plusieurs transitions avec la même lettre.

Au temps initial, la machine est dans un état initial i ∈ I.


Si à l’instant t la machine est dans l’état q et lit a, alors à l’instant t + 1
• si δ(q, a) = ∅, la machine se bloque,
si δ(q, a) ≠ ∅, la machine se met dans l’un des états ∈ δ(q, a) (et lit le symbole suivant).
On voit que le fonctionnement n’est pas complètement « déterminé », car on ne sait pas à
l’avance quel état la machine doit choisir dans δ(q, a), d’où le terme non-déterministe.
On pourrait donc penser que les AFN n’ont aucun intérêt dans la pratique.
En fait, il n’en est rien car on peut montrer que tout AFN peut être remplacé par un AFD
équivalent (voir algorithme de déterminisation).
Donc on se sert des AFN, car ils peuvent être très utiles pour exprimer certains problèmes,
et on les remplace ensuite par des AFD.

Exemple : ensemble des mots qui se terminent par ab

Q = {1, 2, 3},
I = {1},
F = {3}.
δ définie par la table de transition :

1
1 2 3
a {1,2} ∅ ∅
b 1 3 ∅

1.3 Langage reconnu par un AFN

Un mot u est accepté par un AFN s’il existe un chemin d’étiquette u, partant de l’un des
états initiaux, et arrivant à l’un des états finals.

Définition. Le langage reconnu (ou accepté) par un automate AFN est l’ensemble des
mots acceptés par l’AFN, c’est-à-dire qui correspondent à un calcul de l’automate partant
d’un état initial et s’arrêtant dans un état final.

2. Déterminisation d’un AFN

2.1 Algorithme de déterminisation d’un AFN

Un AFD est un cas particulier d’AFN, avec Card(δ(q, a)) ≤ 1 pour tous q ∈ Q, a ∈ Σ.
Donc tout langage reconnu par un AFD est reconnu par un AFN.
Plus surprenant, on a la réciproque.

Théorème (Rabin-Scott). Tout langage reconnu par un AFN peut être reconnu par un
AFD.

Le principe de la construction est de prendre comme états de l’AFD les ensembles d’états
de l’AFN. L’unique état initial de l’AFD est l’ensemble I des états initiaux de l’AFN.

Construction pour la déterminisation d’un AFN :


Si A = (Q, δ, I, F) est un AFN qui reconnaît L, alors
Adet = (P(Q), δdet, I, Fdet) est un AFD qui reconnaît le même langage L, avec :
• Fdet = {X ∈ P(Q), X ∩ F ≠ ∅},
• δdet(X, a) = ∪q∈X δ(q, a).

Pratiquement, on ne construit que les états accessibles à partir de I, de proche en proche :


on part de l’état initial I, puis on calcule toutes les transitions qui partent de I, puis on
recommence avec les nouveaux états obtenus, etc.

Exemple :

2
état initial : I = {1}
transition par a : {1, 2}
transition par b : {1}

état {1, 2}
transition par a : {1, 2}
transition par b : {1, 3}

état {1, 3}
transition par a : {1, 2}
transition par b : {1}

2.2 Équivalence entre AFD et AFN

Conclusion. Les langages reconnus par les AFN sont exactement les langages reconnus
par les AFD.

Dans l’exemple ci-dessus, le nombre d’états de l’automate déterminisé est égal à celui de
l’AFN de départ. On verra que c’est le cas dans certaines applications, notamment pour la
recherche de motifs.
Mais il existe des cas où l’AFD équivalent est beaucoup plus gros. En fait, si Card(Q) = n,
la limite théorique est Card(P(Q)) = 2n . Or il existe des automates qui correspondent à ce
cas limite.
Exemple : L = {a, b}* a{a, b}n
L’AFN a n + 1 états, alors que l’AFD équivalent a 2n états.

3. Automates AFN avec ε-transitions

3.1 Définition

3
Définition. On généralise encore la notion d’automate en introduisant des transitions
avec le mot vide ε. La fonction de transition est alors définie de Q × (Σ ∪ {ε}) dans P(Q),
ensemble des parties de Q. Ces transitions s’effectuent sans lecture de lettre.

Comme pour les AFN, un mot u est accepté par un AFN avec ε-transitions s’il existe un
chemin d’étiquette u, partant de l’un des états initiaux, et arrivant à l’un des états finals.

Exemple :

Langage reconnu : L = {acbc, acc, abc, ac}

3.2 Transformation en AFN sans ε-transition

Proposition. Tout langage reconnu par un AFN avec ε–transitions peut être reconnu par
un AFN (sans ε–transitions) ayant le même nombre d’états.

Construction de l’AFN équivalent à un automate avec ε–transitions, en prolongeant δ en δ1 :


Première étape : Ajoût de ε–transitions par fermeture transitive.
• Pour tous les états q accessibles à partir de p par ε-transition (en plusieurs étapes), on
rajoute une ε–transition directe de p à q.
On prolonge ainsi la fonction δ :
δ1 (p, ε) = tous les états accessibles à partir de p par ε–transition.

Deuxième étape : Ajoût de transitions supplémentaires et d’états initiaux supplémentaires,


puis suppression des ε-transitions.
• Les états initiaux sont tous les états accessibles à partir des états initiaux par ε–transition.
• Pour toute transition de p à q par une lettre, on rajoute des transitions de p à r avec la
même lettre pour toutes les ε–transitions de q à r.
δ1 (p, a) = δ(p, a) ∪ ( ∪q∈δ(p, a) δ1 (q, ε)).
• On supprime toutes les ε–transitions.

On vérifie :
- qu’un chemin correspondant à un calcul réussi pour un mot dans l’automate avec
ε–transitions peut facilement être transformé en chemin dans le nouvel AFN, grâce
aux nouvelles transitions introduites,
- et qu’un chemin correspondant à un calcul réussi dans l’AFN peut être transformé
en chemin dans l’automate avec ε–transitions, en remplaçant chaque nouvelle
transition utilisée par les ε–transitions correspondantes.

4
Exemple :
• Fermeture transitive des ε-transitions

• Ajoût de nouvelles transitions et suppression des ε-transitions

L’AFN obtenu reconnaît bien le même langage : L = {acbc, acc, abc, ac}

4. Clôture des langages reconnus par automates (AFN / AFD)

4.1 Clôture par complémentation

Propriété. Si L est un langage reconnu par AFN, alors son complémentaire Σ* \L l’est
aussi.

Par déterminisation, on peut construire un AFD reconnaissant le même langage L, puis


appliquer la propriété de clôture vue au chapitre 2.

4.2 Clôture par intersection

Propriété. Si L1 , L2 sont des langages reconnus par AFN, alors L1 ∩ L2 l’est aussi.

Exemple : L1 = {w ∈ Σ* , w a au moins un b},


L2 = {w ∈ Σ* , w a un nombre pair de a}

5
Construction par produit d’automates :
Si Ai = (Qi, δi, Ii, Fi) reconnaît Li pour i = 1, 2 sans ε–transition, alors
A = (Q1 × Q2 , δ, I1 × I2 , F1 × F2 ) reconnaît L1 ∩ L2 .

Justification :
• Si un mot correspond à un calcul réussi dans l’automate produit, on obtient une
succession de couples d’états. En projetant sur les premières composantes (resp.
deuxièmes), on obtient un calcul réussi dans le 1er automate (resp. 2ème). Donc le mot
appartient à l’intersection.
• Réciproquement, si un mot appartient aux deux langages, il correspond à deux calculs
réussis dans les automates. S’il n’y a pas de ε–transition, les deux chemins ont même
longueur. En fabriquant des couples à partir des deux séries d’états, on obtient un calcul
réussi dans l’automate produit.

Remarque : Si les automates sont des AFD, alors l’automate produit obtenu pour
l’intersection est aussi un AFD.

4.3 Clôture par union

Propriété. Si L1 , L2 sont des langages reconnus par AFN, alors L1 ∪ L2 l’est aussi.

Il suffit de faire l’union des deux AFN.

4.4 Clôture par concaténation et étoile

Propriété. Si L1 , L2 sont des langages reconnus par AFN, alors L1 L2 l’est aussi.

Pour cette propriété, il est très commode d’utiliser des ε–transitions :

6
on rajoute des ε–transitions de tous les états finals de l’AFN pour L1 , vers tous les états
initiaux de l’AFN pour L2 .

Propriété. Si L est un langage reconnu par AFN, alors L* l’est aussi.

De même, on rajoute des ε–transitions de tous les états finals de l’AFN pour L, vers tous
ses états initiaux.

4.5 Théorème de clôture pour les langages reconnus par AFN ou AFD

Théorème. Si L, L' sont des langages reconnus par AFN, alors il en est de même :
• du complémentaire Σ* \L,
• de l’intersection L ∩ L',
• de l’union L ∪ L',
• de la concaténation LL',
• de l’étoile L*
Les langages reconnus par AFD étant les mêmes que les langages reconnus par AFN, ces
propriétés de clôture sont vraies aussi pour les langages reconnus par AFD.

5. AFN avec probabilités de transitions : chaînes de Markov

Une chaîne de Markov est un AFN dont les transitions sont munies de probabilités.

Exemple : Construction des probabilités de transitions dans un mot.


Soit le mot w = aaabaa
On compte le nombre de fois où chaque lettre est suivie d’une même lettre :

a b
a 3 1
b 1 0

Dans cet exemple, le contexte est de longueur 1, mais on peut étudier des contextes plus
long, par exemple de longueur 2 :
- combien de fois aa est suivi de a ? de b ?
- combien de fois ab est suivi de a ? de b ?
- etc

7
? (creer-table "Bonjour, ceci est le cours sur les automates,
c'est-à-dire une structure de calcul très simple, mais qui a
beaucoup de propriétés intéressantes")
((#\é 3 (#\r 1) (#\s 1) (#\t 1)) (#\q 1 (#\u 1)) (#\p 4 (#\r
2) (#\Space 1) (#\l 1)) (#\è 1 (#\s 1)) (#\d 3 (#\e 2) (#\i
1)) (#\à 1 (#\- 1)) (#\- 2 (#\d 1) (#\à 1)) (#\' 1 (#\e 1))
(#\m 3 (#\p 1) (#\a 2)) (#\a 7 (#\n 1) (#\Space 1) (#\i 1)
(#\l 1) (#\t 1) (#\u 2)) (#\l 5 (#\Space 1) (#\c 1) (#\e 3))
(#\t 10 (#\é 2) (#\u 1) (#\r 2) (#\- 1) (#\e 2) (#\o 1)
(#\Space 1)) (#\s 13 (#\a 1) (#\s 1) (#\i 1) (#\, 1) (#\u 1)
(#\Space 5) (#\t 3)) (#\i 7 (#\n 1) (#\é 1) (#\s 1) (#\m 1)
(#\r 1) (#\Space 2)) (#\e 15 (#\a 1) (#\, 1) (#\Space 6) (#\s
6) (#\c 1)) (#\c 8 (#\u 1) (#\a 1) (#\t 1) (#\' 1) (#\o 2)
(#\i 1) (#\e 1)) (#\Space 21 (#\i 1) (#\p 1) (#\b 1) (#\q 1)
(#\m 1) (#\t 1) (#\d 2) (#\u 1) (#\a 2) (#\s 3) (#\l 2) (#\e
1) (#\c 4)) (#\, 3 (#\Space 3)) (#\r 10 (#\i 1) (#\o 1) (#\è
1) (#\u 1) (#\e 3) (#\Space 1) (#\s 1) (#\, 1)) (#\u 11 (#\p
1) (#\i 1) (#\l 1) (#\c 2) (#\n 1) (#\t 1) (#\r 4)) (#\j 1
(#\o 1)) (#\n 4 (#\t 2) (#\e 1) (#\j 1)) (#\o 6 (#\p 1) (#\m
1) (#\u 3) (#\n 1)) (#\B 2 (#\e 1) (#\o 1)))
? (markov 5000)
"'es s complcai ma truromai cturestéste des quronjop caless s
lc'e dint s primp e, les cop comaure qurouciés cop satés
uctér, e qunjounjomantérèsauiétres quront e ciés ciére s s
près mp esss auci curoulesainjompres cureaintéres, c'e
dintériétététuc'ecuimanjompr a couprèsalcturesi di ecoui près
le aiésuc'estr térintrèsa b"
? (markov 5000)
" i pre al ce c'esulestérsururs ines aur sa t-à-à-dess cout-
à-de les e, diromales p cintes, cis le ul destétre s
trestéroure b"

8
CHAPITRE 4 : RECHERCHE DE MOTIFS

Il existe deux grandes classes d’applications des automates finis :


- recherche de motifs dans un texte (fonction search d’un traitement de texte)
- analyse lexicale dans la compilation des langages de programmation.
La suite du cours présente ces 2 types d’applications.

1. Recherche de motif dans un texte

1.1 Présentation du problème

Problème. Rechercher toutes les occurrences d’un motif x dans un texte t. On généralisera
ensuite à la recherche de plusieurs motifs x1 , … xn . Dans un chapitre ultérieur, on envisagera
la recherche d’une expression régulière r (comme avec la commande egrep d’Unix).

Ce problème est omniprésent en informatique : éditeur de texte, moteur de recherche,


analyse de séquences biologiques, imagerie informatique, composition musicale
automatique, etc.

Il suscite encore de nombreuses recherches, à la fois du point de vue pratique (pour


optimiser les méthodes), et du point de vue théorique.

1.2 Tableau des différentes méthodes

Les méthodes de recherche de motif peuvent être classées en deux familles :


1- celles où le motif x est fixe,
2- celles où c’est le texte t qui est fixe.

Dans le cas n° 1 (motif fixe), il existe deux approches :


- on compare le motif au texte depuis le début du motif (gauche-droite)
- on fait la comparaison à partir de la fin du motif (droite-gauche).
Attention : la comparaison avec le texte, elle, se fait toujours de la gauche vers la droite (à
partir du début du texte).

Exemple : recherche de x = bacbb dans t = abacbacbba


Lecture gauche-droite :
a b a c b a c b b a
b échec

1
b a c b b échec
b échec
b échec
b a c b b = OK

Nombre de comparaisons de lettres ?


= 13 comparaisons

Lecture droite-gauche (x lu à partir de la fin) :


a b a c b a c b b a
b b échec
b échec
b échec
b b échec
b a c b b = OK

Nombre de comparaisons de lettres ?


= 11 comparaisons
Ici la deuxième méthode est meilleure, mais ce n’est pas toujours le cas.
Les deux méthodes peuvent être optimisées : au lieu d’essayer toutes les positions pour x,
on peut effectuer certains sauts.

Exemple : Lecture gauche-droite optimisée par un saut


a b a c b a c b b a
b échec
b a c b b échec
-> pour lire x plus loin, il faut commencer par un b, donc inutile d’essayer pour les a et c
qu’on vient de lire : on saute directement jusqu’au b suivant
b a c b b = OK

Nombre de comparaisons de lettres ?


= 11 comparaisons

Méthode de Morris & Pratt (lecture gauche-droite avec sauts) :


• On construit l’AFD qui reconnaît Σ* x
• On lit le texte t avec l’AFD : chaque fois qu’on passe par un état final, c’est qu’on a
trouvé une occurrence de x
Les transitions de l’AFD indiquent les sauts à effectuer.

2
Coût de la recherche = coût de la construction + coût de la lecture

Dans le cas n° 1, les deux approches gauche-droite et droite-gauche se traduisent par deux
fameux algorithmes :
- Knuth, Morris & Pratt : c’est une variante de Morris & Pratt, c’est-à-dire
avec construction de l’AFD pour Σ* x. Dans ce cas, le texte t est comparé à x
en commençant par le début de x (état initial de l’AFD). Les échecs se
traduisent par le passage par certaines transitions de l’AFD qui reviennent à
décaler x dans le texte (voir plus loin).
- Boyer & Moore : la comparaison entre x et t se fait en commençant par la
fin de x (de droite à gauche), et s’il y a échec, on décale x dans le texte avec
un saut judicieusement choisi.

Dans le cas n° 2 (texte fixe), la méthode générale est basée sur l’AFD reconnaissant les
suffixes de t. Cet AFD ne doit pas nécessairement être complet, mais il doit être accessible
et co-accessible (tout état doit pouvoir être atteint de l’état initial, et de tout état, on doit
pouvoir atteindre un état final). On lit x dans l’AFD : si on peut le lire intégralement, c’est
qu’on a lu un préfixe d’un suffixe de t, donc une occurrence de x dans t.

Lecture gauche-droite Lecture droite-gauche


Motif x fixe Morris & Pratt AFD des suffixes du miroir
x~
= AFD de Σ* x
Knuth, Morris & Pratt Boyer & Moore
(variante de la fonction (variante)
d’échec)
Texte t fixe AFD des suffixes de t

2. Recherche d’un motif fixe par lecture gauche-droite

2.1 Méthode de l’AFD de Σ*x (Morris & Pratt)

L’écorché d’un mot x de longueur n est l’AFD obtenu avec n + 1 états, et n flèches
correspondant aux lettres successives du mot. Dans cet automate, on voit clairement que les
états ont un rôle de « mémoire » : chaque état mémorise la position dans le mot x.

Pour reconnaître Σ* x, il suffit de rajouter une boucle sur l’état initial avec toutes les lettres
de l’alphabet. On obtient un AFN.

On a vu que la déterminisation d’un AFN pouvait être très coûteuse (2n états pour un AFN
ayant n états). Il est remarquable que dans le cas de Σ* x, le coût de la déterminisation soit
faible.

3
Proposition. L’AFD obtenu en déterminisant l’écorché d’un mot x de longueur n,
augmenté d’une boucle sur l’état initial avec toutes les lettres, a au plus n états.

Exemple : x = bacbb

Après déterminisation, on obtient l’AFD suivant. La correspondance entre les états de


l’AFN et ceux de l’AFD est la suivante (0 est inchangé) :
1 = {0, 1}
2 = {0, 2}
3 = {0, 3}
4 = {0, 1, 4}
5 = {0, 1, 5}

Méthode de recherche : on lit le texte t dans l’AFD. Cela revient à le comparer à x en


commençant par le début de x (état initial de l’AFD). Les échecs se traduisent par le
passage par certaines transitions de l’AFD qui reviennent à décaler x dans le texte.

Exemple : recherche de x = bacbb, dans t = abacbacbba


On indique les états d’arrivée après lecture de chaque lettre de t. Les états donnant un échec
sont en gras.

a b a c b a c b b a
0
on reste à l’état 0 comme si on n’avait rien lu, donc cela revient à recommencer la lecture en
se décalant d’une lettre

a b a c b a c b b a
0 1 2 3 4 2 échec
on ne peut aller jusqu’à l’état final 5, donc échec : on revient à l’état 2,

4
cela revient à recommencer la lecture comme si on avait déjà lu ba
donc à se décaler avec un saut comme on l’a vu plus haut.

a b a c b a c b b a
0 1 2 3 4 2 3 4 5 = OK (5 est terminal)

2.2 Fonction d’échec : algorithme de Morris & Pratt

On peut calculer directement l’AFD pour Σ* x, sans passer par la déterminisation de l’AFN,
grâce à une fonction d’échec.

Si p est un état de l’écorché de x (autre que 0) permettant de lire w, on définit f(p) de la


manière suivante :
f(p) = état d’arrivée du plus long suffixe propre de w qui est aussi préfixe de w (donc de x)

Pour calculer facilement f(p), il faut voir si on peut le déduire de f(p-1). Il se trouve que ça
marche.

On note le motif x = x(1)…x(n)


On suppose construits f(1), f(2)… f(p–1) et on cherche f(p).

i = f(p-1)
x(i+1) x(p)
|––––––––––––––––––––––––––––|–––|–––––––––––––––––––––––––––––––––––|–––|

• si la lettre x(i+1) = x(p)


alors x(1)…x(i+1) est à la fois préfixe et suffixe,
donc on peut prolonger le suffixe précédent : f(p) = i + 1 = f(p-1) + 1

• si la lettre est différente, il faut essayer un suffixe plus court, lequel ?


i=?
x(p)
|––––––––––––––––––––––––––––|–––|–––––––––––––––––––––––––––––––––––|–––|

=> ça doit être un suffixe du suffixe précédent :


donc on réapplique f

5
f(f(p-1)) f(p-1) p-1
|––––––•–––––––––––––––––––•–|–––|–––––––––––––––––––––––––––––––––•–|–––|

i = f(f(p-1))
et on recommence la comparaison entre x(i+1) et x(p)

calcul de la fonction d’échec f

f(0) ← -1
i ← -1
pour p = 1 à n
tant que i≥ 0 et x(i+1) ≠ x(p)
i ← f(i)
i ← i+1
f(p) ← i

Exemple : Calcul de f pour le motif x = bacbb

f(0) = -1, i = -1
p = 1 i = -1, on incrémente i = 0, et pose f(1) = 0

p = 2 i = 0, x(1) = b ≠ x(2) = a, on recule i = f(0) = -1


on incrémente i = 0, et on pose f(2) = 0

p=3 i = 0, x(1) = b ≠ x(3) = c, on recule i = f(0) = -1


on incrémente i = 0, et on pose f(3) = 0

p=4 i = 0, x(1) = b = x(4), on incrémente i = 1, et on pose f(4) = 1

p=5 i = 1, x(2) = a ≠ x(5) = b, on recule i = f(1) = 0


on incrémente i = 1, et on pose f(5) = 1

D’où la fonction d’échec :

État p 0 1 2 3 4 5
f(p) 0 0 0 1 1

6
Exemple :
• pour p = 2, le mot lu est ba, il n’a pas de suffixe propre qui soit préfixe, donc f(2) = 0
• pour p = 4, le mot lu est bacb, le plus long suffixe propre qui soit préfixe est b, donc f(4)
=1
• pour p = 5, le mot lu est bacbb, le plus long suffixe propre qui soit préfixe est b, donc f(5)
=1

La fonction d’échec s’utilise de la manière suivante : on lit le texte t directement dans


l’écorché de x (AFD). Quand dans un état donné p on ne peut pas lire la lettre courante du
texte t, on réessaie à partir de f(p), ou 0 si p = 0. Si w est le mot lu en arrivant à l’état p (w =
préfixe de x), alors par définition de la fonction d’échec, celui lu en arrivant à l’état f(p) est
un suffixe de w qui est aussi préfixe de w, donc du motif x. Donc c’est un début possible
pour une occurrence de x.

p
|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––|

|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––|
f(p)
=> Si on veut essayer une position de la fenêtre qui chevauche la partie lue de gauche à
droite, il faut qu’un suffixe de cette partie lue soit préfixe de x.

Exemple : reprenons le motif x = bacbb, et la recherche dans t = abacbacbba


AFD écorché de x avec fonction d’échec f :

a b a c b a c b b a
0
dans l’état 0 de l’écorché de x = bacbb, on ne peut pas lire a, donc on reste en 0

a b a c b a c b b a
0 1 2 3 4 échec
dans l’état 4, on ne peut pas lire a,
donc on revient à l’état 1 = f(4) qui correspond à b = préfixe et suffixe de bacb
puis on lit le a qui fait passer en 2

7
a b a c b a c b b a
0 1 2 3 4 2 3 4 5 = OK (5 est terminal)

En fait, l’algorithme de Morris & Pratt ne construit pas explicitement l’AFD de Σ* x.


Comme on l’a vu dans l’exemple étudié ci-dessus, il se contente de lire le texte t en
avançant dans l’écorché de x (AFD), et en effectuant les retours arrière définis par la
fonction d’échec. Il fonctionne de la manière suivante, en supposant données
• la fonction de transition δ(p, a) de l’écorché de x,
• la fonction d’échec f(p).

p ← 0 (état initial)
tant que t non vide
avancer dans t, a ← 1ère lettre de t
tant que p ≠ 0 et δ(p, a) non défini
p ← f(p)
si δ(p, a) défini alors p ← δ(p, a)
si p ∈ F alors signaler occurrence de x dans t

Une variante de cet algorithme est l’algorithme de Knuth, Morris & Pratt. Il consiste à
optimiser la fonction d’échec (voir TD).

2.3 Construction directe de l’AFD de Σ*x avec la fonction d’échec

Avec la fonction d’échec, on peut aussi reconstruire directement l’AFD reconnaissant Σ* x.


Dans l’exemple ci-dessus, après échec en 4, on reprend en 1 = f(4) pour pouvoir lire a (de
l’état 1 à 2) après avoir lu le préfixe b. Dans l’AFD de Σ* x, cela correspond exactement à la
transition de 1 vers 2 étiquetée par a (voir dessin de l’AFD).
On complète la fonction de transition de la manière suivante :
pour toute lettre a telle que δ(p, a) non défini, on pose
• δ(p, a) = δ(f(p), a) si p ≠ 0,
• δ(0, a) = 0.

On vérifie facilement qu’en rajoutant ces transitions, on obtient exactement l’AFD


déterminisé vu plus haut.

8
3. Recherche d’un motif fixe par lecture droite-gauche

3.1 Méthode de l’AFD des suffixes du motif miroir

Le miroir x~ d’un mot x est le mot lu à l’envers (en commençant par la fin).

Méthode :
• On construit l’AFD qui reconnaît les suffixes du mot miroir x~
• On lit le texte t dans l’AFD par une fenêtre de longueur |x|, dans laquelle on lit de droite à
gauche : en cas d’échec, on revient au dernier état final de l’AFD rencontré. Il correspond à
un suffixe s de x~. Donc de gauche à droite, s~ est un préfixe de x. On se décale dans t
pour commencer une nouvelle lecture à partir de s~. Ces sauts accélèrent considérablement
la recherche.

|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––|

|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––|

=> Si on veut essayer une position de la fenêtre qui chevauche la partie lue de droite à
gauche, il faut prendre le plus grand suffixe de cette partie lue (donné par un état final de
l’AFD) qui soit préfixe de x.
On notera qu’aucune position de la fenêtre située avant le début de ce suffixe (en particulier
avant la partie chevauchante) ne peut convenir, car si on pouvait lire x en entier de cette
position, on aurait lu un plus long suffixe dans la partie lue de droite à gauche.

|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––|

Exemple : motif x = bacbb


AFN pour les suffixes de x~ = bbcab

9
On part de l’écorché de x~, et on rend initiaux tous les états sauf le dernier.

Après déterminisation, on obtient l’AFD suivant. La correspondance entre les états de


l’AFN et ceux de l’AFD est :
0 = {0, 1, 2, 3, 4}
1 = {1, 2, 5}
2, 3, 4, 5 sont identiques

Recherche de x = bacbb, dans t = abacbacbba

a b a c b a c b b a
5 4 3 1
dans l’état 5, on ne peut lire a,
5 étant terminal, il correspond au suffixe bcab de x~ = bbcab,
donc on reprend la lecture avec son miroir bacb, qui est préfixe de x
ce qui revient à se décaler d’une lettre

a b a c b a c b b a
5 4
dans l’état 5, on ne peut lire c,
5 étant terminal, il correspond au suffixe ab de x~ = bbcab,
donc on reprend la lecture avec son miroir ba, qui est préfixe de x
ce qui revient à se décaler de trois lettres

a b a c b a c b b a
5 4 3 2 1 = OK (on a tout lu et 5 est terminal)

3.2 Variante : algorithme de Boyer & Moore

10
Dans la méthode précédente avec l’AFD des suffixes du miroir x~, en cas d’échec dans la
lecture droite-gauche, l’AFD permet de trouver un préfixe du mot qu’on a lu qui est suffixe
de x~, c’est-à-dire dont le miroir est préfixe de x. On se décale pour recommencer la lecture
à partir de ce préfixe de x, qui peut être un début possible pour une nouvelle occurrence.

Dans l’algorithme de Boyer & Moore, qui est une variante, mais sans construire l’AFD
précédent, on ne repart pas d’un préfixe plus court que le mot lu dans la fenêtre droite-
gauche (donné par l’AFD), mais on utilise ce mot complet (on note w son miroir, dans
l’ordre normal gauche-droite) :
1) on numérote les positions dans la fenêtre de 1 à n (où n est la longueur du motif x)
dans l’ordre normal gauche-droite,
2) on construit une fonction f(i) qui donne la position la plus à droite où le suffixe de x
qu’on a lu dans t, noté w = x(i)…x(n) entre les positions i et n de la fenêtre,
réapparaît à gauche dans le motif x (s’il réapparaît en plusieurs endroits, c’est le
premier en partant de la droite qui est pris en compte),
3) on se décale dans le texte t pour faire coïncider la fenêtre avec cette occurrence de w,
soit i–f(i) lettres.
S’il y a eu échec sans lire un suffixe de x (dès la première lettre), on se décale d’une lettre
dans t.

i n
|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––|

|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––|
f(i)
<- i–f(i) ->

Exemple : recherche de x = baacaa, dans t = aaabaacaa


On calcule la fonction f, en attribuant la valeur 0 aux suffixes qui ne réapparaissent pas plus
à gauche dans le mot.

Position i 1 2 3 4 5 6
f(i) 0 0 0 0 2 5

a a a b a a c a a
c a a
5 = échec de la lecture du c attendu au lieu de b, suffixe lu = aa
on décale la fenêtre pour la faire coïncider avec le aa trouvé, en utilisant f(5) = 2
b a a c a a = OK occurrence trouvée
2

11
La différence avec l’AFD des suffixes de x~ est que le décalage ne garantit pas qu’on
repart d’un début de x. Avant l’occurrence du suffixe mise en coïncidence avec une portion
du motif, il peut y avoir des lettres divergentes qui font échec.

i n
|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––|

|––––––––– ?????????????????? ––––––––––––––––––––––––––––––––––––|


f(i)

Exemple : Avec le même motif x = baacaa, si le texte avait été t = aaadaacaa au lieu de
aaabaacaa (avec d à la place de b), le décalage aurait donné un échec.

En fait, dans l’algorithme de Boyer & Moore, on utilise aussi le caractère qui fait l’échec
(ici le b trouvé à la place du c) pour améliorer le décalage. Cela ne change pas le calcul ci-
dessus dans le premier exemple. Mais dans le deuxième avec d à la place de b, cela aurait
évité le décalage donnant un échec.

L’algorithme tel qu’il est décrit ici est inopérant dans les cas où l’échec se produit dès la
première lettre, sans lecture dans la fenêtre droite-gauche : dans ce cas, on se décale d’une
seule lettre. Dans le véritable algorithme de Boyer & Moore, l’utilisation de la lettre d’échec
permet d’améliorer cette situation (voir TD).

Exemple : reprenons l’exemple étudié au début du chapitre avec x = bacbb et t =


abacbacbba
Il correspond à un cas défavorable de cette version simplifiée de Boyer & Moore, car on ne
peut jamais obtenir un décalage de plus d’une seule lettre.

Position i 1 2 3 4 5
f(i) 0 0 0 0 4

a b a c b a c b b a
b b
5 = échec de la lecture du b attendu au lieu de c, suffixe lu = b
on décale la fenêtre pour la faire coïncider avec le b trouvé, en utilisant f(5) = 4
mais cela revient à ne faire un décalage que d’une seule lettre
(b) b
4
échec dès la première lettre, pas de suffixe lu, donc décalage d’une seule lettre
b
b b
5 = échec de la lecture du b attendu au lieu de c, suffixe lu = b
12
on décale la fenêtre pour la faire coïncider avec le b trouvé, en utilisant f(5) = 4
mais cela revient à ne faire un décalage que d’une seule lettre
b a c b b = OK occurrence trouvée
4

4. Remarques complémentaires

4.1 Recherche d’un ensemble fini X de motifs

On a un ensemble de motifs X = {x1 , … xk}.


Les deux principales méthodes ci-dessus se généralisent à ce cas :
1) Construction de l’AFD pour Σ* X
2) Construction de l’AFD pour l’ensemble des suffixes des miroirs X~

4.2 Recherche d’une expression régulière r


On construit un AFN qui reconnaît Σ * r (voir l’algorithme de Thompson au chapitre
suivant).

4.3 Comparaison des deux méthodes : Morris & Pratt, suffixes du miroir
La méthode de l’AFD de Σ* x avec une représentation de l’AFD par une table de transition,
a un coût de construction en O(|x||Σ|), et un coût pour la lecture de t en O(|t|).
Si l’on ne construit par effectivement l’AFD, mais qu’on construit seulement la fonction
d’échec, le coût de la construction est en O(|x|), et celui de l’analyse en O(|t|log|Σ|).

Dans l’exemple traité ci-dessus, la méthode de Morris & Pratt (AFD de Σ* x) donnait les
mêmes sauts que celle de l’AFD des suffixes de x~ (trois positions de la fenêtre dans les
deux cas) Mais ce n’est pas vrai dans le cas général. Voici un exemple qui montre les
différences entre les deux méthodes.

Exemple : recherche de x = bbabbabb, dans t = abaabbabbabb

1) méthode de Morris & Pratt

Position 1 2 3 4 5 6 7 8
f(i) 0 1 0 1 2 3 4 5

a b b b b a b b a b b
0 échec dans la lecture : t contient a, alors qu’il faut un b
on se décale d’une lettre

13
a b b b b a b b a b b
1 2 échec dans la lecture : t contient b, alors qu’il faut un a
1 on reprend à l’état 1 = f(2)

a b b b b a b b a b b
1 2 échec dans la lecture : t contient b, alors qu’il faut un a
1 on reprend à l’état 1 = f(2)

a b b b b a b b a b b
1 2 3 4 5 6 7 8 occurrence de x trouvée

Dans cet exemple, la fonction de Morris & Pratt ne permet d’éviter aucun saut : on essaie
toutes les positions de la fenêtre jusqu’à l’occurrence de x.

2) méthode de l’AFD de x~
Dans ce cas, x est identique à son miroir, on a : x~ = x = bbabbabb.

a b b b b a b b a b b
b b a b b échec dans la lecture droite-gauche
Le principe est de repartir du plus long préfixe w de x lu dans la fenêtre droite-gauche
(c’est-à-dire tel que w~ est suffixe de x~). Ici w = bbabb est le plus long préfixe trouvé,
donc on décale la fenêtre pour la faire coïncider avec bbabb.

a b b b b a b b a b b
occurrence de x trouvée

Dans ce cas, on ne fait qu’un seul saut, et le calcul est bien meilleur qu’avec Morris &
Pratt.

14
CHAPITRE 5 : ANALYSE LEXICALE

L’analyse lexicale est un autre domaine fondamental d’application des automates finis.
Dans la plupart des langages de programmation, les unités lexicales (identificateurs, mots-
clefs du langage, nombres, etc.) sont définies par des expressions régulières (ou
rationnelles). L’analyse lexicale consiste à détecter ces unités dans le flot de caractères
constitué par le programme. Pour ce faire, on associe des automates finis aux expressions
régulières.
Il existe des méthodes efficaces permettant de construire un AFD pour toute expression
régulière.

1. Équivalence entre automates et expressions régulières

1.1 Rappel de la définition des expressions régulières (ou rationnelles)

Définition. Soit Σ un alphabet. Les expressions rationnelles (ou régulières) sur Σ et les
langages correspondants sont définis récursivement :
1) • ∅ est une expression rationnelle, et représente l’ensemble vide,
• ε est une expression rationnelle, et représente l’ensemble {ε},
• pour toute lettre a de Σ, a est une expression rationnelle, et représente l’ensemble
{a}.
2) Si r et s sont des expressions rationnelles, qui représentent les langages R et S,
alors r + s, rs, et r* sont des expressions rationnelles qui représentent les
langages R ∪ S, RS et R* .

1.2 Théorème de Kleene

Théorème (Kleene). Tout langage reconnu par un AFD peut être représenté par une
expression régulière, et réciproquement.

Preuve
• Tout langage représenté par une expression régulière est reconnu par un AFD.
Les langages ∅, {ε} et {a} pour toute lettre a de Σ, sont reconnus par AFD.
De plus, les propriétés de clôture des langages reconnus par AFD montrent que l’union, la
concaténation et l’étoile sont aussi reconnus par AFD. Donc les langages représentés par
expression régulière sont reconnus par AFD (méthode dite de Thompson).
On verra plus loin une deuxième méthode effective pour construire un AFD reconnaissant
le langage décrit par une expression régulière (algo de Glushkov).
• Tout langage reconnu par AFD peut être représenté par une expression régulière.
On peut le montrer directement, ou utiliser une méthode de résolution d’équation (voir plus
loin).
1
1.3 AFN associé à une expression régulière (algorithme de Glushkov)

Exemple : soit une expression rationnelle


(a + ab)*(ε + ab)

Première étape : Linéariser l’expression rationnelle, en remplaçant toutes les lettres par des
symboles distincts (de la gauche vers la droite).
(x1 + x2 x3 )*(ε + x4 x5 )

Deuxième étape : Déterminer


• Premier = ensemble des symboles pouvant commencer un mot = {x1 , x2 , x4 },
• Dernier = ensemble des symboles pouvant finir un mot = {x1 , x3 , x5 },
• Pour tout symbole xi, Suivant(xi) = ensemble des symboles pouvant suivre xi.
Attention : cette fonction Suivant doit tenir compte des étoiles.

Suivant
x1 x1 , x2 , x4
x2 x3
x3 x1 , x2 , x4
x4 x5
x5

Proposition. Un mot w = w 1 w2 … w n appartient au langage décrit par l’expression


linéarisée ssi w1 ∈ Premier, wn ∈ Dernier, et wi+1 ∈ Suivant(wi) pour i =1, … n—1.

Construction de l’AFN pour l’expression linéarisée :


• Q = ensemble des symboles + un état initial i
• F = Dernier, avec en plus l’état initial i si ε appartient au langage
• fonction de transition δ :
δ(i, xi) = xi si xi ∈ Premier,
δ(xi, xj) = xj, pour tout xj ∈ Suivant(xi).
Puis on remplace les symboles xi par les lettres d’origine de l’expression rationnelle.

1.4 AFN associé à une expression régulière (algorithme de Thompson)

2
On construit par induction un AFN avec ε–transition reconnaissant le langage décrit par une
expression rationnelle, à partir des opérations de combinaison définissant les expressions
rationnelles (union, concaténation, étoile).

• Pour ∅, ε, et les lettres, l’automate est évident.


• Pour l’union, la concaténation et l’étoile, on combine les AFN comme cela est indiqué
dans la preuve du théorème de Kleene.

Exemple :
(a + ab)*(ε + ab)

La taille d’une expression rationnelle r, notée |r|, est son nombre de caractères (sans
compter les parenthèses).
Exemple : |(a + ab)*(ε + ab)| = 9.

Complexité. L’algorithme de Thompson construit à partir d’une expression régulière r


un AFN avec ε–transitions où :
• il y a un état initial et un état final,
• le nombre d’états est borné par 2|r|,
• de chaque état sort au plus deux transitions
Le temps pour le construire est O(|r|), de même que l’espace pour le représenter.

La construction de l’AFN par l’algo de Glushkov s’effectue en O(|r|2 ), avec prétraitement


de l’expression régulière.

1.5 Application à la recherche d’une expression régulière r dans un texte

On construit un AFN qui reconnaît Σ* r grâce à l’algorithme de Thompson.


Puis deux possibilités :
- soit on le déterminise en AFD,
- soit on fait la recherche en simulant l’AFD (sans construire tous les états,
mais seulement ceux dont on a besoin au fur et à mesure pour la lecture du
texte t).

Voilà un exemple illustrant la simulation dynamique de l’AFD, c’est-à-dire la construction


de certains états de l’AFD au fur et à mesure où on en a besoin, sans passer par la
construction préalable de tous les états.

• Construction de l’AFN reconnaissant l’expression régulière Σ * r par l’algorithme de


Thompson
3
• Analyse du texte en simulant l’AFD correspondant
Pour P un ensemble d’états de l’AFN, on note
Cloture(P) = les états accessibles à partir de q ∈ P par ε-transition
Transition(P, a) = clôture de l’union des δ(q, a) pour q ∈ P

L’analyse de t dans l’AFN se fait en partant de Cloture(I) où I est l’ensemble des états
initiaux, et en suivant les flèches indiquées par Transition(P, a).

Exemple : recherche du motif r = bba* dans le texte t = babbaab

1) algorithme de Thompson pour l’AFN de (a + b)*bba*

• pour la concaténation : on sépare les éléments concaténés par des ε-transitions


• pour l’union : on regroupe les éléments de la disjonction par des ε-transitions qui partent
d’un même état de départ et se rejoignent sur un même état d’arrivée
• pour l’étoile : on boucle de l’état d’arrivée vers celui de départ par une ε-transition

2) analyse de t = babbaab

initialisation : P = {1, 2, 3, 9, 10, 12}

transition par b : {4, 13}


clôture : {4, 5, 13, 14, 9, 10, 12, 2, 3}

transition par a : {11}


clôture : {11, 14, 9, 10, 12, 2, 3}

transition par b : {4, 13}


clôture : {4, 5, 13, 14, 9, 10, 12, 2, 3}

transition par b : {4, 6, 13}


4
clôture : {4, 5, 13, 6, 7, 8, 15, 14, 9, 10, 12, 2, 3}
8 est final, donc une occurrence a été détectée

transition par a : {11, 16}


clôture : {11, 14, 9, 10, 12, 2, 3, 16, 8, 15}
une occurrence a été détectée

transition par a : {11, 16}


clôture : {11, 14, 9, 10, 12, 2, 3, 16, 8, 15}
une occurrence a été détectée

transition par b : {4, 13}


clôture : {4, 5, 13, 14, 9, 10, 12, 2, 3}

Remarque : dans ce cas, la construction de l’AFD par déterminisation n’aurait pas été trop
coûteuse, car l’AFN pour bba* a 3 états, et on est dans le cas où l’AFD pour Σ* bba* a le
même nombre d’états.

2. Utilisation des expressions régulières dans la construction des compilateurs

1.1 Principe de la compilation

Un compilateur est un programme qui traduit un autre programme dit source en un


programme dit cible.
Le programme source est écrit dans un langage de haut niveau (Java, Lisp, Prolog, etc.).
Le programme cible est écrit dans un langage de bas niveau (assembleur) exécutable par
une machine.

1.2 Les phases de la compilation

Exemple : supposons que le programme source contienne le calcul décrit par l’expression

var1 + 100 * var2


5
Première phase : analyse lexicale
Elle consiste à analyser l’expression (ou en général le programme source) en constituants
minimaux appelés tokens (ou jetons). Elle produit une chaîne de tokens composés de
couples :
(unité lexicale, valeur ou adresse dans la table de symboles)
Dans l’exemple ci-dessus, les unités lexicales sont :
- id : identificateurs var1 ou var2 (les identificateurs rencontrés sont placés dans une
table de symboles)
- op : opérations arithmétiques +, *
- nb : constante entière 100

Tokens reconnus : (id, var1) (op, +) (nb, 100) (op, *) (id, var2)

Cette phase de tokenisation comporte également :


- l’élimination des blancs (et des tabulations, sauts de ligne,…)
- l’élimination des commentaires du programme source.
Les unité lexicales correspondent à des suites de caractères du programme source
(« var1 », « + », « 100 », « var2 », « + »). On les définit par des expressions régulières.
La tokenisation peut donc être réalisée par un automate fini. On donnera ci-après une
présentation détaillée de cette utilisation des automates finis

Deuxième phase : analyse syntaxique


La chaîne de tokens obtenue est ensuite analysée pour construire sa structure syntaxique,
qui a la forme d’un arbre :

Cette phase de la compilation est réalisée par une grammaire (voir deuxième partie du cours
avec Patrice Enjalbert).

On peut résumer la compilation comme une succession de phases dont les deux premières
sont l’analyse lexicale et l’analyse syntaxique :

6
Cette décomposition en phases permet de rendre plus simple et plus efficace la conception
des compilateurs. Le fait que l’analyseur lexical soit la seule phase de la compilation qui
traite directement le texte source est important. À partir de la phase d’analyse syntaxique, le
compilateur ne traite plus que des unités lexicales indépendantes des particularités du
langage source.

1.3 L’analyse lexicale

Les unités lexicales du programme source sont définies par des modèles : identificateurs,
nombres, opérations arithmétiques, etc. Les parties du programme source éliminées au
cours de cette phase correspondent également à des modèles : espaces, commentaires.
Ces modèles ont la forme d’expressions régulières (ou rationnelles).
On a vu qu’on peut les représenter de manière équivalente par des automates finis.

Par exemple, dans un langage comme Pascal, les identificateurs sont définis par le modèle
suivant :
lettre → [A-Za-z]
chiffre→ [0-9]
id → lettre (lettre | chiffre)*

L’expression rationnelle définissant l’unité lexicale id se traduit par un automate :

Dans le fonctionnement d’un tel automate, on associe une action à chaque état final : si on
lit par exemple « var1 », on suit le parcours 0111 dans l’automate, qui s’arrête dans l’état
final 1. On a alors détecté un identificateur.
Action : l’analyseur forme l’unité lexicale (id, var1) et ajoute le symbole var1 dans la table
des symboles.

Les nombres nb sont définis de la manière suivante :


dec → . chiffre+
exp → E (+ | -) chiffre+
nb → chiffre+ (dec | ε) (exp | ε)

7
Certains opérateurs qui ont des caractères communs posent problème :

Si le flot de caractères comporte « <= », l’analyseur lit d’abord <. Il se trouve alors dans
l’état final 1, mais il ne sait pas s’il doit
- s’arrêter et détecter l’unité lexicale (op, <)
- ou continuer et détecter l’unité lexicale (op, <=) dans l’état final 2.
(le problème est le même avec les boucles sur l’état final dans les automates précédents).
L’utilisation de ces automates doit donc être améliorée, en incluant un mécanisme de lecture
d’un caractère « à l’avance » pour permettre de choisir dans de telles situations.

Pour construire un analyseur lexical :


- on définit les modèles des unités lexicales sous forme d’expressions régulières,
- on construit les automates associés à ces expressions régulières (voir algo de
Thompson),
- on réunit tous ces automates en ajoutant un état initial et des ε–transitions vers les
états initiaux de tous les automates des modèles,
- on déterminise l’AFN obtenu pour fabriquer un AFD.
Attention : dans le processus de déterminisation, il est possible qu’apparaisse un ensemble
d’états avec plusieurs états finals de l’AFN. Le problème est que des actions sont associées
aux états finals et qu’il faut choisir quelle action exécuter. Pour cela, on instaure un ordre
dans les modèles d’unités lexicales, et on choisit l’état final qui correspond au modèle placé
en premier.

1.4 La commande lex d’Unix

La commande lex d’Unix permet


- de définir des modèles d’unités lexicales,
8
- de fabriquer un automate à partir de leurs expressions rationnelles.
Le format du fichier source traité par la commande lex comporte trois parties séparées par
%%. La première partie est optionnelle, ainsi que la troisième et les %% qui la précèdent.
<définitions>
%%
<règles>
%%
<instructions C>

Les règles sont du type :


<expression régulière> <instructions C>

Lorsqu’une expression régulière est reconnue (c’est-à-dire un modèle d’unité lexicale),


l’instruction C correspondante est exécutée.
Les expressions régulières sous Unix (pour egrep ou lex) s’écrivent avec un certain
nombre d'abréviations :
x|y pour un choix entre x ou y
x* pour une répétition indéfinie de x
[abc] pour a|b|c
[a-z] pour a|b|…|z
[a-zA-Z0-9] pour toutes les lettres et chiffres
x+ pour au moins une occurrence de x, c’est-à-dire xx*
x? pour un x optionnel
[^x] tout caractère sauf ceux donnés

Exemple : le fichier essai.l contient

%%
abcde printf("[%s]", yytext);
%%
int yywrap() { return 1;}
main() { yylex(); }

Avec ces instructions C minimales, l’automate lit chaque ligne, en la recopiant, et s’il trouve
le mot ‘abcde’, il l’encadre par des crochets (la variable %s renvoie à yytext, qui contient
le motif reconnu).
La commande flex permet de transformer ce fichier en un fichier essai.c de code C :

$ flex -o essai.c essai.l

Puis on compile le code C obtenu en un exécutable essai :

9
$ cc essai.c -o essai

Dans cette version minimale, la fonction essai ne s’arrête pas (faire Ctrl-D pour sortir).

$ essai
aaaaaaaaabcdeeeeeeee
aaaaaaaa[abcde]eeeeeee

Voilà un autre exemple avec des définitions dans la première partie. Les identifiants utilisés
dans ces définitions peuvent être repris dans les expressions régulières de la deuxième
partie, entre accolades :

voyelle [aeiou]
consonne [b-df-hj-np-tv-z]
%%
({consonne}{voyelle})*{consonne} printf("[%s]", yytext);
%%
int yywrap() { return 1; }
main() { yylex(); }

On obtient :

$ essai
consonne
[con][son][ne]
bonjour
[bon][j]ou[r]

Encore un autre exemple qui montre un mini-analyseur lexical pour un langage de type
Pascal, en se limitant aux espacements (blancs, tabulations), identificateurs, nombres,
opérations :

delim [ \t]
bl {delim}+
lettre [A-Za-z]
chiffre [0-9]
id {lettre}+({lettre}|{chiffre})*
nb {chiffre}+(\.{chiffre}+)?(E[+\-]?{chiffre}+)?
%%
{bl} {/* pas d action */}
{id} printf("[id,%s]", yytext);
{nb} printf("[nb,%s]", yytext);
[+*] printf("[op,%s]", yytext);
%%
int yywrap() { return 1;}
main() { yylex(); }

Après compilation du programme lex, on peut analyser des expressions et détecter les
unités lexicales :

10
$ pasc
var1 + var2
[id,var1][op,+][id,var2]
x1 + 1000 * valeur
[id,x1][op,+][nb,1000][op,*][id,valeur]

11
CHAPITRE 6 : MINIMISATION D’UN AFD

1. Théorème d’existence de l’AFD minimal

1.1 Problème

Parmi les AFD reconnaissant un même langage L, peut-on en trouver un qui a le nombre
minimal d’états ?
Oui, et il est unique (à un renommage près des états) :
• on l’obtient en supprimant les états inaccessibles
• puis on identife les états restants qui jouent un rôle identique du point de vue de la
reconnaissance

1.2 Théorème de Myhill-Nérode

Théorème de Myhill-Nérode. Soit L un langage rationnel. Parmi tous les AFD


reconnaissant L, il en existe un et un seul qui a un nombre minimal d’états.

2. Construction de l’AFD minimal

2.1 Suppression des états inaccessibles

Construction par récurrence d’une suite d’ensembles Acci


• Acc0 = {i}
• Acci+1 = Acci ∪ δ(Acci,Σ)
On s’arrête quand Acci+1 = Acci, et on pose alors Q = Acci (c’est-à-dire qu’on supprime les
états de Q \ Acci).

Les états qu’on ne peut atteindre à partir de l’état initial n’apportent aucune contribution au
langage L reconnu par l’AFD.

1.2 Automate quotient

Définition. Un mot u sépare deux états q1 et q2 si


δ(q1 , u) ∈ F et δ(q2 , u) ∉ F
ou δ(q1 , u) ∉ F et δ(q2 , u) ∈ F

Définition. Deux états q1 et q2 sont équivalents si aucun mot ne les sépare :


1
q1 ~ q2 si et seulement si pour tout mot u, on a :
δ(q1 , u) ∈ F implique δ(q2 , u) ∈ Fet réciproquement.

Proposition. La relation ~ est une relation d’équivalence sur l’ensemble des états Q :
réflexive, symétrique, transitive.

Supposons qu’un mot u sépare p1 = δ(q1 , a), et p2 = δ(q2 , a). On a :


δ(q1 , au) = δ(δ(q1 , a), u) = δ(p1 , u), et
δ(q2 , au) = δ(δ(q2 , a), u) = δ(p2 , u).
Donc le mot au sépare q1 et q2 . On en déduit :

Proposition. Si q1 ~ q2 , alors δ(q1 , a) ~ δ(q2 , a) pour toute lettre a.

Construction de l’automate quotient :


On définit une fonction de transition sur les classes d’équivalence des états.
Si [q] est la classe des états équivalents à q, on pose :
δ([q], a) = [δ(q, a)]

Le nombre de classes d’équivalences sur Q est nécessairement inférieur au nombre


d’éléments de Q . Donc l’automate quotient a un nombre d’états inférieur au nombre
d’états de l’AFD de départ. En fait, on montre que si deux AFD reconnaissent le même
langage L, leurs automates quotients ont le même nombre d’états (ce nombre ne dépend que
du langage L, pas de l’AFD qui reconnaît L).

1.3 Construction de l’automate quotient

On définit une suite de relations d’équivalence.


• La relation ~0 ne comporte que deux classes : les états finals F, et les états non finals Q \ F
• Les autres relations s’obtiennent en raffinant les classes d’équivalences. Si [p]k est la
classe d’équivalence de l’état p pour la relation ~ k, on définit [p]k+1 à partir de [p]k, mais en
ne gardant que les états dont toutes les transitions conduisent vers des états équivalents. On
pose donc :
[p]k+1 = [p]k ∩ {q ∈ Q, δ(q, a) ~k δ(p, a) pour toutes lettres a}

Proposition. Pour k ≥ card(Q), les relations d’équivalence se stabilisent ~k+1 = ~k, et


l’équivalence obtenue est celle qui définit l’automate quotient.

Exemple :

2
i = 1, F = {6, 7, 8}

1 2 3 4 5 6 7 8
a 1 2 4 3 2 3 8 7
b 5 5 6 8 4 7 7 7

On voit facilement qu’aucun mot ne sépare les états 7 et 8. À partir de ces états, on peut lire
n’importe quel mot sur {a, b}. Ces deux états pourront donc être confondus dans l’AFD
minimal.

1. Équivalence ~0
Initialement, les deux classes sont celle des états finals [6]0 = {6, 7, 8} et celle des autres
[1]0 = {1, 2, 3, 4, 5}

1 2 3 4 5 6 7 8
~ 0 [1]0 [1]0 [1]0 [1]0 [1]0 [6]0 [6]0 [6]0

2. Équivalence ~1
Pour éventuellement scinder des classes, il faut regarder dans quelles classes vont les
transitions partant des états. On en déduit les nouvelles classes pour ~1 .

1 2 3 4 5 6 7 8
~ 0 [1]0 [1]0 [1]0 [1]0 [1]0 [6]0 [6]0 [6]0
a [1]0 [1]0 [1]0 [1]0 [1]0 [1]0 [6]0 [6]0
b [1]0 [1]0 [6]0 [6]0 [1]0 [6]0 [6]0 [6]0
~ 1 [1]1 [1]1 [3]1 [3]1 [1]1 [6]1 [7]1 [7]1

3. Équivalence ~2
Dans le tableau ci-après, on n’indique que les transitions qui vont dans une autre classe.

3
1 2 3 4 5 6 7 8
~ 1 [1]1 [1]1 [3]1 [3]1 [1]1 [6]1 [7]1 [7]1
a
b [6]1 [7]1 [3]1
~ 2 [1]2 [1]2 [3]2 [4]2 [5]2 [6]2 [7]2 [7]2

4. Équivalence ~3 = ~2
Il n’y a plus aucune scission de classes, donc la suite d’équivalences s’est stabilisée, et on
obtient celle de l’AFD minimal.

1 2 3 4 5 6 7 8
~ 2 [1]2 [1]2 [3]2 [4]2 [5]2 [6]2 [7]2 [7]2
a
b
~ 3 [1]3 [1]3 [3]3 [4]3 [5]3 [6]3 [7]3 [7]3

On voit donc qu’on peut confondre


- les états 1 et 2
- les états 7 et 8
On en déduit l’AFD minimal.

Vous aimerez peut-être aussi