Cours n° 18, 15 mars 2012
Les grammaires "Context-Free" et la hiérarchie de
Chomsky
- Règles sans contexte
- Définition
- Exemples
- Non-exemples
- Lemme
de la double étoile (en anglais pumping lemma)
- Exemples
- Propriétés
de fermeture de la classe des langages CF
- Union
- Produit
- Étoile
- Intersection
avec un langage régulier
- Automates à pile
- Idée
générale
- Les
deux modèles pour l'acceptation
- La
question du déterminisme
- Hiérarchie de Chomsky
- Grammaires
linéaires
- Grammaires
linéaires unilatères et langages
réguliers
-
-
Une grammaire context-free est une grammaire
dont toutes les règles sont de la forme
A -> w
où A
est un symbole non-terminal
et w
un mot quelconque.
On note deux différences par rapport aux grammaires context-sensitive
:
- l'absence de contexte dans le membre gauche :
Dans le modèle context-sensitive, on interprète une
règle du genre uAv -> uwv
comme "le non-terminal A
se réécrit en w
dans le contexte (u
, v
)".
Dans le modèle context-free, on dira que
"le non-terminal A
se réécrit
en w
indépendamment du contexte où il
apparaît".
On voit donc que l'adjectif anglais free doit être
traduit en français non point par libre,
mais par affranchi, libéré, ou
même franc au sens que ce mot avait chez Ronsard
:
Franc des liens du corps
pour n'estre qu'un esprit.
- l'absence de contrainte sur le membre droit :
Le modèle context-sensitive exige que w
soit non-vide, pour assurer le caractère "non-contractant"
de ses règles.
Le modèle context-free n'a que faire de cette
précaution !
Il s'ensuit que stricto sensu la classe des
langages CF n'est pas incluse dans celle des langages CS,
alors que le modèle CF est de toute évidence une particularisation de
CS où tous les contextes sont vides.
Pour éviter cette absurdité, les définitions officielles de CS
(Wikipedia...) ajoutent des règles spéciales S -> ε.
Nous négligeons ces détails de présentation.
-
- Les langage des systèmes bien parenthésés est engendré
par la grammaire
S -> SS
S -> ε
S -> (S)
S -> [S]
S -> {S}
S -> <S>
On peut voir les dérivations de cette grammaire comme les inverses des
chaînes de réécritures-réductions
vues au
cours n° 17 : pour notre exemple favori le mot "([]{<<>>}([(){<>}]))
",
en dérivant "à gauche"
S
-3> (S)
-1> (SS)
-4> ([S]S)
-2> ([]S)
-1> ([]SS)
-5> ([]{S}S)
-6> ([]{<S>}S)
-6> ([]{<<S>>}S)
-2>([]{<<>>}S)
-3> ([]{<<>>}(S))
-4> ([]{<<>>}([S]))
-1> ([]{<<>>}([SS]))
-3> ([]{<<>>}([(S)S]))
-2> ([]{<<>>}([()S]))
-5> ([]{<<>>}([(){S}]))
-6> ([]{<<>>}([(){<S>}]))
-2>
([]{<<>>}([(){<>}]))
On aurait pu aussi bien dériver "à droite" : S
-3> (S)
-1> (SS)
-3> (S(S))
-4> (S([S]))
-1> (S([SS]))
-5> (S([S{S}]))...
- Le langage {
anbn
| n
>0} est
engendré par
S -> aSb
S -> ab
- Le langage des palindromes {
ww~
| w
∈X*}
S -> ε
S -> aSa
S -> bSb
- Le langage des mots contenant autant de
a
que de b
:
S -> SS
S -> aSb
S -> bSa
S -> ε
Si la validité des grammaires des trois exemples précédents est
assez évidente, pour ce dernier il faut faire un petit effort !
On observe que tout mot w contenant autant de a
que de b
se décompose (d'une manière unique)
en facteurs de la forme
a
ub
ou b
va
,
où u et v contiennent autant de a
que de b
: ces facteurs correspondent aux
passages par 0 de la fonction
(nombre de a
- nombre de b
).
Exemple : pour le mot aabaababbbbbbaabaaba
aabaababbb b b b a a
b aa ba -> a
abaababb b / b bbaaba a / ba
1212323210-1-2-3-2-1-2-10-10
Les facteurs en question sont engendrés par les règles 2, 3, 4. La
règle 1 se charge de les concaténer.
Par exemple, le mot en question sera engendré par (en
dérivant à gauche):
S
-1> SS
-1>
SSS
-2> aSbSS
-2> aaSbbSS
-1> aaSSbbSS
-1> aaSSSbbSS
-3> aabSaSSbbSS
-4> aabaSSbbSS
-2> aabaaSbSbbSS
-4> aabaabSbbSS
-2> aabaabaSbbbSS
-4> aabaababbbSS
-3> aabaababbbbSaS
-3> aabaababbbbbSaaS
-1> aabaababbbbbSSaaS
-3> aabaababbbbbbSaSaaS
-4> aabaababbbbbbaSaaS
-2> aabaababbbbbbaaSbaaS
-4> aabaababbbbbbaabaaS
-3> aabaababbbbbbaabaabSa
-4> aabaababbbbbbaabaaba
-
-
Soit L un langage CF infini.
Dans tout mot w de L
suffisamment long on peut trouver deux facteurs u
et v dont l'un au moins n'est pas vide,
w = fugvh ∈ L
tels que, pour tout entier n, on ait fungvnh
∈ L.
Idée de la preuve :
le mot w ∈ L étant
arbitrairement long, la chaîne de réécritures qui l'engendre est aussi
arbitrairement longue ;
[plus précisément, il existe une branche de l'arbre de
dérivation - cf. cours 16 - qui est arbitrairement longue]
par conséquent il y a au moins un symbole non-terminal T
- qui apparaît deux fois dans cette chaîne [branche],
- et qui entre ces deux apparitions apporte une
contribution non nulle au mot terminal
c'est-à-dire que la suite de réécritures a nécessairement la forme
S
->* fT
h
->* fuT
vh
->* fugvh = w∈ L, avec la
contribution uv
non vide.
D'où il suit que T
->* uT
v
et que T
-> g,
donc T
->* ungvn
pour tout entier n,
et puisque S
->* fT
h, S
->* fungvnh,
c'est-à-dire que fungvnh∈ L
pour tout entier n.
Quod erat demonstrandum.
-
Aucun des exemples
de langages CS que nous
avons vus au cours 17 n'est CF.
- {
anbncn
| n
>0} parce que
l'itération simultanée ne concerne que deux
facteurs, pas trois !
on ne pourra donc pas maintenir l'égalité sur les trois a
,
b
et c
simultanément.
De même pour {a
ib
jc
k
| 1≤ i ≤ j ≤ k},
et a fortiori, pour {a
nb
nc
nd
n
| n>0} !
- Le langage des carrés {ww
| w ∈
{
a
, b
}*} demande un peu plus
d'attention. Appelons-le L.
En particulier, il faut un argument qui ne s'applique pas aux
palindromes !
On examine les mots de L qui sont de la forme apbqarbs
avec p, q, r, s positifs.
(en d'autres termes, on prend l'intersection de L
avec le langage régulier a+b+a+b+
, voir
ci-après).
Les mots en question sont de la forme anbmanbm
avec m et n positifs.
L'idée est que la mécanique CF est incapable de maintenir l'égalité à
la fois sur les a
et sur les b
.
Pour le montrer on a besoin d'une forme plus forte du lemme précédent,
qui précise que le "facteur itérant"
ugv a une longueur bornée. Soit p
la borne en question : choisissons m et n
> p.
Alors le facteur itérant doit être logé
- soit dans les
a
du
début, ce qui ruine l'équilibre avec les a
de
la deuxième moitié
- soit à cheval sur les
a
et les b
, ce qui détruit la structure de
"carré" ww
- soit dans les
b
de la
première moitié, ce qui ruine l'équilibre avec les b
de la deuxième moitié
- etc.
-
-
Soient G1 et G2
deux grammaires CF engendrant deux langages L1
et L2, sur le même alphabet
terminal.
Les deux ensembles de symboles non-terminaux peuvent sans inconvénient
être supposés disjoints.
Il suffit pour engendrer l'union L1∪L2
de prendre la grammaire G ainsi construite :
- ses symboles non-terminaux sont ceux de G1,
ceux de G2 et un
nouvel axiome
S
- ses règles sont celles de G1,
celles de G2 et
deux nouvelles règles :
S->S1,
S->S2
où S1
et S2
sont les axiomes de G1 et de G2 .
-
Même raisonnement en remplaçant L1∪L2
par L1L2,
et les "deux nouvelles règles :
S->S1,
S->S2
"
par "une nouvelle règle : S->S1S2
".
-
Même raisonnement avec une seule grammaire G1,
en remplaçant L1L2
par L1*,
et "une nouvelle règle : S->S1S2
"
par "deux nouvelles règles : S->S1S, S->ε
".
-
L'intersection de deux langages CF n'est pas CF en
général !
Contre-exemple :
- L1 = {
anbncp
| n
>0, p
>0}
est CF (produit de {anbn
| n
>0} par c
+)
- L2 = {
apbncn
| n
>0, p
>0}
est CF (produit de a
+ par {bncn
| n
>0} )
- L1∩L2
= {
anbncn
| n
>0} qui n'est
pas CF.
Toutefois, l'intersection d'un langage CF avec un langage régulier
est CF.
Il n'est pas très facile de le démontrer au moyen d'une grammaire.
En revanche, le résultat sera évident quand nous mettrons en marche les
automates à pile,
qui sont les correspondants des grammaires CF du
côté des automates.
Munis de ce résultat, nous pouvons revisiter la preuve que le langage
des carrés n'est pas CF
comme suit : considérons l'intersection {ww
| w ∈
{a
, b
}*}∩a+b+a+b+
:
si le premier langage est CF,
l'intersection l'est aussi. Or cette intersection est exactement {anbmanbm
| m >0, n>0}.
Il ne reste plus qu'à démontrer que ce dernier langage n'est pas CF, ce
qui se fait par le même argument
que ci-dessus, un tantinet simplifié.
-
-
La contrainte qui définit les automates à pile (en anglais pushdown
automata) est de n'utiliser leur mémoire
qu'en se conformant aux règles suivantes (qui, en informatique,
définissent la structure de pile - en anglais stack
ou pushdown stack) :
- la lecture n'est possible qu'au sommet de la pile (
top
)
- pour prendre connaissance des informations qui sont situées au
dessous du sommet,
il faut détruire ce qui se trouve par-dessus (pop
)
jusqu'à ce que l'information désirée apparaisse au sommet
- l'écriture n'est possible qu'en ajoutant un information
sur le sommet de la pile (
push
)
- cette information se trouve alors au sommet
- on ne peut pas lire sur une pile vide (
empty
).
Les quatre mots top
, pop
, push
et empty
résument le comportement de pile.
Voici un schéma emprunté à Wikipedia
, où le lecteur soucieux
d'exactitude trouvera des définitions en forme.
La "contrainte de pile" est une contrainte très forte.
Pourtant, il n'est pas évident qu'elle implique une croissance linéaire
de la consommation de mémoire.
En effet, la définition générale stipule que l'automate est
non-déterministe :
lisant une lettre x
sur sa bande d'entrée,
voyant un symbole p
au sommet de sa pile, se
trouvant dans un état s
,
l'automate choisit d'avancer ou non sa lecture,
d'empiler un autre symbole push(q)
ou au
contraire d'écrêter sa pile pop()
,
et de passer dans un état t
ou dans un
autre...
Dans ces conditions, qu'est-ce qui l'empêche d'empiler une quantité
exponentielle d'informations et d'en faire bon usage ?
Il y a là un point de finesse à éclaircir.
En tout état de cause, il est clair qu'en faisant fonctionner en
parallèle un automate fini et un automate à pile
on obtient un nouvel automate à pile (dont le "contrôle fini" a été
perfectionné, mais dont la gestion de pile n'a pas changé).
C'est ce qui permet d'affirmer, comme nous l'avons fait précédemment,
que l'intersection (= fonctionnement en parallèle)
d'un langage CF (= automate à pile) avec un langage régulier (=
automate fini) est encore CF.
-
La tradition a retenu deux types de comportement pour accepter ou
rejeter un mot-candidat : par état terminal et par pile vide.
- par état terminal : comme pour un
automate fini, on désigne un sous-ensemble de l'ensemble (fini) des
états,
et le mot est accepté si, et seulement si, il existe un calcul de
l'automate lisant ce mot et s'achevant dans un des états désignés.
Dans ce cas le contenu de la pile en fin de calcul est sans importance.
- par pile vide : le mot est accepté
si, et seulement si à la fin du calcul la pile est vide.
Naturellement, on peut inventer d'autres configurations d'acceptation,
et par des artifices variés on montre que ces deux formules sont
équivalentes quant à la classe des langages reconnus.
Exemples : le langage des systèmes bien parenthésés est facilement
reconnu par un automate déterministe avec pile vide.
En revanche, un langage comme le produit a*
{anbn
| n
>0} = {ambn
| m
>= n
>0} sera plus naturellement reconnu
par un automate déterministe avec un état final attestant qu'après
avoir lu des b
on n'a pas rencontré de
nouveau des a
.
Nous verrons apparaître des automates à pile très précisément décrits
quand nous étudierons l'analyse syntaxique,
il est donc inutile de nous embarrasser d'une définition formelle ici.
-
Contrairement à ce
qui se passe pour les automates finis,
il n'est pas possible en général de trouver
un automate à pile déterministe qui reconnaît le
même langage qu'un automate à pile non-déteriniste
donné.
L'exmple classique en la matière est le langage des palindromes {ww~}
:
le fonctionnement de l'automate à pile reconnaissant ce langage est
très simple :
- au fur et à mesure de sa lecture du mot-candidat, il
empile les lettres qu'il lit.
- parvenu au milieu du mot, il change d'état et décide de
vérifier la palindromité
en s'assurant que désormais chaque lettre qu'il lit est identique à
celle qu'il trouve au sommet de sa pile - et l'élimine aussitôt.
La discipline de pile est parfaitement respectée...
mais comment savoir qu'on est au milieu du mot ?
-
Chomsky, Noam (1956). "Three models for the description of language".
IRE Transactions on Information Theory (2): 113–124.
L'article original est accessible en pdf sur Wikipedia.
- "Type 0" (grammaires générales - machines de Turing),
- "Type I" (grammaires CS - automates linéairement
bornés),
- "Type II" (grammaires CF - automates à pile),
- "Type III" (grammaires linéaires unilatères - automates
finis)
-
Une grammaire CF est linéaire si chaque membre
droit de règle contient au plus un symbole non-terminal.
Exemples : sur les
quatre exemples de grammaires
CF que nous avons vues ci-dessus, les n°s 2 et 3 sont linéaires.
Cette terminologie renvoie à l'interprétation des grammaires CF comme
des équations algébriques portant sur des langages :
La grammaire de l'exemple n° 4 :
S -> SS
S -> aSb
S -> bSa
S -> ε
est lue comme l'équation S = SS ∪ aSb ∪ bSa ∪ e
(avec les abus de
notations habituels).
Cette équation sera dite quadratique par les
algébristes, en raison de la présence du carré "SS
".
Au contraire, l'équation S = aSb ∪ ab
qui
traduit la grammaire de l'exemple n° 2
sera dite linéaire, puisque l'indéterminée S
n'y
apparaît qu'au premier degré
- le mot "linéaire"
dans le langage des mathématiciens renvoyant au fait que
la fonction du premier degré y = ax
+ b est représentée par une ligne droite.
Dans une grammaire linéaire, la distinction entre dérivation droite et
dérivation gauche disparaît,
les arbres de dérivation sont des chaînes, et la question de
l'ambiguïté ne se pose pas.
Si on impose à une grammaire linéaire que l'unique non-terminal du
membre droit soit tout à droite (ou tout à gauche)
cette grammaire devient linéaire unilatère.
Par exemple :
S -> yA
A -> xA
A -> xB
B -> yB
B -> x
Attention à ne pas mélanger la droite et la gauche !
Un couple de règles unilatères mais en sens contraire comme S->xA
,
A->Sy
conduit à S
->* xSy
,
qui sort de l'épure comme on va le voir.
-
La classe des langages engendrés par les
grammaires linéaires
unilatères est exactement la classe des langages réguliers.
La raison en est qu'une grammaire linéaitre unilatère n'est qu'une
autre manière d'écrire un automate fini (non-déterministe).
En effet, dans un dérivation d'une grammaire linéaire unilatère, il y a
à chaque pas un seul non-terminal à droite,
que l'on peut interpréter comme dénotant l'état de la dérivation.
Ainsi, avec la grammaire ci-dessus, la dérivation :
S -> yA -> yxA -> yxxA ->
yxxxB -> yxxxyB -> yxxxyyB > yxxxyyx
peut se lire comme le calcul pour le mot d'entrée yxxxyyx
d'un
automate fini non-déterministe
- dont les états sont
S
(initial), A
et B
- avec pour transitions
s(S,y)
= A
, s(x,A)
= {A,B
} (non-déterminisme), s(B,y)
= B
,
- et enfin
s(B,x)
est l'unique
état terminal T
(qu'il faut adjoindre à {S
, A
,
B
}).
On voit ainsi que la grammaire ci-dessus engendre notre
exemple de langage régulier "renversé",
valeur de l'expression régulière yx*xy*x
.
Voici les termes de la correspondance :
- non-terminaux de la grammaire <--> états
de l'automate
- axiome de la grammaire <--> état
initial de l'automate
- règles non-terminales de la grammaire
<--> transitions de l'automate
- règles terminales de la grammaire
<--> transitions de l'automate vers un état terminal
- dérivation d'un mot terminal à partir de l'axiome
<--> calcul de l'automate pour ce mot,
partant de l'état initial et arrivant à un état terminal.
N.B. En toute rigueur, il faut d'abord "normaliser" la grammaire pour
n'avoir que des
lettres dans les règles, et non des mots composés
(avec des non-terminaux supplémentaitres).
-
Exercice :
écrire des grammaires linéaires unilatères (à gauche ou à droite,
selon vos convictions politiques)
- pour les automates finis
- pour les expressions régulières
que nous avons examinés dans la première partie du cours.