C2 — Langages réguliersDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 2 · Théorie des langages

Cours 2Langages réguliers

Le cœur du cours : automates, expressions régulières et le théorème de Kleene qui les relie, jusqu'à savoir prouver qu'un langage n'est pas régulier.

4 chapitres · 20 h de travail estimé

  1. 1. Automates finis déterministes5 h
  2. 2. Automates non déterministes5 h
  3. 3. Expressions régulières5 h
  4. 4. Propriétés et limites5 h

Chapitre 1 · 5 h

Automates finis déterministes

États, transitions, état initial, états acceptants ; exécution et acceptation d'un mot ; construire un AFD pour un langage donné ; automate complet.

Voici le cœur du cours, et l'objet le plus utile de toute l'année. Un automate fini est une machine d'une simplicité extrême — une poignée d'états, une flèche par lettre — et pourtant elle est partout : dans le premier étage de tout compilateur, dans la barre de recherche qui valide une adresse de courriel, dans le protocole réseau qui suit l'état d'une connexion, dans le distributeur de billets. Chaque fois qu'un programme doit reconnaître une forme dans un flux de symboles avec une mémoire bornée, il y a un automate fini, explicite ou caché.

Ce chapitre le définit, montre comment l'exécuter, et surtout comment en construire un pour un langage donné. Le mot d'ordre du bloc — implémenter plutôt que réciter — commence ici : à la fin, vous aurez codé le moteur d'un automate quelconque.

La définition : un quintuplet

Un automate fini déterministe (AFD) est la donnée de cinq éléments, notés (Q,Σ,δ,q0,F)(Q, \Sigma, \delta, q_0, F) :

SymboleNomCe que c'est
QQensemble des étatsfini ; chaque état est une « situation » de la lecture
Σ\Sigmal'alphabetles symboles lus en entrée (chapitre 1)
δ\deltala fonction de transitionδ:Q×ΣQ\delta : Q \times \Sigma \to Q (chapitre 2)
q0q_0l'état initialoù l'on commence ; q0Qq_0 \in Q
FFles états acceptantsFQF \subseteq Q ; où l'on a le droit de finir

L'idée à retenir dès maintenant, et qui donne le sens de tout le reste : chaque état est une mémoire. Il ne mémorise pas les symboles déjà lus — un automate fini ne peut pas se souvenir de tout — mais la seule chose qui reste utile pour la suite : « où en suis-je de ma reconnaissance ? ». Comme QQ est fini, l'automate ne peut distinguer qu'un nombre fini de situations. C'est à la fois sa force et sa limite, et le chapitre 6 en tirera les conséquences.

Le déterminisme

Le mot « déterministe » a un sens précis : depuis un état donné, en lisant une lettre donnée, il y a exactement une transition possible. C'est l'exigence de fonction du chapitre 2 — une seule sortie par entrée. L'automate ne fait donc jamais de choix : à chaque instant, l'état courant et la lettre lue déterminent entièrement l'état suivant.

Cette propriété a une conséquence pratique majeure : l'exécution est rapide et simple. On lit le mot une seule fois, de gauche à droite, en suivant les flèches, sans jamais revenir en arrière ni explorer plusieurs possibilités. Le temps de reconnaissance est proportionnel à la longueur du mot — on ne fait pas mieux. C'est pourquoi les analyseurs lexicaux réels (chapitre 9) sont des AFD.

Exécution et acceptation

Exécuter un AFD sur un mot w=a1a2anw = a_1 a_2 \dots a_n, c'est suivre un chemin :

q0  --a1-->  q1  --a2-->  q2  --a3-->  ...  --an-->  qn

On part de q0q_0, on applique δ\delta à chaque lettre, on arrive dans un état final qnq_n. Puis la règle d'acceptation, unique et sans nuance :

Le mot ww est accepté si et seulement si l'état d'arrivée qnq_n appartient à FF.

Sinon, il est rejeté. Le langage reconnu par l'automate A\mathcal{A}, noté L(A)L(\mathcal{A}), est l'ensemble de tous les mots acceptés. Un langage est dit régulier s'il existe un automate fini qui le reconnaît — c'est la définition que tout le bloc II va explorer.

Notez le cas du mot vide ε\varepsilon : l'automate ne lit aucune lettre, reste en q0q_0, et ε\varepsilon est accepté exactement quand q0Fq_0 \in F. Encore une fois, tout se joue sur le statut du mot vide.

Construire un AFD : penser en situations

C'est le savoir-faire du chapitre, et il s'apprend par une seule question, posée à l'envers de l'intuition :

Quelles situations dois-je distinguer pour décider, à la fin, si j'accepte — et rien de plus ?

Chaque situation distincte devient un état. Prenons L=L = « les mots sur {a,b}\{a, b\} contenant un nombre pair de aa ». Qu'ai-je besoin de retenir pendant la lecture ? Uniquement la parité du nombre de aa vus jusqu'ici. Deux situations, donc deux états :

        b           b       ┌──┐        ┌──┐       ▼  │        ▼  │   ──▶( pair )──a──▶( impair )        ▲               │        └───────a───────┘
  • pair est l'état initial (zéro aa, c'est pair) et le seul état acceptant ;
  • lire un a bascule la parité ; lire un b ne change rien.

Plutôt que de l'imaginer, regardez-le fonctionner. L'animation ci-dessous fait lire le mot abba à cet automate : le ruban d'entrée défile en haut, l'état courant s'allume, et un jeton portant la lettre lue parcourt la transition prise. Comme abba contient deux a, le chemin revient à l'état pair et le mot est accepté.

Animation · 6 étapes

Un AFD lit « abba » : nombre pair de a ?

  1. État initialL'automate démarre dans l'état pair. Mot à lire : « abba ».
  2. Lecture de « a » (1/4)Depuis pair, la lettre « a » mène à impair.
  3. Lecture de « b » (2/4)Depuis impair, la lettre « b » mène à impair.
  4. Lecture de « b » (3/4)Depuis impair, la lettre « b » mène à impair.
  5. Lecture de « a » (4/4)Depuis impair, la lettre « a » mène à pair.
  6. Mot acceptéTout le mot est lu et l'état d'arrivée pair est acceptant : le mot « abba » appartient au langage.

Deux réflexes de conception valent d'être énoncés.

Distinguer juste ce qu'il faut. Retenir le nombre exact de aa demanderait une infinité d'états — impossible. Ne retenir que la parité suffit, parce que c'est la seule information qui sert à la décision finale. Un bon AFD oublie tout le reste.

Ni trop, ni trop peu d'états. Trop d'états distinguent des situations qui mènent au même comportement (gaspillage) ; trop peu confondent des cas qu'il fallait séparer (erreur). Le chapitre 6 donnera l'automate minimal, qui a le nombre exact d'états nécessaires.

Quiz · 1 question

Dans un AFD reconnaissant « les mots contenant un nombre pair de a », que représente chaque état ?

  • Le nombre exact de a lus depuis le début du motcompte exact
  • La parité (pair/impair) du nombre de a lus jusqu'ici — la seule information utile pour la décision finaleparité
  • La dernière lettre luedernière lettre

Réponse : Un automate fini a un nombre FINI d'états : il ne peut pas mémoriser le nombre exact de a, qui peut croître sans borne. Il ne retient que ce qui sert à décider — ici, la parité, deux valeurs, donc deux états. C'est le principe de conception d'un AFD : chaque état est une mémoire réduite à la stricte information nécessaire. Retenir la dernière lettre ne suffirait pas à connaître la parité de tout le préfixe.

L'automate complet

Un détail technique qui a des conséquences. Un AFD est complet si δ(q,a)\delta(q, a) est défini pour tout état qq et toute lettre aa : depuis n'importe quel état, chaque lettre a une flèche sortante. Notre exemple ci-dessus est complet.

Souvent, on dessine un automate incomplet : on omet les transitions qui « mènent à l'échec », pour ne pas surcharger le schéma. Pour le rendre complet, on ajoute un état puits (ou état poubelle) : un état non acceptant, absorbant, vers lequel partent toutes les transitions manquantes, et qui boucle sur lui-même. Une fois entré dans le puits, on n'en sort plus — le mot est condamné au rejet.

Pourquoi s'en soucier ? Parce que certaines constructions l'exigent. La plus importante : pour reconnaître le complément d'un langage — les mots que l'automate rejetait —, il suffit d'échanger les états acceptants et non acceptants. Mais cet échange ne donne le bon résultat que si l'automate est complet : sinon, un mot qui « bloquait » faute de transition ne serait ni dans le langage ni dans son complément. La complétude est le petit soin qui rend les propriétés de clôture du chapitre 6 correctes.

Quiz · 1 question

Un AFD complet reconnaît un langage L. On échange ses états acceptants et non acceptants (F devient Q\\F). Quel langage reconnaît le nouvel automate ?

  • Le même langage Linchangé
  • Le complément de L : tous les mots que l'ancien automate rejetaitcomplément
  • Le langage videvide

Réponse : Puisque l'automate est complet, tout mot atteint un unique état d'arrivée. Échanger acceptants et non acceptants inverse exactement la décision pour chaque mot : ceux qui finissaient dans F (acceptés) finissent maintenant hors de F (rejetés) et inversement. On obtient donc le complément de L. C'est une propriété de clôture des langages réguliers (chapitre 6) — mais elle EXIGE la complétude : sur un automate incomplet, un mot bloqué en cours de route échapperait aux deux langages.

À vous

Le TP fondateur du bloc : implémenter un AFD. Vous écrivez d'abord le moteur d'exécution, une poignée de lignes qui exécute n'importe quel automate décrit par sa table de transition — la preuve que toute la logique du langage tient dans δ\delta, pas dans le code. Vous le testez sur « nombre pair de aa », puis vous concevez vous-même l'automate de « mots se terminant par abab ».

C'est ce passage du papier au code que le programme du cours désigne comme le plus formateur : écrire le moteur fait comprendre l'automate mieux que n'importe quel exercice de tracé.

Exercice de code

Écrivez le moteur d'exécution générique d'un AFD (états, alphabet, transitions, initial, acceptants), testez-le sur « nombre pair de a », puis concevez vous-même l'AFD du langage « mots se terminant par ab ».

