Devoir 1 en temps limité Un corrigé ARBRES DE BETH 1 Arbre de Beth associé à une formule 1. (a) Les branches de N(0, B 1, B 2 ) sont exactement celles de B 1 et celles de B 2 auxquelles on ajoute un 0 en tête. Comme on ne tient pas compte de 0, les clauses de N(0, B 1, B 2 ) sont donc exactement celles de B 1 et celles de B 2. (b) La formule associée à N(0, B 1, B 2 ) est donc ( F 1 F 2 ). Comme F i et F i sont logiquement identiques, ( F 1 F 2 ) est logiquement identique à (F 1 F 2 ). 2. (a) Les branches de B sont les concaténations d une branche de B 1 et d une branche de B 2. Les clauses associées à B sont donc exactement les conjonctions d une clause de F 1 et d une clause de F 2. En notant F 1 = C 1 C u et F 2 = C 1 C v où les C k et C k sont des clauses, la formule associée à B est ainsi C i C j (b) Par distributivité, cette formule est identique à ( ) ( ) C i = F 1 F 2 et donc aussi identique à F 1 F 2. i i,j (c) Comme il convient de parcourir uniquement le premier arbre (la récursivité ne porte pas sur le second) j ai choisi d écrire une fonction auxiliaire locale parcourt : betharbre betharbre qui pr uniquement en argument le premier arbre (et renvoie l arbre obtenu en ajoutant le second comme fils gauche aux feuilles). let concatene b1 b2 = let rec parcourt b = match b with Vide -> Vide N(i,Vide,Vide) -> N(i,b2,Vide) N(i,g,d) -> N(i,parcourt g,parcourt d) in parcourt b1;; 3. Comme (( F 1 )) est identique à F 1, l arbre B 1 convient. 4. Par formule de DeMorgan, (F 1 F 2 ) (( F 1 ) ( F 2 )). Il nous suffit donc de construire les arbres B i associés aux ( F i) et d utiliser la technique de concaténation de la question 2. De même, (F 1 F 2 ) (( F 1 ) ( F 2 )). Il nous suffit donc de construire les arbres B i associés aux ( F i ) et de renvoyer N(0, B 1, B 2 ). 5. On a (F 1 F 2 ) ( F 1 ) F 2. Avec les notations précédentes, on renvoie N(0, B 1, B 2). De même (F 1 F 2 ) F 1 ( F 2 ) et il suffit de renvoyer la concaténation de B 1 et de B 2. 6. Si on suit l algorithme précédent, on trouve l arbre ci-dessous : i C i 1
-3 0 0 0 1 2-3 -1 2 3 7. Il suffit de suivre les formules des questions qui précédent. let rec construitb f = match f with Var i -> N(i,Vide,Vide) Non(Var i) -> N(-i,Vide,Vide) Ou (f1,f2) -> N(0,construitB f1,construitb f2) Et (f1,f2) -> concatene (construitb f1) (construitb f2) Non (Non f) -> construitb f Non (Ou(f1,f2)) -> concatene (construitb(non f1)) (construitb(non f2)) Non (Et(f1,f2)) -> N(0,construitB(Non f1),construitb(non f2)) Imp(f1,f2) -> N(0,construitB (Non f1),construitb f2) Non (Imp(f1,f2)) -> concatene (construitb f1) (construitb (Non f2));; Dans les appels réursifs, le nombre de connecteurs présents dans la formule argument diminue strictement. On finit donc par des appels avec des formules sans connecteurs, ce qui est un cas de base. On prouve donc la terminaison de l appel construitb f par récurrence sur le nombre de connecteurs présents dans f. 2 Forme disjonctive 8. (a) On parcourt la liste en tirant partie de l évaluation paresseuse. let rec mem x l = match l with [] -> false y::q -> (x==y) (mem x q);; (b) Si la clause s écrit [i 1 ;... ; i k ], il suffit de vérifier que u < v, i u i v. On utilise encore l évaluation paresseuse qui évite un appel récursif inutile. let rec satisclause c = match c with [] -> true i::q -> (not (mem (-i) q)) && (satisclause q);; Le choix du cas de base est cohérent avec le cas d une clause de longueur 1. (c) mem étant de complexité linéaire en fonction de la taille de la liste, satisclause est de complexité quadratique en fonction de la taille de la clause. 2
9. Une forme disjonctive est satisfiable si et seulement si l une des clauses qui la composent l est. On teste donc la satisfiabilité de la première clause et, en cas d échec, de la liste des autres (l évaluation paresseuse assure que l appel récursif n est pas effectué quand la première clause est satisfiable). let rec satisfd f = match f with [] -> false c::q -> (satisclause c) (satisfd q) ;; Le choix du cas de base est cohérent avec le cas d une formule comportant une unique clause. 10. (a) On veut montrer que deux clauses c et c sont logiquement identiques si et seulement si on est dans l un des cas suivants : - c et c sont non satisfiables ; - c et c sont composées des mêmes littéraux. Le sens réciproque est immédiat. On remarque qu une valuation satisfait une clause si et seulement si elle r égaux à vrai tous les littéraux composant la clause. Une clause satisfiable a donc une valuation satisfaisante minimum : celle qui r égaux à vrai tous les littéraux et à faux toutes les variables qui n apparaissent pas dans la clause. Supposons donc c c et que c ou c est satisfiable. Ceci entraîne que c et c sont satisfiables et le sont pour les mêmes valuations. Elles ont donc même valuation minimum et donc les mêmes littéraux. (b) On regarde si le premier élément de c1 est dans c2 puis on effectue un appel récursif avec la queue de c1. let rec inferieur c1 c2 = match c1 with [] -> true i::q -> (mem i c2) && (inferieur q c2) ;; (c) Il suffit d appliquer la CNS trouvée plus haut. let testclauses c1 c2 = (not (satisclause c1) && not (satisclause c2)) ( (inferieur c1 c2) && (inferieur c2 c1) );; 11. (a) Fonction récurrente élémentaire sur une liste. let rec ajoute x l = match l with [] -> [] l1::q -> (x::l1)::(ajoute x q) ;; (b) Dans l arbre vide, on n a aucune feuille et donc aucune branche, la forme disjonctive est donc vide. Dans le cas d une feuille on a une branche qui contient un unique élément. Si cet élément est non nul, on a une unique clause de taille 1. Sinon, on a une unique clause de taille 0. Dans le cas général, on a un arbre N(i, g, d). On concatène les formules associées à g et d et à chaque clause de la liste, on ajoute i en tête si i 0 avec la fonction ajoute. let rec beth_to_fd b = match b with Vide -> [] N(0,Vide,Vide) -> [[]] N(i,Vide,Vide) -> [[i]] N(0,g,d) -> (beth_to_fd g)@(beth_to_fd d) N(i,g,d) -> ajoute i ((beth_to_fd g)@(beth_to_fd d));; 3
3 Satisfiabilité d une formule 12. On gère une valuation initialement égale à [ 0; 0; 0; 0; 0 ]. - On parcourt l arbre où l on rencontre 1 puis 2 ce qui nous amène à une valuation égale à [ 0; 1; 1; 0; 0 ]. - On rencontre alors 1 et ceci nous indique que la première branche ne peut être satisfaite (la clause contient x 1 et ( x 1 )). - On revient sur nos pas et on rencontre 2 ce qui indique encore que la seconde branche est non satisfiable. - Il faut alors remonter jusqu au premier embranchement non exploré. Ici, il faut remonter jusqu à la racine. Lors de cette remontée, il faut à nouveau mettre à jour la valuation pour ne pas polluer l exploration suivante et on retombe sur [ 0; 0; 0; 0; 0 ]. - On explore des noeuds d étiquettes 2, 3 et on obtient la valuation [ 0; 0; 1; 1; 0 ]. - On continue et on trouve l étiquette 2 qui interrompt la recherche sur cette branche. - On remonte au précédent embranchement utile ce qui nous fait revenir au noeud d étiquette 2 avec une valuation [ 0; 0; 1; 0; 0 ]. - On rencontre alors 1 puis 3 pour obtenir une valuation [ 0; 1; 1; 1; 0 ]. Comme on est au bout de la branche, on a trouvé une valuation validant une clause. La formule est satisfiable et on a trouvé une valuation convenable. On renvoie [ 1; 1; 1; 1; 0 ]. 13. Le numéro maximal d une variable est le maximum de l étiquette et des numéros de variables des fils droit et gauche. Dans le cas de base où l arbre est vide, on choisit de renvoyer 0 ce qui est cohérent avec le cas d une feuille ou d un noeud ayant un unique fils non vide (cette valeur 0 ne changeant pas la valeur du maximum calculé). let rec nbvariable b = match b with Vide -> 0 N(i,g,d) -> max (abs i) (max (nbvariable g) (nbvariable d));; 14. Le cas de base est celui où l on tombe sur une feuille. Si on peut donner une valeur non contradictoire avec ce que l on a déjà à la variable associée, on le fait et on renvoie true. Sinon, on renvoie false. Ceci nous amène à distinguer deux cas symétriques selon le signe du littéral représenté par la feuille. Notons qu à priori, il ne doit pas exister de feuille étiquetée par 0. Cepant, dans ce cas, il est naturel de renvoyer true sans rien faire car la formule est vide. Dans le cas récurrent, on daoit encore distinguer selon que l étiquette de la racine est nulle ou non. Dans le second cas, on a deux situations symétriques selon le signe de l étiquette. Dans chaque cas, on essaye de donner une valeur à la variable associée. Si ce n est pas possible, on a une contradiction et on renvoie false. Sinon, on donne cette valeur et on examine les fils ; si les deux générent un échec il faut redonner à la variable associée à l étiquette sa valeur initiale. Le cas de l arbre vide est douteux. Si on veut que les appels récurrentes avec l arbre vide soient cohérents, on doit renvoyer la valeur false dans ce cas. let satisfiable b = let n=nbvariable b in let v=make_vect (n+1) 0 in let rec parcourt b = match b with Vide -> false N(0,Vide,Vide) -> true N(i,Vide,Vide) -> if i>0 then begin if v.(i)=(-1) then false v.(i) <- 1; true 4
else begin if v.(-i)=1 then false v.(-i) <- -1; true N(0,g,d) -> (parcourt g) (parcourt d) N(i,g,d) -> if i>0 then begin (* littéral positif *) if v.(i)=(-1) then false (* contradiction *) let x=v.(i) in (* mémorisation de la valeur *) v.(i) <- 1 ; if (parcourt g) (parcourt d) then true (* échec sur les fils *) v.(i) <- x; (* on redonne la valeur à la variable *) false (* cas similaire d un littéral négatif *) if v.(-i)=1 then false let x=v.(-i) in v.(-i) <- -1; if (parcourt g) (parcourt d) then true v.(-i) <- x; false in if parcourt b then begin (* cas satisfiable *) v.(0) <- 1; v v.(0) <- -1; v ;; GRAPHES EULERIENS 1 Chemins Eulériens fermés 1. On suppose G Eulérien. Il existe donc un chemin fermé P = x 1, x 2,..., x k, x 1 contenant toutes les arêtes du graphe. Comme le graphe est connexe, tous les sommets font partie du chemin. Si s est un sommet, le degré de s est le nombre d arêtes auxquelles participe s. Si s x 1, ce nombre est double du nombre d apparitions de s dans P. Si s = x 1, il est égal au double du nombre d apparitions de x 1 parmi x 2,..., x k plus deux (les deux apparitions du bord). Dans tous les cas, s est de degré pair. 5
2. Il suffit de voir si toutes les listes d adjacence sont de longueur paire. On peut s interrompre dès que l une de ces listes est de longueur impaire et on utilise donc une boucle conditionnelle. let est_eulerien g = let n=vect_length g in let i=ref 0 in while!i<n && list_length g.(!i) mod 2 = 0 do incr i done;!i=n;; Dans le cas le pire, on calcule la longueur de toutes les listes et la complexité est donc O( A ) (linéaire en fonction du nombre d arêtes). 3. (a) Soit s 1,..., s k un chemin non prolongeable. Soit H le graphe induit par ce chemin. Comme en question 1, si le chemin n est pas fermé, s 1 et s k ont un degré impair dans H. Comme Ils ont un degré pair dans G par hypothèse, on peut donc trouver une arête s k y dans G et pas dans H et on peut donc prolonger le chemin. On en déduit que s 1 = s k. (b) Dans le cas de l exemple, on obtient le chemin fermé [7; 6; 1; 7]. Le nouveau graphe est alors 7 0 6 1 5 2 4 3 (c) On va avoir besoin de modifier les listes d adjacence en leur retirant des éléments. On écrit donc une fonction enlever : a a list a list qui à partir d une liste et d un élément renvoie la liste privée de la première occurence de l élément. let rec enlever y l = match l with [] -> [] x::q -> if x=y then q else x::(enlever y q);; Dans la fonction demandée, il suffit alors de modifier les cases g.(x) et g.(y) du tableau g. let supprime_arete x y g = g.(x) <- enlever y g.(x) ; g.(y) <- enlever x g.(y) ;; (d) J écris une fonction auxiliaire locale depart : int int list. Dans l appel depart y, on renvoie un chemin de y à x (connu puisque la fonction est locale). Il suffit de l appeler avec x. La fonction est simplement récurrente (on trouve un successeur z de y et on ajoute y à un chemin de z à x obtenu récursivement ; le cas de base est celui où x est le successeur trouvé pour y). let cheminferme g x = let rec depart y = let z=hd g.(y) in supprimer_arete y z g; if z = x then [y;x] else y::(depart z) in depart x 6
4. (a) Le chemin fermé P trouvé induit un sous-graphe dont les sommets sont tous degré pair. On ne change donc pas la parité des degrés en retranchant de G les arêtes de P. (b) On suppose qu il reste une arête xy dans G. Si x est l un des sommets de P alors on a directement un sommet de P de degré non nul dans G. Sinon, par connexité, il existe dans G un chemin de x vers l un des sommets de P. Ce chemin va d un sommet hors de P à un sommet dans P et il existe donc une première arête qui nous fait passer sur un sommet de P. Cette arête relie un sommet z de P à un sommet z hors de P. z est alors un sommet de P de degré non nul dans G. (c) On cherche un élément de la liste dont la liste d ajacence est non vide! let rec cherchersommet g p = match p with [] -> failwith "graphe non Eulérien" x::q -> if g.(x)=[] then cherchersommet g q else x ;; 5. (a) Puisque l on continue à privilégier les sommets de grand numéro, on obtient Incorporer P dans P donne le chemin fermé P = [7; 5; 4; 0; 5; 2; 0; 1; 3; 7] [7; 5; 4; 0; 5; 2; 0; 1; 3; 7; 6; 1; 7] qui est un chemin Eulérien fermé du graphe de départ. (b) La fonction incorporer : chemin chemin chemin pr en argument deux chemins c et p. On SUPPOSE que le premier élément x de c est élément de p et on remplace la première occurence de x dans p par c. let rec incorporer c p = match p with [] -> failwith "graphe non eulérien" x::q -> if x=hd c then c@q else x::(incorporer c q);; 6. (a) On a besoin d une fonction estvide :graphe bool testant si le graphe possède des arêtes. On teste si les listes d adjacence sont vides. On peut s arrêter dès que l on en trouve une non vide et on utilise donc une boucle conditionnelle. let estvide g = let n=vect_length g in let i=ref 0 in while!i<n && g.(!i)=[] do incr i done;!i=n;; Il reste à enchaîner les étapes. let rec agranditchemin g p = if estvide g then p let x=cherchersommet g p in let pp=cheminferme g x in agranditchemin g (incorporer pp p) ;; (b) Pour la fonction principale, j ai choisi ici de travailler avec une copie du graphe (même si l énoncé ne le demande pas). Il nous suffit de lancer le processus avec un chemin de longueur 0 et contenant donc un unique sommet. let chemineulerienferme g = let gg=copy_vect g in agranditchemin gg [0];; 7
2 Chemins Eulériens 7. Considérons l exemple suivant : 3 0 1 2 Ce n est pas un graphe Eulérien d après le théorème d Euler (3 est de degré 3). Cepant, 1, 0, 3, 2, 1, 3 est un chemin Eulérien. Le graphe est donc semi-eulérien. 8. Un graphe connexe est semi-eulérien si et seulement si tous ses sommets sont de degré pair sauf deux qui sont de degré impair. - La condition est nécessaire. En effet, supposons que l on dispose d un chemin Eulérien ouvert x 1,..., x k alors tous les sommets sont parcourus. A part x k et x 1, il font tous partie d un nombre pair d arêtes (à chaque occurence d un sommet dans le chemin sont associées deux arêtes). x k et x 1 sont, eux, de degré impair (égal à 1 plus le nombre de fois où on les trouve parmi x 2,..., x k 1 ). - Réciproquement, supposons que G vérifie la condition sur les degrés. Soit G le graphe obtenu à partir de G en ajoutant un sommet s que l on relie aux deux sommets de degré impair (notés x 1 et x k ). G est alors Eulérien et on trouve un chemin Eulérien fermé pour G. Quitte à permuter, ce chemin fermé s écrit s, x 1,..., x k, s où les x i sont les sommets de G. x 1,..., x k est alors un chemin Eulérien pour G. 9. Il s agit d adapter l algorithme de parcours en profondeur à partir d un sommet. Il y a deux choses à prre en compte : - il faut s arrêter quand on a atteint le sommet cible (on ne veut pas chercher TOUS les sommets atteignables) ; - il faut stocker un chemin depuis le sommet source jusqu au sommets atteints (et pas seulement savoir qu on les a atteints). Pour le premier point, il suffit, dans la fonction récurrente, de ne pas faire l appel récursif quand on tombe sur le sommet cible. Pour le second point, il suffit de gérer un tableau des marques non plus constitué de booléens mais de chemins. Quand on découvre un sommet x, on place dans la case numéro x une liste [x;... ; b] constituant un chemin de x vers b. let route g a b = let n=vect_length g in let marque=make_vect n [] in marque.(b) <- [b] ; let rec parcourt x l = match l with [] -> () y::q -> if y=a then marque.(y) <- a::marque.(x) if marque.(y)=[] then begin marque.(y) <- y::marque.(x); parcourt y g.(y) ; parcourt x q 8
in parcourt b g.(b); marque.(a);; 10. On commence par trouver les deux sommets a et b de degré impair. C est le rôle de extremites : graphe int*int. let extremites g = let a=ref (-1) and b=ref (-1) in for i=0 to (vect_length g - 1) do if list_length g.(i) mod 2 =1 then if!a=(-1) then a:=i else b:=i done;!a,!b;; On cherche alors un chemin P entre les deux sommets trouvés (fonction route). En supprimant P du graphe, on se ramène alors au cas d un graphe Eulérien et on peut réutiliser la technique vue. On a besoin d une fonction supprime : chemin graphe unit transformant un graphe en enlevant les arêtes d un chemin. let rec supprime c g = match c with [] -> () [x] -> () x::y::q -> supprime_arete x y g ; supprime (y::q) g;; On suit alors le programme énoncé ci-dessus. let chemineulerien g = let gg=copy_vect g in let (a,b)=extremites gg in let p=route gg a b in supprime p gg ; agranditchemin gg p;; 9