实验4 加法难题ADDITION

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

实验4加法难题
4.1列表
为了能够更好地表达一组数据,Prolog引入了列表这种数据结构。

列表是一组项目的集合,此项目可以是Prolog的任何数据类型,包括结构和列表。

列表的元素由方括号括起来,项目中间使用逗号分割。

例如下面的列表列出了厨房中的物品。

[apple, broccoli, refrigerator]
我们可以使用列表来代替以前的多个子句。

例如在事实输入框输入:
loc_list([apple, broccoli, crackers], kitchen).
loc_list([desk, computer], office).
loc_list([flashlight, envelope], desk).
loc_list([stamp, key], envelope).
loc_list(['washing machine'], cellar).
loc_list([nani], 'washing machine').
可见使用列表能够简化程序。

当某个列表中没有项目时我们称之为空表,使用“[]”表示。

也可以使用nil 来表示。

下面的句子表示hall中没有东西。

loc_list([], hall)
变量也可以与列表联合,就像它与其他的数据结构联合一样。

假如数据库中有了上面的子句,就可以进行如下的询问。

loc_list(X,kitchen)
X= [apple,broccoli,crackers]
1 Solution
[_,X,_]=[apple,broccoli,crackers]
X= broccoli
1 Solution
最后这个例子可以取出列表中任何一个项目的值,但是这种方法是不切实际的。

你必须知道列表的长度,但是在很多情况下,列表的长度是变化的。

为了更加有效的使用列表,必须找到存取、添加和删除列表项目的方法。

并且,我们应该不用对列表项目数和它们的顺序操心。

Prolog提供的两个特性可以方便的完成以上任务。

首先,Prolog提供了把表头项目以及除去表头项目后剩下的列表分离的方法。

其次,Prolog强大的递归功能可以方便地访问除去表头项目后的列表。

使用这两个性质,我们可以编出一些列表的实用谓词。

例如member/2,它能够找到列表中的元素;append/3可以把两个列表连接起来。

这些谓词都是首先对列表头进行处理,然后使用递归处理剩下的列表。

首先,请看一般的列表形式。

[X | Y]
使用此列表可以与任意的列表匹配,匹配成功后,X绑定为列表的第一个项目的值,我们称之为表头(head)。

而Y则绑定为剩下的列表,我们称之为表尾(tail)。

下面我们看几个例子。

[a|[b,c,d]]=[a,b,c,d]
True
1 Solution
上面的联合之所以成功,是因为等号两边的列表是等价的。

注意表尾tail一定是列表,而表头则是一个项目,可以是表,也可以是其他的任何数据结构。

下面的匹配失败,在“|”之后只能是一个列表,而不能是多个项目。

[a|b,c,d]=[a,b,c,d]
Syntax error in parser
>> Syntax error
下面是其它的一些列表的例子。

[H|T]=[cake,tomato,cabbage,apple]
H= cake, T= [tomato,cabbage,apple]
1 Solution
[H|T] = [a, b, c, d, e].
H = aT = [b, c, d, e]
1 Solution
[H|T] = [apples, bananas].
H = apples
T = [bananas]
1 Solution
[H|T]=[a,[b,c,d]] 这个例子中的第一层列表有两个项目。

H= a, T= [[b,c,d]]
1 Solution
[H|T]=[apple]
H= apple, T= []
1 Solution
空表不能与[H|T]匹配,因为它没有表头。

[H|T]=[]
No solutions
注意:最后这个匹配失败非常重要,在递归过程中经常使用它作为边界检测。

即只要表不为空,那么它就能与[X|Y]匹配,当表为空时,就不能匹配,表示已经到达的边界条件。

错误写法://大小写严格区分
[h|t]=[a]
No solutions
[H|t]=[a]
No solutions
我们还可以在第二个项目后面使用“|”,事实上,|前面的都是项目,后面的是一个表。

[One,Two|Three]=[apple,organge,pear,cake,tomato]
ONE= apple, TWO= organge, THREE= [pear,cake,tomato]
1 Solution
首先我们来编写谓词member/2,它能够判断某个项目是否在列表中。

首先我们考虑边界条件,即最简单的情况。

某项目是列表中的元素,如果此项目是列表的表头。

写成Prolog语言就是:
member(H,[H|T]).
从这个子句我们可以看出含有变量的事实可以当作规则使用。

第二个子句用到了递归,其意义是:如果项目是某表的表尾tail的元素,那么它也是此列表的元素。

member(X,[H|T]) :- member(X,T).
完整的谓词如下:
member(H,[H|T]).
member(X,[H|T]) :- member(X,T).
请注意两个member/2谓词的第二个参数都是列表。

由于第二个子句中的T也是一个列表,所以可以递归地进行下去。

member(apple,[apple,orange,pear])
True
1 Solution
member(broccoli, [apple, broccoli, crackers]).
True
1 Solution
member(bananas,[apple,orange,pear,cake])
No solutions
简单加法:
1.add(X,Y,Z):-Z is X+Y.
add(3,4,X)
X= 7
1 Solution
2.add(S,U,V):-U is V-S.
add(3,U,12)
U= 9
1 Solution
3.add(S,U,V):-S is V-U.
add(S,9,99)
S= 90
1 Solution
% 当然也可以将add改为sub 这个倒是没有关系的啦,仅仅是个谓词,关键是后面运算的定义。

