Prolog编程求解图搜索问题——传道士与野人问题
一.安装prolog集成开发环境
Prolog(Programming in Logic的缩写)是一种逻辑编程语言。它创建在逻辑学的理论基础之上, 最初被运用于自然语言等研究领域。现在它已广泛的应用在人工智能的研究中,它可以用来建造专家系统、自然语言理解、智能知识库等。
- 官网:https://www.swi-prolog.org/build/unix.html
- 安装步骤:
SWI-Prolog 官网有各个操作系统的二进制安装包,下载即可。Debian系统还可以键入如下命令,使用apt安装:
$ sudo apt install swi-prolog
$ prolog --version
SWI-Prolog version 7.6.4 for amd64
查看版本,安装成功
二.prolog语法规则
(1)如何启动?
安装以后,Linux 系统可以命令行启动。键入swipl直接交互,你也可以在后面带上文件名,加载一个文件文件启动,或启动后加载文件,使用方法如下:
$ swipl # 交互
$ swipl filePath # 执行文件
$ [file]. # 直接加载某个文件
# 当然prolog也会被作为启动程序识别,可以将swipl替换为prolog
(2)常量和变量
Prolog 的变量和常量规则很简单:小写字母开头的字符串,就是常量;大写字母开头的字符串,就是变量。
abc
是常量,输出就是自身;Abc
是变量,输出就是该变量的值。如下:
?- write(abc).
abc
true.
?- write(Abc).
_3386
true.
(3)关系和属性
①两个对象之间的关系,使用括号表示。
②如果括号里面只有一个参数,就表示对象拥有该属性
friend(jack, peter).
friend(peter, jack).
(4)规则
①规则是推理方法,即如何从一个论断得到另一个论断。举例来说,我们定下一条规则:所有朋友关系都是相互的,规则写成下面这样。
friend(X, Y) :- friend(Y,X).
②如果一条规则取决于多个条件同时为true
,则条件之间使用逗号分隔
mother(X, Y) :- child(Y,X), female(X).
③如果一条规则取决于某个条件为false
,则在条件之前加上\+
表示否定。
onesidelove(X, Y) :- loves(X, Y), \+ loves(Y,X).
(5)查询
①Prolog 支持查询已经设定的条件。我们先写一个脚本hello.pl
friend(john, julia).
friend(john, jack).
friend(julia, sam).
friend(julia, molly).
②通过规则查询两个人是否为朋友。
?- friend(john, jack).
true.
③通过listing()函数列出所有的朋友关系。
?- listing(friend).
friend(john, julia).
friend(john, jack).
friend(julia, sam).
friend(julia, molly).
true.
④Who
是变量名(任意的变量名都可以)查询规则结果。
?- friend(john, Who).
Who = julia ;
Who = jack.
⑤列表查询成员member
member(X, [One]).
(6)表数据结构
表是Prolog中一种非常有用的数据结构。数字中的序列、集合,通常语言中的数组、记录等均可用表来表示。它分为头和尾两部分,表头是表中第一个元素,而表尾是表中除第一个元素外的其余元素按原来顺序组成的表。在程序中是用“|”来区分表头和表尾的,而且还可以使用变量.
# 表示一个表
[H|T]
# 其中 H、T 都是变量
# H 为表头,T为表尾
三.采用prolog编写传教士与野人问题的源程序
(1)问题描述
三名传教士和三名食人族必须使用最多可搭载两个人的船穿越一条河流,这是因为对于两岸,如果在岸上都有传教士,则食人族不能超过他们(如果如果是的话,食人族会吃掉传教士)。船上没有人时,船不能独自过河。求解一种解决方案,将所有的传教士和食人族运送到对岸 [source: Wikipedia]
有N个传教士和N个野人来到河边渡河,河岸有一条船,每次至多可供2人乘渡。问传教士为了安全起见,应如何规划摆渡方案,使得任何时刻,河两岸以及船上的野人数目总是不超过传教士的数目。即求解过程中,任何时刻满足M(传教士数)≥C(野人数)和M+C≤k的摆渡方案。
(2)形式化MC问题
①状态空间
用一个三元组[m,c,b]来表示河岸上的状态,其中m、c分别代表某一岸上传教士与野人的数目,b=left表示船在左岸,b=right则表示船在右岸。约束条件是: 两岸上M≥C || M=0 , 船上M+C≤2。
综上,我们的状态空间可表示为:(ML,CL,BL),其中0≤ML,CL≤N,BL∈{left, right}。
状态空间的总状态数为(N+1)×(N+1)×2
②初始状态
问题的初始状态是(N,N,left)
③行动
该问题主要有两种操作:从左岸划向右岸和从右岸划向左岸,以及每次摆渡的传教士和野人个数。
使用一个2元组(BM,BC)来表示每次摆渡的传教士和野人个数,我们用i代表每次过河的总人数,i =[1,2…k],则每次有BM个传教士和BC=i-BM个野人过河,其中BM= 0~i,而且当BM!=0时需要满足BM>=BC。则从左到右的操作为:(ML-BM,CL-BC,B = 1),从右到左的操作为:(ML+BM,CL+BC,B = 0)。
Eg.
当N=3,K=2时,满足条件的(BM,BC)有:
从左岸到达右岸:(0,1)、(0,2)、(1,0)、(1,1)、(2,0)
从右岸到达左岸:(0,1)、(0,2)、(1,0)、(1,1)、(2,0)
共10类行动
Action={L(0,1),L(0,2),L(1,0),L(1,1),L(2,0),
R(0,1),R(0,2),R(1,0),R(1,1),R(2,0)}
④转移模型
当前状态+Action构成转移模型。
⑤目标
[0,0,0]
⑥路径耗散
搜索过程中的摆渡次数。
(3)求解思路:
根据问题的形式化结果,结合prolog语言的特点,我使用如下求解思路:
①从初始状态开始,对状态集合进行搜索
②对于当前的一个状态,根据Action集合产生一个子状态Result(S,a),子状态受到约束条件:0≤ML,CL≤N,BL∈{left, right}约束。
③用一个集合Explored保存当前已经扩展过的状态,如果子状态在集合Explored中,应该剪枝避开这个状态。用MoveList保存搜索路径。
④扩展出的状态为目标的时候,算法结束,找到一个Trace
⑤采用回溯的方式打印MoveList列表
(4)代码实现:
①定义状态:
为了便于prolog语言实现,我将状态重新定义为[CL,ML,B,CR,MR]五元组,这样在prolog实现的时候带来许多便利:
% 状态定义 为 [CL,ML,B,CR,MR]
start([3,3,left,0,0]).
goal([0,0,right,3,3]).
②定义Action行动集合
我将10种状态描述为推理,规则是推理方法,从一个论断得到另一个论断。在子状态的产生中将会推理得到下一个状态。
由于篇幅限制,我只列举了从左岸到右岸的5个规则,从右岸到左岸对称。
% 可能的移动:
% 两名传教士从左到右.
move([CL,ML,left,CR,MR],[CL,ML2,right,CR,MR2]):-
MR2 is MR+2,
ML2 is ML-2,
legal(CL,ML2,CR,MR2).
% 两个食人族从左到右.
move([CL,ML,left,CR,MR],[CL2,ML,right,CR2,MR]):-
CR2 is CR+2,
CL2 is CL-2,
legal(CL2,ML,CR2,MR).
% 一位传教士和一个食人族的从左到右.
move([CL,ML,left,CR,MR],[CL2,ML2,right,CR2,MR2]):-
CR2 is CR+1,
CL2 is CL-1,
MR2 is MR+1,
ML2 is ML-1,
legal(CL2,ML2,CR2,MR2).
% 一位传教士从左到右.
move([CL,ML,left,CR,MR],[CL,ML2,right,CR,MR2]):-
MR2 is MR+1,
ML2 is ML-1,
legal(CL,ML2,CR,MR2).
% 一个食人族从左到右.
move([CL,ML,left,CR,MR],[CL2,ML,right,CR2,MR]):-
CR2 is CR+1,
CL2 is CL-1,
legal(CL2,ML,CR2,MR).
③递归推理规则
定义规则search(state1,state2,Explored,MovesList) ,用于搜索状态空间,每次使用Move推理产生一个子状态,要求子状态没有在Explore集合中出现过,如果出现了,执行剪枝。然后对子状态继续调用递归过程。定义递归边界:当子状态等于目标状态的时候,查找结束,此时返回,调用printTrace打印MoveList。
% 递归调用search(state1,state2,Explored,MovesList)
search([CL1,ML1,B1,CR1,MR1],[CL2,ML2,B2,CR2,MR2],Explored,MovesList) :-
move([CL1,ML1,B1,CR1,MR1],[CL3,ML3,B3,CR3,MR3]), % 行动产生state3=[CL3,ML3,B3,CR3,MR3]
not(member([CL3,ML3,B3,CR3,MR3],Explored)), % 要求行动不在Explored集合中,一个剪枝操作
% 递归调用search(state3,state2,[state3|Explored],[ [state3,state1] | MovesList ])
search([CL3,ML3,B3,CR3,MR3],[CL2,ML2,B2,CR2,MR2],[[CL3,ML3,B3,CR3,MR3]|Explored],[[[CL3,ML3,B3,CR3,MR3],[CL1,ML1,B1,CR1,MR1]]|MovesList]).
% 找到解返回,此时扩展集合为
search([CL,ML,B,CR,MR],[CL,ML,B,CR,MR],_,MovesList):-
printTrace(MovesList).
④回溯打印搜索路径
回溯的思想很简单,每次取出队首的两状态,在打印之前对剩下的MoveList [[A,B]|MovesList]执行递归规则,当队列为空的时候,返回。
% 找到解返回,此时扩展状态等于父状态
search([CL,ML,B,CR,MR],[CL,ML,B,CR,MR],_,MovesList):-
printTrace(MovesList),nl,
writeln('推理结束'),
length(MovesList,L),
writeln('路径代价为':L).
⑤入口推理规则:
最后我们需要一个入口,执行推理程序,这里使用关键字find,调用search.提供一个简单的交互,可以输入传教士和野人的的数量,对此进行推理。
% 寻找传教士和食人族问题的解决方案
find:-
write("please input number"),nl,
read(Num),nl,
writeln('传教士和野人的数量为:'),
writeln('Missionaries: '=Num),
writeln('Cannibals: '=Num),nl,
writeln('执行推理:'),
search([Num,Num,left,0,0],[0,0,right,Num,Num],[[Num,Num,left,0,0]],_).
四.编译程序,输出查询问题的结果或数据
(1)程序测试,在这里我准备的样例是Missionaries=Cannibals=3,即3个传教士和三个野人,执行结果如图:
(2)结果分析:
首先我们可以画出MC=3时的状态图,总共有15个合法状态:
执行推理:
初始状态,三个传教士三个野人在左岸:[3,3,left,0,0]
从左岸摆渡2个野人到右岸: [1,3,right,2,0]
从右岸摆渡1个野人到左岸: [2,3,left,1,0]
从左岸拜读2个野人到右岸: [0,3,right,3,0]
从右岸摆渡1个野人到左岸: [1,3,left,2,0]
从左岸摆渡2个传教士到右岸: [1,1,right,2,2]
从右岸摆渡1个传教士1个野人到左岸:[2,2,left,1,1]
从作案摆渡2个传教士到右岸: [2,0,right,1,3]
从右岸摆渡1个野人到左岸: [3,0,left,0,3]
从左岸摆渡2个野人到右岸: [1,0,right,2,3]
从右岸摆渡1个传教士到左岸: [1,1,left,2,2]
从左渡读1个传教士1个野人到右岸: [0,0,right,3,3]
推理结束,最终找到一条代价为11的路径解,如途中的红色线条Trace。
综上所述,实验结果正确。