Point de départ

// Un AFD est un quintuplet (Q, Σ, δ, q0, F). On le décrit ici comme une
// donnée, et on écrit UNE fois le moteur qui exécute n'importe quel AFD.
//
// Langage visé : L = { mots sur {a,b} contenant un nombre PAIR de a }.

const afd = {
  etats: ["pair", "impair"],          // Q
  alphabet: ["a", "b"],               // Σ
  initial: "pair",                    // q0 : zéro 'a' lu, c'est pair
  acceptants: ["pair"],               // F
  // δ : table de transition. delta["etat"]["lettre"] = etat d'arrivée.
  delta: {
    pair:   { a: "impair", b: "pair" },   // lire un a change la parité ; b ne
    impair: { a: "pair",   b: "impair" }, // change rien
  },
};

// ── À VOUS (1) : le moteur d'exécution ──────────────────────────────────────
// Partir de l'état initial, lire le mot lettre par lettre en suivant δ,
// puis accepter si l'état final est dans F.
function accepte(afd, mot) {
  let etat = afd.initial;
  // à compléter : boucle sur les lettres de 'mot', mise à jour de 'etat'
  return false; // à remplacer : etat est-il acceptant ?
}

// ── Vérification ────────────────────────────────────────────────────────────
const tests = [
  ["",       true],   // 0 a : pair
  ["aa",     true],   // 2 a
  ["a",      false],  // 1 a
  ["abab",   true],   // 2 a
  ["baaab",  false],  // 3 a
  ["bbbb",   true],   // 0 a
];
for (const [mot, attendu] of tests) {
  const r = accepte(afd, mot);
  console.log("'" + mot + "'".padEnd(8) + " -> " + r + (r === attendu ? "  ok" : "  ✗"));
}

// ── À VOUS (2) : concevoir un AFD ───────────────────────────────────────────
// Décrivez ci-dessous un AFD 'afd2' pour L2 = { mots qui SE TERMINENT par 'ab' }.
// Indice : 3 états — "rien de spécial", "vient de voir un a", "vient de voir ab".
const afd2 = null; // à construire, sur le même modèle que 'afd'
if (afd2) {
  for (const mot of ["ab", "aab", "abab", "aba", "b", "ba"]) {
    console.log("L2 '" + mot + "' -> " + accepte(afd2, mot));
  }
}

Solution

function accepte(afd, mot) {
  let etat = afd.initial;
  for (const lettre of mot) {
    etat = afd.delta[etat][lettre];   // δ(etat, lettre) : une seule transition
  }
  return afd.acceptants.includes(etat);
}
// Le moteur tient en trois lignes, et il est GÉNÉRIQUE : il exécute n'importe
// quel AFD décrit par la donnée. C'est toute la force du modèle — la logique
// du langage est entièrement dans la table δ, pas dans le code.

// ── L'AFD pour « se termine par ab » ────────────────────────────────────────
const afd2 = {
  etats: ["q0", "qa", "qab"],
  alphabet: ["a", "b"],
  initial: "q0",
  acceptants: ["qab"],               // on accepte si on VIENT de lire ...ab
  delta: {
    // q0 : on n'a rien vu d'utile. Un 'a' amorce un possible "ab".
    q0:  { a: "qa",  b: "q0"  },
    // qa : on vient de voir un 'a'. Un 'b' complète "ab" ; un autre 'a' garde
    //      l'espoir (le dernier a peut encore commencer un ab).
    qa:  { a: "qa",  b: "qab" },
    // qab : on vient de voir "ab". Un 'a' relance (qa) ; un 'b' casse (q0).
    qab: { a: "qa",  b: "q0"  },
  },
};

// ── Ce que le TP enseigne ───────────────────────────────────────────────────
//
// 1. Chaque état est une MÉMOIRE : "où en suis-je de ma reconnaissance ?".
//    L'AFD ne peut retenir qu'un nombre FINI de situations — ici la parité des
//    a, ou les 2 derniers symboles utiles. C'est sa force (rapidité, mémoire
//    bornée) et sa limite (chapitre 6 : il ne sait pas compter jusqu'à n).
//
// 2. L'exécution est DÉTERMINISTE : depuis un état, une lettre mène à UN seul
//    état. Le moteur ne fait jamais de choix — d'où sa simplicité et sa
//    vitesse (une passe, temps proportionnel à la longueur du mot).
//
// 3. Concevoir un AFD, c'est répondre à : « quelles situations dois-je
//    distinguer, et rien de plus ? ». Trop d'états = on distingue l'inutile ;
//    trop peu = on confond des cas qu'il fallait séparer. Le chapitre 6
//    (minimisation) donnera le nombre EXACT d'états nécessaires.

Ce que la suite en fait

Vous savez maintenant ce qu'est un langage régulier — un langage reconnu par un AFD — et comment en construire un. Mais l'AFD est parfois pénible à concevoir directement : son exigence de déterminisme force à tout anticiper.

Le chapitre 4 lève cette contrainte avec l'automate non déterministe, bien plus facile à écrire, puis montre le résultat central du cours : on peut toujours transformer un automate non déterministe en AFD équivalent. C'est la déterminisation — le premier des deux points qui coincent, et celui où le 2n2^n de l'ensemble des parties (chapitre 2) va enfin se manifester.

À retenir

Flashcards · 4 cartes

Quels sont les cinq éléments d'un AFD, et quand un mot est-il accepté ?
Un AFD est un quintuplet (Q, Σ, δ, q0, F) : états, alphabet, fonction de transition δ : Q×Σ→Q, état initial q0, états acceptants F ⊆ Q. Un mot w est accepté si et seulement si, en partant de q0 et en suivant δ lettre par lettre, l'état d'arrivée appartient à F. Le langage reconnu L(A) est l'ensemble des mots acceptés ; un langage est régulier s'il existe un AFD qui le reconnaît.
Que signifie « déterministe », et quel avantage cela procure-t-il ?
Depuis un état, chaque lettre mène à EXACTEMENT UN état (δ est une fonction) : l'automate ne fait jamais de choix. Avantage : l'exécution lit le mot une seule fois, de gauche à droite, sans retour arrière, en temps proportionnel à sa longueur. C'est pourquoi les analyseurs lexicaux réels sont des AFD.
Comment conçoit-on un AFD pour un langage donné ?
En répondant à : « quelles situations dois-je distinguer pour décider à la fin, et rien de plus ? ». Chaque situation utile devient un état — une mémoire réduite à l'information nécessaire (ex. la parité, pas le compte exact). Trop d'états distinguent l'inutile, trop peu confondent des cas à séparer ; le chapitre 6 donne le nombre exact via la minimisation.
Qu'est-ce qu'un AFD complet, et pourquoi la complétude est-elle nécessaire pour le complément ?
Complet : δ(q,a) est défini pour tout état et toute lettre (on ajoute au besoin un état puits absorbant et non acceptant). Pour reconnaître le complément d'un langage, on échange acceptants et non acceptants — mais cet échange n'est correct que sur un automate complet : sinon un mot « bloqué » faute de transition n'appartiendrait ni à L ni à son complément.

Exercices d'entraînement

Exercice 1

Construire un AFD

Sur Σ={a,b}\Sigma = \{a, b\}, construire un AFD reconnaissant les mots qui contiennent le facteur aa. Donner les états (avec leur signification), l'état initial, les états acceptants et la table de transition.

Correction

Il suffit de retenir « combien de a consécutifs viens-je de voir, au plus 2 ». Trois états :

  • q0 : je n'ai pas de a en attente (état initial) ;
  • q1 : je viens de voir un a ;
  • q2 : j'ai déjà vu aa (état acceptant, absorbant).

Table de transition :

étatab
q0q1q0
q1q2q0
q2q2q2

Une fois aa rencontré, on reste dans q2 quoi qu'il arrive : le facteur est là pour de bon. Lire un b en q1 remet le compteur à zéro (q0), car la série de a est interrompue.

Exercice 2

Lire un AFD

Un AFD sur Σ={a,b}\Sigma = \{a, b\} a pour états p (initial) et q (acceptant), et pour transitions : δ(p,a)=q\delta(p,a)=q, δ(q,a)=q\delta(q,a)=q, δ(p,b)=p\delta(p,b)=p, δ(q,b)=p\delta(q,b)=p. Quel langage reconnaît-il ?

Correction

On accepte lorsqu'on termine en q. Observons les transitions : lire a mène toujours en q (depuis p comme depuis q), et lire b mène toujours en p. L'état d'arrivée ne dépend donc que de la dernière lettre lue : q si c'est a, p si c'est b.

Le langage reconnu est donc : les mots qui se terminent par a. (Le mot vide reste en p, non acceptant : il est rejeté, ce qui est cohérent — il ne se « termine pas par a ».)

Exercice 3

Complément d'un langage

En repartant de l'AFD de l'exercice 2 (mots se terminant par a), donner un AFD reconnaissant le complément de ce langage, et décrire ce complément.

Correction

L'AFD est complet (δ\delta est définie pour tout état et toute lettre), donc il suffit d'échanger états acceptants et non acceptants : on prend F={p}F = \{p\} à la place de {q}\{q\}, sans rien changer aux transitions.

Le complément est l'ensemble des mots qui ne se terminent pas par a, c'est-à-dire le mot vide ε\varepsilon (aucune dernière lettre) et tous les mots se terminant par b. L'échange n'aurait pas donné le bon résultat sur un automate incomplet : un mot bloqué faute de transition n'aurait été ni dans le langage ni dans son complément.

Chapitre 2 · 5 h

Automates non déterministes

AFN et ε-transitions ; déterminisation par construction des sous-ensembles ; équivalence AFD/AFN — le premier point qui coince.

Le chapitre 3 a montré comment construire un AFD, et aussi sa difficulté : son déterminisme force à tout anticiper. Pour reconnaître « les mots qui se terminent par abab », il faut prévoir, à chaque aa lu, qu'il pourrait être l'avant-dernière lettre — sans le savoir encore. Cette gymnastique est pénible et source d'erreurs.