2.加法表的生成
get_integer(0,9,X).
get_integer(0,9,Y).
get_integer(0,1,Z).
A is X+Y+Z.
B is A//10.
C is A mod 10.
assert(add(X,Y,Z,B,C)). add(9,7,Z)
Z= 16
1 Solution
add(X,9,99)%执行减法运算X= 90
1 Solution
add(3,X,100)
X= 97
1 Solution
让我们来看一个解密码的题目吧。

有如下的加法算式:
donald+gerald=robert
每个字母代表一个不同的数字,其中d是5。

首先让我们来分析一下工作量,一共有[D,O,N,A,L,G,E,R,T,B]十个字母,每个字母代表一个数字,而已经知道D是5,所以剩下的字母就是0、1、2、3、4、6、7、8、9这些数字,一共有9!=362880种排列可能,如果直接使用把[0,1,2,3,4,6,7,8,9]全排列,然后测试上面的加法算式的方法,显然速度会很慢,在这里我们使用另外一种更加实用的方法。

我们先来定义加法表,加法表和乘法表类似,它标出了0-9这10个数字之间的加法。

也许你会感到奇怪,为什么要定义加法表,在prolog中间不是能够直接使用加法么?在这里我简要的介绍一下。

假设我们定义加法谓词:
add(X,Y,Z):-Z is X+Y.
那么我们就可以使用add来计算两个数字之和了:
例:
add(1,2,X).
X=3.
add(99999,1,X)
X= 100000
1 Solution
add(99999,1,X)
X= 100000
1 Solution
可是这仅仅只能够做加法,而我们需要的功能可远远不止这些。

如果我们使用加法表:
add(0,0,0).
add(0,1,1).
add(0,2,2).
...
add(5,6,11).
add(9,9,18).
那么我们不但能够计算加法,还能够让电脑做更多的事情,例如:add(X,3,8).//执行运算相当于为减法(根据上面列出的加法表进行目标)
X=5.
add(X,Y,3).
X=0,
Y=3;
X=1,
Y=2;
....
也就是说有了加法表就不但可以计算加法,还可以做减法,还可以用来凑某个数字。

第一个add只能够有一种用途,而后面的这个add有许多种用途。

在解密码这个问题中都是未知数,所以只能够使用后面的这种add来解密码,因为它可以用来拼凑答案。

我们实际计算加法的时候,都是从个位开始一位一位的往高位加的,每一位的加法要把对应的两个数字相加,还要加上上一位的进位,
最后如果得到的数字大于9,还要进一位,最后留下一位数字作为本位的答案。

为了能够计算多位加法,我们要设计这样的加法表:add(X,Y,Z,A,B). X,Y是本位对应的两个数字,Z是上一位的进位,而A是进到下一位的数字,B是本位留下的数字。

例如:add(5,6,1,1,2).
对于X,Y的取值范围是0-9,Z的取值范围是0-1,所以总共有200条add子句,手工输入这200条子句太累了,我们让电脑自己完成这个功能。

首先我们需要一个能够产生数字的谓词:
%获得从L到H的整数
get_integer(L,H,X):-L>H,!,fail.
get_integer(L,H,L).
get_integer(L,H,X):-L1 is L+1,get_integer(L1,H,X).
先来编写能够穷举所有情况的谓词。

get_integer(L,H,X):-L>H,!,fail.
get_integer(L,H,L).
get_integer(L,H,X):-L1 is L+1,get_integer(L1,H,X).
谓词get_integer/3,返回从L到H的所有整数。

第一个子句考虑L>H的情况,当然是失败,并且使用截断,不需要再考虑下面的子句了。

(边界条件)
第二个子句直接返回L,它是第一个数。

第三个子句递归调用get_integer/3,不过下限加了1。

好了我们来看看它的功能:
get_integer(3,7,X)
X= 3
X= 4
X= 5
X= 6
X= 7
5 Solutions
不错,正是我们需要的。

再让我们同时使用两个get_integer/3,来产生所有的X和Y的组合。

get_integer(3,5,X),get_integer(X,5,Y).
X= 3, Y= 3
X= 3, Y= 4
X= 3, Y= 5
X= 4, Y= 4
X= 4, Y= 5
X= 5, Y= 5
6 Solutions
delall用来删除内存中原来存在的所有add子句。

delall:-retract(add(_,_,_,_,_)), fail.
delall.
下面是自动产生加法表的谓词makeback.
%产生加法表
makeback:-
delall,%删除以前的加法表
get_integer(0,9,X), %X,Y为两个被加数
get_integer(0,9,Y), %X,Y的取值为0到9之间的数字
get_integer(0,1,Z), %Z为上一位的进位只能够是1或者
A is X+Y+Z,
B is A // 10, %//为整除号
C is A mod 10, assert(add(X,Y,Z,B,C)), %B为这一位的进位,C为本位
的得数。

