Você está na página 1de 51

INTRODUCCIÓN

La Programación Lógica es un paradigma de computación que consiste de un enfoque declarativo para


escribir programas para el computador. PROLOG es un lenguaje de computación basada en ese paradigma. Los
programas lógicos pueden ser entendidos y estudiados a partir de dos conceptos: verdad y deducción lógica. En
programación lógica, uno puede preguntarse si un axioma de un programa es verdad bajo alguna interpretación
de los símbolos del programa y si ciertas declaraciones lógicas son consecuencia del programa. Esas preguntas
pueden ser respondidas independientemente de cualquier mecanismo de ejecución concreto.

Por otro lado, PROLOG es un lenguaje de programación con un significado operacional preciso que toma
prestado sus conceptos básicos de la programación lógica. Los programas PROLOG son instrucciones para
ejecutar sobre el computador. Esas instrucciones casi siempre son leídas como instrucciones lógicas y, lo más
importante, el resultado de una computación de un programa PROLOG es una consecuencia lógica de los
axiomas en éste.

Es ampliamente aceptado que una programación efectiva en PROLOG requiere de una comprensión de la
teoría de programación lógica, por lo menos en sus aspectos fundamentales, pero también que esto redunda en
un mayor dominio del procesamiento de la información usando el conmputador.

UN POCO DE HISTORIA

El comienzo de la programación lógica puede ser atribuido a los profesores Kowalski y Colmerauer. R. A.
Kowalski. trabajando en la Universidad de Edinburgo, formuló la interpretación procedimental de la lógica de
cláusulas de Horn y mostró que el axioma A si B puede ser leído como un procedimiento de un lenguaje de
programación recursivo, donde A es la cabeza del procedimiento y B su cuerpo. Al mismo tiempo, a principios
de 1970, Colmerauer y su grupo en la Universidad De Marseille-Aix desarrolló un probador de teorema
especializado, el cual ellos usaron para implementar sistemas de procesamiento natural. El probador de teorema,
lo llamaron PROLOG (for Programation et Logique or Programming in Logic), basado en la interpretación
procedimental de Kowalski.

El primer interpretador de PROLOG no fue tan rápido como los sistemas LISP, el lenguaje pionero
propuesto por el prof. McCarthy y muy popular en los Estados Unidos. Pero esto cambió a mediados de 1970
cuando David H.D. Warren y sus colegas desarrollaron una implementación eficiente de PROLOG. El
compilador, el cual fue casi completamente escrito en PROLOG, tradujo cláusulas de PROLOG hacia
instrucciones de una máquina abstracta que es ahora conocida como Warren Abstract Machine (WAM). Sin
embargo, la comunidad de la Inteligencia Artificial y de la computación de occidente se mantuvo aún ignorante
e indiferente a la programación lógica.

Esto cambió un poco con el anuncio del Proyecto de Quinta Generación Japonesa el cual claramente
afirmó el rol importante de la programación lógica en la próxima generación de sistemas computarizados. No
sólo los investigadores sino también el público en general comenzaron a deletrear la palabra PROLOG. Desde
entonces, PROLOG está alcanzando nuevas alturas. Actualmente, una de las extensiones más prominentes es la
programación lógica con restricciones (Contraint Logic Programming-CLP), con una cantidad de nuevos
recursos para Ingeniería Lógica. Otras areas con desarrollos importantes son: la Programación Lógica Inductiva
(ILP) y los Agentes en Programación Lógica.

PRIMEROS PASOS EN PROLOG


En este capítulo revisaremos unos pocos ejemplos para ilustrar las ideas básicas detrás de la
programación en PROLOG. Pero primero, unas notas acerca de como usar Sistemas PROLOG.

Un programa PROLOG es un conjunto de procedimientos (el orden es indiferente). Cada procedimiento


consiste de una o más cláusulas (ahora, el orden de las cláusulas si es importante). Hay dos tipos de cláusulas:
hechos y reglas. El programa es almacenado en una base de conocimiento PROLOG. Usualmente cargas un
programa dentro de la base de datos usando el comando consult, en la concha PROLOG, en la siguiente forma:

? consult(‘Nombre_Del_archivo_Con_El_Programa’).

El comando consult agrega las cláusulas y hechos desde el archivo texto especificado a las cláusulas y hechos
ya almacenados en la base de datos. Así puedes cargar más programas dentro de la base de datos a la vez, pero
debes ser cuidadoso si los programas no usan los procedimientos con el mismo nombre. De otra manera, debido
a la acumulación de cláusulas, esos procedimientos podrían comportarse incorrectamente.
Otra forma de invocar este mismo comando es con la notacion de listas:

? [‘Nombre_Del_archivo_Con_El_Programa’].

Puedes también usar el comando reconsult para recargar un programa.

? reconsult('Nombre_Del_Archivo_Con_El_Programa').

Este comando se comporta igual al comando consult (agrega procedimientos dentro de la base de datos)
pero si hay un procedimiento en la base de datos con el mismo nombre de algún procedimiento en el archivo
reconsultado, entonces el primer procedimiento es reemplazado por la nueva definición. Puedes usar el
comando reconsult para cambiar un programa en la base de datos durante la depuración.

El programa PROLOG es ejecutado invocando algún procedimiento del programa de la siguiente forma:

? nombre_Del_Procedimiento(parámetros).

Nota que tu ya has llamado un procedimiento cuando tu consultastes o reconsultastes el archivo. "Llamar un
procedimiento" es equivalente a "preguntando una pregunta" en PROLOG.

BASE DE DATOS GENEALÓGICA

Una base de datos genealógica puede ser una buena introducción a PROLOG , pués no es exactamente un
conjunto de procedimientos y tiene más elementos que una base de datos común. En ésta lección , presentamos
un programa de PROLOG simple que captura relaciones de familia básicas. El programa demuestra algunas de
las características de PROLOG como el usar hechos , reglas , variables y la recursión.

Primero, expresamos la propiedad de ser un hombre o mujer usando hechos de PROLOG:

hombre(adan).
hombre(pedro).
hombre(paul).

mujer(maria).
mujer(eva).

Ahora podemos agregar la relación "pariente" el cual asocia pariente y niño:

pariente(adan,pedro). % Significa adan es pariente de pedro.


pariente(eva,pedro).
pariente(adan,paul).
pariente(marry,paul).

Hasta ahora, hemos agregado sólo hechos a nuestro programa. Pero el poder real de PROLOG está en las
reglas. Mientras que los hechos afirman la relación explícitamente, las reglas definen la relación en una forma
más general. Cada regla tiene su cabeza (nombre de la relación definida), y su cuerpo (las condiciones de
definición de la relación). Las siguientes reglas definen las relaciones de ser un padre y ser una madre usando
las relaciones definidas previamente de ser un hombre o mujer y ser un pariente.

padre(F,C) :-hombre(F), pariente(F,C).

madre(M,C) :- mujer(M), pariente(M,C).

Notar que usamos variables (comienzan con mayúscula) para expresar la característica que cada hombre el cual
es un pariente de cualquier niño es también su padre. Si algún parámetro de la relación no es importante
podemos usar variables anónimas (denotadas _) como en esas definiciones:

es_padre(F) :- padre(F,_).

es_madre(M) :- madre(M,_).

Antes de avanzar más, uno debería saber como ejecutar los programas PROLOG. Tu ejecutas el programa
haciendo preguntas como ésta:

?-padre(X,paul).

La cual expresa: ¿Quién es padre de Paul? (Estrictamente hablando la frase pregunta si existe "algo" que sea el
padre de Paul). La respuesta es X=adan, según nuestro programa anterior.

Ahora extenderemos la base de datos de hechos y trataremos de definir otras relaciones de familia como ser un
hijo, tío o abuelo. También, trataremos de preguntar al sistema PROLOG diversas preguntas para ver que
sucede. Tu puedes comparar tu propio programa con las siguientes reglas:

hijo(S,P):-hombre(S),pariente(P,S).

hija(D,P):-mujer(D),pariente(P,D).

hermanos(A,B):-pariente(P,A), pariente(P,B), A\=B.

% hermanos tienen al menos un pariente común

% la prueba A\=B preserva que hermanos son personas diferentes.

hermanos_completos(A,B):-
pariente(A,F),pariente(B,F),

pariente(A,M),pariente(B,M),

A\=B, F\=M.

% hermanos completos tienen parientes comunes (ambos)

% la prueba F\=M preserva que hermanos completos tienen dos diferentes parientes(padre y
madre,naturalmente)

hermanos2_completos(A,B):-

padre(A,F),padre(B,F),

madre(A,M),madre(B,M), A\=B.

% otra solución para "el problema hermanos completos" que usa las relaciones padre y
madre

tio(U,N):-hombre(U),hermanos(U,P),pariente(P,N).

tia(A,N):-mujer(A),hermanos(A,P),pariente(P,N).

abuelo(G,N):-pariente(G,X),pariente(X,N).

Ahora, usamos sólo una regla para expresar la relación definida nuevamente pero podemos también definir la
relación usando dos o más reglas. Si queremos expresar que ser un humano significa ser un hombre o ser una
mujer, podemos hacerlo por esas dos reglas:

humano(H):-hombre(H).
humano(H):-mujer(H).

El cuerpo de la regla puede también usar la relación que está siendo definida. Esta característica es llamada
recursión y las siguientes reglas muestran su uso típico.

descendiente(D,A):-pariente(A,D).

descendiente(D,A):-pariente(P,D),descendiente(P,A).

Uno puede usar la característica de PROLOG de variables de entrada y salida no determinadas (algunas veces
llamada modo variable o multimodalidad) para definir la relación ancestro.

ancestro(A,D) :- descendiente(D,A).

PRIMERAS ESTRUCTURAS DE DATOS EN PROLOG

Esta lección cubre estructuras de datos en PROLOG. La estructura de datos básica en PROLOG es el
término, el cual es expresado como: nombre(argumentos...). Si el número de argumentos es cero entonces
estamos hablando de un átomo. Un tipo especial de átomo es el número.

ESTRUCTURAS BÁSICAS (en el ejemplo de las fechas)


En esta sección, introducimos una estructura de datos fecha(Dia,Mes,Año) que representa la fecha.
Primero necesitamos un constructor de la estructura de datos fecha que hace la estructura de datos de día, mes y
año:

hacer_fecha(D,M,A,fecha(D,M,A)).

Segundo, definimos las funciones para accesar los componentes de la estructura de datos de la siguiente
forma:

obtener_año(fecha(_,_,A),A).

obtener_mes(fecha(_,M,_),M).

obtener_dia(fecha(D,_,_),D).

obtener_xxx puede ser usado para probar o generar el componente correspondiente de la estructura de
datos, pero no puede ser usado para colocar el valor de la componente. Así, tenemos que definir colocar_xxx
para colocar valores a los componentes de la estructura de datos fecha.

colocar_año(A,fecha(D,M,_),fecha(D,M,A)).

colocar_mes(M,fecha(D,_,A),fecha(D,M,A)).

colocar_dia(D,fecha(_,M,A),fecha(D,M,A)).

Ahora, es fácil encontrar el "mismo" día en año próximo o anterior respectivamente usando las funciones
obtener y colocar.

año_proximo(Hoy,ProximoAño):- obtener_año(Hoy,A), NA is A+1,


colocar_año(NA,Hoy,ProximoAño).

año_anterior(Hoy,AnteriorAño):-obtener_año(Hoy,A), PA is A-1,
colocar_año(PA,Hoy,AnteriorAño).