L'automate non déterministe supprime cette gêne : il s'autorise à « deviner ». Il est bien plus facile à écrire — et c'est là qu'est le miracle du chapitre : cette liberté ne coûte aucune puissance. Tout automate non déterministe se transforme mécaniquement en AFD équivalent. La transformation s'appelle la déterminisation, et c'est le premier des deux points qui coincent de l'année. Il devient mécanique dès qu'on l'a fait tourner une fois — ce que fait l'exercice.

L'automate non déterministe

Un automate fini non déterministe (AFN) a la même définition qu'un AFD, à une différence près, mais décisive : la fonction de transition rend un ensemble d'états.

δ:Q×ΣP(Q).\delta : Q \times \Sigma \to \mathcal{P}(Q).

Depuis un état qq en lisant une lettre aa, l'automate peut avoir plusieurs transitions possibles — ou aucune. Concrètement :

  • δ(q,a)={q1,q2}\delta(q, a) = \{q_1, q_2\} : deux choix possibles ;
  • δ(q,a)=\delta(q, a) = \emptyset : aucune transition — ce chemin meurt.

La règle d'acceptation change en conséquence, et c'est le point le plus important à comprendre :

Un AFN accepte un mot s'il existe au moins un chemin menant de l'état initial à un état acceptant.

Un seul chemin gagnant suffit. On peut imaginer l'automate explorant tous les chemins à la fois, en parallèle, et acceptant si l'un d'eux réussit. C'est cette lecture — « il existe un chemin » — qui rend les AFN si commodes : on écrit les transitions « utiles » et on laisse mourir les autres, sans se soucier d'un état puits.

Reprenons « se termine par abab » en non déterministe :

        a, b       ┌──┐       ▼  │   ──▶( q0 )──a──▶( q1 )──b──▶(( q2 ))

En q0q_0, lire aa offre deux choix : boucler sur q0q_0 (« ce aa n'est pas le bon »), ou partir vers q1q_1 (« je parie que le abab final commence ici »). L'automate n'a pas à trancher : si le pari est bon, le chemin q0q1q2q_0 \to q_1 \to q_2 existe et le mot est accepté. Comparez à l'AFD du chapitre 3, où il fallait gérer explicitement chaque cas : l'AFN dit simplement ce qu'on veut reconnaître.

L'animation déroule la lecture de aab. Observez la nouveauté par rapport au chapitre 3 : l'état courant n'est plus unique mais un ensemble — l'automate explore tous les chemins à la fois, et accepte dès que l'un d'eux atteint un état acceptant. À la fin, l'ensemble contient q2 : le mot est accepté. C'est déjà, en germe, l'idée de la déterminisation.

Animation · 5 étapes

Un AFN lit « aab » : se termine par ab ?

  1. État initialL'automate démarre dans l'état {q0}. Mot à lire : « aab ». En non déterministe, on suit tous les chemins à la fois.
  2. Lecture de « a » (1/3)Depuis {q0}, la lettre « a » mène à {q0, q1}.
  3. Lecture de « a » (2/3)Depuis {q0, q1}, la lettre « a » mène à {q0, q1}.
  4. Lecture de « b » (3/3)Depuis {q0, q1}, la lettre « b » mène à {q0, q2}.
  5. Mot acceptéTout le mot est lu et l'état d'arrivée {q0, q2} contient un état acceptant (q2) : le mot « aab » appartient au langage.

Les ε-transitions

Les AFN se dotent souvent d'un raccourci supplémentaire : les ε-transitions, des flèches étiquetées par le mot vide ε\varepsilon. Elles permettent de changer d'état sans lire aucune lettre.

( q1 )──ε──▶( q2 )

Leur intérêt est purement pratique : elles servent à assembler des automates comme des pièces de Lego — brancher la sortie de l'un sur l'entrée d'un autre par une ε-transition, sans se soucier de raccorder les transitions. C'est exactement ce dont le chapitre 5 aura besoin pour construire un automate à partir d'une expression régulière (construction de Thompson).

Pour raisonner avec elles, on introduit l'ε-fermeture d'un état : l'ensemble de tous les états atteignables en ne suivant que des ε-transitions (y compris l'état lui-même). On l'intègre à la déterminisation ; l'idée générale reste la même, on ajoute simplement « et tout ce qu'on peut atteindre gratuitement par des ε\varepsilon ».

Quiz · 1 question

Un AFN, sur le mot w, possède trois chemins possibles depuis l'état initial : deux se bloquent (δ vide en cours de route) et un seul atteint un état acceptant. Le mot w est-il accepté ?

  • Non : deux chemins sur trois échouent, la majorité l'emportemajorité
  • Oui : il suffit qu'il EXISTE un chemin menant à un état acceptantexistence d'un chemin
  • Cela dépend de l'ordre dans lequel on explore les cheminsordre d'exploration

