Adloun

Automates finis

Cours complet · OCaml (option informatique), chapitre 18 · prépas MPSI et MP, option informatique

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>18.1 Introduction et motivation

Comment décider, mécaniquement, si un mot appartient à un langage — les mots se terminant par a, ceux ayant un nombre pair de a, ceux contenant le motif ab ? L'outil fondamental est l'automate fini : une machine à un nombre fini d'états, qui lit le mot lettre par lettre et change d'état selon des transitions. Le mot est accepté si, une fois lu, la machine se trouve dans un état acceptant.

Ce chapitre, en ouverture du cours, est une belle synthèse : un automate se modélise par un enregistrement (chapitre 7) contenant un tableau de transitions (chapitre 5) ; la reconnaissance lit une chaîne (chapitre 6) ; tester si le langage est vide est une question d'accessibilité dans un graphe (chapitre 15) ; et compter les mots acceptés relève de la programmation dynamique (chapitre 13).

18.2 Représenter un automate

On considère un automate fini déterministe (AFD) complet : un état initial, des états acceptants, et pour chaque état et chaque lettre, exactement une transition. Les états sont numérotés 0..n-1, les lettres codées par des indices 0..k-1.


type automate = {
  nb_etats : int;
  initial : int;
  acceptants : bool array;        (* acceptants.(e) = e est acceptant *)
  delta : int array array;        (* delta.(etat).(lettre) = état suivant *)
}
Exemple 18.1« Se terminer par `a` » sur l'alphabet

Deux états : 0 (la dernière lettre lue n'est pas a, ou rien lu) et 1 (la dernière lettre est a). On code a par 0, b par 1.


let termine_par_a = {
  nb_etats = 2;
  initial = 0;
  acceptants = [| false; true |];
  delta = [| [| 1; 0 |];           (* depuis 0 : lire a -> 1, lire b -> 0 *)
             [| 1; 0 |] |];        (* depuis 1 : lire a -> 1, lire b -> 0 *)
}

L'état 1 (acceptant) est atteint exactement quand la dernière lettre lue est a.

18.3 Reconnaître un mot

On part de l'état initial et l'on applique, pour chaque lettre, la transition correspondante ; le mot est accepté si l'état final est acceptant. La fonction symbole traduit un caractère en indice de lettre (elle dépend de l'alphabet).


let reconnait a symbole mot =
  let etat = ref a.initial in
  for i = 0 to String.length mot - 1 do
    etat := a.delta.(!etat).(symbole mot.[i])
  done;
  a.acceptants.(!etat)

# let sym c = int_of_char c - int_of_char 'a';;   (* a->0, b->1 *)
# reconnait termine_par_a sym "abba";;
- : bool = false
# reconnait termine_par_a sym "abba" ;;
# reconnait termine_par_a sym "baba";;
- : bool = false
# reconnait termine_par_a sym "bba";;
- : bool = true
iRemarque

La reconnaissance est linéaire en la longueur du mot, : une seule passe, une transition par lettre, en temps constant (accès delta.(e).(s)). C'est l'efficacité qui fait la force des automates pour l'analyse lexicale, la recherche de motifs, etc.

18.4 Opérations sur les automates

Exemple 18.2Complément

Échanger états acceptants et non acceptants reconnaît le langage complémentaire (tous les mots sauf ceux acceptés).


let complement a = { a with acceptants = Array.map not a.acceptants }

La mise à jour fonctionnelle { a with ... } (chapitre 7) produit un nouvel automate ; Array.map not inverse chaque état acceptant.

Exemple 18.3Intersection (automate produit)

Pour reconnaître les mots acceptés par deux automates à la fois, on fait fonctionner les deux en parallèle : un état du produit est un couple (p, q), codé par p * b.nb_etats + q.


let intersection a b =
  let nb_sym = Array.length a.delta.(0) in
  let code p q = p * b.nb_etats + q in
  let n = a.nb_etats * b.nb_etats in
  let delta = Array.make_matrix n nb_sym 0 in
  let acceptants = Array.make n false in
  for p = 0 to a.nb_etats - 1 do
    for q = 0 to b.nb_etats - 1 do
      let e = code p q in
      acceptants.(e) <- a.acceptants.(p) && b.acceptants.(q);
      for s = 0 to nb_sym - 1 do
        delta.(e).(s) <- code a.delta.(p).(s) b.delta.(q).(s)
      done
    done
  done;
  { nb_etats = n; initial = code a.initial b.initial; acceptants; delta }

Un état produit est acceptant si les deux composantes le sont ; la transition fait avancer les deux automates simultanément. C'est la construction par produit.

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>18.5 Exercices résolus

Niveau (application directe du cours)

Exercice 1 : Reconnaissance