Notar, que la definición siguiente de año_anterior usando año_proximo no es correcta. ¿Tú sabes Por Qué?

año_anterior(Hoy,AnteriorAño):- año_proximo(AnteriorAño,Hoy)). % incorrrecto

Encontrar el año próximo es relativamente fácil pero ¿Qué sucede al encontrar día próximo, es decir,
mañana?. Estudiar el siguiente programa para encontrar donde los problemas posibles están ocultos. La
definición de probar dia_correcto sigue en la próxima sección que cubre el trabajo con listas.

mañana(Hoy,Tomorrow) :- obtener_day(Hoy,D), ND is
+1,colocar_day(Hoy,ND,Tomorrow),fecha_correcta(Mañana).

%el día dentro del mes,es decir,no el último día del mes

mañana(Hoy,Mañana):- obtener_mes(Hoy,M), NM is
M+1,colocar_mes(Hoy,NM,Tmp),colocar_dia(Tmp,1,Mañana),fecha_correcta(Mañana).

% Ultimo día del mes

mañana(Hoy,Mañana):- obtener_año(Hoy,A), NA is A+1,hacer_fecha(NA,1,1,Mañana).


% Último día del año

Notar que también es posible y quizás más razonable encapsular la prueba fecha_correcta en las definiciones
de hacer_fecha y colocar_xxx.

LISTAS

Una lista es estructura de datos ampliamente usada y construida en PROLOG. Es todavía un término,
ejemplo, [1,2,3] es equivalente a '.'(1,'.'(2,'.'(3,nulo))). Las siguientes funciones permiten accesar los elementos
de la lista.

cabeza(C,[C|_]).

cola(L,[_|L]). % L es una lista

Es fácil accesar el primer elemento de la lista, es decir, la cabeza. Sin embargo, encontrar el último
elemento es un proceso que consume tiempo ya que uno tiene que recorrer la lista completa para encontrarlo.
Notar que los siguientes "procedimientos" pueden ser usados para encontrar el primero/último elemento de la
lista. Podría aún ser usado para generar una lista con el primero/último elemento dado.

primero(P,[P|_]). % El mismo como cabeza

ultimo(U,[U]).

ultimo(U,[C|L]):- ultimo(U,L).

La misma conclusión se mantiene para encontrar prefijo y sufijo respectivamente. De nuevo, el mismo
procedimiento puede ser usado para probar o generar prefijo/sufijo respectivamente también como generar una
lista con el prefijo/sufijo dado. Pruébalo.

prefijo([],_).

prefijo([C|L1],[C|L2]):-prefijo(L1,L2).

sufijo(S,S).

sufijo([C|T],L):-suffix(T,L).

Probar membresía es un método importante para trabajar con listas. La definición de PROLOG de
miembro puede probar la relación de membresía también como generar miembros sucesivos de una lista. Una
función similar, miembro_nth, puede también ser usado para probar o generar el miembro n-th de una lista. Sin
embargo, no puede ser usado para contar un número de secuencia de un elemento dado (definir la función que
cuenta un número de secuencia de un elemento dado como tarea).

miembro(X,[X|_]).

miembro(X,[_|T]):-miembro(X,T).

Miembro_nth(1,[M|_],M).

Miembro_nth(N,[_|T],M):-N>1,N1 is N-1,miembro_nth (N1,T,M).


Otra función popular sobre listas es agregar el cual agrega una lista a otra lista. Puede también ser usada
para separar listas. (ver la siguiente definición de prefijo y sufijo). Este procedimiento es uno de los más
representativos en la programación lógica. Sugerimos que se le compare con su equivalente en otros lenguajes
(Note que no es solamente agregar una lista a otra).

agregar([],L,L).

agregar([C|T],L,[C|LT]):-agregar(T,L,LT).

Ahora, las relaciones prefijo y sufijo pueden ser fácilmente redefinidas usando agregar:

prefijo(P,L):-agregar(P,_,L).

sufijo(S,L):-agregar(_,S,L).

Agregar puede ser exitosamente usado en muchas otras operaciones con listas incluyendo probar (o generar)
sublistas. La siguiente regla explota otra vez el carácter declarativo de PROLOG.

sublista(S,L):-agregar(_,S,P),agregar(P,_,L).

Hay (al menos) dos otras formas de como definir sublista, ejemplo, usando las relaciones prefijo y sufijo.
Todas estas definiciones son equivalentes. Sin embargo, el procedimiento sublista3 es probablemente lo más
cercano al estilo de programación tradicional (no declarativo) ya que usa la técnica conocida como "floating
window".

sublista2(S,L):-prefijo(P,L), sufijo(S,P).

sublista3(S,L) :- prefijo(S,L).

sublista3(S,[_|T]) :- sublista3(S,T).

REGRESO AL EJEMPLO DE FECHA

Vamos a retornar a nuestro ejemplo de la estructura de datos fecha. Ahora, vamos a definir la prueba
fecha_correcta usando listas.

Primero, agregamos dos hechos a la base de datos PROLOG con la distribución de días:

año(regular,[31,28,31,30,31,30,31,31,30,31,30,31]).

año(bisiesto,[31,29,31,30,31,30,31,31,30,31,30,31]).

Entonces, preparamos la prueba para año bisiesto (versión simplificada):

es_año_bisiesto(A):-

Z is A mod 4, Z=0. % cada cuatro años es bisiesto(simplificada)


Finalmente, es posible probar la correctitud de fecha:

fecha_correcta(fecha(D,M,A)):-

mes_correcto(M),

dia_correcto(D,M,A).

mes_correcto(M):- M>0, M<13.

dia_correcto (D,2,A):- % la única diferencia entre años bisiesto y regular es en


febrero

es_año_bisiesto(A),

probar_dia_del_año(D,2,bisiesto).

dia_correcto (D,M,A):-

M\=2,
probar_dia_del_año(D,2,regular).

probar_dia_del_año (Tipo,M,D):-

año(Tipo,Dias),
miembro_nth(M,Dias,Max),
D>0, D=<Max.

Notar, que la definición anteriorde dia_correcto asume que la única diferencia entre años regular y bisiesto es
el número de días en febrero el cual es más alto en años bisiestos.

FLUJO DE DATOS Y RECURSIÓN

En ésta lección hablaremos acerca del flujo de datos especialmente durante la manipulación con
estructuras de datos recursivas. Recursión es una técnica básica que es usada para manipular datos para los
cuales su tamaño no es conocido al comienzo (por otro lado iteración es probablemente más conveniente).
Usualmente, tales datos son representados usando definición recursiva.

REPRESENTACIÓN UNARIA DE LOS NÚMEROS


PROLOG tiene su propia representación de los números. Sin embargo, para propósito de éste tutorial,
definiremos otra representación de los números naturales. Ésta simple representación unaria nos ayudará a
presentar un flujo de datos durante la recursión. Notar que es un ejemplo típico de definición recursiva donde la
"semilla" (cero) es definida y otros elementos (números) son sucesivamente creados de elementos definidos
previamente.

0 es representado como 0

N+1 es representado como s(X),donde X es una representación de N

La recursión puede ser naturalmente expresada en PROLOG (¿recuerdas la definición de último o


miembro en la lección anterior?). Así, es fácil definir una prueba si una estructura dada es un número unario.
Notar otra vez que el mismo procedimiento, es decir, num_unario, puede también ser usado para sucesivamente
generar "todos" los número naturales. Intentalo.

num_unario(0).

num_unario(s(X)):-num_unario(X).

Ahora, la suma de dos números usando el predicado recursivo suma(X,Y,Z) (X+Y=Z).

suma(0,X,X). % 0+X=X

suma(s(X),Y,Z) :- suma(X,s(Y),Z). % (X+1)+Y=Z <== X+(Y+1)=Z

recursión ---^ ^--- acumulador

El predicado suma usa una estructura de datos llamada acumulador para acumular el subresultado durante
el calculo recursivo. Mira el siguiente rastro (trace) del calculo para comprender la idea principal de
acumulador.

?-suma(s(s(s(0))), s(0) ,Sum). ^ Sum=s(s(s(s(0))))

?-suma( s(s(0)) , s(s(0)) ,Sum). | Sum=s(s(s(s(0))))

?-suma( s(0) , s(s(s(0))) ,Sum). | Sum=s(s(s(s(0))))


?-suma( 0 ,s(s(s(s(0)))),Sum). | Sum=s(s(s(s(0))))

?-suma( 0 ,s(s(s(s(0)))),s(s(s(s(0))))). %copia acumulador a resultado.

^-- el resultado es acumulado aquí

Es también posible definir la operación suma sin acumulador usando composición de sustituciones.

suma2(0,X,X). % 0+X=X

suma2(s(X),Y,s(Z)): - suma(X,Y,Z). % (X+1)+Y=Z+1 <== X+Y=Z

^--- composición de sustituciones

Mira el siguiente rastro (trace) de ejecución para ver la diferencia entre acumulador y composición de
sustituciones.

?-suma2(s(s(s(0))),s(0),S1). ^ S1=s(S2)=s(s(s(s(0))))

?-suma2( s(s(0)) ,s(0),S2). | S2=s(S3)=s(s(s(0)))

?-suma2( s(0) ,s(0),S3). | S3=s(S4)=s(s(0))

?-suma2( 0 ,s(0),S4). | S4=s(0)

?-suma2( 0 ,s(0),s(0)).______|

las sustituciones son compuestas aquí ----^

En este caso particular, la complejidad computacional de ambos enfoques es similar. Además, suma y
suma2 son completamente declarativos y pueden ser usados para calcular suma de números también como la
diferencia de números y, algunas restricciones, para generar todos los pares de números y, con algunas
restricciones, generar todos los pares de números para los cuales la suma está dada (intenta ?-
suma(X,Y,s(s(s(0)))).).

Siguiendo las cuatro cláusulas se muestra varias definiciones de resta (X,Y,Z) (X-Y=Z) usando suma y
suma2 respectivamente.

resta1a(X,Y,Z):-suma(Y,Z,X).
resta1b(X,Y,Z):-suma(Z,Y,X).

resta2a(X,Y,Z):-suma2(Y,Z,X).

resta2b(X,Y,Z):-suma2(Z,Y,X).

Por supuesto, resta puede también ser definida desde cero usando recursión.

resta(X,0,X). % X-0=X

resta(s(X),s(Y),Z) :- resta(X,Y,Z). % (X+1)-(Y+1)=Z <== X-Y=Z

Compáralo con el próximo procedimiento resta2 el cual está basado sobre otra característica de la
operación resta. Notar que resta2 requiere la existencia de solución (intenta ?-resta2(0,s(0),Z). ¿Qué sucede?)

resta2(X,X,0). % X-X=0

resta2(X,Y,s(Z)) :- resta(X,s(Y),Z). % X-Y=Z+1 <== X-(Y+1)=Z

LISTAS (continuación)

Lista es otra estructura de datos recursiva típica. Puede ser definida de la siguiente manera:

[] es una lista

[H|T] es una lista si T es una lista y H es un termino (miembro de la lista).

Algunos ejemplos de las operaciones con listas pueden ser encontrados en la lección anterior. Recuerda
que la definición de agregar presentada allá usa composición de sustituciones. En el caso de agregar no es
natural usar el acumulador. La definición de la operación borrar(X,L,DL) (borra un elemento X dado desde la
lista L, la lista resultante es DL) es también más natural usar composición de sustituciones. Compara los
siguientes dos procedimientos: borrar es definido usando composición de sustituciones mientras que borrar2 es
definido usando acumulador.

