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