COGS201 学习笔记 week 6
4.1 - Recursion
WHAT IS RECURSION?
- A statement that refers to itself一个指代自身的陈述
- OR:一个句子中,相同的谓语(predicate)出现在头部和身体中
- 递归是计算机编程中的一种强大工具。
Example: Ancestors
什么是祖先?
• 父母parent
• 祖父母grandparent
• 曾祖父母great-grandparent
• 曾曾祖父母great-great-great-great
• 等等
1 | ancestor(X,Y) :- X is directly above Y, in the family tree, by any number of generations. |
我们如何向Prolog解释这个?
Brute Force Version
1 | ancestor (X,Y):-parent (X,Y). |
A Better Idea.更好的办法
1 | ancestor(X,Y) :- parent(X,Y). |
"Your parent is your ancestor, and any ancestor of your ancestor is also your ancestor.”
你的父母是你的祖先,而你祖先的任何祖先也是你的祖先。
1 | ancestor (X,Y):-parent (X,Y).<-BASE CASE |
How does it work?
1 | ?- ancestor (shmi,ben). |
Prolog 查找atoms和condition
• ancestor(X,Y) :- parent(X,Y).
• ancestor(shmi,ben) :- parent(shmi,ben).
• 但这个结果为false
但还有另一个condition…
Second conditional
1 | ancestor (shmi,ben) :- parent (shmi,Z), ancestor (Z,ben) |
-
Z是谁?
-
只有一个与
parent(shmi,Z)可以unified -
Z = anakin -
现在我们在寻找
ancestor(anakin,ben)
Backchaining again…
- Atom:
ancestor(anakin,ben).(Not found) - First conditional:
ancestor(anakin,ben) :- parent(anakin,ben). - This evaluates false
Second conditional
ancestor(shmi,ben) :- parent(shmi,Z),ancestor(Z,ben).- Who could Z be?
- Only one thing unifies with
parent(shmi,Z) Z = anakin- So now we are looking for
ancestor(anakin,ben)
Backchaining again…再次回溯
• Atom: ancestor(anakin,ben). (Not found)
• First conditional: ancestor(anakin,ben) :- parent(anakin,ben).
• This evaluates false
Infinite Recursion?无限递归
我们如何知道程序不会永远运行?
因为递归的情况结构良好。
• 在recursive case之前有一个base case
• 在recursive case中,parent (not recursive) 在ancestor (recursive)之前被evaluated。
• 每次程序递归时,它都会进入下一代(generations)。
• 知识库有有限数量的代(generations)。
• 因此,在有限的时间内,我们最终会到达base case或fail
More Formally更正式地
我们可以用数学证明递归程序会给我们需要的答案。
我们用数学归纳法来做这件事。(一个温和的版本)
Mathematical Induction数学归纳法
我们证明什么?
对于任何作为Y的direct ancestor的X,若X在family tree中位于Y上方k代,
我们的ancestor谓词对将返回true。
k为任意自然数(1, 2, …,直至无穷)
Base case
k的最小可能值为1。
如果k为1,parent(X,Y)会为true。
ancestor(X,Y) :- parent(X,Y)
就这样完成了!
避免死循环
1 | ancestor(X,Y) :- parent(X,Y). |
1.Base case first, BEFORE recursive case(s)
(Base case 必须放在递归前面)
In a recursive case, non-recursive atoms BEFORE recursive ones
(递归规则中,非递归条件必须写在递归调用前)
ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y)
Prolog的运行顺序
1 | ?-parent(X,luke), \+ male(X). |
4.2 - Lists
有时我们需要一组非常相似但参数个数不同的谓词
例如,测试一组人彼此是否都不同。
1 | uniq_person3(X,Y,Z) :- ... |
如果我们能只写一个对任意数量的人都适用的谓词,那将容易得多,不论人数多或少。
1 | uniq_people(L) :- ... |
这种集合称为列表。
Lists
列表是对象(terms)的序列。
列表中的每一项称为一个元素(elemen)。
它们被放在方括号中并用逗号分隔。
1 | [anna, karenina] |
- 一个包含两个元素的列表。第一个元素是 anna,第二个元素是 karenina。
1 | [intro,to,programming, in, prolog] |
- 一个有五个元素的列表。
1 | [1, 2, 3, 3, 2, 1] |
- 一个包含数字项的六元素列表。
- 列表可能有重复的元素。
1 | [] |
- 一个没有元素的列表。空列表。
Structured Lists
列表可以包含其他列表作为元素。
1 | [[john,23], [mary,14], [hello]] |
- 一个含有三个元素的列表。三个元素中的每一个也是一个列表。
1 | [1, john, ['199y', john]] |
- 另一个三元素列表。
1 | [[]] |
- 一个一元素的列表。它所包含的唯一元素是一个空列表。
Elements and Lists
元素(element)”和“列表(list)”不是一回事
[anna][\neq]anna[[]][\neq][]
Heads and Tails
Prolog 经常通过把列表分解为 头(head) + 尾(Tails) 来递归处理
非空列表(non-empty list)
在一个非空列表里:
- Head(头):列表的第一个元素
- Tail(尾):除了第一个元素以外的剩余部分
并且有一个非常重要的点:
Tail 一定还是一个列表
它等价于“原列表去掉 head 后剩下的列表”
空列表(empty list)
空列表 [] 没有 head,也没有 tail,因为里面什么都没有。
例子
对列表:[a, b, c, d]
- Head =
a - Tail =
[b, c, d]
对列表:[anna]
- Head =
anna - Tail =
[](空列表)
Try it yourself…
Identify the head and the tail of the following lists:
[parsley, sage, rosemary, thyme]
Tail(尾):`[sage, rosemary, thyme]`
Tail(尾):[c, d]
Tail(尾):[]
Lists as Prolog Terms
我们现在知道四种 Prolog 项:常量、变量、数字和列表。
在 Prolog 中有两种不同的写列表的方法:基本方式和带杠方式(barred way)。
到目前为止我们所看到的方式是基本方式
1 | [1,-4, Z, [a, X, ‘b 2’], joe] |
“barred way是一种不同的符号表示,用于将表头与表尾分开。这对于递归列表处理很有用。
列表的头部(head)先出现,然后是竖线|,接着是尾部(tail)。
Basic way: [1, X, 3]
1 | Barred way: [1 | [X, 3]] |
请注意,尾部(tail)是一个列表
因此在带杠(barred)的表示法中,尾部周围总是会多一对括号
Lists and Unification
列表和带变量的谓词()一样,可以进行“统一”(unify)
而且:基本写法的列表可以和带竖线写法的列表统一
- 例子:
1 | unify p([X, 3 | Z]) with p([b, Y, c, b]) |
-
[X] and [a]
- X=a
-
[X,X] and [[Y,a,c],[b,a,c]]
- Y=b, X=[b,a,c]
-
[a,b,c] and [a|[b,c]]
- Same list in different notations
-
[X|Y] and [a]
- X=a, Y=[]
-
[a,b,c] and [X|Y]
- X=a, Y=[b,c]
Lists that Fail to Unify
情况 1:元素个数不同
情况 2:存在不匹配的元素
例子:
- [a] and []
- [] and [[]]
- [X,Y] and [U,V,W]
- [a,b,c] and [X,b,X]
- [X|Y] and []
TRY IT YOURSELF
How do the following lists unify?
-
[a,b,X] and [Y,b,Y]
点击展开答案
Y = a
X = a
-
[[X]] and [Y]
点击展开答案
Y = [X]
-
[a,b,c] and [a|[b|[c]]]
点击展开答案
true
-
[a|[b,c]] and [a,X,Y]
点击展开答案
X = b
Y = c
-
[X,Y|Y] and [a,[b,c],b,c]
点击展开答案
X = a
Y = [b,c]
Built-In List Functions
1 | ?-length([1,3,6],What). |
append被添加的项必须是列表——Prolog将其处理为在竖线符号的后面
结构角度理解 append
append([a],[b]) 本质就是:
1 | [a | []] + [b | []] |
变成:
1 | [a | [b | []]] |
就是
1 |
|
1 | ?-append(X,[1,2],[1,1,2]%求解X + [1,2] = [1,1,2] |
append(L1, L2, Result)意思是L1 + L2 = Result
1 | ?- reverse([1,2],ReversedVersion). |
1 | ?- member(a,[a,b,c]). |
1 | ?- member(Element,[a|[b,c]]). |
Recursive List Predicates 递归列表谓词
检查元素是否在list中
1 | party([H|_]) :- friend(H). |
递归tail列表,每次递归判断head是否是friend如果不是则继续递归直到找到friend= true 或到达list末尾
如何检查一个列表是否包含另一个列表的所有元素
1 | containsAll(_,[]). |
用member判断头元素是否在list内
Practice Question
Can you test if all the items in a list are unique?
-
(Hint: Use member.)
-
unique([a,b,c]) should return true
unique([a,b,b]) should return false
点击展开答案
unique([_]).unique(H|T):- \+ member(H,T),unique(T).
Infinite Recursion
1 | ?- member(3, L). |
会导致无限递归
1 | member(X, [_|T]) :- |
无限增长
原因因为逻辑上:包含 3 的列表有无限多个
正确使用方式
1 | ?- member(3, [1,2,3]). |
- 有限
- 正常
- 会停止