borrar(X,[X|T],T).
borrar(X,[Y|T],[Y|NT]) :- borrar(X,T,NT).

borrar2(X,L,DL) :- del2(X,L,[],DL).

del2(X,[X|T],A,DL) :- agregar(A,T,DL).

del2(X,[Y|T],A,DL) :- del2(X,T,[Y|A],DL).

En este caso particular, la definición de borrar es también más efectiva que borrar2 (omitimos agregar).

Los ejemplos de agregar y borrar no implican que la técnica de acumulador no es útil. Lo opuesto es
verdad y, en muchos casos, es más natural y efectivo usar acumulador. El siguiente ejemplo es el caso donde
usar acumulador es más efectivo computacionalmente que la composición de sustituciones. El procedimiento
revertir revierte una lista dada. La definición de revertir parece natural pero no es efectivo debido a que agrega
un elemento al final de la lista usando agregar en cada paso. revertir2, el cual usa acumulador, es más eficiente.

revertir([],[]).

revertir([H|T],Rev):-revertir(T,RT),agregar(RT,[H],Rev)

revertir2(L,RL):-rev_acc(L,[],RL).

rev_acc([],Acc,Acc). % La lista revertida está en Acc

rev_acc([H|T],Acc,Rev):-rev_acc(T,[H|Acc],Rev).

% Acc contiene parte de la lista revertida hasta ahora.

Algunas veces, es posible combinar ambas técnicas acumulador y composición de sustituciones para
alcanzar la solución. Mira la definición de partir la cual también explota la unificación para probar si ambas
mitades tienen la misma longitud. Sin embargo, probar la longitud en cada paso, la cual es ejecutada por la
primera cláusula hv, no es muy efectiva y por lo tanto partir no es realmente rápido.

partir(L,A,B) :- hv(L,[],A,B).
hv(L,L,[],L).

hv([H|T],Acc,[H|L],B) :- hv(T,[_|Acc],L,B).

La siguiente definición de partir2 es más eficiente que el partir original. partir2 también usa unificación
para distribuir la lista en dos mitades pero es hecho una vez al final del calculo solamente. Compare partir y
partir2 sobre ejemplos grandes (una lista con 100 000+ miembros) para ver la diferencia real.

partir2(L,A,B) :- hv2(L,L,A,B).

hv2([],R,[],R).

hv2([_,_|T],[X|L],[X|L1],R) :- hv2(T,L,L1,R).

¿ Sabes como hacer el procedimiento hv2 aún más eficiente? Hay un cambio pequeño.

Para terminar la lección de hoy, hay dos ejemplos de "vacaciones". Esperamos que estén claros cada uno
de ellos. Compare subpart con la definición de sublista de la sección anterior.

subpart([],_).

subpart([H|T],[H|T2]) :- subpart(T,T2).

subpart(L,[H|T]) :- subpart(L,T).

par_impar(L,E,O) :- impar(L,E,O).

impar([],[],[]).

