Intelligence Artificielle TP 2 : Programmation Prolog 1 Exercice 1
Transcription
Intelligence Artificielle TP 2 : Programmation Prolog 1 Exercice 1
Intelligence Artificielle TP 2 : Programmation Prolog 1 Exercice 1 Un ancêtre est une personne dont quelqu’un descend. Nous allons écrire un prédicat pour la relation “ancêtre” où ancetre(A,B) veut dire que A est un ancêtre de B. Si la base de connaissances contient des relations “parent”, on peut facilement définir la relation “ancêtre”. Un parent est un ancêtre pour son enfant et tous les ancêtres d’un parent sont aussi des ancêtres pour l’enfant. Par exemple, supposons le graphe suivant qui représente des relations “parent” où les parents sont juste au dessus de leur enfants. On peut représenter le graphe en Prolog par les faits suivants, où p(X,Y) traduit que X est le parent de Y : p(a,b). p(a,c). p(c,d). p(d,e). p(d,f). p(n,w). p(w,y). p(z,y). p(y,x). p(y,m). p(x,f). p(x,t). Quel sont les ancêtres de e ? A partir du graphe, on constate qu’ils sont d, c et a. On obtient cela en commençant par trouver les parents de e, puis leurs ancêtres. Ceci nous fournit le cas base (un ancêtre est un parent), et le cas récursif (un ancêtre d’un parent est un ancêtre) pour la définition du prédicat ancetre. 1 Questions : 1. Programmez le prédicat ancetre avec la base de connaissance donnée ci-dessus. ancetre(A,B) :- p(A,B). % un parent est un ancetre ancetre(A,B) :- p(X,B), ancetre(A,X). % un ancetre du parent est un ancetre Effectuez les différents tests suivants : (a) Essayez différents buts et utilisez le backtracking pour trouver les ancêtres. Trouvez la plus longue liste d’ancêtres. (b) Essayez des buts avec des variables dans les paramètres et utilisez le backtracking. 2. Faites les différents tests suivants, en utilisant le debugeur. Dans les parties 2b et 2c vous pouvez observer les appels récursifs au prédicat ancetre. (a) | ?- ancetre(a,e). (b) | ?- trace, ancetre(a,e). (c) | ?- spy(ancetre), ancetre(a,e). (tapez l pour sauter à l’utilisation de ancetre) (d) | ?- nospy(ancetre), spy(p), ancetre(a,e). (tapez l pour sauter à l’utilisation de p) 3. Modifiez la définition du prédicat ancetre comme indiqué dans les cas suivants, et remarquez les différences dans l’exécution. (a) Changez l’ordre des deux clauses de sort que l’appel récursif arrive en premier avant le cas de base. (b) Changer l’ordre des prédicats dans la clause récursif. 4. Modifiez le prédicat ancetre de sorte à ce qu’il affiche le nombre de générations entre les personnes. Par exemple le but | ?- ancetre(a, e, N). retournera N=3. Testez avec différents buts en incluant des buts avec des variables dans les arguments. Observez le résultat avec le débugeur. 2 Exercice 2 Supposez que pour chaque entier naturel n on désire écrire la séquence de tous les entiers successifs entre 0 et n. Pour trouver le programme qui 2 permet de faire cela on peut faire quelques observations. Premièrement, on observe que pour n = 0 la réponse est 0. D’un autre coté, si n > 0, la tâche est accomplie si nous écrivons la séquence de 0 à n − 1 et si ensuite nous ajoutons n à la séquence. Ces observations nous fournissent le cas de base et le cas récursif pour un prédicat seq qui réalise cette tâche. Par exemple : |?-seq(3). devra imprimer la séquence 0123. Une définition du prédicat seq peut être écrite comme suit, où nous avons ajouté une vérification que le nombre est bien un entier naturel. La séquence sera affichée sous forme d’une colonne. seq(0) :- write(0), nl. seq(N) :- nat(N), M is N-1, seq(M), write(N), nl. % % % % nl passe à la ligne vérifie si N est naturel affiche la séquence de 0 à N-1 affiche N nat(X) :- integer(X), X >= 0. % prédicat de vérification des nombres naturels Pour un autre exemple de ce type de programmation, supposez que l’on désire calculer la somme 0 + 1 + 2 + · · · + n pour tout entier naturel n. On peut faire quelques observations sur la somme pour construire le programme. Premièrement, si n = 0 le résultat doit être 0. Ensuite, pour n > 0, la tâche serait accomplie si nous calculons la somme 0 + 1 + · · · + n − 1 à laquelle nous ajoutons n. De la même manière que dans le cas précédent, cette observation nous donne le cas de base et le cas récursif pour le prédicat somme . Par exemple, le but |?- somme(3, Res). devrait donner Res=6. La définition du prédicats somme peut être la suivante : somme(0,0). somme(N,S) :- nat(N), K is N-1, somme(K,T), S is T+N. Questions : 1. Implémentez le programme du prédicat seq et essayez avec différents buts. Ensuite, faites les tests suivants, dans les parties 1b et 1c vous observerez les appels récursifs au prédicat seq. (a) |?- seq(3). (b) |?- trace, seq(3). (c) |?- spy(seq), seq(3). (tapez l pour sauter aux appels de seq) (d) |?- nospy(seq), spy(nat), seq(3). (tapez l pour sauter aux appels de nat) 3 2. Faites les modifications suivantes à la définition du prédicat seq. (a) Changez l’ordre des deux clauses de sorte que le cas de base arrive après la cas récursif. Testez la nouvelle définition et observez les différences avec l’ancienne. (b) A partir de cette dernière modification du prédicat seq, supprimez l’appel au prédicat nat. Observer le comportement, en utilisant le debugeur. 3. Modifiez la définition original du prédicat seq de sorte que les numéros s’affichent dans l’ordre inverse. Par exemple le but |?- seq(3). devrait afficher 3210 4. Implémentez le programme du prédicat somme et essayez avec différents buts pour vérifier qu’il marche bien. Ensuite, faites les tests suivants, dans les parties 4b et 4c vous observerez les appels récursifs au prédicat somme. (a) |?- somme(3,X). (b) |?- trace, somme(3,X). (c) |?- spy(somme), somme(3,X). (tapez l pour sauter aux appels de somme) (d) |?- nospy(somme), spy(is), somme(3,X). (tapez l pour sauter aux appels de is) (e) |?- trace, somme(N,2)., qu’observez vous ? 5. Faites les modifications suivantes à la définition du prédicat somme. (a) Changez l’ordre des deux clauses de sorte que le cas de base arrive après la cas récursif. Testez la nouvelle définition et observez les différences avec l’ancienne. (b) A partir de cette dernière modification du prédicat somme, supprimez l’appel au prédicat nat. Observer le comportement, en utilisant le debugeur. (c) L’ordre des prédicats dans les formules est important, car elle sont exécutées de gauche à droite. Pour observer cela, modifier l’ordre des termes de la clause récursive de somme comme ci-dessous, et observez les différences dans l’exécution du but somme(3,X). somme(0,0). somme(N,S) :- nat(N), somme(K,T), K is N-1, S is T+N. 4 3 Exercice 3 Un ensemble E est définit par induction s’il peut être décrit en nommant quelques éléments spécifiques de E (le cas de base) et en donnant une ou plusieurs règles pour construire de nouveaux éléments de E à partir d’éléments existants dans E (le cas d’induction). On fait l’hypothèse que E ne contient que des éléments construit à partir de ces règles. Par exemple, soit E l’ensemble défini comme suit : – Base : 2 ∈ E – Induction : si x ∈ E alors x + 3 ∈ E Soit le prédicat dansE(x) pour tester si x ∈ E. On peut définir dansE comme suit : dansE(2). dansE(X) :- Y is X-3, Y>=2, dansE(Y). Questions : 1. Donner une définition de l’ensemble E. 2. Implémentez le prédicat dansE, et vérifiez qu’il teste bien l’appartenance à l’ensemble que vous avez définit. Essayez avec des arguments qui ne sont pas dans E et des arguments non entiers. 3. Faites les tests suivants : – |?- dansE(5). – |?- trace, dansE(5). – |?- spy(dansE), dansE(5). (tapez l pour sauter aux appels de dansE) – Observez ce qu’il se passe si vous mettez une variable comme argument. 4. Pour chacune des définitions inductives d’ensemble suivantes, donnez une définition de l’ensemble A, ensuite donnez une définition du prédicat dansA(x) qui teste l’appartenance de x dans A. Testez votre prédicat par rapport à votre définition de l’ensemble. Refaites les tests précédents avec dansA. (a) – Base : 3 ∈ A – Induction : si x ∈ A alors 2x + 1 ∈ A (b) – Base : 0 ∈ A – Induction : si x ∈ A alors x + 4 ∈ A et x + 9 ∈ A 5 4 Exercice 4 Vous devez écrire deux programmes, l’un pour calculer la factorielle d’un entier et l’autre pour calculer le nombre Fibonacci d’un entier. On vous donne les définitions récursives suivantes : 1. Factorielle – Base : 0 ! = 1, – Récursion : n! = (n − 1)! × n 2. Fibonacci – Base : fib(0) = 0, fib(1) = 1 – Récursion : fib(n) = fib(n-1) + fib(n-2). Par exemple |?- fac(5,N). donnerait le résultat suivant : N=120, et |?fibo(8,N). donnerait N=21. Testez vos prédicat avec différentes valeurs. 5 Exercice 5 Le problème des tours de Hanoı̈ est un jeu de réflexion imaginé par le mathématicien français Édouard Lucas, et consistant à déplacer des disques de diamètres différents d’une tour de départ à une tour d’arrivée en passant par une tour intermédiaire et ceci en un minimum de coups, tout en respectant les règles suivantes : – on ne peut déplacer plus d’un disque à la fois, – on ne peut placer un disque que sur un autre disque plus grand que lui ou sur un emplacement vide. La figure suivante montre le point de départ et le point d’arrivée. On suppose que cette dernière règle est également respectée dans la configuration de départ. 6 La clé pour résoudre ce problème est de voir qu’il peut être résolu en le décomposant en une collection de problèmes plus simples qui peuvent eux aussi être décomposés jusqu’à ce que une solution soit trouvée. Ce qui suit montre l’approche : 1. étiqueter les colonnes A, B, C (ces étiquettes peuvent changer à différentes étapes) 2. soit n le nombre total des disques. 3. numéroter les disques de 1 (plus petit/plus haut) à n (plus grand/plus bas) Pour déplacer n disques de la colonne A à la colonne C : 1. déplacer n-1 disques de A à B (ce qui laisse le disque numéro n sur A) 2. déplacer le disque numéro n de A à C 3. déplacer n-1 disques de B à C (qui vont se trouver sur le disque numéro n) L’algorithme ci-dessous est une procédure récursive : pour faire les étapes 1 et 3, on applique le même algorithme pour n-1, n-2 ... etc. La procédure s’exécute en un nombre fini d’étapes, puisque n décroı̂t jusqu’à n=1. Cette étape correspond à déplacer un disque de la colonne A à la colonne B. Le but de cette partie est d’écrire un programme Prolog pour résoudre ce problème. Le prédicat doit prendre 4 arguments, le nombre de disques à déplacer, la colonne de départ, la colonne d’arrivée et la troisième colonne. Par exemple : | ?- hanoi(3, gauche, Déplacer un disque de Déplacer un disque de Déplacer un disque de Déplacer un disque de Déplacer un disque de Déplacer un disque de Déplacer un disque de droite, centre). gauche à droite gauche à centre droite à centre gauche à droite centre à gauche centre à droite gauche à droite true ? yes 7