Écrire reconnait et l'appliquer pour vérifier que &quot;bba&quot; se termine par a mais pas &quot;bab&quot;.

Démonstration

let reconnait a symbole mot =
  let etat = ref a.initial in
  for i = 0 to String.length mot - 1 do
    etat := a.delta.(!etat).(symbole mot.[i])
  done;
  a.acceptants.(!etat)

let sym c = int_of_char c - int_of_char 'a'
(* reconnait termine_par_a sym "bba" = true ; ... "bab" = false *)

Sur &quot;bba&quot; : états (acceptant) true. Sur &quot;bab&quot; : (non acceptant) false.

Exercice 2 : Se terminer par `a`

Définir l'automate termine_par_a (alphabet ).

Démonstration

let termine_par_a = {
  nb_etats = 2; initial = 0;
  acceptants = [| false; true |];
  delta = [| [| 1; 0 |]; [| 1; 0 |] |];
}

Lire a mène toujours à l'état 1 (acceptant), lire b à l'état 0 : l'état final reflète la dernière lettre.

Exercice 3 : Nombre pair de `a`

Définir pair_de_a : les mots ayant un nombre pair de a.

Démonstration

let pair_de_a = {
  nb_etats = 2; initial = 0;       (* état = parité du nombre de a lus *)
  acceptants = [| true; false |];  (* 0 = pair (acceptant), 1 = impair *)
  delta = [| [| 1; 0 |];           (* depuis pair : a -> impair, b -> pair *)
             [| 0; 1 |] |];        (* depuis impair : a -> pair, b -> impair *)
}

Lire a bascule la parité (états 0 et 1), lire b la laisse. L'état acceptant 0 (pair) inclut le mot vide ( occurrence, pair).

Niveau (raisonnement intermédiaire)

Exercice 4 : Contenir le motif `ab`

Définir un automate reconnaissant les mots qui contiennent ab comme facteur.

Démonstration

let contient_ab = {
  nb_etats = 3; initial = 0;
  (* 0 : rien de prometteur ; 1 : on vient de lire un a ; 2 : ab vu (piège acceptant) *)
  acceptants = [| false; false; true |];
  delta = [| [| 1; 0 |];     (* 0 : a -> 1, b -> 0 *)
             [| 1; 2 |];     (* 1 : a -> 1 (reste prêt), b -> 2 (ab trouvé !) *)
             [| 2; 2 |] |];  (* 2 : on y reste, ab est déjà vu *)
}

L'état 1 mémorise « le dernier caractère est un a » ; y lire un b complète le motif et fait passer à l'état acceptant 2, dont on ne sort plus (état puits acceptant).

Exercice 5 : Langage non vide

Écrire langage_non_vide a : existe-t-il un mot accepté ?

Démonstration

let langage_non_vide a =
  let vu = Array.make a.nb_etats false in
  let nb_sym = Array.length a.delta.(0) in
  let rec visite e =
    if not vu.(e) then begin
      vu.(e) <- true;
      for s = 0 to nb_sym - 1 do visite a.delta.(e).(s) done
    end
  in
  visite a.initial;
  let trouve = ref false in
  for e = 0 to a.nb_etats - 1 do
    if vu.(e) && a.acceptants.(e) then trouve := true
  done;
  !trouve

Le langage est non vide si et seulement si un état acceptant est accessible depuis l'état initial dans le graphe des transitions. C'est un parcours en profondeur (chapitre 15) : l'automate est un graphe orienté.

Exercice 6 : Complément

Écrire complement a et vérifier que complement termine_par_a reconnaît les mots qui ne se terminent pas par a.

Démonstration

let complement a = { a with acceptants = Array.map not a.acceptants }

Sur un AFD complet, inverser les états acceptants suffit : un mot est dans le complément ssi il n'était pas accepté. reconnait (complement termine_par_a) sym &quot;bba&quot; vaut false, et ... &quot;bab&quot; vaut true.

Niveau (approfondissement)

Exercice 7 : Intersection

Écrire intersection a b (automate produit) et l'utiliser pour reconnaître les mots qui se terminent par a et ont un nombre pair de a.

Démonstration

let intersection a b =
  let nb_sym = Array.length a.delta.(0) in
  let code p q = p * b.nb_etats + q in
  let n = a.nb_etats * b.nb_etats in
  let delta = Array.make_matrix n nb_sym 0 in
  let acceptants = Array.make n false in
  for p = 0 to a.nb_etats - 1 do
    for q = 0 to b.nb_etats - 1 do
      let e = code p q in
      acceptants.(e) <- a.acceptants.(p) && b.acceptants.(q);
      for s = 0 to nb_sym - 1 do
        delta.(e).(s) <- code a.delta.(p).(s) b.delta.(q).(s)
      done
    done
  done;
  { nb_etats = n; initial = code a.initial b.initial; acceptants; delta }

