Universidade Federal de São Carlos Departamento de Computação Programação Lógica Prof. Dr. Antonio Francisco do Prado e-mail: [email protected] 1 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 2 Programação Lógica Raízes O uso da lógica na representação do raciocínio remonta os estudos de Boole (1815-1864) e de De Morgan (1806-1871) sobre a “Álgebra de Boole”; Em 1879 surge a primeira versão do “Cálculo de Predicados” com o matemático alemão Göttlob Frege, onde era oferecida uma representação adequada para a formalização do raciocínio dedutivo; Em 1930, em estudos simultâneos, o alemão Kurt Gödel e o francês Jacques Herbrand demonstraram que o mecanismo de prova do Cálculo de Predicados poderia oferecer uma prova formal de toda proposição logicamente verdadeira; Ainda na década de 30 diversos estudos, entre eles os de Alan Turing e Alonzo Church, aproximaram muito o Cálculo de Predicado com a forma como hoje é conhecido; Em 1939 toda fundamentação teórica básica da lógica computacional já estava pronta, faltava apenas uma meio prático para realizar o imenso volume de computações necessárias aos procedimentos de prova; 3 Somente nos meados da década de 50 que o desenvolvimento dos computadores permitiu experiências mais significativas com o Cálculo de Predicados; Em 1958 uma forma simplificada do Cálculo de Predicados denominada forma Clausal começou a despertar o interesse dos estudiosos. Nessa forma de cálculo era empregado um tipo muito simples de sentença lógica, a Cláusula; Também nessa época (1960) Dag Prawitz propôs um novo tipo de operação sobre os objetos do Cálculo de Predicados, que foi mais tarde conhecido como Unificação; Apesar de toda base teórica para a Programação Lógica estar pronta, ela somente se tornou possível a partir da pesquisa sobre prova matemática de teoremas, particularmente no desenvolvimento do Princípio da Resolução por J.A. Robinson (1965); A expressão “Programação Lógica” (Logic Programming) surge com Robert Kowalski (1974) e designa o uso da lógica como uma linguagem de programação de computadores. OBS: muitos outros pesquisadores colaboraram para o aparecimento da Programação Lógica. No texto apenas foram relatados alguns deles. 4 Conceitos da Programação Lógica Uma das principais idéias em Programação Lógica é que um algoritmo é constituído por dois elementos disjuntos: Lógica: corresponde à definição do que deve ser solucionado; Controle: estabelece como a solução pode ser obtida; A tarefa do programador é somente descrever (especificar) o componente lógico do algoritmo, deixando o controle da execução para ser exercido pelo sistema de programação em lógica utilizado; Um Programa em Lógica é então a representação de determinado problema ou situação expressa através de um conjunto finito de um tipo especial de sentenças lógicas, denominadas cláusulas; O paradigma fundamental da programação em lógica é o da Programação Declarativa, em oposição à Programação Procedimental típica das linguagens convencionais; O ponto focal da Programação em Lógica consiste em identificar a noção de computação com a noção de dedução, onde a execução de programas se reduzem à pesquisa da refutação das sentenças do programa em conjunto com a negação da sentença que expressa a consulta, seguindo a regra: "uma refutação é a dedução de uma contradição". 5 Os termos "programação em lógica" e "programação Prolog" tendem a ser empregados indistintamente. Deve-se, entretanto, destacar que a linguagem Prolog é apenas uma particular abordagem da Programação em Lógica; Pode-se expressar conhecimento (programas e/ou dados) em Prolog por meio de cláusulas de dois tipos: Fatos: denota uma verdade incondicional; Regras: definem as condições que devem ser satisfeitas para que uma certa declaração seja considerada verdadeira. Características da Programação Lógica Os sistemas de Programação em Lógica em geral e a linguagem Prolog em particular possuem as seguintes propriedades: Funcionam simultaneamente como linguagem de programação e de especificação; Possuem capacidade dedutiva; Operam de forma não-determinística; Permitem a representação de relações reversíveis; Permitem interpretação declarativa, operacional e procedimental; São naturalmente recursivas. 6 Aplicações da Programação Lógica As principais aplicações da Programação em Lógica são: Sistemas Baseados em Conhecimento (SBCs) Sistemas de Bases de Dados (BDs); Sistemas Especialistas (SEs); Processamento da Linguagem Natural (PLN); Educação; Modelagem de Arquiteturas Não-Convencionais. Linguagens Convencionais x Lógicas PROGRAMAS CONVENCIONAIS PROGRAMAS EM LÓGICA Processamento Numérico Processamento Simbólico Soluções Algorítmicas Soluções Heurísticas Estruturas de Controle e Conhecimento Integradas Estruturas de Controle e Conhecimento Separadas Difícil Modificação Fácil Modificação Somente Respostas Totalmente Corretas Incluem Respostas Parcialmente Corretas Somente a Melhor Solução Possível Incluem Todas as Soluções Possíveis 7 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 8 Linguagem PROLOG Linguagens Funcionais x Lógicas Funcional Lógica Programação orientada para funções Diz “como fazer” para atingir um objetivo Mais voltada para algoritmos que exploram o conhecimento Preocupa-se com aspectos relacionados com a construção de listas, tipos, padrão de comparação, estruturas de dados, etc. Camada mais baixa de suporte para as aplicações Programação orientada para objetivos (“goal”) Diz “o que fazer” para atingir um objetivo Mais voltada para o conhecimento Mais declarativa do que procedural Nível mais elevado de abstração na solução dos problemas Conclusão Tanto as linguagens Funcionais como Lógicas são importantes e empregadas principalmente em Sistemas Transformacionais, Inteligência Artificial (IA) e Robótica. 9 Histórico Criado por Alain Colmerauer por volta de 1970 na Universidade de Marselha, França. Seu objetivo inicial era servir de apoio a sistemas de Linguagem Natural. Algol 60 Algol 68 Prolog Programação Prolog Para executar um programa em prolog, primeiro deve-se criar um arquivo com a extensão correspondente do interpretador ( .ari, .pl, etc). Depois, execute no interpretador correspondente. Em seu menu acione a opção File e Consult para carregar o programa no interpretador. Se houver algum erro no programa, este será mostrado na tela, senão o programa está pronto para ser executado (interpretado). 10 A programação Prolog consiste fundamentalmente em: Declarar fatos ou assertivas; Estabelecer regras ou procedimentos; e Fazer perguntas. Os fatos e as regras são fornecidos e a linguagem usa dedução para obter respostas para as questões. 11 Considerações sobre o aprendizado do aluno Temos observado que o estilo procedimental de programação, talvez por ser o primeiro a ser ensinado, interfere de certa forma, na correta aprendizagem de linguagens como Lisp ou Prolog por parte dos estudantes. Tentaremos minimizar essa interferência submetendo o aluno a resolução de conjuntos de problemas semelhantes, principalmente problemas envolvendo listas. É essencial que o aluno tente compreender a lógica de programação Prolog, entendendo mecanismos de unificação, backtracking entre outros. Também é importante que o aluno tente resolver os exercícios propostos no ultimo capítulo, para consolidar o seu aprendizado. 12 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 13 Fatos, Regras e Controle de Corte Introdução Um programa Prolog consiste de : • Definição de fatos a respeito de objetos de dados e suas relações; • Definição de regras a respeito de objetos de dados e suas relações; • Interrogação a respeito de objetos de dados e suas relações. Um objeto de dados em Prolog é definido de acordo com a seguinte árvore : objetos objetos simples constantes átomos números estruturas variáveis 14 • Átomo é a estrutura mais simples do Prolog, eles são construídos por cadeias de letras, dígitos e caracteres especiais( _, +, *). Todo átomo deve possuir a primeira letra minúscula. Exemplos de átomos : x_y, maria, copa2002. • Números em Prolog incluem números inteiros e números reais. A sintaxe é bem simples : 1, -20. • Variáveis são cadeias de letras, dígitos e caracteres sempre começando com letra maiúscula. • Estruturas são objetos de dados que têm vários componentes, podendo cada um deles, por sua vez, ser uma outra estrutura. Por exemplo, uma data pode ser vista como um conjunto de dia, mês e ano. Embora composta por vários componentes, estruturas são tratadas no programa como objetos simples. A combinação dos componentes de um objeto simples é feita através de seu funtor. No caso de data, o funtor pode ser data e pode – se escrever 7 de setembro de 2002 da seguinte maneira : data(7, setembro, 2002) Note que os componentes 7, setembro e 2002 são todos constantes(2 inteiros e 1 átomo) 15 Fatos, Regras e Controle de Corte Fatos Um Fato denota uma verdade incondicional. Uma relação pode ser especificada por um fato. Sintaxe : predicado(arg1[,arg2,...,arg n]). Argumentos (arg1...arg n) são objetos quaisquer e Predicado é a relação que une esses objetos. tom pam liz bob pat ann jim 16 progenitor(pam,bob). progenitor(tom,bob). progenitor(tom,liz). progenitor(bob,ann). progenitor(bob,pat). progenitor(pat,jim). %pam é um dos progenitores de bob %tom é um dos progenitores de liz Em Prolog usam-se maiúsculas para variáveis e minúsculas para átomos. Questões: átomo ?-progenitor(bob,pat). yes ?-progenitor(liz,pat). no ?-progenitor(X,liz). X=tom ; no ?-progenitor(bob,X). X=ann X=pat variável ?progenitor(X,Y). X=pam , Y=bob ; X=tom , Y=bob ; X=tom , Y=liz ; X=bob , Y=ann ; X=bob , Y=pat ; X=pat , Y=jim ; 17 Quem são os avós de Jim? (1) Quem é progenitor de Jim? (Por exemplo, Y) e (2) Quem é progenitor de Y? (Por exemplo, X). X Progenitor Y Avos Progenitor jim "Encontre X e Y tais que X é progenitor de Y e Y é progenitor de Jim". Questões: ?-progenitor(X, Y), progenitor(Y, jim). X=bob Y=pat ; Quem é neto de Tom? Questões: ?-progenitor(tom, X), progenitor(X, Y). X=bob Y=ann; X=bob Y=pat. 18 Definição de uma extensão da base anterior (característica do objeto): mulher(pam). homem(tom). homem(bob). mulher(liz). mulher(pat). mulher(ann). homem(jim). Regras As regras definem as condições que devem ser satisfeitas para que uma certa declaração seja considerada verdadeira. Sintaxe : pred(arg/Var,arg/Var) :- pred(arg/Var,arg/Var). cabeça se (conclusão) corpo (condição) Onde o símbolo :- indica uma condição (se) e separa a regra em conclusão ou cabeça da regra e condição ou corpo da regra. 19 Definir a relação filho. Poderia ser definida como: filho(bob,tom). filho(bob,pam). ... Entretanto existe uma maneira mais elegante, que seguiria a seguinte declaração: Para todo X e Y Y é filho de X se X é progenitor de Y. filho(Y, X) :- progenitor(X, Y). "Para todo X e Y, se X é progenitor de Y, então Y é filho de X". Questões: ?-filho(jim,pat). yes filho(Y, X) :- progenitor(X, Y) cabeça (conclusão) se corpo (condição) 20 Frases Compostas robo(peter). capaz_de_fazer(peter,armas). machuca(armas,pedro). homem(pedro). proibido_fazer(R,B):-robo(R),capaz_de_fazer(R,B), machuca(B,H),humano(H). Questão ? - proibido_fazer(R,B). R = peter , B = armas Significa: R está proibido de fazer um ato B se R for um robô e R for capaz de fazer B e B machuca H e H for humano. Para todo R e B proibido_fazer(R,B) se existe H tal que robo(R) e capaz_de_fazer(R,B) e machuca(B,H) e humano(H). humano(H):-homem(H). Hífen Usa-se o hífen para indicar irrelevância de um objeto Exemplo: aniversario(maria,data(25,janeiro,1979)). aniversario(joao,data(5,janeiro,1956)). signo(Pessoa,aquario):-aniversario(Pessoa,data(Dia,janeiro,_)), Dia >= 20. ? - signo(Pessoa,aquario). Pessoa = maria; no 21 Conjunção e Disjunção Em prolog o operador de conjunção e ( , ) implica na necessidade de aceitação de todas as condições, enquanto o operador de disjunção ou ( ; ) permite a aceitação de uma ou outra condição. Estes operadores permitem a composição de fatos. Exemplos: amiga(X):-(X = maria; X = joana). disjunção ou Definição de uma regra avos: Usando-se a conjunção podemos definir o predicado avos seguindo a seguinte declaração: Para todo X e Z X é progenitor de Y e Y é progenitor de Z. "Para todo X e Z, se X é progenitor de Y, e Y é progenitor de Z, então X é um dos avos de Z". avos(X,Z):-progenitor(X,Y) , progenitor(Y,Z). Questões: ?-avos(bob,jim). yes ?-avos(liz,jim). no conjunção e ?-avos(pam,X). X=ann ; X=pat 22 Definição de uma regra mãe: mae(X,Y):-progenitor(X,Y),mulher(X). Questões: ?-mae(pat,jim). yes ?-mae(bob,ann). no Definição de uma regra irmã: irma(X,Y):-progenitor(Z,X),progenitor(Z,Y),mulher(X). Z progenitor mulher Questões: ?-irma(ann,pat). yes X progenitor irmã Y ?-irma(X,pat). X=ann ; X=pat ; no Definição de uma regra diferente: diferente(X,Y):-X\==Y. %X é diferente de Y 23 Definição de uma regra irmã usando a regra diferente: irma(X,Y):-progenitor(Z,X),progenitor(Z,Y),diferente(X,Y). Questão: ?-irma(X,pat). X=ann ; no Definição de uma regra tia: tia(X,Y):-progenitor(Z,Y),irma(X,Z). Questão: ?-tia(liz,X). X=ann ; X=pat ; no Recursão Definição de uma regra antepassado Necessita de duas regras: antepassados diretos e antepassados indiretos. bob Antepassados diretos pat Antepassados indiretos jim 24 Primeira regra bastante simples: Para todo X e Z X é antepassado de Z se X é progenitor de Z. antepassado(X, Z) :- progenitor(X, Z). Segunda regra complicada. A cadeia de progenitores pode se estender indefinidamente: antepassado(X, Z) :- progenitor(X, Y), progenitor(Y, Z). antepassado(X, Z) :- progenitor(X, Y1), progenitor(Y1, Y2), progenitor(Y2, Z). antepassado(X, Z) :- progenitor(X, Y1), progenitor(Y1, Y2), progenitor(Y2, Y3), progenitor(Y3, Z). .... 25 Solução: definir a regra recursivamente Para todo X e Z X é antepassado de Z se existe um Y tal que X é progenitor de Y e Y é antepassado de Z. X Progenitor Y Antepassado Antepassado Z A segunda regra fica: antepassado(X, Z) :- progenitor(X, Y), antepassado(Y, Z). Questões: ?-antepassado(X,liz). X=tom ; no ?-antepassado(X,pat). X=bob ; X=pam ; X=tom ; no 26 Controle de Corte Há ocasiões em que, por um ou outro motivo, desejamos controlar a execução do programa. Para isso, utilizamos o mecanismo de corte. Operador Cut Algumas das principais aplicações do cut são as seguintes: Unificação de padrões, de forma que quando um padrão é encontrado os outros padrões possíveis são descartados (quando uma determinada regra é a última a ser analisada e haveria problemas na continuidade da verificação das demais regras); Para eliminar da árvore de pesquisa soluções alternativas quando uma só é suficiente; Especificar regras mutuamente exclusivas, expressas na forma: Se P então Q senão R; Como indicativo do caso limite ou para evitar que a pesquisa prossiga indefinidamente através do backtracking. Simbolizado Seu pela exclamação (“!”). uso deve ser considerado pelas seguintes razões: Execução mais rápida do programa (não desperdiça tempo tentanto satisfazer objetivos que não contribuirão para a solucão desejada);27 Economiza memória (corta a ávore de pesquisa). Exemplos: Podemos simular a programação procedimental em Prolog. Aqui simularemos a estrutura if-then-else: ifThenElse(X,Y,Z) "Se X for verdadeiro, então execute Y, senão execute Z". ifThenElse(X, Y, _) :- X, !, Y. ifThenElse(_, _, Z) :- Z. Questão: ?-ifThenElse(X, Y is Z+1, Y is 0). OBS: este programa emprega meta-variáveis (variáveis que podem ser instanciadas com chamadas a predicados) Exemplo do operador Cut: amigo(joana,ana). amigo(maria,ana). amigo(pedro,jose). amigo(pedro,ana). um_unico_amigo(X,Y):-amigo(X,Y),!. Questões: ?- um_unico_amigo(X,ana). ?- um_unico_amigo(pedro,X). X = joana X = jose Mínimo entre dois números: minimo(X,Y,X):-X =< Y, !. minimo(X,Y,Y):-X > Y, !. Questões: ?- minimo(10,3,Min). ?- minimo(12,56,Min). Min = 3 Min = 12 28 Fail O predicado Fail, força um retrocesso, como se indicasse uma falha. Junto com o corte (Cut), acelera a avaliação de regras economizando memória. Exemplos: Exemplo do operador Fail: cliente(ana,123,bradesco). cliente(jose,456,itau). executa :- cliente(Nome,Conta,Agencia), write(Nome),write(’ tem conta ’),write(Conta), write(’ na agencia ’),write(Agencia),nl,fail. Neste caso fail força um backtracking e repete a a impressão. Questões: ?-executa. ana tem conta 123 na agencia bradesco jose tem conta 456 na agencia itau no Not (X) O operador unário not define uma forma particular de negação denominada "negação por falha”. 29 Definição de uma regra para exemplificar o not: estudante(jorge). casado(jose). estudante_solteiro(X):-not casado(X), estudante(X). Questões: ?-estudante_solteiro(jorge). yes ?-estudante_solteiro(jose). no Cuidados com o Cut e a Negação O uso do cut pode levar a perda da correspondência entre o significado declarativo e a interpretação operacional do programa: Se não houver cuts no programa pode-se trocar a ordem das cláusulas e objetivos que seu significado declarativo não será alterado (há apenas uma possível perda de performance); Se houver cuts essa alteração pode afetar o significado declarativo levando a resultados inesperados. O uso da negação também pode levar a resultados inesperados. Qual o problema do programa abaixo? Questões: r(a). q(b). ?-q(X), p(X). ?-p(X), q(X). p(X) :- not r(X). X = b. no 30 Outros Exemplos de Regra Definição das regras positivo e negativo: positivo(X):-X>=0. %X é maior ou igual a 0 negativo(X):-X<0. %X é menor que 0 Questões: ?-positivo(7). ?-negativo(10). yes no Máximo entre 2 números: max(X,Y,X):-X>=Y. max(X,Y,Y):-X<Y. Máximo entre 2 números usando corte: max(X,Y,X):-X>=Y,!. max(X,Y,Y). Questão: ?-max(10,3,Max). Max=10 Definição das regras par e impar: par(X):-X mod 2 =:=0 % o mod de 2 é igual a 0 impar(X):-par(X),!,fail. impar(X). Questões: ?-par(6). ?-impar(6). yes no 31 Definição de uma função f(X): f(X, 0):- X < 3. f(X, 2):- 3=< X, X < 6. f(X, 4):- 6 =< X. 4 3 Y=F(X) 2 Y=f(X) 1 0 0 1 2 3 4 5 6 7 8 9 10 X Definição de uma função f(X) com corte: f(X, 0):- X < 3, ! . f(X, 2):- 3 =< X, X < 6, ! . f(X, 4). Questões: ?-f(1,Y),2<Y. no ?-f(7,Y). Y=4 Definição de uma regra para fatorial: fatorial(0, 1):- !. fatorial(N, F):- N1 is N - 1, %Atribuição fatorial(N1, F1), F is F1 * N. Questão: ?- fatorial(4, F). F = 24 32 Exemplo de recursividade Para o exemplo anterior da regra do Fatorial recursivo, é mostrado a execução passo a passo, note que em todos os passos(com excessão do 5) há unificação com a segunda regra. (primeira regra é critério de parada). Questão: ?- fatorial(4, F). F = 24 Passo 1 : fatorial(4, F) { N = 4, N1 =3, F = F1 * 4, fatorial(N1, F1) } F=6 Passo 2 : fatorial(3, F) { N = 3, N1 =2, F = F1 * 3, fatorial(N1, F1) } F=2 Passo 3 : fatorial(2, F) { N = 2, N1 =1, F = F1 * 2, fatorial(N1, F1) } F=1 Passo 4 : fatorial(1, F) { N = 1, N1 =0, F = F1 * 1, fatorial(N1, F1) } F=1 Passo 5 : fatorial(0, F) {F = 1 } 33 Definição de uma regra para fatorial com recursão de cauda: fact(N,Fn):-fact(N,Fn,0,1). fact(N,Fn,N,Fn):-!. fact(N,Fn,I,P):-Novoi is I+1,Novop is P*Novoi, fact(N,Fn,Novoi,Novop). Questão: ?- fact(6, F). F = 720 Recursão de cauda. A recursividade é a última. Definição de uma regra para fibonacci: fib(1, 1). fib(2, 1). fib(N, F):- N > 2, N1 is N - 1, fib(N1, F1), N2 is N - 2, fib(N2, F2), F is F1 + F2. Questão: ?- fib(6, F). F=8 ; no 34 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 35 Operadores e Listas Operadores Aritméticos + Adição - Subtração * Multiplicação / Divisão mod is Módulo, o resto inteiro da divisão Atribuição Exemplo: Cálculo de um número elevado ao quadrado: ao_quadrado(X,Y):-Y is X*X. Questão: ?- ao_quadrado(6,X). X=36 36 Operadores de Comparação X>Y X é maior que Y X<Y X é menor que Y X >= Y X é maior ou igual a Y X =< Y X é menor ou igual a Y X =:= Y Os valores de X e Y são iguais X \== Y Os valores de X e Y são diferentes Exemplo: Definição de uma regra intervalo aberto: interv_aberto(K,X1,X2):-K > X1,K < X2. Questões: ?- interv_aberto(5,0,5). no ?- interv_aberto(2,0,5). yes 37 Listas As listas são estruturas de dados dinâmicas com as quais é possível a construção de vetores e matrizes. Sintaxe : pred([elem,...],[],[elem[elem,...],...],arg,...). Onde elem pode ser qualquer tipo sintático. Listas são compostas por cabeça e cauda. A cabeça de uma lista pode ser qualquer objeto Prolog e a cauda deve ser uma lista. Como a cauda, por sua vez, é uma lista, ela á a lista vazia ou tem sua própria cabeça e cauda. Sintaxe para Listas A lista vazia é denotada por [] ; a lista que tem cabeça X e cauda Y é denotada por [X|Y] . Dentro das listas, os elementos são separados por vírgula. Exemplo : Lista [gosto,de, vinho] [X,Y|Z] [[o,gato]] Cabeça Cauda gosto X [o,gato] [de, vinho] [Y|Z] [] 38 Unificação em listas Em Prolog, a mais importante operação envolvendo termos é chamada unificação. Dois termos T e S unificam se : 1. Se T e S são constantes, então unificam se e só se S e T são o mesmo objeto. 2. Se S for uma variável e T qualquer termo, então unificam, e S é instanciado com T ; vicer – versa, com a variável T instanciada com S. 3. Se S e T são estruturas, eles unificam se e só se S e T tem o mesmo funtor principal e todos os elemtentos correspondentes unificam. A seguir são mostrados vários exemplos de unificação em listas : Lista 1 [mesa] [a,b,c,d] [[ana, Y] | Z] [ano, bissexto] Lista 2 [X|Y] Unificação X / mesa Y/[] [X,Y|Z] X/a Y/b Z / [c,d] [[X, foi], [ao, cinema]] X / ana Y / foi Z / [[ao, cinema]] [X,Y|Z] X / ano Y/ bissexto 39 Z/[] Lista 1 [ano, bissexto] Lista 2 [X,Y,Z] [data(7,Z,W), hoje] [X,Y] [data(7,W,1993), hoje] [data(7,X,Y), Z] Unificação não unifica (aridade diferente) X / data(7,Z,W) Y / hoje X/W Y / 1993 Z / hoje Busca recursiva Freqüentemente é necessário procurar por algum termo Prolog ; isto resulta em uma busca recursiva. Para verificar se um elemento pertence à uma lista, usamos o seguinte predicado: pertence(X, [X | _]). % verifica se X esta na cabeça pertence(X, [_ | Y]):- pertence(X, Y). % verifica se X % esta na cauda Questões: ?- pertence(a, [h, a, b]). yes ?- pertence( a, [hoje, amanha]). no ?- pertence(X, [a, b, c]). X = a; X = b; X = c; no 40 Goal ?- X = [1,2,3],pertence(a,X). no ?- pertence( a, X),X = [1,2,3]. X = [a |_] %pode ser o 1º elemento de X X = [_ , a | _] %pode ser o 2º elemento de X X = [_ , _ , a | _] %pode ser o 3º elemento de X . . . %computação infinita! Sequência de ligação -> da esquerda para a direita Conclusão Escolha o “subgoal” com o menor número de soluções como “goal” a adivinhar. No caso anterior o “goal” a adivinhar correto é X = [1,2,3], porque tem apenas uma solução e esta solução não satisfaz pertence(a,X), porque “a” não está na lista [1,2,3]. 41 Controle em Prolog 1. Iniciar pelo goal 2. Enquanto o goal corrente não é vazio faça 2.1. Comece pelo subgoal mais à esquerda 2.2. Se uma regra aplica ao subgoal então 2.2.1. Para esta regra, usando unificadores genéricos, volte para 2.1 Senão backtrack fim-se fim-enquanto Exemplo: anexa([ ],Y,Y ). anexa([H | X],Y,[H| Z]):- anexa(X, Y,Z). prefix(X,Z):-anexa(X,Y,Z). sufix(Y,Z):-anexa(X,Y,Z). 42 1º Subgoal ?- sufix ( [ a ] , L ) , prefix ( L , [ a , b , c ] ). L=[ a] ; %sem backtraing X = [ ] Y = [ b , c ] O fato sufix ( Y´ , Z´ ) :- anexa ( X´ , Y´ , Z´ ) aplica ao subgoal mais à esquerda sufix ( [ a ] , L ) A substituição Y´ -> [ a ] , Z´ -> L Unifica a cabeça da regra com o subgoal sufix ( [ a ] , L ) Tem-se então: sufix ( [ a ] , L ) if anexa ( _1 , [ a ] , L ) nome Prolog Substituindo pela condição tem-se: anexa ( _1 , [ a ] , L ) , prefix ( L , [ a , b , c ] ) O fato anexa ( [ ] , Y´´ , Y´´ ) aplica ao novo subgoal mais á esquerda: [ ] -> _1 , Y´´ -> [ a ] , Y´´ -> L Uma vez que o fato consiste de uma cabeça e nenhuma condição, o novo goal corrente torna-se: prefix ( [ a ] , [ a , b , c ] ) substituída -> L Tem-se então: prefix ( [ a ] , [ a , b , c ] ) if anexa ( [ a ] , _2 , [ a , b , c ] ) . anexa ( [ a ] , _2 , [ a , b , c ] ) if anexa ( [ ] , _2 , [ b , c ] ). anexa ( [ ] , _2 , [ b , c ] ) 2 -> [ b , c ] anexa ( [ ] , [ b , c ] , [ b , c ] ) . L=[ a] ; 43 Modos de Chamada Usando o exemplo do pertence([arg1], [arg2]) mostrado anteriormente, o que aconteceria se as interrogações fossem : ?- pertence(a,X) % ou ?- pertence(X,Y) Pode – se observar que cada uma delas tem “infinitas” respostas, uma vez que existem infinitas listas que validam estas interrogações para o programa pertence. Portanto, é necessário definir se os argumentos devem estar ou não instanciados. Usaremos a seguinte notação para documentar as formas corretas de interrogação : modo(<arg – 1>, <arg – 2>, .....,<arg – n>) onde : <arg – i > = + se <arg – i > deve estar instanciado. <arg – i > = - se <arg – i > deve ser variável livre. <arg – i > = ? se <arg – i > puder ser qualquer um dos casos. Exemplo : o exemplo pertence ([arg1], [arg2]) tem o modo(? , +). 44 Outros Exemplos de Lista Verificação se um elemento de uma lista não pertence a outra lista, modo(+, +) : nao_pertence(X,Y):- \+ (pertence(Z,X),pertence(Z,Y)). Questões: ?-nao_pertence([a,b],[b,c]). ?- nao_pertence([a,b,c],[d,e,f]). no yes Soma dos elementos de uma lista, modo(+, ?) : soma([ ], 0). soma( [Elem | Cauda], S):- soma(Cauda, S1), S is S1 + Elem. Questão: ?- soma([1, 2, 3, 4, 5, 6], S). S = 21 Verificação se uma lista está ordenada, modo(+) : ordenada([ ]). ordenada([X,Y | Resto]):-X=<Y,ordenada([Y | Resto]). Questões: ?-ordenada([1,5,6,6,9,12]). yes ?-ordenada([3,2,1]). no 45 Verificação se um termo é uma lista, modo(+) : eh_lista([ ]). eh_lista(X):- not var(X), not atom(X). Onde os predicados: var(X) - Verdadeiro se o termo X é uma variável atom(X) - Verdadeiro se o termo X é um átomo Questões: ?- eh_lista([a, b, c, d, e, f]). yes ?- eh_lista( 2 ) no ?- eh_lista([ ]). yes Duplicação dos elementos de uma lista, modo(+,?) : duplica([ ], [ ]). duplica( [X | L], [X, X | L1]):- duplica(L, L1). Questão: ?- duplica([1, 2, 3], L). L = [1, 1, 2, 2, 3, 3] 46 Concatenação de duas listas, modo(+,?,?) : concatena([ ], Lista, Lista). concatena([ Elem | Lista1], Lista2, [Elem | Lista3]):concatena(Lista1, Lista2, Lista3). Questão: ?- concatena([um, dois], [tres, quatro], L). L = [um, dois, tres, quatro] Também pode – se usar o modo modo(-,-,+) para decompor uma lista em duas sublistas. Maior elemento de uma lista numérica, modo(+,?) : max([X], X). % Maior elemento de uma lista com 1 % elemento é o próprio elemento max([X, Y | Cauda], Max):- X >= Y, ! , max([X | Cauda], Max). max([X, Y | Cauda], Max):- max([Y | Cauda], Max). Questão: ?- max([1, 10, 100, -1, 20], M). M = 100 ; no N_ésimo elemento de uma lista, modo(?,?,+) : n_esimo(1, Elem, [Elem | _]). n_esimo(N, Elem, [ _ | Cauda]):- n_esimo(M, Elem, Cauda), N is M + 1. Questão: ?- n_esimo(4, Elem, [a, b, c, d, e]). Elem = d ; no 47 Inversão de uma lista, modo(+,?) : inverter([ ],[ ]). inverter([Elem | Lista1], Lista3):-inverter(Lista1,Lista2), concatena(Lista2,[Elem], Lista3). Questões: ?- inverter([a,b,c], L). L = [c,b,a] ?-inverter([1,2,3,4,5,6],L). L = [6,5,4,3,2,1] Número de elementos em uma lista, modo(+,?) : tamanho([ ],0). tamanho([_ | R], N):-tamanho(R, N1),N is N1+1. Questão: ?- tamanho([a,b,c,d,e], N). N=5 Seleção de determinados elementos de uma lista, em uma lista separada, identificados pela sua posição, modo(+,+,?) : seleciona([ ], _, [ ]). seleciona([M | N], L, [X | Y]):-n_esimo(M, X, L), seleciona(N, L, Y). Questão: ?- seleciona([2, 4], [a, b, c, d, e], L). L = [b, d] ; no 48 Ultimo elemento de uma lista, modo(+,?) : ultimo([Elem], Elem). ultimo([_|Cauda], Elem) :- ultimo(Cauda, Elem). Questões: ?- ultimo([casa, bola, carro], X) X = carro ; no Elementos consecutivos em uma lista, modo(?,?,+) : consecutivos(E1,E2, [E1, E2|_]). consecutivos(E1, E2, [_|Cauda]) :consecutivos(E1, E2, Cauda). Questões: ?- consecutivos(a, b, [d, e, f, g, a, b]). yes ?- consecutivos(X, Y, [d, e, f, g]). X=d Y=e ; X=e Y=f; X=f Y=g; no 49 Inserção de um elemento na 1ª posição de uma lista, modo(?,?,?) : inserir(Elem, Lista, [Elem | Lista]). Questão: ?- inserir(a, [b, c, d], L). L = [a, b, c, d] Exclusão de um elemento de uma lista, modo(?,+,?) ou modo(+,?,+) : del(X,[X | Corpo],Corpo). del(X,[Y | Corpo], [Y | Corpo1]):-del(X,Corpo,Corpo1). Questão: ?- del(b,[a,b,c], L). L = [a,c] ; no Permutação de uma lista, modo(+,?) : permuta([ ],[ ]). permuta(L,[X | P]):-del(X,L,L1),permuta(L1,P). Questão: ?- permuta([vermelho,azul,verde], L). L = [vermelho,azul,verde] ; L = [vermelho,verde,azul] ; L = [azul,vermelho,verde] ; L = [azul,verde,vermelho] ; L = [verde,vermelho,azul] ; L = [verde,azul,vermelho] ; no 50 Retirar todas as ocorrências de um elemento em uma lista, modo(+,+,?) : retirar_todas(_, [], []). retirar_todas(Elem, [Elem|Cauda], L):retirar_todas(Elem, Cauda, L). retirar_todas(Elem, [Elem1|Cauda1], [Elem1|Cauda1]) :Elem \== Elem1, retirar_todas(Elem, Cauda, Cauda1). Questões: ?- retirar_todas(a, [c,a, s, a], L). L = [c, s] ; no Retirar elementos repetidos de uma lista, modo(+,?) : retirar_rep([], []). retirar_rep([Elem|Cauda], [Elem|Cauda1]):retirar_todas(Elem, Cauda, Lista), retirar_rep(Lista, Cauda1). Questões: ?- retirar_rep([a, b, a, b, a, b, a, a, b, a], L). L = [a, b] ; no retirar_rep([a, b, c, d, a, a, a], [a, b, c, d]). yes 51 Inserção do elememento em qualquer posição da lista, modo(?,?,+) ou modo(+,+,?) : Inserir_2(Elem, Lista, Lista1) :retirar_elemento(Elem, Lista1, Lista). Questões: modo(+,+,?) ? – inserir_2(ana, [pedro, maria], L). L = [ana, pedro, maria] ; L = [ pedro, ana, maria] ; L = [ pedro, maria, ana] ; no modo(?,?,+) : ? – inserir_2(X, Y, [1, 2, 3]). X=1 Y = [2, 3] ; X=2 Y = [1, 3] ; X=3 Y = [1, 2] ; no Substituir um elemento em uma lista por outro elemento, modo(?,?,+,?) : substitui(X, Y, [], []). substitui(X, Y, [X|L], [Y|L1]) : - substitui(X, Y, L, L1). substitui(X, Y, [Z|L], [Z|L1]) :- X \== Z, substitui(X, Y, L, L1). Questões: ? – substitui(2, 1000, [0, 4, 2, 2],L). L = [0, 4, 1000, 1000] ; no 52 Verificação se uma lista é sublista de outra, modo(?,+) : sublista(S, L) :-concatena(L1, L2, L),concatena(S, L3, L2). Questões: ?-sublista(S,[a,b,c]). ?- sublista([c,d,e], [a,b,c,d,e,f]). S=[] ; yes S = [a] ; S = [a,b] ; ?-sublista([c,e],[a,b,c,d,e,f]). S = [a,b,c] ; no S = [b] ; S = [b,c] ; S = [c] ; no Intersecção de duas listas em uma terceira, modo(+,?,?) : intersec([X | Y],L,[X | Z]):-pertence(X, L),intersec(Y, L, Z). intersec([_ | X], L, Y):-intersec(X, L, Y). intersec(_, _, [ ]). Questão: ?-intersec([a,b,c,d],[aa,b,d],L). L = [b,d] ; Deslocamento de uma lista para a esquerda, modo(+,?) : desloca([Elem | Cauda], Lista):concatena(Cauda, [Elem], Lista). Questão: ?- desloca([1, 2, 3, 4, 5], L). L = [2, 3, 4, 5, 1] 53 Seleção dos elementos diferentes da 1ª lista em relação a 2ª lista, armazenando-os em uma lista separada, modo(+,+,?) : diferente([ ],_,[ ]). diferente([X | L1],L2,L):-pertence(X,L2),!, diferente(L1,L2,L). diferente([X | L1],L2,[X | L):-diferente(L1,L2,L). Questão: ?-diferente([a,b,c,d],[b,d,e,f],L). L = [a,c] Divisão de uma lista numérica em duas listas, uma com números positivos outra com negativos, modo(+,?,?) ou modo(?,+,+) : divide([ ], [ ], [ ]). divide([Elem | Cauda], [Elem | Positivos], Negativos):Elem >= 0, ! , divide(Cauda, Positivos, Negativos). divide([Elem | Cauda], Positivos, [Elem | Negativos]):divide(Cauda, Positivos, Negativos). Questão: ?- divide([1, 4, -1, 0, -2, 6], Pos, Neg). Pos = [1, 4, 0, 6] Neg = [-1, -2] 54 Achatar uma lista, modo(+,?) : achatar([], []). achatar([Cab|Caud], ListaA) :eh_lista(Cab), achatar(Cab, CabA), achatar(Caud, CaudA), concatenar(CabA, CaudA, ListaA). achatar([Cab|Caud], [Cab,CaudA]) :- not lista(Cab), achatar(Caud, CaudA). Questões: ? – achatar([a, [b, [c, d]],[[e, f, g], h], i], L). L = [a, b, c, d, e, f, g, h, i] ; no Verificar se intersecção entre 2 conjuntos é não vazia, modo(+,+) : nao_vazia(L1, L2) :- pertence(Elem, L1), pertence(Elem, L2). Verificar se 2 conjuntos são disjuntos, modo(+,+) : conj_disjuntos(L1, L2) :- not nao_vazia(L1, L2). Questões: ? – conj_disjuntos([a, b, c, d], [d, c, b, a]). no 55 Verificar se 2 conjuntos são iguais, modo(+,+) : conj_iguais([], []). conj_iguais([X|L1], L2) :- del(X, L2, L3), conj_iguais(L1, L3). Questões: ? – conj_iguais([a, b, c], [c, b, a]). yes União de 2 conjuntos, modo(+,+,?) : uniao(L1, L2, U) :- concatenar(L1, L2, L3), retirar_rep(L3, U). Questões: ? – uniao([a, b, c], [b, c, d], L). L = [a, b, c, d] ; no 56 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 57 Entrada e Saída read(X) Este predicado destina-se à leitura de termos do teclado. Todo o termo escrito deve terminar com um ponto ( . ). Este termo pode ser uma palavra isolada, uma lista de letras ou ainda uma string de caracteres. Definição de uma regra para exemplificar o predicado read: oi:-read(X),write(‘Resposta: ’),write(X). Questões: ?-oi. ola. Resposta: ola yes ?-oi. Ola. Resposta: _00A0 yes % variável de memória ?- oi. ‘Ola, como vai?’. Resposta: Ola, como vai? yes ?- oi. “Ola, como vai?”. Resposta:[79,108,97,44,32, 99,111,109,111,32, 118,97,105,63] yes %lista de caracteres ASCII 58 write(X) Imprime termos associados a X. São válidas as mesmas observações feitas para read. Definição de uma regra quadrado: quadrado:-read(N),Result is N*N,write(Result). Questão: ?-quadrado. (4). 16 yes Definição de uma regra cubo: cubo:-write(‘Próximo item, por favor: ’), read(X), calculocubo(X). Calculocubo(stop):- !. Calculocubo(N):-C is N * N * N, write(‘Cubo de ’),write(N),write(‘ é ’), write(C),nl,cubo. Questão: ?-cubo. Próximo item, por favor: 5. Cubo de 5 é 125 Próximo item, por favor: 12. Cubo de 12 é 1728 Próximo item, por favor: stop. yes 59 tab(X) Escreve uma quantidade X de caracteres “espaço” na saída. Definição de uma regra para exemplificar o predicado tab: escrevetab:-tab(15),write(‘Soma de 2 Elementos’),nl,nl, tab(1),write(‘Entre com o 1º elemento: ’),read(X), tab(1),write(‘Entre com o 2º elemento: ’),read(Y), tab(25),write(‘________’),nl,Total is X+Y, tab(28),write(Total),nl,nl,confirma. confirma:-tab(1),write(‘Deseja calcular novamente (s/n): ’), read(Resp), Resp = s , escrevetab, ! . Questão: ?-escrevetab. Soma de 2 Elementos Entre com o 1º elemento: 123. Entre com o 2º elemento: 456. 579 Deseja calcular novamente (s/n): n. 60 Torre de Hanói: hanoi(N):-move(N,a,b,c). move(1,A,_,C):-informa(A,C),!. move(N,A,B,C):N1 is N-1, move(N1,A,C,B), informa(A,C), move(N1,B,A,C). informa(Loc1, Loc2):-write( ‘Move um disco de ’) , write(Loc1),write(‘ para ’), write(Loc2),nl. Questão: ?-hanoi(3). Move um disco de a para c Move um disco de a para b Move um disco de c para b Move um disco de a para c Move um disco de b para a Move um disco de b para c Move um disco de a para c yes 61 Criação e Escrita em Arquivo ?- fcreate(arq,'c:\saida.pl',2). yes ?- output(arq). yes ?- hanoi(3). yes ?- fclose(arq). yes Arquivo saida.pl Move o disco de a para c Move o disco de a para b Move o disco de c para b Move o disco de a para c Move o disco de b para a Move o disco de b para c Move o disco de a para c Nota: Caso o arquivo já exista, utilize o comando fopen(arq,’c:\saida.pl’,2) no lugar de fcreate(arq,’saida.pl’,2). 62 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 63 Manipulando Base de Dados Definição de uma Base de Dados: gosta(maria, cafe). gosta(jose, cha). gosta(ana, guarana). gosta(pedro, vinho). gosta(jose, coca). gosta(maria, vinho). bagof(X,P,L) Recupera da Base de Dados. L é uma lista formada por todo X que satisfaz a condição P. Questões: ?-bagof(X,gosta(X,Y),L). X = _00E4 , % variável de memória Y = café , L = [maria] ; X = _00E4 , Y = cha , L = [jose] ; X = _00E4 , Y = coca , L = [jose] ; 64 X = _00E4 , Y = guarana , L = [ana] ; X = _00E4 , Y = vinho , L = [pedro, maria] ; no ?-bagof(X,Y^gosta(Y,X),L). %Lista única X = _00E4 , Y = _00F4 , L = [cafe, cha, guarana, vinho, coca, vinho] setof(X,P,L) Recupera da Base de Dados. L é uma lista ordenada, sem repetição, formada por todo X que satisfaz a condição P. Similar ao predicado bagof. Questão: ?- setof(X, Y^gosta(Y, X), L). X = _00E4 , Y = _00F4 , L = [cafe, cha, coca, guarana, vinho] 65 findall(X,P,L) L é uma lista formada por todo X que satisfaz a condição P. A diferença com o bagof é que todos os objetos X são coletados mesmo que as variáveis em P não sejam parte de X. Definição de uma Base de Dados: pessoa(joao,itau,30). pessoa(jose,nacional,35). pessoa(maria,real,30). Questões: ?- findall(Idade,pessoa(_,_,Idade),Lista). Idade = _0084 , Lista = [30, 35, 30] %Lista com todas as idades ?- findall(Idade/Quem,pessoa(Quem,_,Idade),Lista). Idade = _ , Quem = _ , Lista = [30 / joao,35 / jose,30 / maria] ?- findall(Idade,pessoa(Quem,_,Idade),Lista). Idade = _ , Quem = _ , Lista = [30,35,30] ?-findall(Quem,pessoa(Quem,_,30),Lista). Quem = _0084 , Lista = [joao,maria] %Lista de nomes com 30 anos 66 consult(X) Realiza uma consulta a um determinado Banco de Dados . Consult(X) carrega o conteúdo do Banco de Dados para a memória. Uma vez carregado o Banco de Dados, os fatos e regras nele contidos poderão ser utilizados. Arquivo quad.pl quadrado:read(A), B is A * A, write(B). Questões: ?- consult(‘c:\quad.pl’). yes ?- quadrado. 5. 25 yes listing(X) Permite a listagem de um determinado predicado que está sendo utilizado. Questão: ?- listing(quadrado). quadrado:read(A), B is A * A, write(B). yes 67 Definição de uma Base de Dados: rapido(ann). lento(tom). lento(pat). assert Adiciona cláusulas na Base de Dados. Questões: ?- assert( ( maisrapido(X, Y):- rapido(X), lento(Y))). X = _0084 , Y = _0098 ?- maisrapido(A, B). A = ann , B = tom ; A = ann , B = pat asserta e assertz asserta(C) = Adiciona C no começo da Base de Dados. assertz(C) = Adiciona C no fim da Base de Dados. 68 Questões: ?- assert( p(a) ), assertz( p(b) ), asserta( p(c) ). yes ?- p(X). X=c ; X=a ; X=b retract Remove dados e predicados da Base de Dados em memória. Base de Dados antes da remoção: rapido(ann). lento(tom). lento(pat). Questões: ?- retract( lento(X) ). X = tom ; X = pat ; no ?- maisrapido(A, B). no 69 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 70 Outros Exemplos Problema dos Missionários e Canibais Na margem esquerda de um rio há 5 missionários e 5 canibais, que querem atravessá-lo para a outra margem . Há um bote que pode transportar no máximo 3 pessoas ao mesmo tempo. Os missionários não podem ficar em menor número que os canibais em qualquer margem, caso contrário serão devorados. Como fazer para que todos cheguem a salvo na outra margem do rio? Resolução: Consideraremos a seguinte descrição para o conjunto de estados possíveis utilizados para representar o problema (Me, Ce, Bte) onde, Me = número de missionários na margem esquerda Ce = número de canibais na margem esquerda Bte = 1 se o bote está na margem esquerda, 0 na direita Logo, o estado inicial é (5, 5, 1) e o final (0, 0, 0) 71 Representação do Espaço de Estados do Problema (5,5,1) (5,4,0) (5,3,0) (5,2,0) (5,4,1) (4,4,0) (5,3,1) (3,3,0) (5,1,0) (5,0,0) (4,4,1) (5,2,1) (5,1,1) (2,2,0) (3,3,1) (0,3,0) (0,4,1) (0,1,0) (1,1,1) (0,5,1) (0,2,0) (0,3,1) (0,4,0) (2,2,1) (0,2,1) (0,0,0) 72 O programa prolog que encontra a solução para este problema usando busca em largura é mostrado abaixo. largura([ ],_,_):- write('Solução não encontrada!'),nl. largura([G|T],_,G):- write('Solução encontrada!'),nl. largura([S|T],C,G):write('Listas a Percorrer: '),write([S|T]),nl, write('Listas Percorridas: '), write(C),nl, bagof(X,movelargura( S, T, C, X ),Lista), append(T,Lista,O ),!, largura(O, [S|C],G ). largura([S|T],C,G):- largura(T,[S|C],G). movelargura(S,T,C,X):- move(S,X), X \== S, not member(X,T), not member(X,C). % representação do espaço de estados move([5,5,1],[5,2,0]). move([5,5,1],[5,4,0]). move([5,5,1],[4,4,0]). move([5,5,1],[5,3,0]). move([5,2,0],[5,3,1]). move([5,2,0],[5,4,1]). move([4,4,0],[5,4,1]). move([0,1,0],[0,2,1]). 73 move([5,3,0],[5,4,1]). move([5,3,1],[3,3,0]). move([5,3,1],[5,0,0]). move([5,3,1],[5,1,0]). move([5,4,1],[3,3,0]). move([5,4,1],[5,1,0]). move([5,0,0],[5,1,1]). move([5,1,0],[5,2,1]). move([2,2,0],[3,3,1]). move([0,3,0],[0,4,1]). move([0,4,1],[0,2,0]). move([0,5,1],[0,2,0]). move([0,2,0],[2,2,1]). move([0,1,0],[0,3,1]). move([0,1,0],[1,1,1]). move([0,3,1],[0,0,0]). move([1,1,1],[0,0,0]). move([0,2,1],[0,0,0]). move([5,0,0],[5,2,1]). move([3,3,0],[4,4,1]). move([5,2,1],[2,2,0]). move([3,3,1],[0,3,0]). move([0,3,0],[0,5,1]). move([0,4,1],[0,1,0]). move([0,5,1],[0,4,0]). move([0,2,0],[0,3,1]). ?- largura([[5,5,1]],[ ],[0,0,0]). Neste caso partiu-se com o Estado Inicial, a Lista dos Percorridos e o Estado Final. 74 Organização de uma Lista Bubblesort bubblesort(List,Sorted):-swap(List,List1),!, bubblesort(List1,Sorted). bubblesort(Sorted,Sorted). swap([X,Y|Rest],[Y,X|Rest]):-gt(X,Y). swap([Z|Rest],[Z|Rest1]):-swap(Rest,Rest1). gt(X,Y):-X>Y. Questão: ?- bubblesort([9,5,3,8],L). L = [3,5,8,9] Quicksort quicksort([],[]). quicksort([X|Tail],Sorted):-split(X,Tail,Small,Big), quicksort(Small,Sortedsmall),quicksort(Big,Sortedbig), conc(Sortedsmall,[X|Sortedbig],Sorted). 75 split(X,[],[],[]). split(X,[Y|Tail],[Y|Small],Big):-gt(X,Y),!, split(X,Tail,Small,Big). split(X,[Y|Tail],Small,[Y|Big]):-split(X,Tail,Small,Big). conc([],L,L). conc([X|L1],L2,[X|L3]):-conc(L1,L2,L3). gt(X,Y):-X>Y. Questão: ?- quicksort([5,8,0,2,7],L). L = [0,2,5,7,8] ; no Permutation Sort psort(Xs,Ys) :- permutation(Xs,Ys), ordenado(Ys). permutation(Xs,[Z|Zs]) :- select(Z,Xs,Ys), permutation(Ys,Zs). permutation([],[]). ordenado([]). ordenado([X]). ordenado([X,Y|Ys]) :- X <= Y, ordenado([Y|Ys]). Questão: ?- psort([3,4,2,1],Y). Y = [1,2,3,4] 76 Insertion Sort isort([X|Xs],Ys) :- isort(Xs,Zs), insert(X,Zs,Ys). isort([],[]). insert(X,[],[X]). insert(X,[Y|Ys],[Y|Zs]) :- X > Y, insert(X,Ys,Zs). insert(X,[Y|Ys],[X,Y|Ys]) :- X <= Y. Questão: ?- isort([3,2,1],A). A = [1,2,3] Investigando um Crime Definição de uma base de dados: possivel_suspeito(fred). possivel_suspeito(mary). possivel_suspeito(jane). possivel_suspeito(george). crime(roubo,john,terca,parque). crime(roubo,robin,quinta,bar). crime(assalto,jim,quarta,bar). estava(fred,terca,parque). inveja(fred,john). 77 principal_suspeito(Pessoa,Crime):crime(Crime,Vitima,Dia,Lugar), possivel_suspeito(Pessoa), estava(Pessoa,Dia,Lugar), tem_motivo_contra(Pessoa,Vitima). principal_suspeito(desconhecido,Crime). tem_motivo_contra(Pessoa,Vitima):inveja(Pessoa,Vitima). Questões: ?- principal_suspeito(Quem,roubo). Quem = fred ; Quem = desconhecido ?- crime(Crime,Vitima,Dia,bar). Crime = roubo , Vitima = robin , Dia = quinta ; Crime = assalto , Vitima = jim , Dia = quarta ; 78 Calculando o M.D.C. de uma Lista de Números Uma das formas de calcularmos o m.d.c de dois números é fatorarmos os dois e selecionarmos os fatores primos com os menores expoentes em cada um deles, por exemplo: 18 = 2^1 * 3^2 12 = 2^2 * 3^1 mdc(12, 18) = 2^1 * 3^1 = 6 Outra forma de calcularmos o m.d.c. de dois números é seguindo a seguinte regra: 1) Se a divisão do maior número pelo menor for exata então o mdc é igual ao menor número. 2) Senão o processo é repetido usando o menor número e o resto da divisão do maior pelo menor. Ex: mdc(18, 12) 18 | 12 01 --06 mdc(12, 06) 12 | 06 12 02 --00 mdc(18, 12) = 6 79 Nosso programa PROLOG utiliza o segundo método com a diferença que o estendemos para uma lista de números e não apenas para dois números: mdc([A|[]], A) :- !. mdc([A|Calda], N) :- mdc(Calda, B), mdc2(A, B, N), !. mdc2(A, B, B) :- X is A mod B, X is 0, !. mdc2(A, B, N) :- X is A mod B, mdc2(B, X, N). O predicado mdc possui duas clausulas, a primeira existe para determinarmos qual o mdc de uma lista com apenas um elemento, e a segunda clausula determina o mdc de uma lista com dois ou mais elementos, calculando recursivamente o mdc da calda desta lista e em seguida o mdc da cabeca da lista com o mdc da calda da lista já calculado. As clausulas do predicado mdc2 aplicam os passos 1 e 2 do segundo método mostrado acima. Abaixo mostramos alguns exemplos de execução do programa: ?- mdc([18,30,12,60], R). R=6 ?- mdc([18,30,10,60], R). R=2 ?- mdc([15,6,12,9], R). R=3 ?- mdc([15,30,20,12], R). R=1 ?- mdc([16,8,32,64], R). R=8 80 O Problema das Damas no Tabuleiro de Xadrez O enunciado do problema é o seguinte: “Em um tabuleiro de xadrez, ou de um modo mais geral um tabuleiro de N*N casas, queremos posicionar N damas de modo que nenhuma das damas ataca qualquer outra”. A resolução do problema está diretamente relacionada à movimentação exercida pela dama; No jogo de xadrez a dama caminha nas verticais, horizontais e diagonais, deste modo já eliminamos a possibilidade de haver duas ou mais damas numa mesma coluna ou numa mesma linha, restando apenas verificar se todas elas não se atacam nas diagonais. Podemos mapear o problema da seguinte maneira: Para um tabuleiro de N linhas por N colunas colocamos uma dama em cada coluna, cada uma em uma linha diferente da outra e então começamos a permutar cada dama de uma certa coluna com cada outra dama de outra coluna, desta forma realizaremos N! permutações, entre estas permutações eventualmente encontraremos situações onde nenhuma dama ataca nenhuma outra, esta situação representa uma solução. A figura abaixo representa uma das 82 soluções encontrada pelo programa PROLOG para um tabuleiro de 8*8 com 8 damas: ?- damas(8,R). R = [1,5,8,6,3,7,2,4] 81 Na figura contamos as linhas de baixo para cima, por exemplo o número 5 da resposta corresponde a dama da segunda coluna colocada na quinta linha. A baixo mostramos o código em PROLOG. damas(NumDamas,Solucao) :- intervalo(1,NumDamas,Lista), permuta(Lista,Solucao), seguro(Solucao). intervalo(A,A,[A]). intervalo(A,B,[A|Calda]) :- A < B, Proximo is A+1, intervalo(Proximo,B,Calda). permuta([],[]). permuta(Lista,[Elemento|Calda]) :retira(Elemento,Lista,NovaLista), permuta(NovaLista,Calda). retira(X,[X|Calda],Calda). retira(X,[Y|Calda],[Y|NovaCalda]) :- retira(X,Calda,NovaCalda). seguro([]). seguro([Cabeca|Calda]) :- seguro(Calda), not ataca(Cabeca,Calda). ataca(Cabeca,Calda) :- ataca(Cabeca,1,Calda). ataca(X,Distancia,[Y|Calda]) :- X is Y+Distancia; X is Y-Distancia. ataca(X,Distancia,[Y|Calda]) :- N1 is Distancia+1, ataca(X,N1,Calda). O predicado damas é composto de dois argumentos NumDamas (número de damas) e Solucao (lista que contém a solução), e dos predicados intervalo (que cria uma lista do tipo [1, 2 ,3 , 4 ,5 ,6 ,7 ,8]), permuta (que realiza todas as permutações na lista que criamos) e seguro (que testa se uma dada situação gerada a partir de uma permutação é uma situação aceitável, ou seja nenhuma dama ataca nenhuma outra). 82 Calculando Números Primos Os números primos são aqueles divisíveis por si e pelo número um, desta forma poderíamos determinar se um número é primo dividindo-o por todos os números anteriores a ele e contando quantos números conseguiram dividi-lo sem deixar resto. Este processo seria muito demorado, principalmente se desejássemos calcular uma lista de números primos. Podemos resolver este problema de uma maneira diferente: 1) Criamos uma lista de números consecutivos que começa em 2 e vai até o maior número do intervalo no qual queremos encontrar os números primos. 2) Pegamos o primeiro número da lista e cortamos todos os seus múltiplos. 3) Repetimos o processo para cada novo elemento na lista. No final do processo a lista restante contém apenas elementos que não possuem divisores diferente do número 1, ou seja, apenas números primos. A figura seguir ilustra o processo para os números de 2 a 100. 83 O código PROLOG é mostrado a seguir: primos(Limite, Primos) :- geraLista(2, Limite, Lista), geraPrimos(Lista, Primos). geraLista(Menor, Maior, [Menor|Lista]) :- Menor =< Maior, N is Menor+1, geraLista(N, Maior, Lista), !. geraLista(_, _, []). geraPrimos([], []) :- !. geraPrimos([Primo|Lista], [Primo|MaisPrimos]) :removeMultiplos(Primo, Lista, NovaLista), geraPrimos(NovaLista, MaisPrimos). removeMultiplos(Num, [], []) :- !. removeMultiplos(Num, [Cabeca|Lista], [Cabeca|NovaLista]) :not(0 is Cabeca mod Num), removeMultiplos(Num, Lista, NovaLista), !. removeMultiplos(Num, [Cabeca|Lista], NovaLista) :0 is Cabeca mod Num, removeMultiplos(Num, Lista, NovaLista). O predicado removeMultiplos gera uma lista que possui apenas números que não são múltiplos de um certo número dado. O predicado geraLista constrói uma lista com todos os números entre um número menor e um maior. O predicado geraPrimos pega o primeiro número de uma lista e arranca todos os múltiplos deste elemento, então pega a esta nova lista e repete o processo. O predicado primos gera uma lista inicialmente com todos os números num intervalo dado e após isto chama o predicado geraPrimos. A seguir mostramos uma execução: ?- primo(100,L). L = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73, 79,83,89,97] 84 Encontrando o Menor Caminho Entre Duas Cidades Quando desejamos encontrar o menor caminho entre duas cidades devemos primeiramente conhecer todos os caminhos que nos levam da cidade origem até a cidade destino, para isso devemos montar um grafo onde os nós são as cidades e os arcos são as distâncias entre uma cidade e outra. O grafo abaixo representa um exemplo: Nosso programa PROLOG deve encontrar todos os caminhos possíveis entre a cidade origem e a cidade destino e escolher aquele cuja a soma das distâncias correspondesnte às viagens de uma cidade para outra é a menor. O programa é o seguinte: distancia(a, d, 200). distancia(a, e, 500). distancia(b, e, 100). distancia(b, f, 100). distancia(d, e, 100). distancia(e, g, 300). distancia(f, i, 100). distancia(g, h, 300). vizinho(X, Y, Dist) :- distancia(X, Y, Dist). vizinho(X, Y, Dist) :- distancia(Y, X, Dist). pertence(X, [X|_]). pertence(X, [_|L]) :- pertence(X,L). distancia(b, c, 100). distancia(c, f, 100). distancia(e, i, 400). distancia(h, i, 200). 85 caminho(Origem, Origem, _, [], 0). caminho(Origem, Destino, CidadesVisitadas, [Intermediario|Caminho], Distancia) :vizinho(Origem, Intermediario, Dist1), not(pertence(Intermediario, CidadesVisitadas)), caminho(Intermediario, Destino, [Intermediario|CidadesVisitadas], Caminho, Dist2), Distancia is Dist1 + Dist2. primeiro([[Caminho,Distancia]|Lista], Caminho, Distancia). resolve(Origem, Destino, [Origem|Caminho], Distancia) :bagof([Camin,Dist], caminho(Origem, Destino, [Origem], Camin, Dist), Lista), sort(Lista,ListaOrdenada,[2]), primeiro(ListaOrdenada, Caminho, Distancia). O predicado distancia determina as distâncias entre as cidades, o predicado vizinho determina que se uma cidade dista x de outra então esta segunda dista x da primeira, o predicado pertence determina se uma elemento já pertence a uma lista ou não (no nosso caso se uma cidade já pertence a uma lista de cidades), o predicado caminho encontra o caminho de uma cidade até outra bem como a distância total do percurso, o predicado primeiro pega o primeiro elemento de uma lista e o predicado resolve é o responsável por pegar todos os caminhos encontrados e escolher o de menor distância. A seguir mostramos um exemplo para o grafo anterior: ?- resolve(a, h, Caminho, Distancia). Caminho = [a,d,e,b,f,i,h] , Distancia = 800 86 Busca em Largura em Árvore Binária Freqüentemente necessitamos realizar buscas em árvores para solucionar problemas, como por exemplo quando queremos verificar todos os estados de um jogo de dama para chegar a uma posição vitoriosa. O backtracking de PROLOG nos permite realizar buscas em profundidade com certa facilidade, no entanto, para alguns problemas necessitamos realizar buscas em largura, o que não é uma tarefa tão simples. A busca é feita da seguinte forma: 1) Coloca-se o nó raiz em uma lista 2) Expande-se (gera-se todos os seus filhos) o primeiro elemento da lista e coloca-se o resultado no final da lista 3) Repete-se o processo para o segundo da lista, terceiro da lista, etc..., até que não haja mais elementos para expandir. No final teremos uma lista de nós cuja ordem é a ordem da busca em largura. A figura abaixo mostra uma árvore exemplo e os passos do processo descrito acima. 87 O código PROLOG é mostrado a seguir: buscaLargura([], []). buscaLargura([tree(No, Esq, Dir)|Calda], [No|Resultado]) :bagof(X, filho(tree(No, Esq, Dir), X), Lista), append(Calda, Lista, NovaLista), buscaLargura(NovaLista, Resultado), !. buscaLargura([tree(No, Esq, Dir)|Calda], [No|Resultado]) :buscaLargura(Calda, Resultado), !. filho(tree(No, Esq, Dir), Esq) :- Esq \== null. filho(tree(No, Esq, Dir), Dir) :- Dir \== null. O predicado buscaLargura realiza o processo de expandir o primeiro nó da lista, criar uma nova lista que contém também os nós expandidos e repetir o processo para cada nó sucessivo da lista. O predicado filho é usado para retornar cada filho de um certo nó. A seguir mostramos a execução: ?- buscaLargura( [tree(3, tree(1, null, tree(2, null, null)), tree(7, tree(5, tree(4, null, null), tree(6, null, null)), tree(8, null, null)))],R). R = [3,1,7,2,5,8,4,6] 88 Verificação de Árvores AVL Neste exemplo mostraremos como verificar se uma dada árvore é uma árvore AVL. Árvores AVL são árvores binárias de busca que possuem altura mínima, ou seja, em uma árvore de N nós a distância máxima da raiz a qualquer folha é no máximo log(N). Árvores de altura mínima são chamadas árvores balanceadas. Portanto devemos verificar duas condições: 1)A árvore binária deve ser uma árvore de busca. 2)A árvore binária deve estar balanceada. Dizemos que uma árvore binária é de busca se todos os elementos da subárvore esquerda de qualquer nó são menores que este nó e todos os elementos da subárvore direita de qualquer nó são maiores que este nó. Dizemos que uma árvore é balanceada se a diferença de altura da subárvore esquerda para a subárvore direita não exceder em módulo a unidade. Desta forma o teste de árvores AVL fica como mostrado a seguir: ehAVL(Arvore) :- arvoreBusca(Arvore), balanceada(Arvore). arvoreBusca(null). arvoreBusca(tree(Raiz, ArvoreEsq, ArvoreDir)) :- maior(Raiz, ArvoreEsq), menor(Raiz, ArvoreDir). maior(_, null). maior(Maior, tree(Raiz, ArvoreEsq, ArvoreDir)) :- Maior > Raiz, maior(Raiz, ArvoreEsq), menor(Raiz, ArvoreDir), maior(Maior, ArvoreDir). 89 menor(_, null). menor(Maior, tree(Raiz, ArvoreEsq, ArvoreDir)) :Maior < Raiz, maior(Raiz, ArvoreEsq), menor(Raiz, ArvoreDir), menor(Maior, ArvoreEsq). balanceada(null). balanceada(tree(_, ArvoreEsq, ArvoreDir)) :balanceada(ArvoreEsq), balanceada(ArvoreDir), altura(ArvoreEsq, HE), altura(ArvoreDir, HD), DIF is HE - HD, abs(DIF) =< 1. altura(null, 0). altura(tree(_, ArvoreEsq, ArvoreDir), Altura) :altura(ArvoreEsq, HE), altura(ArvoreDir, HD), Altura is max(HE, HD) + 1. Na chamada ao predicado ehAVL devemos passar uma estrutura de árvore como a mostrada nos exemplos a seguir: ?- ehAVL(tree(3, tree(1, null, tree(2, null, null)), tree(7, tree(5, tree(4, null, null), tree(6, null, null)), tree(8, null, null)))). yes ?- ehAVL(tree(3, tree(1, null, tree(2, null, null)), tree(7, tree(5, tree(0, null, null), tree(6, null, null)), tree(8, null, null)))). no ?- ehAVL(tree(3, tree(1, null, tree(2, null, null)), tree(7, tree(5, tree(4, null, null), tree(6, null, null)), null))). no 90 Reconhecendo Cadeias de Símbolos Através de MEFs Máquinas de Estados Finita (MEF) podem ser representadas de modo simplificado por um conjunto de estados e um conjunto de transições de estados, além dos estados finais e do estado inicial. Freqüentemente desejamos verificar se uma certa cadeia de símbolos pertence a linguagem determinada por uma MEF, para isso devemos aplicar uma seqüência de transições correspondentes aos símbolos dessa seqüência que nos levarão de um estado inicial a um estado final. Caso o estado final pertença ao conjunto de estados finais dizemos que a seqüência de entrada pertence a linguagem determinada pela MEF. Existem ainda MEFs que possuem transições de um determinado estado para mais de um estado, estas MEFs são chamadas MEF indeterminísticas (MEF_ind), diferentemente da primeira conhecida como MEF determinística (MEF_det). Quando trabalhamos com MEFs_ind devemos ser capazes de determinar qual o conjunto de estados resultante da aplicação de uma transição sobre vários estados da MEF e não apenas um único estado resultante da aplicação de uma única transição sobre um único estado. O programa que implementamos em PROLOG aceita MEFs_ind embora o exemplo escolhido represente uma MEF_det. terminal(q8). transicao(q0, a, q3). transicao(q1, b, q2). transicao(q3, b, q4). transicao(q5, a, q8). transicao(q7, a, q4). transicao(q0, b, q1). transicao(q2, a, q5). transicao(q4, a, q7). transicao(q6, a, q3). transicao(q7, b, q8). transicao(q1, a, q4). transicao(q3, a, q6). transicao(q4, b, q5). transicao(q6, b, q7). transicao(q8, a, q5). 91 verifica(Estados, Entradas, []) :- aplicaTransicoes(Estados, Entradas, []), write('Nao ha transicao prevista, a MEF para: '), !. verifica(Estados, Entradas, Resposta) :aplicaTransicoes(Estados, Entradas, Resposta), temTerminal(Resposta), write('Sequencia valida: '), !. verifica(Estados, Entradas, Resposta) :aplicaTransicoes(Estados, Entradas, Resposta), write('Sequencia invalida: '), !. aplicaTransicoes([], Entradas, []) :- !. aplicaTransicoes([Estado|[]], [], [Estado]) :- !. aplicaTransicoes([Estado|[]], [Entrada|Sequencia], EstadosFinais) :- transicoes(Estado, Entrada, ListaEstados), aplicaTransicoes(ListaEstados, Sequencia, EstadosFinais), !. aplicaTransicoes([Estado|MaisEstados], Entradas, EstadosFinais) :- aplicaTransicoes([Estado], Entradas, ListaA), aplicaTransicoes(MaisEstados, Entradas, ListaB), uniao(ListaA, ListaB, EstadosFinais). temTerminal([Estado|_]) :- terminal(Estado), !. temTerminal([_|Estados]) :- temTerminal(Estados). transicoes(Estado, Entrada, ListaEstados) :bagof(NovoEstado, transicao(Estado, Entrada, NovoEstado), ListaEstados), !. transicoes(Estado, Entrada, []). uniao([], [], []) :- !. uniao([], Lista, NovaLista) :- tiraRepetidos(Lista, NovaLista), !. uniao(Lista, [], NovaLista) :- tiraRepetidos(Lista, NovaLista), !. uniao([Elem|ListaA], ListaB, NovaLista) :- pertence(Elem, ListaB), uniao(ListaA, ListaB, NovaLista), !. uniao([Elem|ListaA], ListaB, NovaLista) :- pertence(Elem, ListaA), uniao(ListaA, ListaB, NovaLista), !. uniao([Elem|ListaA], ListaB, [Elem|NovaLista]) :- uniao(ListaA, ListaB, NovaLista). tiraRepetidos([], []) :- !. 92 tiraRepetidos([Elem|[]], [Elem]) :- !. tiraRepetidos([Elem|Calda], Saida) :- pertence(Elem, Calda), tiraRepetidos(Calda, Saida), !. tiraRepetidos([Elem|Calda], [Elem|Saida]) :tiraRepetidos(Calda, Saida). pertence(Elem, [Elem|_]) :- !. pertence(Elem, [_|Lista]) :- pertence(Elem, Lista). Os fatos terminal e transicao determinam a estrutura da MEF (estados, transições e estado final). O predicado verifica possui três argumentos: uma lista de estados, uma lista representando a seqüência de símbolos de entrada e uma lista de estados finais da MEF correspondente às transições realizadas após a verificação. No processo de verificação devemos aplicar transições correspondentes aos símbolos de entrada, esta tarefa é realizada pelo predicado aplicaTransicoes, neste predicado encontramos um conjunto de estados resultantes das transições correspondentes a uma determinada entrada e repetimos este processo até que se esgotem os símbolos de entrada. No final do processo de aplicação de transições teremos duas listas com estados finais da MEF (uma gerada a partir do primeiro estado da lista de estados e outra gerada a partir dos estados restantes). Devemos agora unir estas duas listas em uma (tarefa realizada pelo predicado uniao) retirando as possíveis repetições de estados através do predicado tiraRepetidos. Por fim determinamos se o conjunto de estados finais possui ao menos um elemento terminal, esta tarefa é realizada pelo predicado temTerminal. A estrutura da MEF é mostrada na figura abaixo e aceita as cadeias de ‘a’s e ‘b’s que possuem número par de ‘a’s (2, 4, ..) e exatamente 2 ‘b’s em qualquer ordem. 93 Representação gráfica da MEF Algumas possíveis execuções seriam: ?- verifica([q0], [a,b,a,a,b,a,a,a], R). Sequencia valida: R = [q8] ?- verifica([q0], [b,a,a,b,b,a,a], R). Nao ha transicao prevista, a MEF para: R = [] ?- verifica([q0], [a,b,a,a], R). Sequencia invalida: R = [q4] Poderiamos ainda iniciar a MEF a partir de mais de um estado como por exemplo q0 e q4, isso acarretaria no reconhecimento de seqüências com um único ‘b’ e um número impar de ‘a’s (1, 3, ...) além das seqüências reconhecidas anteriormente a partir de q0. ?- verifica([q0,q4], [a,b,a,a], R). Sequencia valida: R = [q4,q8] ?- verifica([q0,q4], [a,b,b,a,a], R). Sequencia invalida: R = [q5] ?- verifica([q0,q4], [a,a,b,b,a,a], R). Sequencia valida: R = [q8] 94 O Problema das Jarras D’água O problema das jarras d’água tradicional consiste de duas jarras de capacidades 3 e 5 litros (inicialmente vazias) e uma fonte d’água para encher as jarras. O objetivo do problema é deixar 4 litros na jarra de capacidade 5 litros, para isso podemos realizar as seguintes ações: encher uma jarra, esvaziar uma jarra ou despejar o conteúdo de uma jarra na outra. Poderíamos representar a seqüência de transições que nos conduzem à resposta representando os estados das jarras (através de pares) entre cada ação tomada. A solução seria a seguinte: (0,0), (0,5), (3,2), (0,2), (2,0), (2,5), (3,4). Outra maneira de representarmos a solução seria reportando a seqüência de passos a serem tomados: encher a 2o. jarra, virar a 2o. jarra na 1o., esvaziar a 1o. jarra, virar a 2o. jarra na 1o., encher a 2o. jarra, virar a 2o. jarra na primeira. Esquema das transições que levam o estado inicial a um estado final 95 O problema que mostramos a seguir estende o problema tradicional das jarras d’água para qualquer número de jarras, com qualquer configuração inicial, qualquer capacidade e qualquer configuração final da jarras. Além disso impomos um limite de transições para alcançarmos o estado final, isto significa que eventualmente podemos não encontrar uma solução, não porque esta solução não existe, mas sim porque ela necessita de mais transições do que o limite que colocamos. move(JarraA, CapacA, JarraB, CapacB, NovoA, NovoB) :NovoA is CapacA, NovoB is JarraB, JarraA\=CapacA. move(JarraA, CapacA, JarraB, CapacB, NovoA, NovoB) :NovoA is 0, NovoB is JarraB, JarraA\=0. move(JarraA, CapacA, JarraB, CapacB, NovoA, NovoB) :NovoA is 0, NovoB is JarraA+JarraB, NovoB<CapacB, JarraA\=0. move(JarraA, CapacA, JarraB, CapacB, NovoA, NovoB) :NovoA is JarraA-(CapacB-JarraB), NovoB is CapacB, CapacB-JarraB=<JarraA, JarraB\=CapacB. altera([Cabeca|Calda], 0, Valor, [Valor|Calda]). altera([Cabeca|Calda], Posicao, Valor, [Cabeca|NovaCalda]) :NovaPosicao is Posicao-1, altera(Calda, NovaPosicao, Valor, NovaCalda). pegaItem([Cabeca|Calda], Cabeca, 0). pegaItem([_|Calda], Elemento, NovoContador) :pegaItem(Calda, Elemento, Contador), NovoContador is Contador+1. pegaJarra(Configuracao, Jarra, Posicao) :pegaItem(Configuracao, Jarra, Posicao). pegaCapacidade(Capacidades, Capac, Posicao) :pegaItem(Capacidades, Capac, Posicao). pertence(X, [X|_]) :- !. pertence(X, [_|Z]) :- pertence(X, Z). 96 transicao(ConfFinal, ConfFinal, Capacidades, Lista, Contador) :- reverse(Lista, NovaLista), write(NovaLista), !. transicao(ConfInicial, ConfFinal, Capacidades, Lista, Contador) :- Contador>0, NovoContador is Contador-1, pegaJarra(ConfInicial, JarraA, PosicaoA), pegaCapacidade(Capacidades, CapacA, PosicaoA), pegaJarra(ConfInicial, JarraB, PosicaoB), pegaCapacidade(Capacidades, CapacB, PosicaoB), PosicaoA\=PosicaoB, move(JarraA, CapacA, JarraB, CapacB, NovoA, NovoB), altera(ConfInicial, PosicaoA, NovoA, ConfTemp), altera(ConfTemp, PosicaoB, NovoB, NovaConf), not(pertence(NovaConf, Lista)), transicao(NovaConf, ConfFinal, Capacidades, [NovaConf|Lista], NovoContador), !. resolve(Inicial, Final, Capacidade, Contador) :caminho(Inicial, Final, Capacidade, [Inicial], Contador). Os predicados move representam as transições que podemos realizar entre duas jarras: encher, esvaziar e virar uma em outra (virando até encher ou virando todo o conteúdo). O predicado altera recebe uma lista, uma posição e um valor e retorna uma nova lista igual, exceto na posição recebida, onde o valor será alterado para o valor recebido. O predicado pegaItem recebe uma lista e devolve um valor e sua posição correspondente na lista, caso a posição já esteja definida pegaItem retorna apenas o valor associado à posição. Os predicados pegaJarra e pegaCapacidade apesar de possuírem nomes diferentes realizam a mesma tarefa, selecionam um valor e uma posição em uma lista dada. 97 O predicado transicao é o coração do algoritmo, no primeiro predicado representamos um estado solução, ou seja, a configuração atual do problema é igual a configuração final do problema. Neste caso mostramos na tela a seqüência de estados que representam a solução. No segundo predicado, primeiramente testamos e decrementamos um contador para que possamos garantir que não iremos procurar soluções que possuam mais que um determinado número de transições, após isso selecionamos duas jarras (na verdade todas as combinações de duas jarras) e suas respectivas capacidades, então conferimos se não pegamos duas vezes a mesma jarra. Caso não tenhamos pego, criamos uma nova lista parecida com a lista que possui a configuração atual, mas com os conteúdos das duas jarras alterados, após isso verificamos se a nova configuração que construímos não é um estado que já faz parte de nossa lista de transições. Caso afirmativo, continuamos o processo procurando agora chegar à configuração final à partir do novo estado que geramos (com a possibilidade de realizarmos uma transição a menos e com um estado a mais na lista de estados que constituem o caminho solução). O predicado resolve recebe uma lista que representa a configuração inicial das jarras, uma lista que representa o estado final, uma lista que representa a capacidade das jarras e o número máximo de transições que desejamos realizar. A seguir mostramos algumas execuções. ?- resolve( [0,1,2], [2,2,4], [2,3,5], 4 ). [ [0,1,2], [1,0,2], [1,2,0], [1,2,5], [2,2,4] ] yes ?- resolve( [3,4], [1,0], [5,7], 4 ). no 98 Note que no último exemplo não conseguimos encontrar uma solução com 4 transições ou menos. No entanto, se estipularmos 5 transições o problema já passa a ser solúvel. ?- resolve( [3,4], [1,0], [5,7], 5 ). [ [3,4], [3,0], [0,3], [5,3], [1,7], [1,0] ] yes Outro aspecto importante do problema é que podemos omitir partes do estado solução quando não estamos interessados em alguma jarra em específico: ?- resolve( [0,1], [3,2], [6,4], 10 ). no ?- resolve( [0,1], [3,_], [6,4], 5 ). [ [0,1], [6,1], [3,4] ] yes Também podemos estabelecer relações de igualdade entre os conteúdos finais das jarras: ?- resolve( [1,1,3,2], [X,2,X,1], [2,4,5,3], 3 ). [ [1,1,3,2], [0,2,3,2], [0,2,2,3], [2,2,2,1] ] X = 2 99 O Problema da Coloração de Mapas Quando estamos colorindo um mapa queremos que cada país, ou estado, possua uma cor diferente de qualquer um de seus vizinhos para que possa ser bem delimitado. Matematicamente está provado que em um plano não podemos interconectar totalmente mais que 4 elementos, ou seja, teremos no máximo 4 países que fazem fronteira um com o outro sem possibilidade de repetirmos cores. Se inserirmos um quinto país, onde quer que seja, haverá sempre uma cor que já usamos e assim poderemos repeti-la. Desta forma nosso programa de coloração de mapas necessita apenas 4 cores. Uma das diversas resoluções é a que se segue: 1) Definimos os países e as fronteiras entre eles. 2) Escolhemos um país e damos uma cor a ele temporariamente. 3) Preenchemos os seus vizinhos com as cores restantes, também temporariamente. 4) Pegamos outro país e repetimos o processo. 5) Caso não seja possível distribuir as cores para o país e seus vizinhos, voltamos no Backtracking de PROLOG e escolhemos outras combinações de cores. OBS: Como sempre pintamos um país com uma cor e os vizinhos com as restantes, sempre que conseguirmos pintar o último país teremos certeza que ele não tem a mesma cor que nenhum de seus vizinhos, e portanto todos ou outros países já estão pintados corretamente. O código PROLOG é o seguinte: cores([vermelho, amarelo, azul, verde]). 100 mapa([ fronteira(paisA, A, [B, C, D, F]), fronteira(paisB, B, [A, C, D, E, F]), fronteira(paisC, C, [A, B, D]), fronteira(paisD, D, [A, B, C, E, F]), fronteira(paisE, E, [B, D, F]), fronteira(paisF, F, [A, B, D, E]) ] ). pintaMapa(Solucao) :- mapa(Solucao), coloreMapa(Solucao). coloreMapa([Pais|PaisesRestantes]) :- coloreRegiao(Pais), coloreMapa(PaisesRestantes). coloreMapa([]). coloreRegiao(fronteira(Pais, Cor, Vizinhos)) :- cores(Cores), seleciona(Cor, Cores, CoresRestantes), preenche(Vizinhos, CoresRestantes). seleciona(Elem, [Elem|Calda], Calda). seleciona(Elem, [ElemDifer|Calda], [ElemDifer|NovaLista]) :seleciona(Elem, Calda, NovaLista). preenche([Elem|Calda], Lista) :- member(Elem, Lista), preenche(Calda, Lista). preenche([], Lista). O predicado mapa possui informações sobre os nomes dos países, suas cores e as cores de seus vizinhos, servindo com uma base de dados de países. Note que a cor de cada país bem como a cor de seus vizinhos são representadas por variáveis (letras maiúsculas), isto é, não estão instanciadas e portanto poderão assumir diversos valores durante a execução do programa. O predicado pintaMapa lê este mapa que definimos e o colore. O predicado coloreMapa pega o primeiro país e atribui uma cor a ele e aos seus vizinhos, após isso repete o processo aos países restantes. O predicado coloreRegiao seleciona uma cor para um país e utiliza as cores restantes para preencher os países restantes. 101 O predicado seleciona pega uma cor entre as 4 cores e retorna uma lista com as cores restantes, e o predicado preenche atribui cores aos países não permitindo que um país possua uma cor diferente das cores passadas no segundo argumento deste predicado. A saída do programa contém os nomes dos países, suas respectivas cores e a listas de cores correspondentes aos países vizinhos. Note que para cada saída do programa a cor do país nunca pertence a sua lista de cores de países vizinhos. A seguir mostramos 3 dos 24 modos diferentes de colorir o mapa do nosso exemplo, e um mapa ilustrativo para cada caso. ?- pintaMapa(R). R = [ regiao(paisA, vermelho, [amarelo, azul, verde, azul]), regiao(paisB, amarelo, [vermelho, azul, verde, vermelho, azul]), regiao(paisC, azul, [vermelho, amarelo, verde]), regiao(paisD, verde, [vermelho, amarelo, azul, vermelho, azul]), regiao(paisE, vermelho, [amarelo, verde, azul]), regiao(paisF, azul, [vermelho, amarelo, verde, vermelho]) ] ; R = [ regiao(paisA, amarelo, [azul, verde, vermelho, verde]), regiao(paisB, azul, [amarelo, verde, vermelho, amarelo, verde]), regiao(paisC, verde, [amarelo, azul, vermelho]), regiao(paisD, vermelho, [amarelo, azul, verde, amarelo, verde]), regiao(paisE, amarelo, [azul, vermelho, verde]), regiao(paisF, verde, [amarelo, azul, vermelho, amarelo]) ] ; 102 R = [ regiao(paisA, verde ,[amarelo, vermelho, azul, vermelho]), regiao(paisB, amarelo ,[verde, vermelho, azul, verde, vermelho]), regiao(paisC, vermelho, [verde, amarelo, azul]), regiao(paisD, azul, [verde, amarelo, vermelho, verde, vermelho]), regiao(paisE, verde, [amarelo, azul, vermelho]), regiao(paisF, vermelho, [verde, amarelo, azul, verde]) ] ; 103 Sistema Especialista no Planejamento Empresarial Os sistemas especialistas são aplicações computacionais capazes de simular o comportamento de um especialista humano, em um determinado assunto, a fim de descobrir ou resolver um determinado problema. Os sistemas especialistas estão presentes em vários ramos da computação como: OCRs, reconhecimento de voz, diagnóstico médico, jogo de xadrez, sistema de computação de bordo de carros, sistema de controle de robôs, etc. Nosso exemplo de sistema especialista identifica alguns dos vários problemas que uma empresa tem que resolver do ponto de vista estratégico. No exemplo colhemos do usuário informações sobre o estado da empresa bem como as condições gerais do mercado e, através destas informações, inferimos outras coisas (assim como um especialista humano faria). As informações que colhemos devem estar relacionadas, ou encadeadas, de forma a podermos construir grafos de causa-conseqüência. O grafo abaixo representa três possíveis problemas que devemos verificar e seus relacionamentos de causa-conseqüência. 104 Podemos dizer por exemplo que se: Funcionarios fazem hora-extra frequentemente for verdadeiro e Projetos estao atrasados for verdadeiro então concluímos (inferimos) que Servico esta sobrecarregado é verdadeiro. Nosso programa deve verificar cada um dos três problemas que representamos fazendo perguntas quando necessário (texto em preto), tirando conclusões (texto em azul) e identificando o problema (texto em vermelho). O código PROLOG é o seguinte: conclusao(‘Ha necessidade de contratar pessoal’, [[‘Servico esta sobrecarregado’, 1], [‘As vendas aumentaram’, 1], [‘Nivel de informatizacao e bom’, 1]]). conclusao(‘Ha necessidade de informatizar o setor’, [[‘Servico esta sobrecarregado’, 1], [‘As vendas aumentaram’, 1], [‘Nivel de informatizacao e bom’, 0]]). conclusao(‘Ha necessidade de terceirizar o servico’, [[‘Ha viabilidade em terceirizar o servico’, 1], [‘Determinado servico esta custando caro para a empresa’, 1], [‘Investir em tecnologia ao inves de terceirizar o servico nao e viavel’, 1]]). regra(‘Servico esta sobrecarregado’, [[‘Funcionarios fazem horaextra frequentemente’, 1], [‘Projetos estao atrasados’, 1]]). regra(‘Nivel de informatizacao e bom’, [[‘Hardware e software estao atualizados’, 1], [‘Numero de computadores e satisfatorio’, 1]]). regra(‘Ha viabilidade em terceirizar o servico’, [[‘Existe uma empresa especializada no servico a ser terceirizado’, 1], [‘Transporte e comunicacao com a empresa de terceirizacao e caro’, 0], [‘O servico da empresa de terceirizacao e caro’, 0]]). regra(‘Investir em tecnologia ao inves de terceirizar o servico nao e viavel’, [[‘Novas tecnologias para o setor a ser terceirizado sao caras’, 1], [‘Pesquisa com os funcionarios aponta rejeicao com a tecnologia da empresa de terceirizacao’, 1]]). 105 pergunta(Frase, Resposta) :- write(Frase), display(' ? (s ou n) : '), read(SimOuNao), trata_resposta(Frase, SimOuNao, Resposta). trata_resposta(Frase, s, 1) :- assert(fato(Frase, 1)), !. trata_resposta(Frase, _, 0) :- assert(fato(Frase, 0)). conhecido(Frase) :- fato(Frase, _). nao_perguntavel(Frase) :- conclusao(Frase, _). nao_perguntavel(Frase) :- regra(Frase, _). calcula_regra([], 1) :- !. calcula_regra([[Frase, Esperado]|Calda], Resultado) :fato(Frase, Esperado), calcula_regra(Calda, Resultado), !. calcula_regra([[Frase, Esperado]|Calda], Resultado) :regra(Frase, Condicoes), calcula_regra(Condicoes, PreResultado), Esperado==PreResultado, calcula_regra(Calda, Resultado), !. calcula_regra([[Frase, Esperado]|Calda], Resultado) :not(conhecido(Frase)), not(nao_perguntavel(Frase)), pergunta(Frase, PreResultado), Esperado==PreResultado, calcula_regra(Calda, Resultado), !. calcula_regra(_, 0). resolve(Problema) :- retractall(fato(_, _)), conclusao(Problema, Condicoes), calcula_regra(Condicoes, 1), !. resolve('Não consegui chegar a nenhuma conclusão') :- !. O predicado conclusao relaciona as informações que podemos inferir para chegar a uma conclusão final do problema bem como os fatores que determinam esta inferência. O predicado regra relaciona todos as informações que podemos inferir, exceto as inferências que nos levam a uma conclusão. O predicado pergunta é utilizado quando precisamos de uma informação mas não a temos, neste caso, o predicado pergunta lê a resposta do usuário e trata esta resposta. 106 O predicado trata_resposta utiliza a predicado assert de PROLOG para inserir uma nova clausula chamada fato em tempo de execução, esta clausula fato relaciona uma pergunta e uma resposta dada pelo usuário. O predicado conhecido verifica se já existe um fato (uma resposta do usuário) relacionado a uma determinada pergunta. O predicado nao_perguntavel verifica se uma certa pergunta não é do tipo que o usuário pode responder, ou seja, se ela não é uma pergunta e sim o resultado de uma inferência. O predicado calcula_regra retorna o valor de uma certa inferência feita através de um fato existente, do processamento de outras regras ou de uma pergunta ao usuário. O predicado resolve busca cada um dos problemas possíveis e verifica se algum está ocorrendo. A seguir algumas execuções do programa: ?- resolve(Problema). Funcionarios fazem hora-extra frequentemente ? (s ou n) : ? s. Projetos estao atrasados ? (s ou n) : ? s. As vendas aumentaram ? (s ou n) : ? s. Hardware e software estao atualizados ? (s ou n) : ? s. Numero de computadores e satisfatorio ? (s ou n) : ? n. Problema = 'Ha necessidade de informatizar o setor’ Podemos verificar também se um determinado problema está ocorrendo, a resposta será então do tipo yes ou no. ?- resolve('Ha necessidade de terceirizar o servico'). Existe uma empresa especializada no servico a ser terceirizado ? (s ou n) : ? s. Transporte e comunicacao com a empresa de terceirizacao e caro ? (s ou n) : ? n. O servico da empresa de terceirizacao e caro ? (s ou n) : ? s. no 107 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 108 Metodologia de Programação Critérios de Qualidade de Programação Para o desenvolvimento de Programas Procedimentais Convencionais de boa qualidade diversos critérios de Engenharia de Software devem ser observados, assim como técnicas e práticas; Para um bom estilo de programação em Prolog esses critérios também devem ser observados. Cabe salientar que os critérios de Correção e Eficiência são os mais importantes para a construção de programas de boa qualidade. Princípios Gerais de uma Boa Programação Critérios de avaliação: Correção; Eficiência; Transparência e Legibilidade; Modificabilidade; Robustez; Documentação; 109 A importância de cada critério vai depender do problema e das circunstâncias em que o programa é desenvolvido, e do ambiente em que será utilizado (o critério mais importante sem dúvida é o da correção); Uma das regras gerais para se atingir esses critérios é primeiro “pensar” sobre o problema a ser resolvido e somente iniciar a codificação depois de se ter formulado uma idéia clara sobre o que deve ser feito; O processo de conversão dessa idéia pode ser difícil. Uma abordagem consagrada é a de utilizar o "princípio dos refinamentos sucessivos“. Essa estratégia apresenta as seguintes vantagens: Permite a formulação de uma solução inicial nos termos mais relevantes ao problema; Essa solução inicial é, por conseguinte, mais simples e sucinta, sendo a sua correção facilmente verificável, e; Se cada passo de refinamento for pequeno o suficiente para ser manejado intelectualmente, preserva mais facilmente a sua correção. No caso da linguagem Prolog, pode-se pensar em tal processo como sendo o de refinamento de relações. Ou pensar em refinamentos de algoritmos, adotando então a visão procedimental do Prolog. 110 Como Pensar em PROLOG Uma característica importante da linguagem Prolog é permitir que seus programas sejam pensados tanto declarativa quanto procedimentalmente; Normalmente as souções declarativas são mais fáceis de desenvolver e possuem a clareza e limpidez da pura lógica; Durante o processo de desenvolvimento de uma solução, deve-se buscar as idéias adequadas para decompor um problema em subproblemas de solução mais fácil. "Como encontrar os subproblemas apropriados?" Uso de Recursão Na solução de problemas envolvendo o processamento sequencial por meio de recursão, é uma boa heurística aplicar pensamento indutivo e resolver os seguintes dois casos separadamente: Os casos triviais, ou básicos, em que o argumento é uma lista vazia ou unitária, e Os casos gerais, em que o argumento é uma lista [Cabeça|Corpo] e o problema é assumido resolvido para "Corpo“; 111 Exemplo: Processar uma lista de itens de tal maneira que cada item seja operado por uma mesma regra de transformação: transforma(Lista, F, NovaLista) onde Lista é a lista original, F é uma regra de transformação e NovaLista é a lista de todos os itens transformados. O problema de transformar Lista em NovaLista pode ser subdividido em dois casos: Caso Básico: Lista = [] Se Lista = [], então NovaLista = [], independentemente de F. Caso Geral: Lista = [X | Resto] Para transformar uma lista do tipo [X | Resto] em uma lista do tipo [NovoX | NovoResto], transforme Resto, obtendo NovoResto e transforme X, obtendo NovoX. Em Prolog: transforma([], _, []). transforma([X | Resto], F, [NovoX | NovoResto]) :- G =.. [F, X, NovoX], call(G), transforma(Resto, F, NovoResto). 112 Generalização Uma boa idéia é generalizar o problema original, de forma a permitir que a solução do problema generalizado seja formulada recursivamente; A generalização de uma relação envolve tipicamente a introdução de um ou mais argumentos extras. "Como encontrar a generalização correta?“ Exemplo: Problema das oito damas. Temos a relação: oitoDamas(Posição) que será verdadeira se Posição representar uma posição do tabuleiro tal que nenhuma dama ataque as restantes. Uma idéia interessante, nesse caso é generalizar o número de damas de oito para N, de forma que o número de damas se torna o argumento adicional; nDamas(Posição, N) A vantagem dessa generalização é que há uma formulação recursiva imediata para a relação nDamas/2. Uma vez que o problema generalizado está solucionado, a solução do problema original é imediata: oitoDamas(Posição) :- nDamas(Posição, 8). 113 Represetação Gráfica de Problemas Na busca por idéias para solucionar um dado problema, frequentemente é de grande utilidade introduzir alguma representação gráfica do mesmo. No caso do Prolog, essa técnica parece ser especialmente produtiva, pois: Prolog é particularmente adequado para problemas envolvendo objetos e relações entre objetos. De modo geral tais problemas podem ser naturalmente ilustrados por meio de grafos, onde os nodos correspondem a objetos e os arcos a relações; Os objetos estruturados em Prolog são naturalmente representados por meio de árvores; O significado declarativo dos programas Prolog facilita a tradução de representações gráficas porque, em princípio, a ordem na qual o desenho é feito não constitui um fator importante. Exemplo: Definição de uma regra irmã (X é irmã de Y): Z progenitor(Z,X) irmã(X,Y) :progenitor(Z,Y) progenitor(Z,X), progenitor(Z,Y), mulher(X) X Y irmã (X,Y) mulher(X). 114 Regras Gerais para uma Boa Programação As cláusulas do programa devem ser curtas. Seu corpo não deve conter mais que uns poucos objetivos. Exemplo: proc1A :- a, b, c. proc1B :- d, e, f. ao invés de proc1 :- a, b, c, d, e, f. Os procedimentos do programa devem também ser curtos (conter poucas cláusulas); Adotar nomes mnemônicos para procedimentos e variáveis; O lay-out dos programas é importante, incluindo um bom espaçamento, uso de linhas em branco, e identação; É importante que as mesmas convenções sejam usadas de forma consistente em todo o programa; O operador cut deve ser usado com cuidado. Seu uso deve ser evitado quando não for absolutamente necessário; O operador not, devido a sua relação com o cut também pode apresentar comportamento inesperado. Se entretanto estivermos em dúvida entre usar o not ou o cut, o primeiro é preferível; 115 A modificação do programa por meio dos predicados assert/1 e retract/1 pode degradar em grande escala a transparência do seu comportamento; A legibilidade pode ser algumas vezes incrementada pela divisão da cláusula que contém o “;” (correspondendo ao conetivo "ou") em duas. Exemplo Para ilustrar os pontos discutidos até aqui, vamos considerar a seguinte relação: merge(L1, L2, L3) onde L1 e L2 são listas ordenadas que são reunidas ordenadamente em L3. A abaixo é apresentada uma implementação sem muitos critérios: merge(L1, L2, L3) :L1 = [], !, L3 = L2; L2 = [], !, L3 = L1; L1 = [X | Resto1], L2 = [Y | Resto2], (X < Y, !, Z = X, merge(Resto1, L2, Resto3); (Z = Y, merge(L1, Resto2, Resto3))), L3 = [Z | Resto3]. 116 Agora uma outra implementação é apresentada, mas observando-se as regras apresentadas: merge([], L, L). merge(L, [], L). merge([X | R1], [Y | R2], [X | R3]) :X < Y, !, merge(R1, [Y | R2], R3). merge(L1, [Y | R2], [Y | R3]) :merge(L1, R2, R3). Além dessas regras, duas outras observadas para uma boa programação: podem ser Organização tabular de procedimentos longos, e; Utilização de comentários. 117 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 118 Lógica Fuzzy O que é Lógica Fuzzy? Apostadores como Blaise Pascal inventaram a estatística para calcular as probabilidades de resultados precisos -- que um dado, digamos, venha a cair mostrando o quatro. Os meteorologistas na TV usam números para prever se vai, ou não, chover amanhã. Mas, de fato, o próprio conceito de "chuva" é difuso. Se dois pingos de água caem do céu, isso é chuva? E o que dizer de cinqüenta pingos? E de mil? Suponhamos que a neblina esteja espessa e baixa, e você sinta gotas de água em seu rosto. Isso é chuva? Onde passa a linha divisória? Quando a não-chuva se torna chuva? Assim, uma definição precisa para Lógica Fuzzy não existe, mas o significado pode ser explicado. Enquanto a Teoria dos Conjuntos e a lógica matemática divide o mundo com “branco” e “preto”, conjuntos fuzzy definem os tons de “cinza” entre o “branco” e o “preto”. Seu uso é significativo quando aplicado à fenômenos complexos que não são facilmente descritos pelos métodos matemáticos tradicionais, especialmente quando o objetivo é obter uma resposta aproximada. Se a "lógica difusa" tem uma origem, esta reside na tentativa da Lógica de se adaptar aos paradoxos de Russel e à incerteza de Heisenberg. O lógico polonês Jan Lukasiewicz desenvolveu uma lógica "multivalente" nos anos de 1920, refinando a lógica binária do sim-não, da física newtoniana, para permitir estados indeterminados. Em 1965, o matemático Lotfi Zadeh, de Berkeley, aplicou essa nova lógica à teoria dos conjuntos, em seu artigo "Conjuntos Difusos", que depois emprestou seu nome à lógica 119 O que é Lógica Fuzzy? (Cont.) . • Os conjuntos difusos são a chave para as máquinas difusas. A maioria dos artefatos com os quais você está familiarizado são "burros" — isto é, rigidamente programados. Um sistema de aquecimento controlado por termostato é o exemplo clássico da máquina burra. Quando a temperatura cai abaixo de determinado ponto, o aquecedor é ligado; quando ela ultrapassa uma outra determinada temperatura, o aquecedor é desligado. O mecanismo é binário: o aquecedor está "ligado" ou "desligado", e quando está ligado, está sempre aquecendo o ambiente com a mesma intensidade. • As máquinas difusas, por outro lado, usam conjuntos difusos para produzir respostas mais flexíveis, permitindo que se estabeleça um determinado grau de quente ou frio. Se decidimos que 20° é a temperatura perfeita, podemos dizer a um aquecedor/condicionador de ar para modular seu comportamento, dependendo de quanto a temperatura atual difere de 20° . O aparelho nunca estaria só ligado ou desligado — ele estaria sempre ligado em um grau variável de aquecimento. • Concluindo, os conjuntos Fuzzy são uma generalização dos Conjuntos Clássicos, assim como a Lógica Infinito-Valorada é uma generalização da Lógica Clássica. Lógica Fuzzy é uma incorporação dos Conjuntos Fuzzy e pode ser visto como uma extensão da Lógica Infinito-Valorada por aplicá-la aos conjuntos fuzzy. 120 Lógica Clássica Estende Lógica Infinito Valorada Correspondência Lógica Fuzzy Está para Conjuntos Clássicos Estende Conjuntos Fuzzy Números Fuzzy Figura F1. Envolvimento da Lógica Fuzzy com a lógica tradicional Elementos da lógica Fuzzy • Um número Fuzzy é uma função F(x) composta por várias outras funções contínuas tal que os valores de retorno estão entre 0 e 1, obrigatoriamente, em um intervalo [a1, b1], e os valores em F(a1) e F(b1) são 0 (zero). Exemplo: F(x) 1 0 a1 b1 x Gráfico F2: exemplo de número fuzzy 121 Elementos da lógica Fuzzy (continuação) • Números Fuzzy é um caso particular dentro dos Conjuntos Fuzzy, cujo o conjunto é definido como um par ordenado (x, (x)) onde (x) devolve um valor entre [0, 1] mas não necessariamente sempre incluindo o 0 e o 1, requisitos obrigatórios para um número fuzzy. Exemplos de um conjunto fuzzy: µ 1 x 0 Gráfico F3: exemplo de conjunto fuzzy • Enquanto isso, na Lógica Clássica, uma proposição é uma sentença que é, logicamente, ou verdade, denotada por 1, ou falsa, denotada por 0. O conjunto T={0, 1} é chamado de conjuntos de valores verdadeiros. Exemplo: a proposição o número 3 é um inteiro é verdadeira. • Já na Lógica Multivalorada é permitida a proposição ter mais do que um valor verdadeiro que esteja entre [0, 1], como por exemplo T8 = {0, 1/7, 2/7, 3/7, 4/7, 5/7, 6/7, 1} representa um conjunto com 8 valores verdadeiros. Se o conjunto de valores verdadeiros compreende todos os números reais em [0, 1], então chamamos a lógica multivalorada de Lógica Infinito-valorada 122 Elementos da lógica Fuzzy (continuação) •A Lógica Fuzzy tem foco em variáveis linguísticas da linguagem natural e visa prover fundamentos para resultados aproximados com proposições imprecisas. Exemplo: •Velocidade, na linguagem natural, trata de quão rápido algo se move, mas dizer se ele é rápido depende de cada indivíduo. Assim, podemos definir velocidade como sendo uma variável lingüística consistindo de conjuntos fuzzy como rápido, meio-rápido, normal, meio-devagar e devagar. devagar µ rápido normal Meio-devagar Meio-rápido 1 0 30 40 50 60 75 80 95 115 150 x Gráfico F4: conjuntos fuzzy descrevendo velocidade 123 Por que se usa Lógica Fuzzy Porque facilita o desenvolvimento de softwares baseados em situações onde a imprecisão pode responder melhor às necessidades. Engenheiros acusam uma redução de metade do tempo de desenvolvimento dos sistemas, aproximadamente. No início, foram usados sistemas com fuzzy para inúmeros produtos comerciais, como lavadoras, chuveiros, aspiradores de pó, etc. Por exemplo, a cidade Sendai, Japão, vem usando lógica fuzzy para controle de seus trens do metrô desde 1986. Aplicações da Lógica Fuzzy • O emprego de Controle de Fuzzy é recomendável: •1 - Para processos muito complexos, quando não há modelo matemático simples; 2 - Para processos altamente não lineares; •O emprego do Controle de Fuzzy não é recomendável se: •1 - A teoria convencional de controle dá resultados satisfatórios; 2 - Um modelo matemático adequado e de fácil resolução já existe; Exemplos de aplicações Controle simplificado de robôs (Fuji Eletric, Toshiba), Prevencão contra flutuações de temperatura em sistemas de ar condicionado (Mitsubishi, Sharp), Controle eficiente e estável de fábrica de carros (Nissan), Planejamento otimizado de tabelas de controle de horários de ônibus (Toshiba), Combinação de Lógica Fuzzy e Redes Neurais (Matsushita), Desenvolvimento de sistemas anti-vibração para câmera filmadora (Matsushita) 124 Estrutura de um programa Fuzzy Os estágios de fuzzificação, aplicação das regras fuzzy e de-fuzzificação correspondem ao fato do programa ler um dado do meio, digamos a velocidade do carro, descobrir quão “rápido”, “meio-rápido”, “normal”, “meio-devagar”, “devagar” (fuzzificação - transformar um dado do meio em uma variável lingüística), aplicando em seguida as regras fuzzy para chegar em outra variável lingüística, por exemplo, pressão da aceleração, com valores “máxima”, “média” e “mínima”. Depois disso tudo, a variável resultado (pressão da aceleração) é transformada em um valor real (de-fuzzificação) a ser enviado de volta para o meio. Por exemplo, nosso sistema obteria a velocidade atual do carro, digamos de 120 Km/h, e transformaria esse valor em uma variável lingüística velocidade, que, pelo gráfico F4, podemos concluir que esta será 57,14% rápida e 0% para as outras definições (meio-rápida, normal, meio-devagar e devagar). Aplicando as regras fuzzy (serão abordadas mais à frente) podemos chegar numa outra variável lingüística pressão da aceleração com valores, digamos de 50% em mínima, e 0% em média e máxima. 125 Exemplo (viagem de carro) VARIÁVEIS FUZZY REGRAS FUZZY VARIÁVEIS FUZZY PRESSÃO DA ACELERAÇAO VELOCIDADE RAPIDO MAXIMA MEIO RAPIDO 120 REGRAS MEDIA NORMAL Km/h 10 PSI FUZZY MEIO DEVAGAR DEVAGAR FUZZIFICAÇÃO PROPAGAÇÃO MINIMA 126 DE-FUZZIFICAÇÃO Ferramentas para Prolog Existem muitas ferramentas especializadas para o desenvolvimento de programas usando fuzzy, entre elas o FLINT (Fuzzy Logic Inference Toolkit), para o Win-Prolog. Variáveis Fuzzy no FLINT • Pertencem a uma faixa de valores (ex: 0 a 100) • Armazenam um único valor (ex: velocidade) • Possuem qualificadores, que subdividem a faixa de valores, compostos de: • um nome (qualificador lingüístico - rápido) • uma função membro que define o grau de pertinência do valor para este qualificador. A função membro é definida por: •Forma ( /, \, /\, /-\, \-/, \/ ou ?) •Curvatura (linear ou curve) •Pontos Relevantes (pontos da forma) • Possuem ainda a definição do tipo de método de De-fuzzificação a ser usado (ex: peak, centroid ou uma expressão definida pelo usuário) 127 Qualificadores de variáveis Fuzzy \ / /\ \/ [A, [A, [A, [A, B] B] B, C] B, C] descida de rampa subida de rampa triângulo para cima triângulo para embaixo /-\ \-/ ? [A, B, C, D] trapezóide para cima [A, B, C, D] trapezóide para baixo [V1/M1, V2/M2, Vk/Mk] forma livre Exemplos de Qualificadores Exemplo de subida de rampa (símbolo ‘/’) com pontos A e B e curvatura linear A B Exemplo de descida de trapézio (símbolo ‘\-/’) com pontos A, B, C e D com curvatura linear B C D A Exemplo de forma livre (representada por ‘?’). Os pontos V1/M1 é um dos segmentos de reta que compoem o gráfico. V1/M1 128 Tipos de Curvatura •Linear (ex: triângulos, trapézios) •Curve •Menor que 1 •Igual a 1 •Maior que 1 Métodos de De-fuzzificação Centroid - centro de gravidade (default) obtém o valor final calculando o centro da área do gráfico gerado pela variável fuzzy resultante. Peak - maior nível da função. Obtém o valor final calculando a média dos picos da função resultante Expressão definida pelo usuário 129 Exemplo de variável fuzzy no FLINT Faixa da variável fuzzy (opcional) Nome da variável fuzzy fuzzy_variable(velocidade) [0, 200]; rápido, / , linear, meio-rápido, /\, linear, normal, /\, linear, meio-devagar, /\, linear, devagar, \, linear, peak. :[ [75, [40, [30, [ 0, 80, 150]; 95, 115]; 60, 80]; 40, 50]; 40 ]; Pontos do gráfico Método de-fuzzy Nome do qualificador Formas Curvatura Definição de Regra Fuzzy •Consistem de conjuntos de condições IF (usando conectivos and e or) •Uma conclusão (then) •Uma conclusão opcional (else) •São aplicadas às variáveis através de um processo chamado “Propagação” 130 Exemplo de declaração de regra no FLINT Nome da regra fuzzy Nome de variável de condição Nome do qualificador de condição fuzzy_rule(risco1) if velocidade is rápida then pressão_da_aceleração is mínima else pressão_da_aceleração is média. Nome de variável de conclusão Nomes de qualificador de conclusão Programa exemplo Implementando um sistema controlador de turbina 131 Variáveis Fuzzy São usadas Throttle, Pressure e Temperature como variáveis fuzzy para representar as características da turbina, além de podermos definir que Pressure e Temperature serão variáveis de entrada enquanto Throttle será de saída. Para a variável Temperature definiremos os seguintes modificadores: cold, cool, normal, warm, hot. Para a variável Pressure defineremos os modificadores: high, low, medium. Para a variável Throttle, os modificadores negative-large, negative-medium, negativesmall, zero, positive-small, positive-medium, positive-large. Operadores Linguísticos É necessário definir alguns operadores linguísticos (sempre definidos dessa maneira para todo programa que usa o FLINT): :- op( 1200, xfy, ( if ) ), op( 1200, xfy, ( then ) ), op( 1200, xfx, ( else ) ), op( 1100, xfy, ( or ) ), op( 1000, xfy, ( and ) ), op( 700, xfx, ( is ) ), op( 600, fy, ( not ) ). Estes são os operadores para as regras fuzzy, definindo condição, conclusão, conjunção, disjunção e negação. 132 Definindo as Variáveis Fuzzy Definindo a variável temperature: fuzzy_variable( temperature ) :[ 0 , 500 ]; cold, \, linear, [ 110 , 165 cool, ]; /\, linear, [ 110 , 165 , 220 ]; normal, /\, linear, [ 165 , 220 , 275 ]; warm, /\, linear, [ 220 , 275 , 330 ]; hot, /, linear, [ 275 , 330 ]. O gráfico da função seria equivalente a: 1 cold 0 110 cool normal warm 165 220 275 hot 330 temperature 133 Definindo as Variáveis Fuzzy Definindo a variável pressure: fuzzy_variable( pressure ) :[ 0 , 300 ] ; weak, \, linear, [ 10 , 70 low, /\, linear, [ 10 , 70 , 130 ok, /\, linear, [ 70 , 130 , 190 strong, /\, linear, [ 130 , 190 , 250 high, / , linear, [ 190 , 250 ]; ]; ]; ]; ]. O gráfico da função seria equivalente a: 1 weak 0 10 low ok 70 130 strong 190 high 250 pressure 134 Definindo as Variáveis Fuzzy Definindo a variável throttle: fuzzy_variable( throttle ) :[ -60 , 60 ]; negative_large, \, linear, [ -45, -30 ]; negative_medium, /\, linear, [ -45, -30, -15 ]; negative_small, /\, linear, [ -30, -15, zero, /\, linear, [ -15, positive_small, /\, linear, [ positive_medium, /\, linear, [ positive_large, /, 0 ]; 0, 15 ]; 0, 15, 30 ]; 15, 30, 45 ]; linear, [ 30 , 45 ]; centroid . O gráfico da função seria equivalente a: Negative_medium Positive_medium 1 Negative_small Positive_small Negative_large positive_large zero 0 10 70 130 190 250 throttle 135 Definindo as Regras Fuzzy Pode-se definir as regras fuzzy através de decisões, como por exemplo : ’se a temperatura é fria e a pressão é fraca então aumente a válvula em grandes quantidaddes’ pode ser definida por: fuzzy_rule(throttle1) if temperature is cold and pressure is weak then throttle is positive_large. Mas como estas regras irão sempre se aplicar às variáveis fuzzy temperature, pressure e throttle, podemos então colocar todas as regras dentro de uma única matriz (onde a primeira linha indica quais as variáveis a serem usadas, o símbolo ‘*’ é equivalente ao símbolo ‘and’ da regra fuzzy acima, assim como o símbolo ‘->’ equivale ao ‘then’): fuzzy_matrix( t ) :temperature cold cold cold cold cold cool cool cool cool * * * * * * * * * * pressure weak low ok strong high weak low ok strong -> -> -> -> -> -> -> -> -> -> throttle ; positive_large ; positive_medium; positive_small ; negative_small ; negative_medium; positive_large ; positive_medium; zero ; negative_medium; (continuna na próxima página). 136 Definindo as Regras Fuzzy (Continuação) (continuação da matriz de regras) cool normal normal normal normal normal warm warm warm warm warm hot hot hot hot hot * * * * * * * * * * * * * * * * high weak low ok strong high weak low ok strong high weak low ok strong high -> -> -> -> -> -> -> -> -> -> -> -> -> -> -> -> negative_medium; positive_medium; positive_small ; zero ; negative_small ; negative_medium; positive_medium; positive_small ; negative_small ; negative_medium; negative_large ; positive_small ; positive_small ; negative_medium; negative_large ; negative_large . Definindo a Propagação Definindo uma nova prograpação das regras fuzzy, ou seja, dados as variáveis de entrada (temperature e pressure), como obter a variável de saída, throttle, através dos predicados fuzzy_propagate e fuzzy_variable_value (obtenção dos valores) e o predicado fuzzy_reset_membership para garantir que não haverá velhos valores armazenados (continuna na próxima página). 137 Definindo a Propagação (Continuação) Exemplo: find_throttle(Temperature, Pressure, Throttle):fuzzy_reset_membership( throttle ), fuzzy_variable_value( temperature, Temperature), fuzzy_variable_value( pressure, Pressure ), fuzzy_propagate( minimum, maximum, complement,[t]), fuzzy_variable_value( throttle, Throttle ). Rodando o exemplo Após declarado os operadores lingüísticos, as variáveis fuzzy, a matriz de regras e finalmente, a propagação, todos listados acima, usaremos o predicado find_throttle para a execução: ?- find_throttle( 300, 150, Throttle ) . Throttle = -26.040413058933 E assim termina nosso exemplo de como usar lógica fuzzy no Win-Prolog com o módulo FLINT. 138 Programação Lógica Linguagem PROLOG Fatos, Regras e Controle de Corte Operadores e Listas Entrada e Saída Manipulando Base de Dados Outros Exemplos Metodologia de Programação Lógica Fuzzy Exercícios 139 Exercícios 1. Um programa Prolog tem as seguintes cláusulas : vertical(Seg(ponto(X,Y), ponto(X,Y1)). horizontal(Seg(ponto(X,Y), ponto(X1,Y)). Dê os resultados das seguintes consultas : ?- vertical(seg(ponto(1,1), ponto(1,2))). ?- vertical(seg(ponto(1,1), ponto(2,Y))). ?- horizontal(seg(ponto(1,1), ponto(2,Y))). ?- vertical(seg(ponto(2,3),P)). 2. Dê o resultado das seguintes instanciações : a) point(A,B) = point(1,2) b) point(A,B) = point(X,Y,Z) c) [a, b, c, [ab], [], [[a], c]] = [X, Y|Z] d) [p, [seg], t, q] = [X, [Y], Z|S] e) [X, Y|Z] = [2, [3,4]] f) [lista, de, exercicios, de, PL] = [X, de|W] 3. Construa uma base de dados sobre livros com pelo menos cinco estruturas do tipo : livro( nome(‘C completo’), autor(‘Schildt’), pal_chave([linguagemc, programacao, computacao])) 140 a) Escreva consultas para encontrar : • • • • Nome do autor, dado o nome do livro. Nome do livro, dado o nome do autor. As palavras chave, dado o nome do livro. Nome do autor e nome do livro, dado uma palavra chave. b) Escreva um programa Prolog para, dada uma lista de palavras chave, encontrar nome e autor dos livros que tem pelo menos uma das palavras chave fornecidas. Os livros encontrados devem ser dados um de cada vez. 4. Defina os predicados n_par(Lista) e n_impar(Lista) dara determinar se uma lista tem número par ou ímpar de elementos. 5. Defina a relação traduz(L1,L2) para traduzir uma lista de números entre 0 e 9 para a palavra correspondente. Por exemplo : traduz([1,2,3], L). L = [um, dois, tres] Use a relação auxiliar : t(0, zero), t(1, um).......... 6. Quais seriam os modos de chamada para os predicados do exercício anterior? 7. Defina o predicado between(N1, N2, L), tal que, para dois números inteiros dados N1 e N2, guarde na lista L todos os valores X tal que N1 <= X <= N2. 141 8. Escreva um programa Prolog que possa responder a seguinte pergunta : “Eu sou meu próprio avô?”, criando um conjunto de relações que representem a seguinte situação : “Eu me casei com uma viúva(W) que tem uma filha adulta(D). Meu pai(F), que nos visitava frequentemente, se apaixounou por minha enteada e casou – se com ela. Logo, meu pai se tornou meu enteado e minha enteada tornou – se minha madrasta. Alguns meses depois, minha mulher teve um filho(S1), que se tornou cunhado do meu pai, assim como meu tio. A mulher do meu pai, isto é, minha enteada, também teve um filho(S2).” 9. Mostre a execução de um programa Prolog para o exemplo da regra de Fibonacci recursiva. Procure deixar bem claro os passos onde ocorrem unificação e backtracking. 10. Baseando – se no programa que faz busca em largura apresentado no slide 62, faça um programa que execute busca em profundidade. Dica : É necessário apenas uma pequena modificação no algoritmo de busca em largura. 11. Faça, sem olhar em slides anteriores, um programa Prolog que resolva o problema dos missionários e canibais. 12. Faça, sem olhar em slides anteriores, um programa Prolog que resolva o problema das rainhas em um tabuleiro de xadrez. 142 13. Considere o seguinte problema : “Um fazendeiro está na margem norte de um rio com uma cabra, um lobo e um repolho. Existe um bote através do qual o fazendeiro pode transportar um elemento por vez para a outra margem do rio. Se o lobo e a cabra ficarem sozinhos numa margem, o lobo comerá a cabra. Se a cabra e o repolho ficarem sozinhos em uma margem, a cabra comerá o repolho. Como transportar com segurança os três elementos, considerando o respolho um passageiro?” Crie as cláusulas move da busca em largura para resolver esse problema. 14. Crie uma representação qualquer em Prolog para árvores. a) Faça um código que verifica se um dado elemento pertence à uma árvore. b) Faça um código que mostra todos os elementos de uma árvore(ou guarda todos os elementos em uma lista). c) Faça um código que verifica se a árvore é uma AVL. d) Faça códigos de inserção e remoção de elementos da árvore. 143