Réponse : La règle d'acceptation d'un AFN est existentielle : le mot est accepté dès qu'IL EXISTE au moins un chemin de l'état initial vers un état acceptant. Les chemins qui se bloquent ou finissent hors de F ne comptent pas — un seul chemin gagnant suffit. L'ordre d'exploration est sans effet sur le résultat (il n'affecte que la manière de le calculer). C'est cette souplesse « il existe un chemin » qui rend les AFN faciles à écrire.

La déterminisation : construction des sous-ensembles

Comment exécuter un automate qui « devine » sur une machine réelle, qui elle ne devine pas ? En le transformant en AFD. L'idée est d'une élégance qu'il faut avoir comprise une fois pour toutes :

Un état de l'AFD représente l'ensemble de tous les états où l'AFN pourrait se trouver après avoir lu le préfixe courant.

Le non-déterminisme « où suis-je ? — plusieurs réponses possibles » devient un déterminisme « je suis dans cet ensemble d'états — une seule réponse ». La question redevient à réponse unique, donc déterministe. La construction, dite des sous-ensembles, procède ainsi :

  1. L'état initial de l'AFD est {q0}\{q_0\} (avec son ε-fermeture s'il y a des ε-transitions).
  2. Depuis un état-ensemble EE, en lisant une lettre aa, on va dans l'union des δ(e,a)\delta(e, a) pour tous les eEe \in E : « tous les états où l'on peut arriver ».
  3. Un état-ensemble est acceptant dès qu'il contient au moins un état acceptant de l'AFN — parce qu'il suffit qu'un chemin possible soit gagnant.
  4. On ne construit que les ensembles réellement atteignables, en partant de {q0}\{q_0\} et en suivant les transitions.

Le point 3 est le plus souvent raté : on accepte dès qu'un des états possibles est acceptant, pas quand ils le sont tous. C'est le reflet direct de la règle existentielle de l'AFN.

Le coût, et l'équivalence

Combien d'états peut avoir le déterminisé ? Ses états sont des sous-ensembles de QQ. Si l'AFN a nn états, il y a au plus 2n2^n sous-ensembles — c'est le 2n2^n de l'ensemble des parties du chapitre 2, qui se manifeste enfin. C'est le prix théorique du non-déterminisme : la déterminisation peut faire exploser le nombre d'états.

Deux nuances tempèrent cette borne. En pratique, on ne construit que les ensembles atteignables, et ils sont presque toujours bien moins nombreux que 2n2^n — dans l'exercice, 3 sur 8. Mais le pire cas existe réellement : certains langages exigent bel et bien un nombre exponentiel d'états une fois déterminisés, et c'est pourquoi on garde parfois l'AFN tel quel.

Au-delà du coût, la conséquence est le théorème central du chapitre :

Tout AFN peut être transformé en un AFD reconnaissant le même langage. AFD et AFN reconnaissent donc exactement la même classe de langages : les langages réguliers.

Le non-déterminisme est plus commode à écrire, jamais plus puissant. C'est un confort de conception, pas une extension du modèle — et c'est un résultat profond, car on aurait pu croire le contraire.

Quiz · 1 question

Un AFN a 4 états. Après déterminisation par construction des sous-ensembles, combien d'états l'AFD obtenu peut-il avoir au maximum, et pourquoi ne les atteint-on presque jamais tous ?

  • 4 états, car la déterminisation préserve le nombre d'étatspréservation
  • 2⁴ = 16 au maximum (les sous-ensembles des 4 états), mais on ne construit que les ensembles atteignables, souvent bien moins nombreux2ⁿ sous-ensembles, atteignables seulement
  • 8 états, soit le double, à cause du non-déterminismele double

Réponse : Les états de l'AFD sont les sous-ensembles de l'ensemble des états de l'AFN : au plus 2⁴ = 16, la taille de l'ensemble des parties (chapitre 2). Mais la construction ne crée que les ensembles réellement atteignables depuis {q0} en suivant les transitions ; les autres n'apparaissent jamais. En pratique on en obtient souvent une poignée. Le pire cas exponentiel existe cependant pour certains langages — d'où l'intérêt de parfois conserver l'AFN.

À vous

L'exercice fait tomber le blocage en rendant l'algorithme mécanique : vous implémentez la construction des sous-ensembles, puis vous la faites tourner sur l'AFN de « se termine par abab ». Vous voyez les états du déterminisé apparaître comme des ensembles {q0}\{q_0\}, {q0,q1}\{q_0, q_1\}, {q0,q2}\{q_0, q_2\} — et la règle « acceptant = contient un acceptant » cesse d'être abstraite.

Codez-le, exécutez-le, relisez la trace : la déterminisation n'est plus un mystère mais une procédure que vous savez dérouler à la main.

Exercice de code

Implémentez la déterminisation par construction des sous-ensembles : chaque état de l'AFD est l'ensemble des états où l'AFN peut se trouver. Faites-la tourner sur l'AFN de « se termine par ab » et lisez le résultat — c'est le point qui coince, il devient mécanique une fois codé.

Point de départ

// Un AFN reconnaît L = { mots sur {a,b} se terminant par 'ab' }.
// Non déterminisme : depuis q0, lire 'a' peut RESTER en q0 (boucle) OU aller
// en q1 (parier que le 'ab' final commence ici). δ rend un ENSEMBLE d'états.

const afn = {
  etats: ["q0", "q1", "q2"],
  alphabet: ["a", "b"],
  initial: "q0",
  acceptants: ["q2"],
  // delta[etat][lettre] = LISTE d'états (peut être vide, ou à plusieurs)
  delta: {
    q0: { a: ["q0", "q1"], b: ["q0"] },   // le choix est ici
    q1: { a: [],           b: ["q2"] },   // depuis q1, un 'b' complète "ab"
    q2: { a: [],           b: []      },
  },
};

// ── Outil : δ étendu à un ENSEMBLE d'états ──────────────────────────────────
// Depuis un ensemble E d'états, en lisant 'lettre', où peut-on être ?
// Réponse : l'union des δ(e, lettre) pour tout e dans E.
function transitionEnsemble(afn, ensemble, lettre) {
  const arrivee = new Set();
  for (const e of ensemble)
    for (const cible of afn.delta[e][lettre])
      arrivee.add(cible);
  return [...arrivee].sort();
}

// ── À VOUS : la construction des sous-ensembles ─────────────────────────────
// Construire l'AFD dont les états sont des ENSEMBLES d'états de l'AFN.
//   - état initial de l'AFD = { initial de l'AFN }
//   - depuis un état-ensemble, δ_AFD(E, lettre) = transitionEnsemble(...)
//   - un état-ensemble est acceptant s'il CONTIENT un acceptant de l'AFN
//   - on n'explore que les ensembles ATTEIGNABLES (pas les 2^n en aveugle)
function determiniser(afn) {
  const nom = (E) => "{" + E.join(",") + "}";
  const initial = [afn.initial];
  const aTraiter = [initial];
  const vus = new Set([nom(initial)]);
  const deltaAFD = {};
  const acceptantsAFD = [];

  while (aTraiter.length > 0) {
    const E = aTraiter.shift();
    // à compléter :
    //  1. si E contient un état de afn.acceptants, ajouter nom(E) aux acceptants
    //  2. pour chaque lettre, calculer l'ensemble d'arrivée A
    //     enregistrer deltaAFD[nom(E)][lettre] = nom(A)
    //     si nom(A) jamais vu, l'ajouter à 'vus' et 'aTraiter'
  }
  return { initial: nom(initial), acceptants: acceptantsAFD, delta: deltaAFD };
}

// ── Vérification ────────────────────────────────────────────────────────────
const afd = determiniser(afn);
console.log("État initial :", afd.initial);
console.log("Acceptants   :", afd.acceptants.join(" "));
console.log("Transitions :");
for (const [E, trans] of Object.entries(afd.delta))
  for (const [l, A] of Object.entries(trans))
    console.log("  " + E + " --" + l + "--> " + A);

Solution

function determiniser(afn) {
  const nom = (E) => "{" + E.join(",") + "}";
  const initial = [afn.initial];
  const aTraiter = [initial];
  const vus = new Set([nom(initial)]);
  const deltaAFD = {};
  const acceptantsAFD = [];

  while (aTraiter.length > 0) {
    const E = aTraiter.shift();
    deltaAFD[nom(E)] = {};
    // 1. acceptant si E contient AU MOINS un acceptant de l'AFN
    if (E.some((e) => afn.acceptants.includes(e))) acceptantsAFD.push(nom(E));
    // 2. une transition par lettre, vers l'ensemble atteignable
    for (const lettre of afn.alphabet) {
      const A = transitionEnsemble(afn, E, lettre);
      deltaAFD[nom(E)][lettre] = nom(A);
      if (!vus.has(nom(A))) { vus.add(nom(A)); aTraiter.push(A); }
    }
  }
  return { initial: nom(initial), acceptants: acceptantsAFD, delta: deltaAFD };
}

// ── Résultat, lu comme un AFD ───────────────────────────────────────────────
//   {q0}      --a--> {q0,q1}   --b--> {q0}
//   {q0,q1}   --a--> {q0,q1}   --b--> {q0,q2}   <- acceptant (contient q2)
//   {q0,q2}   --a--> {q0,q1}   --b--> {q0}
//
// L'AFN avait 3 états ; le déterminisé en a 3 ATTEIGNABLES sur 2^3 = 8
// possibles. On n'a exploré que les ensembles réellement atteints — c'est ce
// qui évite l'explosion dans la pratique.
//
// ── La clé pour ne plus être bloqué ─────────────────────────────────────────
//
// 1. Un état de l'AFD = « l'ENSEMBLE de tous les états où l'AFN pourrait se
//    trouver après avoir lu ce préfixe ». Le non-déterminisme « où suis-je ?
//    plusieurs réponses » devient un déterminisme « je suis dans CET ensemble,
//    une seule réponse ». C'est toute l'idée.
//
// 2. Acceptant = l'ensemble contient AU MOINS un acceptant de l'AFN. Car
//    l'AFN accepte s'il EXISTE un chemin acceptant : il suffit qu'un des états
//    possibles soit acceptant.
//
// 3. Au pire 2^n états (l'ensemble des parties, chapitre 2), mais on ne
//    construit que les ATTEIGNABLES — souvent bien moins. Le pire cas existe
//    pourtant réellement, et c'est pourquoi on préfère parfois garder l'AFN.
//
// 4. Conséquence théorique majeure : AFN et AFD reconnaissent EXACTEMENT les
//    mêmes langages. Le non-déterminisme est plus COMMODE à écrire, jamais
//    plus PUISSANT. C'est l'équivalence AFD/AFN.

Ce que la suite en fait

Vous disposez maintenant de deux descriptions équivalentes des langages réguliers : l'AFD (exécutable, efficace) et l'AFN (facile à écrire), reliés par la déterminisation.

Le chapitre 5 en ajoute une troisième, purement textuelle : les expressions régulières, celles que vous croisez déjà dans les éditeurs et les outils de recherche. Le théorème de Kleene établira qu'elles décrivent exactement les mêmes langages que les automates — et la construction de Thompson, qui traduit une expression en automate, s'appuiera précisément sur les ε-transitions vues ici pour assembler les pièces.

À retenir

Flashcards · 4 cartes

En quoi un AFN diffère-t-il d'un AFD, et quelle est sa règle d'acceptation ?
La transition rend un ENSEMBLE d'états : δ : Q×Σ → P(Q). Depuis un état, une lettre peut mener à plusieurs états, ou à aucun (le chemin meurt). Un AFN accepte un mot s'il EXISTE au moins un chemin de l'état initial vers un état acceptant — un seul chemin gagnant suffit. Cette souplesse existentielle rend les AFN faciles à écrire.
À quoi servent les ε-transitions ?
Ce sont des flèches étiquetées par ε : elles changent d'état SANS lire de lettre. Leur usage est pratique — assembler des automates comme des pièces (brancher la sortie de l'un sur l'entrée d'un autre), ce dont la construction de Thompson (chapitre 5) a besoin. Pour raisonner, on utilise l'ε-fermeture : tous les états atteignables en ne suivant que des ε-transitions.
Sur quel principe repose la déterminisation par construction des sous-ensembles ?
Un état de l'AFD = l'ENSEMBLE de tous les états où l'AFN pourrait se trouver après le préfixe lu. Initial : {q0}. Transition : union des δ(e,a) pour e dans l'ensemble. Acceptant : l'ensemble CONTIENT au moins un acceptant de l'AFN (règle existentielle). On ne construit que les ensembles atteignables. Le non-déterminisme « plusieurs réponses » devient « je suis dans cet ensemble », donc déterministe.
Quel est le coût de la déterminisation, et qu'établit l'équivalence AFD/AFN ?
Au pire 2ⁿ états (les sous-ensembles des n états — l'ensemble des parties), mais en pratique on ne crée que les atteignables, souvent bien moins ; le pire cas exponentiel existe cependant pour certains langages. Équivalence : tout AFN se transforme en AFD reconnaissant le même langage. AFN et AFD reconnaissent exactement les langages réguliers — le non-déterminisme est plus commode, jamais plus puissant.

Exercices d'entraînement

Exercice 1

Déterminiser un AFN

Soit l'AFN sur Σ={a,b}\Sigma = \{a, b\} reconnaissant « les mots contenant le facteur ab » : états q0 (initial), q1, q2 (acceptant), avec δ(q0,a)={q0,q1},δ(q0,b)={q0},δ(q1,b)={q2},δ(q2,a)=δ(q2,b)={q2}.\delta(q_0, a) = \{q_0, q_1\},\quad \delta(q_0, b) = \{q_0\},\quad \delta(q_1, b) = \{q_2\},\quad \delta(q_2, a) = \delta(q_2, b) = \{q_2\}. Déterminiser par la construction des sous-ensembles. Quels états-ensembles sont acceptants ?

Correction

On part de {q0}\{q_0\} et on ne construit que les ensembles atteignables :

ensembleab
{q0}\{q_0\}{q0,q1}\{q_0, q_1\}{q0}\{q_0\}
{q0,q1}\{q_0, q_1\}{q0,q1}\{q_0, q_1\}{q0,q2}\{q_0, q_2\}
{q0,q2}\{q_0, q_2\}{q0,q1,q2}\{q_0, q_1, q_2\}{q0,q2}\{q_0, q_2\}
{q0,q1,q2}\{q_0, q_1, q_2\}{q0,q1,q2}\{q_0, q_1, q_2\}{q0,q2}\{q_0, q_2\}

L'AFD a 44 états atteignables (sur 23=82^3 = 8 possibles). Sont acceptants ceux qui contiennent q2 : {q0,q2}\{q_0, q_2\} et {q0,q1,q2}\{q_0, q_1, q_2\} — car un ensemble accepte dès qu'un état acceptant y figure.

Exercice 2

ε-fermeture

Un AFN possède les états A, B, C et les transitions AεBA \xrightarrow{\varepsilon} B, BεCB \xrightarrow{\varepsilon} C, AaAA \xrightarrow{a} A. Donner l'ε-fermeture de A.

Correction

L'ε-fermeture d'un état est l'ensemble des états atteignables en ne suivant que des ε-transitions, l'état lui-même inclus. Depuis A : on s'y trouve, on atteint B par ε\varepsilon, puis C par ε\varepsilon. La transition AaAA \xrightarrow{a} A n'est pas une ε-transition et ne compte pas. ε-fermeture(A)={A,B,C}.\varepsilon\text{-fermeture}(A) = \{A, B, C\}.

Exercice 3

Le non-déterminisme, plus concis

Donner un AFN à 33 états pour « l'avant-dernière lettre du mot est a » sur Σ={a,b}\Sigma = \{a, b\}. Combien d'états son AFD déterminisé possède-t-il ?

Correction

AFN : état q0 (initial) qui boucle sur a et b, une transition q0aq1q_0 \xrightarrow{a} q_1 (on devine que ce a est l'avant-dernière lettre), puis q1aq2q_1 \xrightarrow{a} q_2 et q1bq2q_1 \xrightarrow{b} q_2, avec q2 acceptant. Trois états suffisent — c'est le pari commode du non-déterminisme.

Le déterminisé a 44 états atteignables (les sous-ensembles {q0}\{q_0\}, {q0,q1}\{q_0,q_1\}, {q0,q1,q2}\{q_0,q_1,q_2\}, {q0,q2}\{q_0,q_2\}). C'est l'illustration du coût de la déterminisation : ici modéré, mais en généralisant à « la kk-ième lettre avant la fin », l'AFN garde k+1k+1 états quand l'AFD en exige 2k2^k.

Chapitre 3 · 5 h

Expressions régulières

Syntaxe et sémantique ; théorème de Kleene, expressions régulières ↔ automates finis ; construction de Thompson et élimination d'états.

Vous connaissez déjà les expressions régulières sans le savoir : c'est la syntaxe \d{2}/\d{2} qui valide une date dans un formulaire, [a-z]+@[a-z]+\.[a-z]+ qui filtre une adresse de courriel, le motif tapé dans la recherche d'un éditeur de texte. Ce chapitre montre ce qu'elles sont vraiment — une troisième façon de décrire un langage régulier, purement textuelle — et révèle le résultat qui unifie tout le bloc II : le théorème de Kleene, qui affirme qu'expressions régulières et automates finis décrivent exactement les mêmes langages.

Trois descriptions, un seul monde. C'est l'un des plus beaux résultats du cours, et le plus utile en pratique.

Syntaxe : trois opérations et rien d'autre

Une expression régulière sur un alphabet Σ\Sigma se construit inductivement (chapitre 2), à partir de briques élémentaires et de trois opérations. Les briques :

  • \emptyset dénote le langage vide ;
  • ε\varepsilon dénote le langage {ε}\{\varepsilon\} ;
  • une lettre aΣa \in \Sigma dénote {a}\{a\}.

Les opérations, appliquées à des expressions déjà construites :

ÉcritureNomPriorité
EFE \mid Funion (alternative)la plus faible
EFE \cdot F ou EFEFconcaténationintermédiaire
EE^*étoile de Kleenela plus forte

La priorité se lit comme en arithmétique : l'étoile est le « puissance », la concaténation le « produit », l'union la « somme ». Ainsi abcab^* \mid c se lit (a(b))c\big(a(b^*)\big) \mid c, et les parenthèses servent à forcer un autre regroupement, comme dans (ab)(a \mid b)^*.

Ce sont exactement les trois opérations sur les langages du chapitre 1. Une expression régulière n'est rien d'autre qu'une notation compacte pour les combiner — pas un mécanisme nouveau.

Sémantique : le langage dénoté

À chaque expression EE correspond un langage L(E)L(E), défini en suivant sa construction :

L(a)={a},L(EF)=L(E)L(F),L(EF)=L(E)L(F),L(E)=L(E).L(a) = \{a\}, \quad L(E \mid F) = L(E) \cup L(F), \quad L(E \cdot F) = L(E) \cdot L(F), \quad L(E^*) = L(E)^*.

Lisons quelques exemples sur Σ={a,b}\Sigma = \{a, b\} — cette lecture est le savoir-faire du chapitre :

  • abab dénote {ab}\{ab\} — un seul mot ;
  • aba \mid b dénote {a,b}\{a, b\} — « aa ou bb » ;
  • aa^* dénote {ε,a,aa,aaa,}\{\varepsilon, a, aa, aaa, \dots\} — noter que ε\varepsilon en fait partie ;
  • (ab)(a \mid b)^* dénote tous les mots sur {a,b}\{a, b\}, c'est-à-dire Σ\Sigma^* ;
  • (ab)ab(a \mid b)^* ab dénote « tous les mots qui se terminent par abab » — le langage même des chapitres 3 et 4, décrit ici en cinq symboles.

Ce dernier exemple mérite qu'on s'y arrête : ce que l'AFD du chapitre 3 exprimait par trois états et six transitions tient en une courte expression. C'est la force de la notation — mais rappelez-vous qu'elle décrit exactement le même langage, ni plus ni moins.

Deux pièges classiques, tous deux liés au chapitre 1 :

  • L'étoile inclut toujours le mot vide. L(E)L(E^*) contient ε\varepsilon même si εL(E)\varepsilon \notin L(E) — la concaténation de « zéro copie ». Oublier ce cas fausse la moitié des exercices.
  • \emptyset et ε\varepsilon ne sont pas la même chose. ={ε}\emptyset^* = \{\varepsilon\}, tandis que \emptyset tout court dénote le langage vide. C'est la distinction du chapitre 1, qui revient à l'identique.

Quiz · 1 question

Que dénote l'expression régulière (a|b)*a sur Σ = {a, b} ?

  • Les mots qui contiennent au moins un aprésence d'un a
  • Les mots qui se terminent par la lettre adernière lettre
  • Les mots formés uniquement de aque des a

Réponse : (a|b)* dénote n'importe quelle suite de a et de b — c'est-à-dire Σ* tout entier, un préfixe quelconque. La concaténation avec le a final impose que ce préfixe quelconque soit suivi d'un a : le mot se TERMINE donc par a. « Contenir au moins un a » se noterait (a|b)*a(a|b)* ; « uniquement des a » se noterait a*. La position du a dans l'expression fixe sa position dans le mot.

Le théorème de Kleene

Voici le résultat qui referme le bloc et justifie tout ce qui précède :

Théorème de Kleene. Un langage est décrit par une expression régulière si et seulement si il est reconnu par un automate fini.

Les trois descriptions — AFD, AFN, expression régulière — sont donc équivalentes : elles définissent une seule et même classe, les langages réguliers. C'est pourquoi on peut passer librement de l'une à l'autre selon ce qui est commode : écrire une expression régulière (concis), la transformer en automate pour l'exécuter efficacement (rapide), raisonner sur l'automate pour prouver une propriété.

La preuve se fait dans les deux sens, et chaque sens est une construction algorithmique — pas seulement une existence. C'est ce qui la rend utile : elle donne les recettes qu'emploient les outils réels.

D'une expression à un automate : Thompson

Le sens « expression → automate » emploie la construction de Thompson. Elle procède inductivement, exactement selon la structure de l'expression, en assemblant de petits AFN avec des ε-transitions (chapitre 4) — dont c'est ici l'usage principal :

  • une lettre aa : deux états reliés par une flèche aa ;
  • une union EFE \mid F : un nouvel état initial pointe par ε\varepsilon vers les automates de EE et de FF (les deux branches de l'alternative) ;
  • une concaténation EFE \cdot F : la sortie de l'automate de EE est branchée par ε\varepsilon sur l'entrée de celui de FF ;
  • une étoile EE^* : on ajoute des ε-transitions pour pouvoir répéter l'automate de EE ou le sauter (le cas « zéro copie »).

Le résultat est un AFN avec ε-transitions, qu'on rend déterministe par la construction du chapitre 4 si l'on veut l'exécuter. C'est très exactement la chaîne qu'un moteur d'expressions régulières parcourt en interne.

D'un automate à une expression : élimination d'états

Le sens inverse, « automate → expression », emploie l'élimination d'états. On retire les états un par un ; à chaque retrait, on reporte l'information perdue sur les transitions restantes, en les étiquetant non plus par des lettres mais par des expressions régulières. À la fin, il ne reste qu'une transition de l'entrée à la sortie, étiquetée par l'expression régulière cherchée.

Vous n'avez pas à mémoriser les détails de ces deux constructions en L2 ; vous devez retenir qu'elles existent, qu'elles sont effectives (on peut les programmer), et qu'ensemble elles prouvent le théorème de Kleene par double construction.

Quiz · 1 question

Un ingénieur écrit une expression régulière pour valider un format de saisie, puis a besoin de la vérifier des millions de fois par seconde sur un flux. Que permet le théorème de Kleene dans ce contexte ?

  • Rien : les expressions régulières et les automates sont des mondes séparésmondes séparés
  • Transformer l'expression en automate (Thompson, puis déterminisation) pour obtenir une reconnaissance rapide en une seule passeconversion vers un automate exécutable
  • Prouver que l'expression est fausse si elle décrit un langage infinilangage infini

Réponse : Le théorème de Kleene garantit qu'à toute expression régulière correspond un automate reconnaissant le même langage, et la construction de Thompson suivie de la déterminisation (chapitre 4) le produit effectivement. On écrit donc l'expression pour sa concision, puis on la compile en AFD pour l'exécuter en une seule passe, en temps proportionnel à la longueur de l'entrée. C'est exactement ce que fait un analyseur lexical (chapitre 9). Un langage infini n'a rien d'anormal — a* en décrit déjà un.

À vous

L'exercice donne aux expressions régulières leur sens en code. Vous implémentez la sémantique — chaque opérateur (union, concaténation, étoile) est l'opération correspondante sur les langages du chapitre 1 — puis vous calculez le langage de (ab)ab(a \mid b)^* ab et vérifiez quels mots il contient.

L'étoile vous forcera à traiter le cas « zéro copie » : oublier le mot vide initial rend l'opération fausse. Et vous devrez borner par une longueur, parce que le langage est infini — une petite expression décrit un langage sans fin, tout comme un automate avec ses boucles. C'est là, concrètement, le théorème de Kleene.

Exercice de code

Implémentez la sémantique d'une expression régulière : chaque opérateur (union, concaténation, étoile) est une opération sur les langages du chapitre 1. Calculez le langage de (a|b)*ab et vérifiez quels mots il contient — l'étoile vous rappellera le piège du « zéro copie ».

Point de départ

// Une expression régulière décrit un LANGAGE. Ici on la représente par un
// arbre, et on calcule le langage qu'elle dénote — jusqu'à une longueur max,
// car l'étoile produit un ensemble infini.
//
// Opérateurs (rappel chapitre 1) :
//   lettre  -> { "a" }              vide -> {}          epsilon -> { "" }
//   union(E,F)  = L(E) ∪ L(F)
//   concat(E,F) = L(E) · L(F)   (tous les u·v, u∈L(E), v∈L(F))
//   etoile(E)   = L(E)*         (zéro, un ou plusieurs mots de L(E))

// Constructeurs d'arbre :
const lettre = (c) => ({ type: "lettre", c });
const epsilon = { type: "epsilon" };
const vide = { type: "vide" };
const union  = (g, d) => ({ type: "union", g, d });
const concat = (g, d) => ({ type: "concat", g, d });
const etoile = (e) => ({ type: "etoile", e });

// ── À VOUS : la sémantique ──────────────────────────────────────────────────
// langage(expr, max) rend l'ENSEMBLE (Set) des mots de longueur <= max décrits
// par expr.
function langage(expr, max) {
  switch (expr.type) {
    case "vide":    return new Set();          // aucun mot
    case "epsilon": return new Set([""]);      // le seul mot vide
    case "lettre":  return new Set([expr.c]);  // un seul mot d'une lettre
    case "union": {
      // à compléter : réunir les deux langages
      return new Set();
    }
    case "concat": {
      // à compléter : tous les u+v (u du gauche, v du droit), longueur <= max
      return new Set();
    }
    case "etoile": {
      // à compléter : ε, puis L, puis L·L, ... tant que la longueur <= max
      return new Set();
    }
  }
}

// ── L'expression (a|b)*ab  :  « se termine par ab » ─────────────────────────
const expr = concat(etoile(union(lettre("a"), lettre("b"))), concat(lettre("a"), lettre("b")));

const mots = [...langage(expr, 3)].sort((x, y) => x.length - y.length || x.localeCompare(y));
console.log("(a|b)*ab, mots de longueur <= 3 :");
console.log("  [" + mots.join(", ") + "]");
console.log("  ab present ?", mots.includes("ab"));   // attendu true
console.log("  aab present ?", mots.includes("aab")); // attendu true
console.log("  ba present ?", mots.includes("ba"));   // attendu false

Solution

function langage(expr, max) {
  switch (expr.type) {
    case "vide":    return new Set();
    case "epsilon": return new Set([""]);
    case "lettre":  return new Set([expr.c]);
    case "union": {
      const g = langage(expr.g, max), d = langage(expr.d, max);
      return new Set([...g, ...d]);              // ∪
    }
    case "concat": {
      const g = langage(expr.g, max), d = langage(expr.d, max);
      const r = new Set();
      for (const u of g) for (const v of d)
        if (u.length + v.length <= max) r.add(u + v);   // ·, borné
      return r;
    }
    case "etoile": {
      // ε toujours (zéro copie), puis on ajoute L·(ce qu'on a déjà) tant
      // que ça grandit et que la longueur reste <= max.
      const base = langage(expr.e, max);
      const r = new Set([""]);
      let frontiere = new Set([""]);
      while (frontiere.size > 0) {
        const suivante = new Set();
        for (const u of frontiere) for (const v of base) {
          const w = u + v;
          if (w.length <= max && !r.has(w)) { r.add(w); suivante.add(w); }
        }
        frontiere = suivante;
      }
      return r;
    }
  }
}
// (a|b)*ab, longueur <= 3 : ab, aab, bab, abab n'apparaît pas (longueur 4).
// → [ab, aab, bab]. « ab » et « aab » présents, « ba » absent. ✓

// ── Ce que l'exercice enseigne ──────────────────────────────────────────────
//
// 1. Une expression régulière n'est PAS de la magie de recherche : c'est une
//    notation pour trois opérations sur les langages du chapitre 1 — union,
//    concaténation, étoile de Kleene. Chaque case du switch EST une de ces
//    opérations. Comprendre une regex = savoir quel langage elle dénote.
//
// 2. L'étoile force à gérer le cas « zéro copie » (ε ∈ L* toujours) — le
//    piège du chapitre 1 ressort ici : oublier le new Set([""]) initial rend
//    l'étoile fausse.
//
// 3. On a dû BORNER par une longueur max : le langage de (a|b)*ab est infini.
//    Une expression finie décrit un langage potentiellement infini — c'est
//    précisément ce que fait aussi un automate avec ses boucles, et le
//    théorème de Kleene dit que ce sont les MÊMES langages.

Ce que la suite en fait

Le bloc II tient debout : trois descriptions équivalentes des langages réguliers, reliées par des constructions effectives. Une question reste ouverte, et c'est la plus importante — tous les langages sont-ils réguliers ?

Le chapitre 6 y répond, et la réponse est non. Il donne d'abord l'outil pour ranger un automate (la minimisation), recense ce que la classe régulière sait faire (les propriétés de clôture), puis l'outil qui prouve qu'un langage échappe aux automates : le lemme de pompage, second point qui coince de l'année, avec son exemple canonique anbna^n b^n — celui-là même que l'exercice du chapitre 1 avait déjà mis de côté.

À retenir

Flashcards · 4 cartes

De quelles briques et de quelles opérations une expression régulière est-elle faite ?
Briques : ∅ (langage vide), ε (le mot vide), et chaque lettre a. Opérations : union E|F (∪), concaténation E·F (·), étoile E* (étoile de Kleene) — par priorité croissante étoile > concaténation > union. Ce sont EXACTEMENT les trois opérations sur les langages du chapitre 1 ; une expression régulière n'est qu'une notation compacte pour les combiner.
Que dit le théorème de Kleene ?
Un langage est décrit par une expression régulière si et seulement s'il est reconnu par un automate fini. Les trois descriptions — AFD, AFN, expression régulière — sont donc équivalentes et définissent une seule classe : les langages réguliers. On passe de l'une à l'autre par des constructions effectives (Thompson dans un sens, élimination d'états dans l'autre).
Comment passe-t-on d'une expression régulière à un automate, et inversement ?
Expression → automate : la construction de Thompson, inductive sur la structure de l'expression, assemble de petits AFN à l'aide d'ε-transitions (une branche par |, un branchement pour ·, une boucle sautable pour *). Automate → expression : l'élimination d'états retire les états un à un en reportant l'information sur des transitions étiquetées par des expressions régulières. Les deux sont effectives — on peut les programmer.
Quel piège du chapitre 1 ressort dans l'interprétation d'une étoile ?
E* dénote toujours un langage contenant ε (la « zéro copie »), même si ε ∉ L(E). Oublier ce cas rend l'interprétation fausse. Corollaire souvent piégeant : ∅* = {ε}, alors que ∅ seul est le langage vide. C'est la distinction ε / ∅ du chapitre 1, qui revient à l'identique.

Exercices d'entraînement

Exercice 1

Écrire des expressions régulières

Sur Σ={a,b}\Sigma = \{a, b\}, donner une expression régulière pour chacun de ces langages :

  1. les mots contenant au moins un a ;
  2. les mots de longueur paire ;
  3. les mots ne contenant pas le facteur aa.

Correction

  1. (ab)a(ab)(a \mid b)^* \, a \, (a \mid b)^* : un a quelque part, entouré de n'importe quoi.
  2. ((ab)(ab))\big((a \mid b)(a \mid b)\big)^* : on répète des blocs de deux lettres, ce qui donne exactement les longueurs paires (dont ε\varepsilon).
  3. (bab)(aε)(b \mid ab)^* \, (a \mid \varepsilon) : tout a est immédiatement suivi d'un b (bloc ab), sauf éventuellement un a final isolé. Aucun aa ne peut ainsi apparaître.

Exercice 2

Décrire un langage

Décrire en français le langage dénoté par chacune de ces expressions, sur Σ={a,b}\Sigma = \{a, b\} :

  1. aba^* b^* ;
  2. (ab)(ab)^* ;
  3. (ab)aa(ab)(a \mid b)^* \, aa \, (a \mid b)^*.

Correction

  1. Les mots formés d'une suite de a suivie d'une suite de b (chacune éventuellement vide) : autrement dit, aucun a n'apparaît après un b. Attention, ce n'est pas {anbn}\{a^n b^n\} : les nombres de a et de b sont indépendants.
  2. Les mots obtenus en répétant le bloc ab : ε\varepsilon, ab, abab, ababab, …
  3. Les mots contenant le facteur aa (deux a consécutifs quelque part).

Exercice 3

Du théorème de Kleene

Donner une expression régulière décrivant le langage reconnu par l'AFD « mots se terminant par a » (croisé au chapitre 3). Que garantit le théorème de Kleene à propos d'une telle conversion ?

Correction

Un tel mot est une suite quelconque de lettres, suivie d'un a final : (ab)a.(a \mid b)^* \, a.

Le théorème de Kleene garantit que cette conversion est toujours possible, dans les deux sens : à tout automate fini correspond une expression régulière du même langage (par élimination d'états), et réciproquement (par la construction de Thompson). Automates et expressions régulières décrivent exactement la même classe — les langages réguliers.

Chapitre 4 · 5 h

Propriétés et limites

Minimisation d'automate, propriétés de clôture, lemme de pompage et preuves de non-régularité — le cas a^n b^n, le second point qui coince.

Le bloc II touche à sa fin avec la question la plus profonde du cours. Jusqu'ici, chaque langage rencontré était régulier — on lui trouvait un automate. Mais est-ce toujours le cas ? Tous les langages sont-ils réguliers ?

La réponse est non, et savoir le prouver est le second point qui coince de l'année. Ce chapitre y mène en trois temps : d'abord un outil pour obtenir l'automate le plus économe (la minimisation), ensuite l'inventaire de ce que la classe régulière sait faire (les propriétés de clôture), enfin l'outil qui trace sa frontière (le lemme de pompage), avec l'exemple que le chapitre 1 avait mystérieusement mis de côté : anbna^n b^n.

La minimisation

Pour un langage régulier donné, il existe une infinité d'automates qui le reconnaissent — on peut toujours ajouter des états inutiles. Mais il en existe un seul de taille minimale, à renommage près : l'automate minimal. La minimisation est la procédure qui le trouve.

L'idée repose sur la relation d'équivalence du chapitre 2. Deux états sont indistinguables si, depuis l'un ou l'autre, exactement les mêmes mots mènent à l'acceptation — les fusionner ne change rien au langage reconnu. La minimisation regroupe les états indistinguables en classes d'équivalence, chaque classe devenant un état unique du minimal.

Deux usages justifient ce travail :

  • Comparer deux langages. Deux automates reconnaissent le même langage si et seulement si leurs minimaux sont identiques (à renommage près). C'est le test d'équivalence, sinon délicat.
  • Optimiser. Un analyseur lexical avec le moins d'états possible est plus rapide et plus léger — ce qui compte au chapitre 9.

Retenez surtout le résultat d'existence : à chaque langage régulier correspond un automate minimal unique, qui en est en quelque sorte l'empreinte.

Les propriétés de clôture

Une classe de langages est close par une opération si, en l'appliquant à des langages de la classe, on reste dans la classe. Les langages réguliers sont remarquablement stables :

OpérationLes réguliers sont-ils clos ?Comment on le voit
Union L1L2L_1 \cup L_2ouiun AFN qui lance les deux automates en parallèle
Concaténation L1L2L_1 \cdot L_2ouibrancher le premier sur le second (ε-transition)
Étoile LL^*ouiboucler l'automate sur lui-même
Complément L\overline{L}ouiéchanger acceptants/non-acceptants (automate complet)
Intersection L1L2L_1 \cap L_2ouiautomate produit, ou via De Morgan

Ces clôtures ne sont pas de simples curiosités : ce sont des outils de preuve. Elles permettent de construire de nouveaux langages réguliers sans repartir de zéro — et, retournées, de prouver la non-régularité. Si L1L2L_1 \cap L_2 n'était pas régulier alors que L2L_2 l'est, on en déduirait que L1L_1 ne l'est pas ; c'est une technique de repli quand le lemme de pompage est malcommode à appliquer directement.

Le complément mérite un rappel : sa clôture exige un automate complet (chapitre 3). C'est là que le soin apporté à l'état puits porte ses fruits.

Quiz · 1 question

On sait que L₁ ∩ L₂ n'est pas régulier, et que L₂ est régulier. Que peut-on conclure sur L₁ ?

  • L₁ est régulier, puisque L₂ l'estrégulier
  • L₁ n'est pas régulier : sinon L₁ ∩ L₂ le serait, par clôture de l'intersectionclôture par intersection, à rebours
  • On ne peut rien conclure sans connaître L₁ explicitementindécidable

Réponse : Les langages réguliers sont clos par intersection : si L₁ et L₂ étaient tous deux réguliers, L₁ ∩ L₂ le serait aussi. Or on sait que L₁ ∩ L₂ n'est PAS régulier, et que L₂ l'est. La seule possibilité est donc que L₁ ne soit pas régulier. C'est l'usage « retourné » des propriétés de clôture : elles servent autant à prouver la non-régularité qu'à construire des langages réguliers. C'est souvent plus simple que d'appliquer le lemme de pompage directement.

Le lemme de pompage

Voici l'outil central du chapitre, et l'un des plus subtils de l'année. Il repose sur une intuition simple qu'il faut avoir en tête avant la formule :

Un automate fini a un nombre fini d'états, donc une mémoire bornée. S'il lit un mot plus long que son nombre d'états, il repasse forcément par un même état — et la portion de mot lue entre ces deux passages forme une boucle que l'on peut répéter à volonté.

C'est le principe des tiroirs : plus de lettres que d'états, donc un état revisité.

L'animation rend cette boucle visible. L'automate ci-dessous compte les a modulo 3 ; sur le mot aaa, son chemin r0 → r1 → r2 → r0 revient à son point de départ. Ce cycle est exactement le facteur yy que le lemme « pompe » : puisqu'il ramène au même état, on peut le répéter (aaaaaa) ou le retirer (ε) sans jamais quitter le langage. Retenez l'image — c'est tout le mécanisme du lemme.

Animation · 5 étapes

Un cycle d'automate : la boucle que le pompage exploite

  1. État initialL'automate démarre dans l'état r0. Mot à lire : « aaa ».
  2. Lecture de « a » (1/3)Depuis r0, la lettre « a » mène à r1.
  3. Lecture de « a » (2/3)Depuis r1, la lettre « a » mène à r2.
  4. Lecture de « a » (3/3)Depuis r2, la lettre « a » mène à r0.
  5. Mot acceptéTout le mot est lu et l'état d'arrivée r0 est acceptant : le mot « aaa » appartient au langage.

Formellement :

Lemme de pompage. Si LL est régulier, alors il existe une longueur pp (la « longueur de pompage ») telle que tout mot wLw \in L avec wp|w| \geq p peut s'écrire w=xyzw = xyz avec :

  • xyp|xy| \leq p,
  • y1|y| \geq 1 (le facteur yy n'est pas vide),
  • et pour tout i0i \geq 0, le mot xyizxy^iz appartient encore à LL.

Autrement dit, la boucle yy peut être répétée (i2i \geq 2), supprimée (i=0i = 0), ou laissée telle quelle (i=1i = 1), sans jamais sortir du langage. « Pomper » yy, c'est jouer sur ce ii.

L'utiliser : un jeu, et un sens de lecture

Le lemme sert dans un seul sens : prouver qu'un langage n'est pas régulier. C'est un raisonnement par l'absurde, et le meilleur moyen de ne pas s'y perdre est de le voir comme un jeu à quatre coups, dont vous devez sortir gagnant :

  1. L'adversaire suppose LL régulier et fournit la longueur pp (vous ne la connaissez pas).
  2. Vous choisissez un mot wLw \in L malin, avec wp|w| \geq p. C'est le coup décisif.
  3. L'adversaire découpe w=xyzw = xyz comme il veut, en respectant xyp|xy| \leq p et y1|y| \geq 1.
  4. Vous exhibez un ii tel que xyizLxy^iz \notin L — contradiction.

Si vous gagnez quel que soit le découpage, LL n'est pas régulier. L'ordre des quantificateurs est tout : « pour tout pp, il existe ww, pour tout découpage, il existe ii ». Vous contrôlez ww et ii ; l'adversaire contrôle pp et le découpage.

Le cas anbna^n b^n

Appliquons le jeu à L={anbnn0}L = \{a^n b^n \mid n \geq 0\} — le langage « autant de aa que de bb, les aa avant les bb » que le chapitre 1 avait déjà isolé.

Le choix du mot est tout : on joue w=apbpw = a^p b^p. Pourquoi celui-là ? Parce que la contrainte xyp|xy| \leq p oblige le bloc xyxy à tomber entièrement dans la zone des aa — le mot commence par pp lettres aa. Le facteur yy ne contient donc que des aa, et il en contient au moins un.

Il suffit alors de pomper : prendre i=2i = 2 donne ap+kbpa^{p+k} b^p avec k=y1k = |y| \geq 1 — plus de aa que de bb, donc hors de LL. Contradiction. Comme ce raisonnement vaut pour n'importe quel découpage, LL n'est pas régulier.

La leçon générale dépasse cet exemple : un automate fini ne sait pas compter jusqu'à un nombre arbitraire. Reconnaître anbna^n b^n exigerait de retenir nn, qui n'est pas borné, alors que l'automate n'a qu'un nombre fini d'états. Le lemme de pompage est la traduction rigoureuse de cette limite — et c'est justement pour compter qu'on introduira, au chapitre 8, une mémoire supplémentaire : la pile.

Quiz · 1 question

Pour prouver que L = {aⁿbⁿ} n'est pas régulier avec le lemme de pompage, pourquoi choisit-on le mot w = aᵖbᵖ plutôt que, par exemple, w = (ab)ᵖ ?

  • Parce que aᵖbᵖ est plus long, ce qui facilite le découpagelongueur
  • Parce que la contrainte |xy| ≤ p force alors y à ne contenir que des a, si bien que pomper déséquilibre le compte a/by coincé dans les a
  • Parce que (ab)ᵖ n'appartient pas au langage Lappartenance

Réponse : Le choix du mot est le cœur de la preuve. Avec w = aᵖbᵖ, la condition |xy| ≤ p oblige le facteur xy à rester dans les p premières lettres, qui sont toutes des a : y n'est donc fait que de a. Pomper y ajoute (ou retire) des a sans toucher aux b, ce qui rompt l'égalité du nombre de a et de b — le mot sort de L. Avec un mauvais choix comme (ab)ᵖ, le découpage pourrait pomper un bloc « ab » entier et rester dans un langage équilibré : l'adversaire s'en tirerait. (ab)ᵖ appartient d'ailleurs à Σ*, mais pas à L de toute façon.)

À vous

L'exercice transforme le lemme en jeu jouable. L'adversaire annonce pp et essaie tous les découpages possibles ; vous devez fournir le mot w=apbpw = a^p b^p et le facteur pompé ii qui casse, et gagner contre chaque découpage.

C'est en jouant qu'on comprend pourquoi le choix du mot est décisif : c'est lui qui coince le facteur yy dans les aa. Une fois cette mécanique vue tourner, le lemme de pompage cesse d'être un enchaînement de quantificateurs opaque et devient une stratégie que vous savez dérouler.

Exercice de code

Prouvez que a^n b^n n'est pas régulier, en jouant contre un adversaire : il annonce une longueur p et découpe votre mot, vous choisissez le mot puis le facteur pompé qui sort du langage. Le secret est le choix du mot a^p b^p — il coince le facteur y dans les a.

Point de départ

// On veut PROUVER que L = { a^n b^n | n >= 0 } n'est pas régulier.
//   L = { ε, ab, aabb, aaabbb, ... }  (autant de a que de b, a avant b)
//
// Le lemme de pompage, vu comme un JEU en 4 coups :
//   1. L'adversaire (qui affirme « L est régulier ») annonce une longueur p.
//   2. VOUS choisissez un mot w de L, avec |w| >= p.
//   3. L'adversaire découpe w = x y z avec |xy| <= p et |y| >= 1 (y non vide).
//   4. VOUS choisissez un entier i tel que x y^i z ne soit PAS dans L.
//   Si vous gagnez QUEL QUE SOIT le découpage, L n'est pas régulier.

// Appartenance à L : autant de a que de b, tous les a avant tous les b.
function estDansL(mot) {
  const m = mot.match(/^(a*)(b*)$/);          // a...a puis b...b
  if (!m) return false;                        // un b avant un a -> non
  return m[1].length === m[2].length;          // même nombre
}

// ── À VOUS (2) : choisir le bon mot ─────────────────────────────────────────
// Pour une longueur p donnée, quel mot de L rend l'adversaire perdant ?
// Indice : il faut que |xy| <= p FORCE y à ne contenir que des a.
function choisirMot(p) {
  return ""; // à compléter, en fonction de p
}

// ── À VOUS (4) : choisir i qui casse ────────────────────────────────────────
// Étant donné un découpage x, y, z (avec y = que des a, forcé par l'étape 2),
// rendez un i tel que x + y.repeat(i) + z ne soit PAS dans L.
function choisirI(x, y, z) {
  return 1; // à corriger (i = 1 redonne w, qui EST dans L : mauvais choix)
}

// ── Le jeu : l'adversaire essaie TOUS les découpages valides ────────────────
function jouer(p) {
  const w = choisirMot(p);
  if (w.length < p || !estDansL(w)) { console.log("Mot invalide."); return; }
  console.log("p = " + p + ", vous jouez w = " + w + " (dans L, |w| >= p)");
  let vousGagnezToujours = true;
  for (let coupe = 1; coupe <= p; coupe++) {       // |xy| <= p
    for (let ly = 1; coupe - ly >= 0 && ly <= coupe; ly++) {
      const x = w.slice(0, coupe - ly);
      const y = w.slice(coupe - ly, coupe);
      const z = w.slice(coupe);
      if (y.length < 1) continue;
      const i = choisirI(x, y, z);
      const pompe = x + y.repeat(i) + z;
      if (estDansL(pompe)) {
        console.log("  PERDU sur x=" + x + " y=" + y + " z=" + z + " : " + pompe + " est dans L");
        vousGagnezToujours = false;
      }
    }
  }
  console.log(vousGagnezToujours ? "GAGNÉ pour tout découpage -> L n'est pas régulier." : "à revoir");
}

jouer(3);
jouer(5);

Solution

function choisirMot(p) {
  // Le mot a^p b^p : sa longueur 2p >= p, et surtout comme |xy| <= p, le
  // bloc xy tombe ENTIÈREMENT dans les a. y ne contient donc que des a.
  return "a".repeat(p) + "b".repeat(p);
}

function choisirI(x, y, z) {
  // y = k a's (k >= 1). Pomper à i = 2 ajoute k a's SANS toucher aux b :
  // on obtient a^(p+k) b^p, qui a plus de a que de b -> hors de L.
  // (i = 0 marche aussi : a^(p-k) b^p, moins de a que de b.)
  return 2;
}

// jouer(3) et jouer(5) : GAGNÉ pour tout découpage -> L n'est pas régulier.
//
// ── Pourquoi ça marche, et pourquoi c'est LE raisonnement à retenir ─────────
//
// 1. Le choix du mot est le cœur de la preuve. En jouant a^p b^p, on exploite
//    la contrainte |xy| <= p : le facteur pompé y est COINCÉ dans la zone des
//    a. Pomper y déséquilibre alors le compte a/b, ce que L interdit. Un
//    mauvais choix de mot (ex. (ab)^p) laisserait l'adversaire s'en tirer.
//
// 2. C'est vous qui choisissez w et i ; l'adversaire choisit p et le découpage.
//    Vous devez gagner CONTRE TOUS ses choix — d'où la boucle qui teste tous
//    les découpages. La preuve est un « pour tout p, il existe w, pour tout
//    découpage, il existe i ».
//
// 3. L'intuition profonde : un automate fini a une MÉMOIRE BORNÉE (p états).
//    Pour reconnaître a^n b^n, il faudrait COMPTER les a jusqu'à un n
//    arbitraire, donc une mémoire non bornée. Le lemme de pompage est la
//    formalisation de « les automates finis ne savent pas compter ».
//
// 4. Portée : le lemme sert UNIQUEMENT à prouver qu'un langage N'EST PAS
//    régulier. Le vérifier ne prouve JAMAIS qu'un langage EST régulier — pour
//    ça, on exhibe un automate ou une expression régulière.

Ce que la suite en fait

Le bloc II est complet : vous savez décrire les langages réguliers de trois façons équivalentes, les optimiser, connaître leurs clôtures, et surtout prouver qu'un langage leur échappe. La frontière est tracée.

Le bloc III la franchit. Puisque les automates finis ne savent pas compter, on leur ajoute une mémoire : une pile. Le chapitre 7 introduit d'abord les grammaires hors contexte, une manière de engendrer les langages plutôt que de les reconnaître, capable justement de décrire anbna^n b^n ; le chapitre 8 leur associe les automates à pile, et donnera un lemme de pompage algébrique qui tracera, un cran plus haut, la frontière suivante.

À retenir

Flashcards · 4 cartes

Qu'est-ce que l'automate minimal, et à quoi sert la minimisation ?
Pour un langage régulier, c'est l'unique automate de taille minimale (à renommage près) qui le reconnaît. La minimisation regroupe les états INDISTINGUABLES (mêmes mots menant à l'acceptation) en classes d'équivalence. Usages : comparer deux langages (mêmes minimaux ⟺ même langage) et optimiser un automate (analyseur lexical plus rapide).
Par quelles opérations les langages réguliers sont-ils clos, et à quoi servent ces clôtures ?
Union, concaténation, étoile, complément (automate complet requis) et intersection. Ces clôtures servent à construire de nouveaux langages réguliers ET, retournées, à prouver la non-régularité : si L₁∩L₂ n'est pas régulier et L₂ l'est, alors L₁ ne l'est pas. C'est souvent plus simple que le lemme de pompage direct.
Que dit le lemme de pompage, et dans quel sens s'utilise-t-il ?
Si L est régulier, il existe p tel que tout mot w ∈ L avec |w| ≥ p s'écrit w = xyz avec |xy| ≤ p, |y| ≥ 1, et xyⁱz ∈ L pour tout i ≥ 0 (la boucle y se répète, se supprime ou reste). Il sert UNIQUEMENT à prouver qu'un langage N'EST PAS régulier (par l'absurde). Il ne prouve jamais la régularité — pour ça, on exhibe un automate ou une expression régulière.
Comment prouve-t-on que aⁿbⁿ n'est pas régulier, et quelle intuition cela illustre-t-il ?
On joue w = aᵖbᵖ : |xy| ≤ p force y à n'être que des a ; pomper (i = 2) donne a^(p+k)bᵖ, plus de a que de b, hors de L — contradiction, pour tout découpage. Intuition : un automate fini a une mémoire bornée et ne sait pas COMPTER jusqu'à un n arbitraire. Reconnaître aⁿbⁿ exige de retenir n ; d'où la pile du chapitre 8.

Exercices d'entraînement

Exercice 1

Non-régularité par pompage

Montrer, à l'aide du lemme de pompage, que le langage L={anbmn>m0}L = \{a^n b^m \mid n > m \geq 0\} n'est pas régulier.

Correction

Supposons LL régulier, de longueur de pompage pp. Choisissons le mot w=ap+1bpw = a^{p+1} b^{p}, qui appartient à LL (il y a bien plus de a que de b) et vérifie wp|w| \geq p.

Le lemme décompose w=xyzw = xyz avec xyp|xy| \leq p et y1|y| \geq 1. Comme xyp|xy| \leq p, le bloc xyxy tombe entièrement dans les p+1p+1 premières lettres, qui sont des a : donc y=aky = a^{k} avec k1k \geq 1.

Pompons vers le bas (i=0i = 0) : on retire yy, d'où xz=ap+1kbpxz = a^{p+1-k} b^{p}. Puisque k1k \geq 1, on a p+1kpp+1-k \leq p, c'est-à-dire un nombre de a au plus égal au nombre de b. La condition n>mn > m est violée : xzLxz \notin L. Contradiction. Donc LL n'est pas régulier.

Exercice 2

Non-régularité par clôture

Soit LL l'ensemble des mots sur {a,b}\{a, b\} comptant autant de a que de b. En utilisant une propriété de clôture, montrer que LL n'est pas régulier (on admet que {anbnn0}\{a^n b^n \mid n \geq 0\} ne l'est pas).

Correction

Supposons LL régulier. Le langage aba^* b^* est régulier (une expression régulière le décrit). Les langages réguliers étant clos par intersection, LabL \cap a^* b^* serait régulier.

Or LabL \cap a^* b^* est exactement {anbnn0}\{a^n b^n \mid n \geq 0\} : les mots à la fois « autant de a que de b » et « des a puis des b ». Ce langage n'est pas régulier — contradiction. Donc LL n'est pas régulier.

Cette technique évite d'appliquer le lemme de pompage directement : on se ramène à un langage dont on connaît déjà la non-régularité.

Exercice 3

Minimisation

Un AFD à 44 états A (initial), B, C, D reconnaît « les mots contenant au moins un a ». Ses transitions sont : δ(A,a)=C\delta(A,a)=C, δ(A,b)=B\delta(A,b)=B, δ(B,a)=D\delta(B,a)=D, δ(B,b)=A\delta(B,b)=A, et depuis C ou D, toute lettre mène à C ou D (états acceptants, absorbants). Montrer que son automate minimal a 22 états.

Correction

On regroupe les états indistinguables (mêmes mots menant à l'acceptation).

  • C et D sont tous deux acceptants et absorbants : depuis l'un ou l'autre, tout mot est accepté. Ils sont indistinguables → une seule classe, « un a a déjà été lu ».
  • A et B sont non acceptants, et depuis chacun, un mot est accepté ssi il contient un a. Ils sont indistinguables → une seule classe, « aucun a lu pour l'instant ».

L'automate minimal a donc 22 états : « pas encore de a » (initial) et « au moins un a » (acceptant), avec δ(pas de a,a)=au moins un a\delta(\text{pas de }a, a) = \text{au moins un }a, les autres transitions bouclant sur la classe. Les états B et D étaient des doublons.