impar([H|T],E,[H|O) :- par(T,E,O).

par([],[],[]).

par([H|T],[H|E],O) :- impar(T,E,O).

Otra solución a la distribución par-impar de una lista.


par_impar2([],[],[]).

par_impar2([H|T],E,[H|O]):-par_impar2(T,O,E).

La Recursión es poder.

EJECUCIÓN DE ARRIBA HACIA ABAJO (Top Down) VS. EJECUCIÓN DE ABAJO HACIA
ARRIBA (Bottom Up)

En el capítulo anterior, estudiamos la recursión, un poderoso método para resolver problemas usando la
descomposición a problemas más pequeños del mismo tipo. La recursión en PROLOG usa el método de
ejecución de arriba hacia abajo (top down) típicamente.

EJECUCIÓN DE ARRIBA HACIA ABAJO (Top Down)

El cálculo de arriba hacia abajo (Top Down), típicamente usado en PROLOG, comienza con el problema
original y lo descompone en problemas más y más simples hasta alcanzar un problema trivial, es decir, el hecho
en la base de datos PROLOG es alcanzado. Entonces, la solución de los problemas más grandes está compuesta
de las soluciones de los problemas más simples, etc. Hasta que la solución del problema original es obtenida.
Los siguientes dos ejemplos presentan programas que usan el cálculo de arriba hacia abajo (Top Down).

FACTORIAL

Para obtener el factorial de N primero calculamos el factorial de N-1 usando el mismo procedimiento
(recursión) y entonces, usando el resultado del subproblema, calculamos el factorial de N. La recursión para
cuando un problema trivial es alcanzado, es decir, cuando el factorial de 0 es alcanzado, es decir, cuando el
factorial de 0 es computado.

fact_td(0,1).
fact_td(N,F) :- N>0, N1 is N-1, fact_td(N1,F1), F is N*F1.
Notar, que la complejidad de procedimiento de arriba es lineal, es decir, necesitamos N+1 llamadas del
procedimiento fact_td.

FIBONACCI

El siguiente programa calcula los números de Fibonacci usando el método de arriba hacia abajo (Top
Down), es decir, si estamos buscando el número de Fibonacci de N>1, primero calculamos los números de
Fibonacci de N-1 y N-2 (usando el mismo procedimiento) y, entonces, componemos el número de Fibonacci de
N partiendo de los subresultados. En este caso, la recursión para tan pronto como el número de Fibonacci de 0 o
1 es computado.

fibo_td(0,0).
fibo_td(1,1).
fibo_td(N,F) :-
N>1, N1 is N-1, N2 is N-2,
fibo_td(N1,F1), fibo_td(N2,F2),
F is F1+F2.

Notar, que el procedimiento de arriba es muy ineficiente si la regla de ejecución de PROLOG estándar es
usada porque calculamos la misma cosa muchas veces. Por ejemplo, para calcular F(N-1) necesitamos calcular
F(N-2) lo cual también es requerido para calcular F(N) que fue descompuesto en F(N-1) y F(N-2). De hecho, la
complejidad del procedimiento de arriba es exponencial eso es "no muy eficiente".

EJECUCIÓN DE ABAJO HACIA ARRIBA (Bottom Up)

Aún si el cálculo de arriba hacia abajo es típica para PROLOG, podemos aún simular el cálculo de abajo
hacia arriba (Bottom Up) también sin hacer cambios al interpretador de PROLOG. El cálculo de abajo hacia
arriba (Bottom Up) comienza con conocer los hechos y extender el conjunto de verdades conocidas usando
reglas, es decir, derivar nuevos hechos de reglas y hechos viejos. Esta extensión continúa hasta que el problema
resuelto es presentado en el conjunto computado de hechos. En general, el método de abajo hacia arriba
(Bottom Up) puro es menos eficiente que el método de arriba hacia abajo (top down)porque muchos hechos son
derivados y no tienen nada en común con el problema original. Por lo tanto, PROLOG usa la evaluación de
arriba hacia abajo (top down) como un método estándar de ejecución. Sin embargo, en los mismos casos, es
posible guiar la evaluación de abajo hacia arriba (Bottom Up) hacia la solución del problema original. Los
siguientes dos ejemplos presentan versiones de abajo hacia arriba (Bottom Up) de los dos procedimientos de
arriba para calcular factorial y números de Fibonacci. La ventaja de la evaluación de abajo hacia arriba (Bottom
Up) es visible principalmente en el procedimiento fibo_bu que es más rápido que fibo_td.
FACTORIAL

Ahora, calculamos el factorial usando el método de abajo hacia arriba (Bottom Up) comenzando con el
problema trivial de calcular el factorial de 0 y continuar con el factorial de 1,2 y así sucesivamente hasta que el
factorial de N es conocido. Ya que no queremos cambiar la conducta estándar de PROLOG, que es por
naturaleza top down, necesitamos simular el cálculo de abajo hacia arriba (Bottom Up), es decir, tenemos que
almacenar los hechos calculados usando parámetros adicionales. En este caso, recordamos sólo el último
"hecho" calculado a saber el factorial de M en el paso M-th.

fact_bu(N,F) :- fact_bu1(0,1,N,F).

fact_bu1(N,F,N,F).
fact_bu1(N1,F1,N,F) :-
N1<N, N2 is N1+1, F2 is N2*F1, fact_bu1(N2,F2,N,F).

Notar, que la complejidad del procedimiento de arriba es lineal otra vez, es decir, necesitamos N+1
llamadas del procedimiento fact_bu1. Sin embargo, el procedimiento fact_bu1 es más eficiente que fact_td
porque puede ser traducido usando iteración, es decir, sin una pila la cual es requerida para la recursión en
fact_td.

FIBONACCI

Podemos usar el mismo principio como en " factorial bottom-up" para calcular los números Fibonacci
usando el método de abajo hacia arriba (Bottom Up). Ahora, necesitamos recordar los últimos dos números de
Fibonacci para ser capaz de calcular el próximo número de Fibonacci.

fibo_bu(N,F) :- fibo_bu1(0,0,1,N,F).

fibo_bu1(N,F,_,N,F)
fibo_bu1(N1,F1,F2,N,F) :-
N1<N, N2 is N1+1, F3 is F1+F2,
fibo_bu1(N2,F2,F3,N,F).

Notar, que la complejidad de fibo_bu es lineal y por lo tanto el procedimiento fibo_bu es más eficiente que fibo_td el cual es
exponencial.
El número de Fibonacci de 0 es F(0)=0
El número de Fibonacci de 1 es F(1)=1
El número de Fibonacci de N>1 es F(N)=F(N-1)+F(N-2)

ESTRUCTURAS DE DATOS DE PROLOG

En este punto podemos hablar más formalmente de las estructuras de datos (suponiendo que las secciones
anteriores han sido cubiertas). El término (term) es la estructura de datos básica en PROLOG. Es decir, todas las
cosas incluyendo un programa y los datos puede ser expresados en forma de término. Hay cuatro tipos básicos
de términos en PROLOG: variables, términos compuestos, átomos y números. El siguiente diagrama muestra la
correlación entre ellos también como ejemplos de los términos correspondientes:

término
|-- var {X , Y}
|-- nonvar {a,1,f(a),f(X)}
|-- compuesto {f(a),f(X)}
|-- atómico {a, 1}
|-- átomo {a}
|-- número {1}

Es posible usar los predicados var, nonvar, compuesto, atómico, átomo y numero para probar el tipo
del término dado (ver copiar_term abajo). PROLOG también provee predicados predefinidos para accesar la
estructura de términos nonvar y también para construir términos.

arg(N,Term,Arg)
- obtiene el argumento N th del término (get(2,f(a,b,c),X) -> X=b)
- coloca el argumento N th del término (get(2,f(X,Y,Z),b) -> f(X,b,Z))

functor(Term,Functor,NumberOfArgs)
- obtiene el nombre de la función y el número de argumentos desde el término
(functor(f(a,b),F,N) -> F=f,N=2)
- construye el término con la función dada y el número de argumentos libres
(functor(F,f,2) -> F=f(_,_))
=..
- descompone la estructura del término hacia la lista (f(a,b)=..X -> X=[f,a,b])
- construye el término desde la lista dada (T=..[f,a,X] -> T=f(a,X))

name(Text,List)
- convierte el nombre hacia la lista de códigos (name(abc,Y) -> Y=[97,98,99])
- construye el nombre desde la lista de códigos (name(X,[97,98,99]) -> X=abc)

Si alguien necesita copiar un término (copiar tiene la misma estructura como el término original pero
introduce nuevas variables), es posible usar el predicado copiar_term/2 el cual está incorporado en la mayoría
de los sistemas PROLOG. Sin embargo, es directo escribir un código de copiar_term en PROLOG usando los
predicados mencionados arriba.

copiar_term(A,B) :- cp(A,[],B,_).

cp(A,Vars,A,Vars) :-
atómico(A).
cp(V,Vars,NV,NVars) :-
var(V),registrar_var(V,Vars,NV,NVars).
cp(Term,Vars,NTerm,NVars) :-
compuesto(Term),
Term=..[F|Args], % descomponer el término
cp_args(Args,Vars,NArgs,NVars),
NTerm=..[F|NArgs]. % construye copia del término

cp_args([H|T],Vars,[NH|NT],NVars) :-
cp(H,Vars,NH,SVars),
cp_args(T,SVars,NT,NVars).
cp_args([],Vars,[],Vars).

Durante el copiado alguien tiene que recordar las copias de variables las cuales pueden ser usadas
adicionalmente durante el copiado. Por lo tanto, el registro de las copias de las variables es mantenido.

registrar_var(V,[X/H|T],N,[X/H|NT]) :-
V\==X, % variables diferentes
registrar_var(V,T,N,NT).

registrar_var(V,[X/H|T],H,[X/H|T]) :-
V==X. % variables iguales

registrar_var(V,[],N,[V/N]).

Aquí está un ejemplo que clarifica la noción de copiar un término:


f(X,g(X)) es copia de f(Y,g(Y)) pero no de f(U,g(V)).

UNIFICACIÓN

Unificación es un mecanismo fundamental en todos los lenguajes de programación lógica y crucial en el


PROLOG. Con unificación se trata de encontrar la mayoría de sustituciones generales de variables en dos
términos tal que después de aplicar ésta sustitución a ambos términos, los términos lleguen a ser iguales. Para
unificar los términos A y B, uno puede fácilmente invocar la unificación incorporada A=B. Tratar de unificar
términos diferentes para ver lo que la noción de "unificación" realmente significa. Otra vez, es directo escribir
código PROLOG de unificación (usamos '=' para probar la igualdad de dos términos atómicos o para unificar la
variable con el término solamente).

unificar(A,B) :-
atómico(A),atómico(B),A=B.
unificar(A,B) :-
var(A),A=B. % sin occurs check
unificar(A,B) :-
nonvar(A),var(B),A=B. % sin occurs check
unificar(A,B) :-
compuesto(A),compuesto(B),
A=..[F|ArgsA],B=..[F|ArgsB],
unificar_args(ArgsA,ArgsB).
unificar_args([A|TA],[B|TB]) :-
unificar(A,B),
unificar_args(TA,TB).
unificar_args([],[]).

¿Averiguastes lo que significa "chequeo de ocurrencia"? OK, intenta unificar los siguientes términos X y
f(X). ¿Qué sucede? La mayoría de los sistemas PROLOG llenarán la memoria completa ya que tratarán de
construir el término infinito f(f(f(...))) el cual debería ser el resultado de la unificación. Tales sistemas PROLOG
no incorporan chequeo de ocurrencia (occurs check) debido a su consumo de tiempo. El occurs check
comprueba la ocurrencia de la variable X en el término T (el cual no es una variable) durante la unificación de
X y T. Pero, en general, hacer esto es difícil y consume mucho tiempo de procesamiento.
OPERADORES

Escribir términos en la forma funcion(arg1,arg2,...) no es frecuentemente apropiado desde el punto de


vista humano. Sólo compara las siguientes dos transcripciones de la misma cláusula PROLOG:

p(X,Z) :- q(X,Y),r(Y,Z),s(Z).

' :- '(p(X,Z),(','(q(X,Y),','(r(Y,Z),s(Z))))).

¿Cuál prefieres?

Para simplificar la entrada de términos, PROLOG introduce los operadores para permitir el "azúcar
sintáctico", es decir, una forma más natural de escribir términos. Los Operadores son usados con términos
binarios y unarios solamente. Ellos permiten colocar la localización de la función (prefijo, infijo, posfijo), la
característica asociativa y, finalmente, la prioridad entre operadores.

op(Prioridad, Apariencia, Nombre)


| |
| -- xfy, yfx, xfx, fx, fy, xf, yf
-- El número más alto tiene la prioridad más baja

En lugar de explicar el significado de la definición de arriba, mira el siguiente ejemplo.

op(400,yfx,'*'). % a*b*c significa ((a*b)*c)


op(500,yfx,'+').
op(500,yfx,'-'). % cuidado a-b-c significa ((a-b)-c)
op(700,xfx,'=') % no es posible escribir a=b=c
op(900,fy,not). % uno puede escribir not not a
% significa not(not(a))

not 1=2+3+4*5 es equivalente a:

not(1=((2+3)+(4*5)))
not('='(1,'+'('+'(2,3),'*'(4,5)))).
Nota que los números indicando prioridad pueden ser diferentes en diversas implementaciones de PROLOG (
Los números en el ejemplo de arriba son tomados desde el PROLOG abierto para Macintosh).

Importante!

La definición de operador no es un nuevo programa para operador sino la "llamada" del objetivo op. Así, si
tu quieres definir un operador en el programa, debes escribir :- op(400,yfx,'*').

Los Términos son la base de PROLOG.

PROCESAMIENTO DE LISTA

Lista es una estructura de datos directamente apoyada en PROLOG a través de operaciones para acceder
la cabeza y la cola de la lista. Sin embargo, la lista es aún un término de PROLOG tradicional, si alguien usa la
notación punto obvia. Describimos la manipulación con listas en la sección "Representando Estructuras de
Datos" así miraremos sólo listas diferenciales aquí y continuaremos presentando áreas de uso de listas.

LISTAS DIFERENCIALES o en diferencias.

El problema principal de la implementación de PROLOG de listas es su naturaleza un-camino (one-way).


Que significa que si uno quiere acceder al elemento n-th, el / ella tiene que acceder todos los elementos previos
en la lista. En particular, si uno quiere agregar un elemento al final de la lista, es necesario ir a través de todos
los elementos en la lista como el siguiente programa muestra (compárelo con la implementación de agregar):

adicionar2final(X,[H|T],[H|NewT]) :- agregar(X,T,NewT).
adicionar2final(X,[],[X]).

Pero hay una técnica, llamada listas diferenciales, que permite agregar listas o agregar elementos al final
de la lista en un paso. Sin embargo debería ser notado que esta técnica no remueve la desventaja de visitar todos
los elementos previos en la lista si el elemento n-th es accesado.
Una lista diferencial consiste de dos partes A-B y representa la lista que es obtenida desde A removiendo
la cola B, ejemplo, [1,2,3,4]-[3,4] representa la lista [1,2]. Por supuesto, si ambas listas A y B son básicas,
entonces no hay ventaja de usar listas diferenciales, pero si ésta técnica es combinada con las ventajas de
variables libres y unificación, podemos obtener resultados impresionantes. A saber, la lista [1,2] puede ser
representada por la lista diferencial [1,2|X]-X.

Si estandarizamos las listas diferenciales en la última forma, podemos escribir procedimientos en un sólo
paso para agregar listas y adicionar elementos al final de la lista:

agregar(A-B,B-D,A-D).
adicionar2final(X,A-B,[A|NuevoB]-NuevoB) :- NuevoB=[X|B].

Notar, que la otra lista de operaciones, ejemplo, miembro, tienen que también ser reescritas para trabajar
con listas diferenciales.

COMBINATORIAS

Esta clase cubre algoritmos combinatorios básicos los cuales generan sucesivamente todas las
permutaciones, combinaciones y variaciones respectivamente. Refresca tu memoria! ¿Cuántas permutaciones,
combinaciones y variaciones se pueden generar de un conjunto de N elementos? y ¿Qué pasa si los elementos
repetidos son permitidos?

PERMUTACIONES

La permutación de una lista L es una lista que contiene todos los elementos de la lista L en algún orden.
Adivina cual permutación es generada primero usando el siguiente procedimiento. Y ¿Qué pasa con el segundo?

perm(Lista,[H|Perm]) :-
borrar(H,Lista,Rest),perm(Rest,Perm).
perm([],[]).

borrar(X,[X|T],T).
borrar(X,[H|T],[H|NT]) :- borrar(X,T,NT).

COMBINACIONES
Una combinación es un subconjunto arbitrario del conjunto que contiene un número dado de
elementos. El orden de los elementos es irrelevante.

comb(0,_,[]).
comb(N,[X|T],[X|Comb]) :- N>0,N1 is N-1,comb(N1,T,Comb).
comb(N,[_|T],Comb) :- N>0,comb(N,T,Comb).

Es posible programar un generador de combinaciones sin aritmética: el siguiente procedimiento comb2


asume que la lista con N variables libres como su segundo argumento y enlaza esas variables. Así, usar ?-
comb2([1,2,3,4],[X,Y]) para generar combinaciones con dos elementos.

comb2(_,[]).
comb2([X|T],[X|Comb]) :- comb2(T,Comb).
comb2([_|T],[X|Comb]) :- comb2(T,[X|Comb]).

COMBINACIONES CON ELEMENTOS REPETIDOS

Este tipo de combinación puede contener un elemento más veces. Así, éste no es un conjunto sino un
multiconjunto.

comb_rep(0,_,[]).
comb_rep(N,[X|T],[X|RComb]) :-
N>0,N1 is N-1,comb_rep(N1,[X|T],RComb).
comb_rep(N,[_|T],RComb) :- N>0,comb_rep(N,T,RComb).

Intenta programar combinaciones con elementos repetidos también como algoritmos combinatoriales
sin aritmética, es decir, sin contador numérico.

VARIACIONES

Una variación es un subconjunto con un número dado de elementos. El orden de los elementos en la
variación es importante.
varia(0,_,[]).
varia(N,L,[H|Varia]) :- N>0,N1 is N -1,
borrar(H,L,Rest),varia(N1,Rest,Varia).

VARIACIONES CON ELEMENTOS REPETIDOS

Otra vez, este tipo de variación puede contener elementos repetidos.

varia_rep(0,_,[]).
varia_rep(N,L,[H|RVaria]) :-
N>0,N1 is N-1,borrar(H,L,_),varia_rep(N1,L,RVaria).

Las combinatorias, permutaciones y variaciones son poderosas para expresar la complejidad de un


algoritmo pero no las incorpore en sus programas.

ORDENAMIENTOS

Esta clase cubre los algoritmos de ordenamiento. Noten que los algoritmos de ordenamiento se codifican
en PROLOG en términos muy parecidos al lenguaje natural y, por demás, muy breves. Al mismo tiempo, es
también posible codificar las estrategias más eficiente de ordenamiento.

ORDENAMIENTO INGENUO (naive sort)

El ordenamiento ingenuo (Naive sort) no es un algoritmo muy eficiente. Genera todas las permutaciones
y luego prueba si la permutación es una lista ordenada. Sin embargo, es muy fácil entender la idea de
ordenamiento en este contexto.

ordenamiento_ingenuo(Lista,Ordenado) :-
perm(Lista,Ordenado),esta_ordenado(Ordenado).
esta_ordenado([]).
esta_ordenado)[_]).
esta_ordenado([X,Y|T]) :- X<=Y,esta_ordenado([Y|T]).