let les_deux = intersection termine_par_a pair_de_a

L'automate produit fait avancer les deux automates en parallèle ; un état est acceptant ssi ses deux composantes le sont. les_deux reconnaît l'intersection des deux langages.

Exercice 8 : Compter les mots acceptés

Écrire compte_mots a longueur : le nombre de mots de longueur exactement longueur acceptés par a.

Démonstration

let compte_mots a longueur =
  let nb_sym = Array.length a.delta.(0) in
  let nb = Array.make a.nb_etats 0 in
  nb.(a.initial) <- 1;
  for _i = 1 to longueur do
    let suivant = Array.make a.nb_etats 0 in
    for e = 0 to a.nb_etats - 1 do
      for s = 0 to nb_sym - 1 do
        let e' = a.delta.(e).(s) in
        suivant.(e') <- suivant.(e') + nb.(e)
      done
    done;
    for e = 0 to a.nb_etats - 1 do nb.(e) <- suivant.(e) done
  done;
  let total = ref 0 in
  for e = 0 to a.nb_etats - 1 do
    if a.acceptants.(e) then total := !total + nb.(e)
  done;
  !total

Programmation dynamique (chapitre 13) sur les états : nb.(e) compte les mots (de la longueur courante) menant à l'état e. À chaque lettre lue, on propage ces comptes le long des transitions. À la fin, on somme sur les états acceptants. Coût — sans énumérer les mots un à un.

Exercice 9 : Les multiples de 3 en binaire

Construire un automate qui, lisant un entier écrit en binaire (poids fort en tête, le bit 0 codé a, le bit 1 codé b), accepte ssi cet entier est multiple de .

Démonstration

(* état = reste modulo 3 du préfixe lu ; lire un bit b : reste := (2*reste + b) mod 3 *)
let mult3 = {
  nb_etats = 3; initial = 0;
  acceptants = [| true; false; false |];   (* reste 0 : multiple de 3 *)
  delta = [| [| 0; 1 |];      (* reste 0 : bit0 -> 0, bit1 -> 1 *)
             [| 2; 0 |];      (* reste 1 : bit0 -> 2, bit1 -> 0 *)
             [| 1; 2 |] |];   (* reste 2 : bit0 -> 1, bit1 -> 2 *)
}

L'état est le reste modulo de la valeur lue : lire un bit b transforme le reste r en (2r + b) mod 3 (décalage binaire). On accepte au reste 0. Par exemple &quot;bb&quot; code : accepté. Un automate à trois états reconnaît ainsi une infinité de nombres — la puissance de la mémoire finie*.

Exercice 10 : Équivalence jusqu'à une longueur

Écrire equivalents a b longueur : a et b acceptent-ils les mêmes mots de longueur longueur ?

Démonstration

let equivalents a b longueur =
  let nb_sym = Array.length a.delta.(0) in
  let ok = ref true in
  let rec explore reste ea eb =
    if a.acceptants.(ea) <> b.acceptants.(eb) then ok := false;
    if reste > 0 then
      for s = 0 to nb_sym - 1 do
        explore (reste - 1) a.delta.(ea).(s) b.delta.(eb).(s)
      done
  in
  explore longueur a.initial b.initial;
  !ok

On fait avancer simultanément les deux automates (comme dans le produit) le long de tous les préfixes de longueur longueur : à chaque étape, l'acceptation doit coïncider. Inutile de construire les mots — seuls comptent les deux états courants (ea, eb). Pour une équivalence exacte (toutes longueurs), on testerait le vide de intersection a (complement b) et réciproquement : l'équivalence des AFD est décidable.

Synthèse du chapitre (à retenir)
  • Un AFD : états 0..n-1, un initial, des acceptants, et delta.(etat).(lettre) (enregistrement + tableau de transitions).
  • Reconnaître : partir de l'initial, suivre une transition par lettre, accepter si l'état final est acceptant. Coût .
  • Construire un automate, c'est trouver la bonne information à mémoriser dans l'état (dernière lettre, parité, reste modulo , avancement d'un motif).
  • Complément : inverser les acceptants (AFD complet). Intersection : automate produit (états = couples, avancer les deux en parallèle).
  • Langage non vide : accessibilité d'un acceptant (parcours, chapitre 15). Compter les mots acceptés : programmation dynamique sur les états (chapitre 13).
  • Un nombre fini d'états reconnaît des langages infinis ; l'équivalence des AFD est décidable.

18.6 Exercices d'entraînement

Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.

Thème A — Construire des automates.
Thème B — Reconnaissance et variantes.
Thème C — Opérations.
Thème D — Modélisation.

Continuer sur Adloun : animation, QCM, fiches, exercices