fail. makeback.
makeback使用assert动态的在内存中加入这200条add子句,
使用get_integer谓词产生所有的可能情况,然后计算出相应的进位
和本位,最后加入到内存中间,最后的fail引起回溯,这样就可以继续计算下一种情况。

这个makeback实际上类似于普通的程序设计语言中的三重循环。

由于第一个makeback子句总是失败的,为了让makeback总是成功,就在最后面加上了makeback.这样一条子句。

有了上面的加法表,我们还需要两个列表谓词,下面先来认识下列表:%列表谓词
member(A,[A|X]).
member(A,[B|X]) :- member(A,X).
%列表的元素都不相同时为真。

diff([X]).
diff([A|B]):-not(member(A,B)), diff(B).
diff/1的参数是一个列表,当列表中的元素全部不相同的时候成功。

好了,我们可以开始编写主程序了:
% donald+g erald=r obert
% 主调用
main:-
makeback, %产生加法表
!,
D is 5, %根据条件编写的加法顺序
add(D,D,0,J1,T), %从单词的低位加起
add(L,L,J1,J2,R),
add(A,A,J2,J3,E),
add(N,R,J3,J4,B),
add(O,E,J4,J5,O),
add(D,G,J5,0,R),
G =\=0, %首位不能够为零。

D=5题目已经告知
R =\=0,
List = [D,O,N,A,L,G,E,R,T,B], %所有字母代表的数字应该不同
diff(List),
write('the puzzle is donald+gerald=robert, and d is 5'),
nl,
write([d,o,n,a,l,g,e,r,t,b]),
nl,
write(List),
nl,
write([D,O,N,A,L,D]), %输出
nl,
write([G,E,R,A,L,D]),
nl,
write([R,O,B,E,R,T]),
nl,
nl,
fail. %寻找下一个解
这个主程序很容易理解,首先加入加法表,makeback后面的截断表示回溯时不需要重新产生加法表。

接下来的几个add子句表示了题目中的字母在这个加法算式中的关系。

例如:
add(D,D,0,J1,T),是个位的加法,个位的上一位进位为0,所以第三个参数为0,进到下一位的数字保存在J1中间,本位的数字为T。

add(L,L,J1,J2,R),是十位的加法,把十位上的两个字母相加,并且加上上一位的进位,最后得到十位的答案R和进位J2。

如此这般,一直到所有的位都加完毕。

最后一位的进位为0。

这样子加完毕以后,还有一些其它的限制:首位的字母不能够为0,每个字母所代表的数字应该不同,这一点使用前面编写的diff来判断。

如果通过了上面所有的关卡,就是最终的答案了,于是输出。

main.
the puzzle is donald+gerald=robert, and d is 5
[d,o,n,a,l,g,e,r,t,b]
[5,2,6,4,8,1,9,7,0,3]
[5,2,6,4,8,5]
[1,9,7,4,8,5]
[7,2,3,9,7,0]
即 526485+197485=723970。

这个算法之所以比前面所说的全排列的方法速度快,是因为每一步的add子句都可能引起回溯,从而可以尽早的排除许多不可能的情况。

其实这个题目不需要规定d=5,你可以把D is 5 那句话删掉,同样也可以找到答案,不过这一次花的时间就更长了。

makeback:-
delall,%删除以前的加法表
get_integer(0,9,X), %X,Y为两个被加数
get_integer(0,9,Y), %X,Y的取值为0到9之间的数字
get_integer(0,1,Z), %Z为上一位的进位只能够是1或者0
A is X+Y+Z,
B is A // 10, %//为整除号
C is A mod 10,
assert(add(X,Y,Z,B,C)), %B为这一位的进位,C为本位的得数。

fail.
makeback.
member(A,[A|X]).
member(A,[B|X]) :- member(A,X).
diff([X]).
diff([A|B]):-not(member(A,B)), diff(B).
delall:-retract(add(_,_,_,_,_)), fail.
delall.
main:-
makeback, %产生加法表
!,
D is 5, %根据条件编写的加法顺序
add(D,D,0,J1,T), %从单词的低位加起
add(L,L,J1,J2,R),
add(A,A,J2,J3,E),
add(N,R,J3,J4,B),
add(O,E,J4,J5,O),
add(D,G,J5,0,R),
G =\=0, %首位不能够为零。

D=5题目已经告知
R =\=0,
List = [D,O,N,A,L,G,E,R,T,B], %所有字母代表的数字应该不同 diff(List),
write('the puzzle is donald+gerald=robert, and d is 5'), nl,
write([d,o,n,a,l,g,e,r,t,b]),
nl,
write(List),
nl,
write([D,O,N,A,L,D]), %输出
nl,
write([G,E,R,A,L,D]),
nl,
write([R,O,B,E,R,T]),
nl,
nl,
fail. %寻找下一个解
作业:
1.列表描述事实法
2.编写出两个数字相乘、相减、相除及求余的谓词语句,并给出测试用例及结果
3.使用get_integer()谓词产生,X,Y,Z,三个数字,要求,X取值为[0,7],Y的取值为[X,9]且Z=X+Y.。

相关文档
最新文档