El ordenamiento ingenuo (Naive sort) usa el enfoque generar y probar para resolver problemas, el cual es
usualmente utilizado en el caso que todas las demás cosas fallen. Sin embargo, en ordenamiento hay varias
otras, muy eficientes, estrategias.

ORDENAMIENTO POR INSERCIÓN (insert sort)

El ordenamiento por inserción (Insert sort) es un algoritmo de ordenamiento tradicional. La


implementación de PROLOG del ordenamiento por inserción (Insert sort) está basada en la idea de acumulador.

ordenamiento_insercion(Lista,Ordenado) :-
ordenamiento_i(Lista,[],Ordenado).
ordenamiento_i([],Acc,Acc).

ordenamiento_i ([H|T],Acc,Ordenado) :-
insercion(H,Acc,NAcc), ordenamiento_i (T,NAcc,Ordenado).

insercion(X,[Y|T],[Y|NT]) :- X>Y,insercion(X,T,NT).
insercion(X,[Y|T],[X,Y|T] :- X<=Y.
insercion(X,[],[X]).

ORDENAMIENTO BURBUJA (bubble sort)

El ordenamiento burbuja (Bubble sort) es otro algoritmo de ordenamiento tradicional el cual no es muy
efectivo. Otra vez, usamos acumulador para implementar el ordenamiento burbuja (Bubble sort).

ordenamiento_burbuja(Lista,Ordenado) :-
ordenamiento_b(Lista,[],Ordenado).
ordenamiento_b([],Acc,Acc).
ordenamiento_b([H|T],Acc,Ordenado) :-
burbuja(H,T,NT,Max),ordenamiento_b(NT,[Max|Acc],Ordenado).

burbuja(X,[],[],X).
burbuja(X,[Y|T],[Y|NT],Max) :- X>Y,burbuja(X,T,NT,Max).
burbuja(X,[Y|T],[X|NT],Max) :- X<=Y,burbuja(Y,T,NT,Max).
ORDENAMIENTO DE MEZCLA (merge sort)

El ordenamiento de mezcla (Merge sort) es usualmente usado para ordenar grandes archivos pero su idea
puede ser utilizada con listas. Si es apropiadamente implementado podría ser un algoritmo muy eficiente.

ordenamiento_mezcla([],[]).
ordenamiento_mezcla(Lista,Ordenado) :-
divide(Lista,L1,L2),
ordenamiento_mezcla(L1,Ordenado1),
ordenamiento_mezcla(L2,Ordenado2),
mezcla(Ordenado1,Ordenado2,Ordenado).
mezcla([],L,L).
mezcla(L,[],L) :- L\=[].
mezcla([X|T1],[Y|T2],[X|T]) :- X<=Y,mezcla(T1,[Y|T2],T).
mezcla([X|T1],[Y|T2],[Y|T]) :- X>Y,mezcla([X|T1],T2,T).

Podemos usar distribución hacia elementos pares e impares de una lista (otras distribuciones son también
posible).

divide(L,L1,L2) :- par_impar(L,L1,L2).

ORDENAMIENTO RÁPIDO (quick sort)

El ordenamiento rápido (Quick sort) es uno de los algoritmos de ordenamiento más rápido. Sin
embargo, su poder es frecuentemente sobrevaluado. La eficiencia del ordenamiento rápido (Quick sort) es muy
sensible a la escogencia del pivote, el cual es usado para dividir la lista en dos.

quick_sort([],[]).
quick_sort([H|T],Ordenado) :-
pivote(H,T,L1,L2),quick_sort(L1,Ordenado1),quick_sort(L1,Ordenado2),
agregar(Ordenado1,[H|Ordenado2]).
pivote(H,[],[],[]).
pivote(H,[X|T],[X|L],G) :- X<=H,pivote(H,T,L,G).
pivote(H,[X|T],L,[X|G]) :- X>H,pivote(H,T,L,G).

Similarmente a merge sort, quick sort explota el método divide y vencerás para resolver problemas.

La implementación de arriba de quick sort usa agregar no es muy efectiva. Podemos escribir un mejor
programa usando acumulador.

quick_sort2(Lista,Ordenado) :- q_sort(Lista,[],Ordenado).

q_sort([],Acc,Acc).
q_sort([H|T],Acc,Ordenado) :-
pivote(H,T,L1,L2),
quick_sort(L1,Acc,Ordenado1),quick_sort(L1,[H|Ordenado1],Ordenado)

Algún tipo de ordenamiento puede ser encontrado en casi todo los programas actuales.

CONJUNTOS EN PROLOG

Los Conjuntos son una poderosa estructura de datos que pueden ser naturalmente expresados usando
listas en PROLOG. Para mejorar la eficiencia de la implementación usamos listas ordenadas como
representación de conjuntos. Así, definimos la relación "menor que " y "está en la lista" de la siguiente manera:

lista([]).
lista([_|_]).

lt(X,Y) :- var(X);var(Y).
lt(X,Y) :- nonvar(X),nonvar(Y),X<Y.

UNIÓN, INTERSECCIÓN, DIFERENCIA Y SELECCIÓN

Ahora, agregamos definiciones para operaciones de conjuntos obvias como unión, intersección y
diferencia. Notar, como explotamos el orden de los elementos del conjunto en los procedimientos siguientes.
union([],S,S).
union(S,[],S) :- S\=[].
union([X|TX],[X|TY],[X|TZ]) :-
union(TX,TY,TZ).
union([X|TX],[Y|TY],[X|TZ]) :-
lt(X,Y),
union(TX,[Y|TY],TZ).
union([X|TX],[Y|TY],[Y|TZ]) :-
lt(Y,X),
union([X|TX],TY,TZ).

interseccion([],S,[]).
interseccion(S,[],[]) :- S\=[].
interseccion([X|TX],[X|TY],[X|TZ]) :-
interseccion(TX,TY,TZ).
interseccion([X|TX],[Y|TY],TZ) :-
lt(X,Y),
interseccion(TX,[Y|TY],TZ).
interseccion([X|TX],[Y|TY],TZ) :-
lt(Y,X),
interseccion([X|TX],TY,TZ).

diferencia([],S,[]).
diferencia(S,[],S) :- S\=[].
diferencia([X|TX],[X|TY],TZ) :-
diferencia(TX,TY,TZ).
diferencia([X|TX],[Y|TY],[X|TZ]) :-
lt(X,Y),
diferencia(TX,[Y|TY],TZ).
diferencia([X|TX],[Y|TY],TZ) :-
lt(Y,X),
diferencia([X|TX],TY,TZ).

Podemos también definir una operación selección la cual selecciona los elementos satisfaciendo una
condición dada desde un conjunto. Noten como usamos copiar_term y llamada para probar la condición.
También, usamos la operación si-entonces-sino (Cond -> entonces_rama ; sino_rama) para acortar el
procedimiento.
seleccionar([],X,Cond,[]).
seleccionar([H|T],X,Cond,Sel) :-
copiar_term(X-Cond,XC-CondC),
H=XC,
(llamada(CondC) -> Sel=[H|R] ; Sel=R),
seleccionar(T,X,Cond,R).

Si no comprendes completamente el significado y uso de la operación selección intenta ejemplos como el


siguiente:

?-seleccionar([1,2,3,4],X, X>2,Resultado).

SUBCONJUNTO Y MEMBRESÍA

Es también posible definir relaciones naturales entre conjuntos como subconjunto y membresía. Otra
vez notar el uso del orden del elemento que mejora la eficiencia de los procedimientos.

subconjunto([],V).
subconjunto([H|T1],[H|T2]) :- subconjunto(T1,T2).
subconjunto([H1|T1],[H2|T2]) :- lt(H2,H1),subconjunto([H1|T1],T2).

in(X,[X|T]).
in(X,[Y|T]) :- lt(Y,X),in(X,T).

OPERADORES PARA CONJUNTOS

Finalmente, definimos operadores los cuales nos ayudan a escribir operaciones de conjuntos y
relaciones de una manera natural. Notar la prioridad diferente de los operadores.

:- op(400,yfx,/-\). % interseccion
:- op(500,yfx,\-/). % union
:- op(600,yfx,\). % diferencia
:- op(700,xfx,es_conjunto).
:- op(700,xfx,esta_in).
:- op(700,xfx,es_subconjunto).

La operación "es_conjunto" evalúa la expresión con conjuntos. Corresponde a la operación de PROLOG


"is".

S es_conjunto S1 \-/ S2 :-
SS1 es_conjunto S1,
SS2 es_conjunto S2,
union(SS1,SS2,S).

S es_conjunto S1 /-\ S2 :-
SS1 es_conjunto S1,
SS2 es_conjunto S2,
interseccion(SS1,SS2,S).

S es_conjunto S1 \ S2 :-
SS1 es_conjunto S1,
SS2 es_conjunto S2,
diferencia(SS1,SS2,S).

S es_conjunto sel(X,Cond,Set) :-
SSet es_conjunto Set,
seleccionar(SSet,X,Cond,S).

S es_conjunto S :- Lista(S).

Podemos agregar las operaciones miembro y subconjunto.

X is_in S :- SS es_conjunto S, in(X,SS).

U is_subconjunto V :- US es_conjunto U, VS es_conjunto V, subconjunto(US,VS).

Ok, ahora podemos usar conjuntos en una forma obvia.

?- [1,2,3] /-\ [2,3,4] es_subconjunto [1,2,3] \-/ [2,3,4].


?- S es_conjunto ([1,2,3] \ [2,4,5] /-\ [2,6,7])\-/[2,3,6].
?- X is_in [1,2,3] \-/ [3,4,5].
...

REPRESENTACIÓN COMPACTA

La representación de arriba de conjuntos es satisfactoria para los conjuntos pequeños, pero no es


eficiente para conjuntos grandes que contienen bloques compactos. Asi, ofrecemos la siguiente representacion
compacta de conjuntos:

conjunto [1,2,3,4,5,7,8,9] es representado como [1...5,7...9]

Para usar esta representación definimos "el operador compacto ..." primero:

op(100,xfx,'...')

Por supuesto, tenemos que redefinir los procedimientos de arriba para unión, intersección, diferencia,
seleccionar, subset, y in.

c_in(X,[X|T]) :- X\=A...B.
c_in(X,[A...B|T]) :- in_intervalo(X,A,B).
c_in(X,[Y|T]) :- Y\=A...B,lt(Y,X),c_in(X,T).
c_in(X,[A...B|T]) :- lt(B,X),c_in(X,T).

in_intervalo(X,X,B).
in_intervalo(X,A,B) :- nonvar(X),X=B.
in_intervalo(X,A,B) :- nonvar(X),lt(A,X),lt(X,B).
in_intervalo(X,A,B) :- var(X),lt(A,B),A1 is A+1,in_intervalo(X,A1,B).

Reescriba los otros procedimientos como tarea.

La representación compacta en conjunción con la poderosa operación seleccionar permite compactar la


descripción de diversos conjuntos.

sel(X,par(X),[1...100]) % conjunto de números pares entre 1 y 100.


Los operadores pueden mejorar legibilidad de los programas PROLOG.

PROCESADOR DE LISTA GENERALIZADO

En esta sección presentamos un procesador de lista generalizado, el cual es capaz de hacer diversas
operaciones de lista dependiendo de la definición de las funciones e y f. Este programa es una versión
simplificada del procesador de lista generalizado de R.A. O'Keefe's mostrado en el libro The Craft of PROLOG,
MIT, 1990.

procesador_lista([],R) :- e(R).
procesador_lista([H|T],R) :-
procesador_lista(T,TR),
f(H,TR,R).

Si definimos las funciones e y f en la siguiente forma:

e(0).
f(A,B,C) :- C is A+B.

El programa resultante suma los elementos en la lista.

La siguiente definición:

e(0/0).
f(X,A/B,A1/B1) :- A1 is A+X, B1 is B+1.

Puede ser usada para calcular el promedio de los elementos de la lista dada.

El mismo esquema puede ser usado para ordenar los elementos en la lista o para generar permutaciones.
Primero definimos las funciones e y f.

e([]).
f(X,L,R) :- insertar(X,L,R).
Si el procedimiento insertar es definido en la siguiente forma:

insertar(X,[],[X]).
insertar(X,[Y|T],[X,Y|T]) :- X<=Y.
insertar(X,[Y|T],[Y|NT]) :- X>Y,insertar(X,T,NT).

El programa resultante ordena la lista.

Si la siguiente definición de insertar es usada:

insertar(X,T,[X|T]).
insertar(X,[Y|T],[Y|NT]) :- insertar(X,T,NT).

El programa resultante genera las permutaciones de la lista dada sucesivamente.

EXPRESIONES ARITMÉTICAS

En esta clase trabajaremos con expresiones aritméticas en forma simbólica, lo cual es natural para
PROLOG. Primero escribimos un programa para evaluar expresiones aritméticas y entonces desarrollar un
simple compilador el cual traduce una expresión aritmética hacia un código lineal para un mecanismo de pila.

EVALUACIÓN

Podemos fácilmente evaluar la expresión aritmética usando el evaluador incorporado de PROLOG.

eval_ingenua(Expr,Res) :- Res is Expr.

Sin embargo, para propósitos de éste tutorial preferimos el siguiente evaluador el cual atraviesa la
estructura del término evaluado. Note, la descomposición natural del término a través de la unificación y las
funciones incorporadas +,-,*. Para simplificar el problema, omitimos el operador división(/).

eval(A+B,CV) :- eval(A,AV),eval(B,BV),CV is AV+BV.


eval(A-B,CV) :- eval(A,AV),eval(B,BV),CV is AV-BV.
eval(A*B,CV) :- eval(A,AV),eval(B,BV),CV is AV*BV.
eval(Num,Num) :- numero(Num).

Ahora, podemos fácilmente extender el programa de arriba para permitir "variables" en el término
evaluado. Esas variables son representadas por átomos de PROLOG como a, b o c, así ellos no corresponden a
variables PROLOG. Por supuesto, tenemos que notificar los valores de las variables al programa evaluador.
Así, la lista de pares variable / valor también como la expresión evaluada es pasada al evaluador. Para obtener el
valor de la variable dada utilizamos la función miembro que es definido en una de las clases anteriores.

eval_v(A+B,CV,Vars) :- eval_v(A,AV,Vars),eval_v(B,BV,Vars),CV is AV+BV.


eval_v(A-B,CV,Vars) :- eval_v(A,AV,Vars),eval_v(B,BV,Vars),CV is AV-BV.
eval_v(A*B,CV,Vars) :- eval_v(A,AV,Vars),eval_v(B,BV,Vars),CV is AV*BV.
eval_v(Num,Num,Vars) :- numero(Num).
eval_v(Var,Valor,Vars) :- atomo(Var),miembro(Var/Valor,Vars).

Intenta ?-eval_v(2*a+b,Val,[a/1,b/5]) para probar el programa de arriba.

COMPILACIÓN

La evaluación de expresiones aritméticas puede ser fácilmente extendida hacia generar código lineal
para algún mecanismo de pila abstracto. Usamos un mecanismo de pila con las siguientes instrucciones:

 sacar(X)- coloca un elemento X en el tope de la pila


 meter(X)- remueve el elemento X del tope de la pila
 sumar,restar,(*)times(X,Y,Z)- calcula el valor correspondiente de Z a partir de los números X,Y
 obt_valor(X,V)- obtiene el valor de la variable X desde memoria
 col_valor(X,V)- coloca el valor de la variable X en memoria
 X,Y,Z,V son asumidos como registros permanentes del mecanismo de pila.

Notar, que usamos el acumulador para recoger el código y éste es realmente generado desde el final al
comienzo.

expr_gen(A+B,InCode,OutCode) :-
expr_gen(B,[sacar(X),sacar(Y),sumar(X,Y,Z),meter(Z)|InCode],TempCode),
expr_gen(A,TempCode,OutCode).

expr_gen(A-B,InCode,OutCode) :-
expr_gen(B,[sacar(X),sacar(Y),restar(X,Y,Z),meter(Z)|InCode],TempCode),
expr_gen(A,TempCode,OutCode).

expr_gen(A*B,InCode,OutCode) :-
expr_gen(B,[sacar(X),sacar(Y),times(X,Y,Z),meter(Z)|InCode],TempCode),
expr_gen(A,TempCode,OutCode).

expr_gen(Num,InCode,[meter(Num)|InCode]) :- numero(Num).
expr_gen(Var,InCode,[obt_valor(Var,Valor),meter(Valor)|InCode]) :- atomo(Var).

Si podemos generar el código para evaluar expresiones es fácil adicionar un generador por asignación. El
programa compilado es una lista de asignaciones entonces.

prog_gen([A=Expr|Rest],InCode,Code) :-
atomo(A),
prog_gen(Rest,InCode,TempCode),
expr_gen(Expr,[sacar(X),col_valor(A,X)|TempCode],Code).

prog_gen([],Code,Code).

Ahora, escribimos un interpretador de código máquina generado. El interpretador usa Pila para evaluar
expresiones aritméticas y Memoria para recordar los valores de las variables. El código PROLOG del
interpretador sigue naturalmente la semántica de las instrucciones usadas: sacar, meter, sumar, restar, times,
obt_valor y col_valor.

eval_prog([meter(X)|Code],Pila,Memoria) :-
eval_prog(Code,[X|Pila],Memoria).
eval_prog([sacar(X)|Code],[X|Pila],Memoria) :-
eval_prog(Code,Pila,Memoria).
eval_prog([sumar(X,Y,Z)|Code],Pila,Memoria) :-
Z is X+Y,
eval_prog(Code,Pila,Memoria).
eval_prog([restar(X,Y,Z)|Code],Pila,Memoria) :-
Z is X-Y,
eval_prog(Code,Pila,Memoria).
eval_prog([times(X,Y,Z)|Code],Pila,Memoria) :-
Z is X*Y,
eval_prog(Code,Pila,Memoria).
eval_prog([obt_valor(X,Valor)|Code],Pila,Memoria) :-
miembro(X/Valor,Memoria),
eval_prog(Code,Pila,Memoria).
eval_prog([col_valor(X,Valor)|Code],Pila,Memoria) :-
col_valor(X,Valor,Memoria,NuevaMemoria)
eval_prog(Code,Pila,NuevaMemoria).
eval_prog([],Pila,Memoria) :-
imprimir_Memoria(Memoria).

El colocar el valor de la variable no es tan directo como obtener el valor de la variable usando miembro. Si
la variable está en la memoria, su valor tiene que ser cambiado, sino un nuevo par variable / valor es agregado a
la memoria.

col_valor(X,Valor,[X/_|T],[X/Valor|T]).
col_valor(X,Valor,[Y/V|T],[Y/V|NuevoT) :-
X\=Y,col_valor(X,Valor,T,NuevoT).
col_valor(X,Valor,[],[X/Valor]).

Finalmente, cuando el interpretador eval_prog encuentra el final del código el cual es indicado por la lista
vacía, imprime los contenidos de la memoria, es decir, los valores de todas las variables las cuales son usadas en
el programa.

imprimir_Memoria([X/Valor|T]) :-
write(X=Valor),nl,imprimir_Memoria(T).
imprimir_Memoria([]) :- nl.

Para encapsular el generador compilador /código y el evaluador interpretador/código, introducimos la


siguiente cláusula.

ejecutar(Prog) :-
prog_gen(Prog,[],Code),
eval_prog(Code,[],[]).

Puedes intentar ?-ejecutar([a=5,b=a+2,a=3*a+b]) para probar el programa. Pero, si uno usa la variable con
un valor indefinido, ejemplo, ?-ejecutar([a=b+2])? El programa falla. Mejorar el programa en una forma que
imprima un mensaje notificando las variables indefinidas durante la interpretación o mejor, detectará las
variables indefinidas durante la compilación.

OPTIMIZACIÓN

Mire el código generado por prog_gen. ¿Es posible optimizar el código en alguna forma? Por supuesto,
es posible. Aquí está un ejemplo de tal optimizador trivial el cual remueve todos los pares sucesivos meter-sacar
y unifica sus argumentos. Está claro que si un elemento es metido a una pila y otro elemento es sacado desde la
misma pila entonces inmediatamente ambos elementos son iguales (por unificación).

optimizar([meter(X),sacar(Y)|T],OptCode) :-
X=Y,
optimizar(T,OptCode).
optimizar([H|T],[H|OptCode]) :-
optimizar(T,OptCode).
optimizar([],[]).

Ahora, insertamos el optimizador entre el generador y el ejecutor para obtener la ejecución del programa
optimizado.

opt_ejecutar(Prog) :-
prog_gen(Prog,[],Code),
optimizar(Code,OptCode),
eval_prog(OptCode,[],[]).

¿Te gustó la aplicación presentada arriba? Si es así, puedes extenderlo a tu gusto desarrollando un
compilador y ejecutor completo para un lenguaje de programación escogido. Por ejemplo, pensar en incorporar
una construcción si-entonces-sino para el lenguaje de arriba.

PROLOG es un lenguaje de programación ideal para manejar datos simbólicos.

EXPRESIONES BOOLEANAS
Este capítulo extiende el trabajo con expresiones en una dirección diferente, pero muy interesante para la
computación fundamental. Primero, escribimos un programa para evaluar una expresión booleana el cual es
similar a la evaluación de expresiones aritméticas. En la segunda parte, trabajaremos con la expresión en una
manera simbólica y escribiremos un programa para transformar una expresión booleana en forma normal
conjuntiva.

DEFINICIÓN DE OPERADORES

Antes de comenzar a trabajar con expresiones booleanas (lógicas), definimos algunos operadores los
cuales simplifican la entrada de tales expresiones.

:- op(720,fy,non).
:- op(730,yfx,and).
:- op(740,yfx,or).

Ahora, podemos escribir (non a and b) en lugar de and(non(a),b) que es más voluminoso.

Podemos definir meta-operaciones las cuales pueden ser completamente transformadas hacia operaciones
clásicas and, or, non.

:- op(710,yfx,implica).
:- op(710,yfx,equiv).
:- op(740,yfx,xor).

EVALUACIÓN

Durante la evaluación de una expresión aritmética explotamos el evaluador incorporado is el cual


calcula el valor de una expresión numérica. Ahora, tenemos que definir procedimientos para evaluar las
operaciones and, or, not en PROLOG.

and_d(false,true,false).
and_d(false,false,false).
and_d(true,false,false).
and_d(true,true,true).
or_d(false,true,true).
or_d(false,false,false).
or_d(true,false,true).
or_d(true,true,true).

non_d(true,false).
non_d(false,true).

Deberiamos también indicar cuales valores puden ser usados en las expresiones.

logic_const(true).
logic_const(false).

Ahora, es fácil escribir un evaluador para expresiones booleanas.

eval_b(X,X) :- logic_const(X).
eval_b(X and Y,R) :- eval_b(X,XV),eval_b(Y,YV),and_d(XV,YV,R).
eval_b(X or Y,R) :- eval_b(X,XV),eval_b(Y,YV),or_d(XV,YV,R).
eval_b(non X,R) :- eval_b(X,XV),non_d(XV,R).

La evaluación de meta-operaciones es transformada hacia la evaluación de operaciones clásicas en forma


obvia.

eval_b(X implica Y,R) :- eval_b(Y or non X, R).


eval_b(X equiv Y,R) :- eval_b(X implica Y and Y implica X, R).
eval_b(X xor Y,R) :- eval_b((X and non Y) or (Y and non X), R).

NORMALIZACIÓN

En esta sección, escribiremos un programa PROLOG para la transformación de expresiones booleanas a


la forma normal conjuntiva. La forma normal conjuntiva es una expresión en la siguiente forma:
(a or non b) and c and (b or d or non c).
Notar, que trabajamos con átomos arbitrarios en expresiones booleanas ahora y esos átomos no son
interpretados, es decir, no conocemos su valor (true o false).

Primero, removemos las meta-expresiones, es decir, implica, equiv y xor las cuales son sustituidas por
or, and y non.

ex2basic(X implica Y, R) :- ex2basic(Y or non X,R).


ex2basic(X equiv Y,R) :- ex2basic(X implica Y and Y implica X, R).
ex2basic(X xor Y,R) :- ex2basic((X and non Y) or (Y and non X), R).
ex2basic(X or Y, XB or YB) :- ex2basic(X,XB),ex2basic(Y,YB).
ex2basic(X and Y, XB and YB) :- ex2basic(X,XB),ex2basic(Y,YB).
ex2basic(non X, non XB) :- ex2basic(X,XB).
ex2basic(X,X) :- atomo(X).

Segundo, movemos la negación non a las fórmulas átomicas.

non2basic(non (X and Y),XN or YN) :- non2basic(non X,XN),non2basic(non Y,YN).


non2basic(non (X or Y),XN and YN) :- non2basic(non X,XN),non2basic(non Y,YN).
non2basic(non non X, XB) :- non2basic(X,XB)
non2basic(non X,non X) :- atomo(X).
non2basic(X and Y,XB and YB) :- non2basic(X,XB),non2basic(Y,YB).
non2basic(X or Y,XB or YB) :- non2basic(X,XB),non2basic(Y,YB).
non2basic(X,X) :- atomo(X).

Finalmente, podemos construir una forma normal conjuntiva.

ex2conj(X and Y,XC and YC) :- ex2conj(X,XC),ex2conj(Y,YC).


ex2conj(X or Y, R) :- ex2conj(X,XC),ex2conj(Y,YC),join_disj(XC,YC,R).
ex2conj(non X,non X).
ex2conj(X,X) :- atomo(X).

join_disj(X and Y,Z,XZ and YZ) :- join_disj(X,Z,XZ),join_disj(Y,Z,YZ).


join_disj(X,Y and Z,XY and XZ) :- X\=(_ and _),join_disj(X,Y,XY),join_disj(X,Z,XZ).
join_disj(X,Y,X or Y) :- X\=(_ and _),Y\=(_ and _).
Ahora, enlazamos los tres procedimientos de arriba hacia una forma compacta la cual transforma una
expresión arbitraria a su forma normal conjuntiva. Nosotros llamamos este proceso normalización.

normalize(Ex,Norm) :-
ex2basic(Ex,Bas),
non2basic(Bas,NonBas),
ex2conj(NonBas,Norm).

Intenta optimizar la forma normal conjuntiva resultante removiendo las disyunciones que contiene un
literal y su negación (ejemplo, a or non a, lo cual se sabe es true). Escribir procedimientos similares para la
transformación a forma normal disyuntiva.

PROLOG es el lenguaje ideal para la computación simbólica.

GRAFOS EN PROLOG

Un Grafo es otra estructura de datos que es ampliamente usada en los algoritmos actuales. En esta clase
describiremos una representación de grafos en PROLOG y desarrollaremos algunos programas para operaciones
de grafo típicas (coloreado, búsqueda).

REPRESENTACIÓN

Un grafo es usualmente definido como un par (V,E), donde V es un conjunto de vértices y E es un


conjunto de arcos o aristas (edges). Hay muchas representaciones posibles de grafos en PROLOG, mostraremos
dos de ellas.

Representación A mantiene los vértices y arcos en dos listas diferentes (conjuntos):

g([Vertice, ...],[e(Vertice1,Vertice2,Valor), ...])


Notar, que ésta representación es apropiada para grafos dirigidos también como para grafos no dirigidos. En
caso de grafos no dirigidos, uno puede agregar cada una de los arcos no dirigidos e(V1,V2,H) como dos arcos
dirigidos e(V1,V2,H), e(V2,V1,H) o, mejor, es posible ajustar el acceso al procedimiento arco (definido abajo).

Representación B está basada en la idea de vecindad (adyacencia) y el grafo es representado como una
lista de vértices y sus vecinos.

[Vertice-[Vertice2-Valor, ...], ...]

En este caso, la representación de grafos no dirigidos contiene cada uno de los arcos dos veces.

Aquí está el procedimiento para acceder a los arcos en la representación A.

arco(g(Es,Vs),V1,V2,Valor) :-
miembro(e(V1,V2,Valor),Vs).

Si el grafo es no dirigido, el procedimiento arco puede ser ajustado de la siguiente forma:

arco(g(Es,Vs),V1,V2,Valor) :-
miembro(e(V1,V2,Valor),Vs) ; miembro(e(V2,V1,Valor),Vs).

Aquí está el procedimiento arco para la representación B.

arco(Grafo,V1,V2,Valor) :-
miembro(V1-NB,Grafo),
miembro(V2-Valor,NB).

Ahora, es posible definir el procedimiento para encontrar la vecindad de un vértice usando el procedimiento
arco.

vecindad(Grafo,V,NB) :-
setof(V1-E,arco(Grafo,V,V1,E),NB).
En caso de la representación B es mejor (más eficiente) definir la vecindad directamente.

vecindad(Grafo,V,NB) :- miembro(V1-NB,Grafo).

Notar, que algunos grafos no usan valores en los arcos mientras otros asignan valores también a los
vértices. En esos casos, los procedimientos de arriba tienen que ser reescritos por consiguiente.

COLOREADO

La meta del coloreado de grafo es agregar un color (de la paleta limitada de colores) a cada uno de los
vértices en tal forma que los vértices adyacentes (a través de los arcos) tengan asignado diferentes colores. Aún
si el coloreado de grafo parece ser un problema sólo-teórico, los algoritmos para coloreado de grafo son
ampliamente usados en aplicaciones prácticas (satisfacción de restricción).

En ésta clase presentaremos tres algoritmos para coloreado de grafo. Comenzaremos con el algoritmo
ingenuo (naive algoritm) que implementa un método de generar y probar en una forma basta. Entonces
mejoramos el algoritmo enlazando las fases de generar y probar en un procedimiento. Finalmente,
implementamos un método más sofisticado llamado chequeo hacia delante (forward checking).

El siguiente programa usa el método generar y probar para colorear los vértices de un grafo. Primero, el
color es asignado a cada uno de los vértices y entonces el programa prueba la validez del coloreado.

% coloreado1(+Grafo, +Colores, -Coloreado)


coloreado1(g(Vs,Es),Colores,Coloreado) :-
gener(Vs,Colores,Coloreado),
probar(Es,Coloreado).

% gener(+Vertices,+Colores,-Coloreado)
gener([],_,[]).
gener([V|Vs],Colores,[V-C|T]) :-
miembro(C,Colores), % generador de colores no determinista
gener(Vs,Colores,T).

% probar(+Arcos,+Coloreado)
probar([],_).
probar([e(V1,V2)|Es],Coloreado) :-
miembro(V1-C1,Coloreado), % encuentra el color del Vértice V1
miembro(V2-C2,Coloreado), % encuentra el color del Vértice V2
C1\=C2, % prueba la diferencia de colores
probar(Es,Coloreado).

El programa de arriba no es muy eficiente porque genera muchos coloreados erróneos los cuales son
rechazados en la fase de prueba. Además, el generador omite los vértices en conflicto y genera otros coloreados
independientemente del conflicto.

Está claro que podemos probar la validez del coloreado durante la generación de colores. El siguiente
programa enlaza la generación y la prueba en un procedimiento. Notar, que usamos acumulador para salvar el
coloreado parcial.

% coloreado2(+Grafo,+Colores,-Coloreado)
coloreado2(g(Vs,Es),Colores,Coloreado) :-
gat(Vs,Es,Colores,[],Coloreado). % generar y probar

% gat(Vertices,Arcos,Colores,ColoredVertices,FinalColoreado)
gat([],_,_,Coloreado,Coloreado).
gat([V|Vs],Es,Cs,Acc,Coloreado) :-
miembro(C,Cs), % generar el color para el vértice V
probar2(Es,V,C,Acc), % probar la validez del coloreado actual
gat(Vs,Es,Cs,[V-C|Acc],Coloreado).

% probar2(+Arcos,+Vertice,+Color,+ActColoreado)
probar2([],_,_,_).
probar2([e(V1,V2)|Es],V,C,CColoreado) :-
(V=V1 -> (miembro(V2-C2,CColoreado) -> C\=C2 ; true)
;(V=V2 -> (miembro(V1-C1,CColoreado) -> C\=C1 ; true)
;true)),
probar2(Es,V,C,CColoreado).

El programa de arriba usa backtracking para encontrar otro coloreado válido, pero no es capaz de detectar
un conflicto antes de que el conflicto realmente ocurra, es decir, después de asignar el color al segundo vértice
del arco en conflicto.
Es posible mejorar la conducta del algoritmo por chequeo hacia delante (forward checking) de
conflictos. Primero, asignamos el conjunto de todos los colores posibles a cada uno de los vértices (prep).
Entonces, escogemos un vértice y su color (del conjunto de posibles colores asignados a éste vértice) y
removemos éste color de todos los vértices adyacentes (fc), es decir, removemos (alguno) de los conflictos
futuros. Por lo tanto, conocemos que el color asignado no está en conflicto con los vértices ya coloreados.

Notar, que el chequeo hacia adelante agrega alguna sobrecarga adicional al algoritmo, es posible que el
backtracking clásico podría ser más eficiente en algunos casos. También, la eficiencia del algoritmo con
chequeo hacia adelante depende de la estrategia de escoger las variables y colores para la asignación.

% coloreado3(+Grafo,+Colores,-Coloreado)
coloreado3(g(Vs,Es),Colores,Coloreado) :-
prep(Vs,Colores,ColoredVs),
gtb(ColoredVs,Es,[],Coloreado).

% prep(+Vertices,+Colores,+SuperColoreado)
prep([],_,[]).
prep([V|Vs],Colores,[V-Colores|CVs]) :-
prep(Vs,Colores,CVs).

% gtb(+SuperColoreado,+Arcos,+PartialColoreado,-Coloreado)
gtb([],_,Coloreado,Coloreado).
gtb([V-Cs|Vs],Es,Acc,Coloreado) :-
miembro(C,Cs), % selecciona solamente un color
fc(Es,V,C,Vs,ConstrainedVs), % chequeo hacia adelante
gtb(ConstrainedVs,Es,[V-C|Acc],Coloreado).

% fc(+Arcos,+Vertice,+VerticeColor,+InputSuperColoreado,-OutputSuperColoreado)
fc([],_,_,Vs,Vs).
fc([e(V1,V2)|Es],V,C,Vs,ConstrVs) :-
(V=V1 -> constr(Vs,V2,C,NuevoVs)
;(V=V2 -> constr(Vs,V1,C,NuevoVs)
;NuevoVs=Vs)),
fc(Es,V,C,NuevoVs,ConstrVs).

% constr(+InputSuperColoreado,+Vertice,-VerticeForbiddenColor,+OutputSuperColoreado)
constr([V-Cs|Vs],V,C,[V-NuevoCs|Vs]) :-
borrar(Cs,C,NuevoCs),NuevoCs\=[].
constr([V1-Cs|Vs],V,C,[V1-Cs|NuevoVs]) :-
V\=V1,
constr(Vs,V,C,NuevoVs).
constr([],_,_,[]).

borrar([],_,[]).
borrar([X|T],X,T).
borrar([Y|T],X,[Y|NuevoT]) :-
X\=Y,
borrar(T,X,NuevoT).

Noten que borrar no falla si el elemento no está presente en la lista.

BÚSQUEDA

Otro grupo de algoritmos con relación a grafos son los de búsqueda (sobre el grafo). En ésta clase
presentaremos dos algoritmos: búsqueda simple que encuentra el camino entre dos vértices y el algoritmo de
Dijkstra el cual encuentra el camino de distancia mínima desde un vértice a todos los vértices.

El siguiente programa encuentra un camino desde vértice a otro vértice. El mismo programa puede ser
usado para encontrar un camino en grafos dirigidos y no dirigidos dependiendo de la definición del
procedimiento arco. Notar, que usamos acumulador para que contenga parte del camino y prevenir ciclos.

% camino(+Grafo,+Start,+Stop,-Camino)
camino(Grafo,Start,Stop,Camino) :-
camino1(Grafo,Start,Stop,[Start],Camino).

camino1(Grafo,Stop,Stop,Camino,Camino).
camino1(Grafo,Start,Stop,ActCamino,Camino) :-
Start\=Stop,
arco(Grafo,Start,Proximo),
non_miembro(Proximo,ActCamino),
camino1(Grafo,Proximo,Stop,[Proximo|ActCamino],Camino).

non_miembro(_,[]).
non_miembro(X,[Y|T]) :-
X\=Y,
non_miembro(X,T).

El algoritmo de Dijkstra es bien conocido por encontrar el camino mínimo en grafos con arcos (no
negativos). Aquí está su implementación en PROLOG el cual encuentra la distancia mínima a todos los vértices
desde un vértice dado.

% min_dist(+Grafo,+Start,-MinDist)
min_dist(Grafo,Start,MinDist) :-
dijkstra(Grafo,[],[Start-0],MinDist).

% dijkstra(+Grafo,+CerradoVertices,+AbiertoVertices,-Distancias)
dijkstra(_,MinDist,[],MinDist).
dijkstra(Grafo,Cerrado,Abierto,MinDist) :-
escoger_v(Abierto,V-D,RestAbierto),
vecindad(Grafo,V,NB), % NB es una lista de vértices adyacentes + distancia a V

diff(NB,Cerrado,NuevoNB),
merge(NuevoNB,RestAbierto,D,NuevoAbierto),
dijkstra(Grafo,[V-D|Cerrado],NuevoAbierto,MinDist).

% escoger_v(+AbiertoVertices,-VerticeToExpand,-RestAbiertoVertices)
escoger_v([H|T],MinV,Rest) :-
escoger_minv(T,H,MinV,Rest).

escoger_minv([],MinV,MinV,[]).
escoger_minv([H|T],M,MinV,[H2|Rest]) :-
H=V1-D1, M=V-D,
(D1<D -> ProximoM=H,H2=M
; ProximoM=M,H2=H),
escoger_minv(T,ProximoM,MinV,Rest).

% diff(+ListaOfVertices,+Cerrado,-ListaOfNonCerradoVertices)
diff([],_,[]).
diff([H|T],Cerrado,L) :-
H=V-D,
(miembro(V-_,Cerrado) -> L=NuevoT ; L=[H|NuevoT]),
diff(T,Cerrado,NuevoT).
% mezclar(+ListaOfVertices,+OldAbiertoVertices,-AllAbiertoVertices)
mezclar([],L,_,L).
mezclar([V1-D1|T],Abierto,D,NuevoAbierto) :-
(remover(Abierto,V1-D2,RestAbierto)
-> VD is min(D2,D+D1)
; RestAbierto=Abierto,VD is D+D1),
NuevoAbierto=[V1-VD|SubAbierto],
mezclar(T,RestAbierto,D,SubAbierto).

remover([H|T],H,T).
remover([H|T],X,[H|NT]) :-
H\=X,
remover(T,X,NT).

Compara el procedimiento remover con el procedimiento borrar (parte de coloreado). ¿Ves la diferencia?

Extiende el programa de arriba en una forma que también encuentre el camino mínimo (no sólo la distancia
mínima) a todos los vértices.

Los algoritmos de grafo pueden ser usados para resolver muchos tipos de problemas.

ALGORÍTMO GENERALIZADO PARA BÚSQUEDA DE GRAFOS

En esta sección presentamos un esquema general de un algoritmo para búsqueda en grafos. Este esquema
está basado sobre las nociones de vértices abiertos y cerrados. Un vértice abierto fue visitado por el algoritmo
pero no ha sido explorado | procesado todavía, mientras que un vértice cerrado ya ha sido visitado y explorado.

El algoritmo toma algún vértice abierto V y lo expande, es decir, el algoritmo procesa el vértice V,
encuentra su vecindad, enlaza ésta vecindad con el resto de los vértices abiertos y agrega éste vértice V al
conjunto de vértices cerrados. Notar, que los vértices cerrados son removidos de la vecindad antes de enlazarlos
con los vértices abiertos. El algoritmo para tan pronto como el conjunto de vértices abiertos está vacío.

% busqueda_cerr_abier(+Grafo,+Abierto,+Cerrado,-Resultado)
busqueda_cerr_abier(Grafo,[],Cerrado,Resultado) :-
afinar_resultado(Grafo,Cerrado,Resultado).

busqueda_cerr_abier(Grafo,[Vertice|Abierto],Cerrado,Resultado) :-
explorar(Vertice,Grafo,Vecindad,CerradoVertice),
diff(Vecindad,Cerrado,AbiertoNB), % removedor de vértices cerrados
mezclar(AbiertoNB,Abierto,NuevoAbierto), % enlaza con el resto
% de vértices abiertos
busqueda_cerr_abier(Grafo,NuevoAbierto,[CerradoVertice|Cerrado],Resultado).

El programa de arriba contiene "hooks", como por ejemplo, afinar_resultado o explorar, el cual tiene que
ser programado para obtener un algoritmo particular. Mostramos tales extensiones ahora (los procedimientos
para hooks son etiquetados con texto en bold).

COLOREADO

Primero, programamos el algoritmo de coloreado de grafos usando el esquema general presentado arriba.
Este algoritmo corresponde al bien conocido algoritmo que colorea un vértice y, entonces, colorea la vecindad
del vértice y así sucesivamente. Notar, que después de colorear un segmento del grafo, tenemos que recomenzar
la búsqueda en el procedimiento afinar_resultado si allá permanecen otros componentes.

% abierto_close_coloreado(+Grafo,+Colores,-Coloreado)
abierto_close_coloreado(Grafo,Colores,Coloreado) :-
vertices(Grafo,[V|Vertices]),
busqueda_cerr_abier(Grafo-Colores,[V-Colores],[],Coloreado).

explorar(V-Cs,Grafo-Colores,Vecindad,V-C) :-
miembro(C,Cs), % Asignar color al vértice
vecindad(Grafo,V,NB), % encontrar la vecindad
borrar(C,Colores,NBColores), % preparar los posibles colores para la vecindad
adicionar_Colores(NB,NBColores,Neigbourhood). % asignar colores a la vecindad

adicionar_Colores([],_,[]).
adicionar_Colores([V|Vs],Cs,[V-Cs|CVc]) :-
adicionar_Colores(Vs,Cs,CVs).
diff([],_,[]).
diff([V-Cs|CVs],Cerrado,NonCerrado) :-
(miembro(V-_,Cerrado) -> NonCerrado=[V-Cs|Rest] ; NonCerrado=Rest),
diff(CVs,Cerrado,Rest).

mezclar([],Abierto,[]).
mezclar([V-Cs|CVs],Abierto,[V-NCs|Rest]) :-
(miembro(V-OCs,Abierto)
-> interseccion(Cs,OCs,NCs) % interseccion de conjunto clásica
; NCs=Cs),
NCs\=[], % Es posible asignar color
mezclar(CVs,Abierto,Rest).

afinar_resultado(Grafo-Colores,Cerrado,Resultado) :-
vertices(Grafo,Vertices),
adicionar_Colores(Vertices,Colores,CVertices),
diff(CVertices,Cerrado,NonCerrado),
(NonCerrado=[CV|_] % ¿Hay otro componente del grafo?
-> busqueda_cerr_abier(Grafo-Colores,[CV],Cerrado,Resultado)
; Resultado=Cerrado).

ALGORÍTMO DE DIJKSTRA

Programaremos ahora la extensión del esquema abierto|cerrado que se comporta parecido al algoritmo de
Dijkstra el cual usa los conjuntos de vértices abiertos y cerrados naturalmente. Recuerda, que el algoritmo de
Dijkstra encuentra la distancia mínima a todos los vértices en el grafo desde un vértice dado.

% abierto_close_dijkstra(+Grafo,+Start,-MinDist)
abierto_close_dijkstra(Grafo,Start,MinDist) :-
busqueda_cerr_abier(Grafo,[Start-0],[],MinDist).

explorar(V-D,Grafo,Neigbourhood,V-D) :-
vecindad(Grafo,V,NB),
adicionar_dist(NB,D,Vecindad).

adicionar_dist([],_,[]).
adicionar_dist([V-D1|Vs],D,[V-VD|Rest]) :-
VD is D+D1,
adicionar_dist(Vs,D,Rest).

diff([],_,[]).
diff([V-D|VDs],Cerrado,NotCerrado) :-
(miembro(V-_,Cerrado) -> NotCerrado=[V-D|Rest] ; NotCerrado=Rest),
diff(VDs,Cerrado,Rest).

mezclar([],Abierto,Abierto).
mezclar([V-D1|VDs],Abierto,NuevoAbierto) :-
(del(V-D2,Abierto,RestAbierto)
-> min(D1,D2,D),ins(V-D,RestAbierto,SAbierto)
; ins(V-D1,Abierto,SAbierto),
mezclar(VDs,SAbierto,NuevoAbierto).

del(X,[X|T],T).
del(X,[Y|T],Rest) :- X\=Y,del(X,T,Rest).

ins(VD,[],[VD]).
ins(V-D,[U-D1|T],[V-D,U-D1|T]) :- D<=D1.
ins(V-D,[U-D1|T],[U-D1|Rest]) :- D>D1,ins(V-D,T,Rest).
afinar_resultado(_,Cerrado,Cerrado).

La generalización puede simplificar el desarrollo y comprensión del programa.

Você também pode gostar