AI-Lab2

基本概念

  • 常量:任何类型的实体

  • 变量:x,y未知量

  • 项:谓词、变量的参数项,由递归定义

    • 变量是项(可以看成是0元函数)
    • t1,t2,t3……tn是项,f是n元函数,则f(t1,t2,,,,tn)也是项
    • 一阶逻辑中谓词不是项,即不能作为函数/谓词的参数,也就是不存在f(P(x))这种复合方式,但是二阶逻辑中是可以的
  • 谓词:谓词是对其参数的

    • 零元谓词:退化为命题
    • 单元谓词:只有一个参数,表示参数具备某种属性
    • 多元谓词:多个参数,表示参数之间的关系
  • 事实:谓词中变量实例化后得到的事实

  • 规则:公式,通过递归定义

  • 可满足性:对该可满足性问题,只要能找到一组赋值,使得这个公式成立,这个公式就是可满足的

  • 逻辑蕴含和逻辑推论

    • 逻辑蕴含S |= α指对于任意变量赋值,如果S正确,则α也正确
    • 逻辑推论S |- α指存在一条推理路径,从S出发,推导证明α
  • 一阶逻辑中谓词不是项,即不能作为函数/谓词的参数,也就是不存在f(P(x))这种复合方式,但是二阶逻辑中是可以的

归结算法

归结反演:

steps:

  1. 将α取否定,放到KB中
  2. 将更新的KB转换为clausal form得到新的子句集S
  3. 反复调用单步归结
    • 如果得到空子句,即S|- (),说明KB ∧¬α 不可满足,算法终止,可得KB |= α
    • 如果一直归结直到不产生新的子句,在这个过程中没有得到空子句,则KB |= α不成立

clausal form–便于计算机处理的形式

  • 每一个子句对应一个元组,元组中的每一个元素是一个原子公式或原子公式的否定,元素之间的关系是析取关系,表示只要一个原子成立,即子句成立。
    • ps:元组是用逗号分隔的,即使只有一个元素,也需要在元素后面加上逗号,以区分它与普通变量的不同
    • 元组的集合组合子句集S,子句集中的每个句子之间是合取关系
  • 单步归结:
    • 从两个子句中分贝寻找相同的原子及其对应的原子否定(只能找其中一组,如果出现两组互补对不可以同时归结)
    • 去掉该互补对,并将剩余元素合并为新子句

举例:example_归结

最一般合一算法

合一

  • 通过变量替换使得两个子句能够被归结(有相同的原子),所以合一也被定义为使得两个原子公式等价的一组变量替换/赋值

  • 由于一阶逻辑中存在变量,所以归结之前需要进行合一,如(P(john),Q(fred),R(x))(¬P(y),R(susan),R(y))两个子句中,我们无法找到一样的原子及其对应的否定,但是不代表它们不能够归结

  • 通过将y替换为john(替换一定要全部替换),我们得到了(P(john),Q(fred),R(x))(¬P(john),R(susan),R(john)),此时我们两个子句分别存在原子P(john)和它的否定¬P(john),可以进行归结

最一般合一

指使得两个原子公式等价,最简单的一组变量替换

  • 输入:两个原子公式,它们有相同的谓词,不同的参数项和‘’¬“
  • 输出:一组变量替换/赋值
  • 流程:最一般合一算法流程

Tips:变量替换是从两个原子公式中找到的,但是最后要施加给整个子句的

一阶逻辑归结算法:

一阶逻辑归结算法

要求

存储公式的python数据结构

  • 用字符串存储

  • 符号¬用‘~’代替

  • 谓词首字母大写, 例如用A, B, C, P1, P2, Student等表示; 谓词的每个参数之间用逗号“,”间隔且不加空格

  • 常量用小写单词或a, b, c等小写字母表示;

  • 本次作业的公式中不含∃,∀量词符号

    • 例子: ¬child存储为 “~child” boy存储为“boy”

    • 几个公式: “R(a)”, “~P(a,zz)”, “Student(tony)”. 这里应该将a,tony看做常量,将zz看做变量

存储子句的python数据结构

  • 用tuple的方式存储
    • 例子:
    • ¬child˅¬male˅boy存储为(‘child’, ‘male’, ‘boy’)
    • ¬S(z) ˅ L(z, snow)存储为(‘~S(z)’, ‘L(z,snow)’)

存储子句集的python数据结构

  • 子句集用set(可以去重)的方式存储, 每个元素是子句(元组)

  • 小心归结的时候重复生成一样的子句

实现过程

实验一:命题逻辑的归结推理

内容:

编写函数ResolutionProp实现命题逻辑的归结推理,函数要点如下:

  • 输入为子句集, 每个子句中的元素是原子命题或其否定.
  • 输出归结推理的过程, 每个归结步骤存为字符串, 将所有归结步骤按序存到一个列表中并返回, 即返回的数据类型为 list[str].
  • 一个归结步骤的格式为 步骤编号 R[用到的子句编号] = 子句. 如果一个字句包含多个公式,则每个公式用编号 a,b,c...区分,如果一个字句仅包含一个公式,则不用编号区分.(见课件和例题)

例子: 输入子句集

1
KB = {(FirstGrade,), (~FirstGrade,Child), (~Child,)}

则调用 ResolutionProp(KB)后返回推理过程的列表如下:

1
2
3
4
5
1 (FirstGrade,),
2 (~FirstGrade,Child)
3 (~Child,),
4 R[1,2a] = (Child,),
5 R[3,4] = ()

ResolutionProp()函数

working on…

  • 这里我们设置了两个字典用于存储子句的编号以及子句中元素的编号,方便后续打印;同时先将基本的子句集列举出来
  • 设置processed_KB用于记录已经归结过的子句,同时建立一个new_KB用于记录每轮新增子句,原KB用于不断更新–保存旧子句和加入新子句
  • 在进行归结时,需要将完整子句集与新增子句集进行归结,进行归结的子句需要没被归结过–也就是不在processed_KB之中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def ResolutionProp(KB):
steps = []
k = 1
clause_dict = {} # 记录子句及其编号--同时加入新增语句
clause_labels = {} # 记录子句内元素的编号

# 初始化子句编号,同时将子句集列举出来
for clause in KB:
clause_dict[clause] = k
steps.append(f"{k} {clause}")
if len(clause) > 1: #有多个元素的子句编号a,b...
clause_labels[clause] = {ele: chr(97 + i) for i, ele in enumerate(clause)}
k += 1

clauses = list(KB)
processed_KB = set() # 记录已经归结过的子句--保证不会重复归结
new_KB = set(clauses) # 初始子句集--需要进行更新

while new_KB:
current_clauses = list(new_KB) # 当前需要归结的新子句--上一轮生成的语句
new_KB.clear()



for i in range(len(current_clauses)):
for j in range(len(clauses)): # 之前的子句 + 新生成的子句
if current_clauses[i] == clauses[j]: # 避免自身归结
continue
if current_clauses[i] in processed_KB : # 这里是防止出现重复归结
continue
if clauses[j] in processed_KB :
continue

res, resolved_literals = resolve(current_clauses[i], clauses[j])
if res is not None:
clause1_id = str(clause_dict[current_clauses[i]])
clause2_id = str(clause_dict[clauses[j]])

# 处理字句编号
if len(current_clauses[i]) > 1 and resolved_literals[0] in clause_labels[current_clauses[i]]:
clause1_id += clause_labels[current_clauses[i]][resolved_literals[0]]
if len(clauses[j]) > 1 and resolved_literals[1] in clause_labels[clauses[j]]:
clause2_id += clause_labels[clauses[j]][resolved_literals[1]]
#在这里把已经归结的加入
processed_KB.add(current_clauses[i])
processed_KB.add(clauses[j])

if res == ():#空子句
steps.append(f"{k} R[{clause1_id},{clause2_id}] = ()")
return steps

if res not in clause_dict:
clause_dict[res] = k
if len(res) > 1:
clause_labels[res] = {lit: chr(97 + i) for i, lit in enumerate(res)}
new_KB.add(res)
clauses.append(res)
steps.append(f"{k} R[{clause1_id},{clause2_id}] = {res}")
k += 1


#clauses.extend(current_clauses) # 将新子句加入原始子句集中


return steps

resolve()函数

!这个函数主要用于做单步归结

思路:

  • 遍历子句1中的所有元素,对每一个元素都取反,查看这个取反的元素是否在子句2中,如果存在,说明找到了一组互补对,进行归结,并返回新的子句集(合并并去掉互补对)以及互补对;如果取反的元素不在子句2中继续遍历。
  • 如果遍历到结尾都没有找到,说明这两句无法进行归结,返回空。
1
2
3
4
5
6
7
8
9
10
11
12
def resolve(clause1, clause2):
for literal in clause1:
complement = f"~{literal}" if literal[0] != '~' else literal[1:]
if complement in clause2:
# 新的子句应该去除字面量和互补字面量
new_clause = tuple(sorted(set(clause1).union(set(clause2)) - {literal, complement}))

# 如果新子句为空,返回空子句
if not new_clause:
return (), (literal, complement)
return new_clause, (literal, complement)
return None, None

其他注意点:

  • 子句是元组,但由于元组是一种不可修改的数据结构,我们需要在合并子句集的时候就去除互补对。

main函数

main函数比较简单,只需要设置好子句集,然后调用ResolutionProp函数,再输出steps这个字符串列表就好了。可以增加一些新例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def main():
KB = {
('FirstGrade',),
('~FirstGrade', 'Child'),
('~Child',)
}
KB1={
('a',),
('b',)
}
KB2 = {
('A',),
('~A', 'B'),
('~B',)
}
KB3 = {
('P', 'Q'),
('~P', 'R'),
('~Q', 'R'),
('~R',)
}
KB4 = {
('A', 'B'),
('~A', 'C'),
('~B', 'D'),
('~C', '~D')
}
steps = ResolutionProp(KB)
for step in steps:
print(step)

实验二:最一般合一算法

编写函数 MGU实现最一般合一算法. 该函数要点如下:

  • 输入为两个原子公式, 它们的谓词相同. 其数据类型为 str, 格式详见课件.
  • 输出最一般合一的结果, 数据类型为 dict, 格式形如**{变量: 项, 变量: 项}**, 其中的变量和项均为字符串.
  • 若不存在合一, 则返回空字典.

例子:

调用 MGU('P(xx,a)', 'P(b,yy)')后返回字典 {'xx':'b', 'yy':'a'}.

调用 MGU('P(a,xx,f(g(yy)))', 'P(zz,f(zz),f(uu))')后返回字典 {'zz':'a', 'xx':'f(a)', 'uu':'g(yy)'}.

提示

  1. 只含一个元素的 tuple类型要在末尾加 ,. 例如 ('a')是错误的写法, 而正确的写法是 ('a',).
  2. {}会被解释成空字典. 若要定义空集合请用 set().

这一部分只写了一个函数(MGU)和一个辅助函数(用于判断是否含有变量)

MGU(literal1,literal2)

实现思路:

  • 输入:两个谓词相同的原子公式(字符串类型);
  • 返回/输出:字典,格式:{变量: 项, 变量: 项}
  1. 首先处理原子公式,我们需要对各个位置的变量or项进行比较,在此之前,我们可以去掉谓词P(),然后再使用分词器split来将字符串分割成列表元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#其实这部分可以直接使用find()来查找第一个左括号,再使用切片
i=0
for i in range(len(literal1)):
if literal1[i]!='(':
i+=1
else:
break

# 去掉P(),然后使用split以逗号划分项
lit1=literal1[i+1:-1].split(',')
lit2=literal2[i+1:-1].split(',')

#不等长无法合并--特殊情况
if len(lit1)!=len(lit2):
return None
  1. 最重要的处理部分–主要思想就是一个个元素比较,分三类
    • 两个元素都是变量–无法合一,返回None
    • 一个元素是变量,另一个是项–用项替代变量,并加入到res字典中,同时把式子中所有相同变量用项替代
    • 两个元素都是项–分析是不是f()或f(g())这种函数嵌套的项,如果都含有f(),需要进行递归判断(递归调用MGU()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#遍历两个集合
for i in range(len(lit1)):
if lit1[i]==lit2[i]:
continue

if lit1[i] != lit2[i]:
# 查看字符串里是否存在变量?如果都是变量无法合一--单独一个变量,不是项
v1=Check_Var(lit1[i])
v2=Check_Var(lit2[i])
#全都含有变量,无法合一
if v1 and v2:
return None
elif v1 is True and v2 is False:
if v1 in res:
#如果v1在res中已经进行了项的替换,不能再替换成其他的
return None
#v1是变量,v2是项
res[lit1[i]]=lit2[i]
for j in range(i+1,len(lit1)):
lit1[j]=lit1[j].replace(lit1[i],lit2[i])
lit2[j]=lit2[j].replace(lit1[i],lit2[i])
elif v2 is True and v1 is False:
if v2 in res:
#如果v1在res中已经进行了项的替换,不能再替换成其他的
return None
#v1是变量,v2是项
res[lit2[i]]=lit1[i]
for j in range(i+1,len(lit1)):
lit1[j]=lit1[j].replace(lit2[i],lit1[i])
lit2[j]=lit2[j].replace(lit2[i],lit1[i])
else:
#单独判断--可能存在嵌套的函数项
if '(' in lit1[i] and '(' in lit2[i]:
sub_res=MGU(lit1[i],lit2[i])
if sub_res is None:
return None
else:
res.update(sub_res)
else:
return None

Check_Var()

很普通的一个函数,这里我只判断了是否存在x,y,z,u这几个变量,如果有更多函数符号可以添加,或者后面修改成传递变量集(but…lazy)

简单测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def main():
# literal1='P(f(zz))'
# literal2='P(f(f(u)))'
# literal1='P(xx,a)'
# literal2='P(b,yy)'
# literal1='P(zz,xx)'
# literal2='P(a,f(zz))'
literal1='P(a,xx,f(g(yy)))'
literal2='P(zz,f(zz),f(uu))'
res=MGU(literal1,literal2)
if res is not None:
for key,value in res.items():
print(key,":",value)
else:
print("NO MGU found")

输出:

1
2
3
zz : a
xx : f(a)
uu : g(yy)

一阶逻辑的归结推理

编写函数 ResolutionFOL实现一阶逻辑的归结推理. 该函数要点如下:

  • 输入为子句集, KB子句中的每个元素是一阶逻辑公式(不含全称量词和存在量词等量词符号)
  • 输出归结推理的过程, 每个归结步骤存为字符串, 将所有归结步骤按序存到一个列表中并返回, 即返回的数据类型list[str]
  • 一个归结步骤的格式为 步骤编号 R[用到的子句编号]{最一般合一} = 子句, 其中最一般合一输出格式为”{变量=常量, 变量=常量}”.如果一个字句包含多个公式,则每个公式用编号 a,b,c...区分,如果一个字句仅包含一个公式,则不用编号区分。

例题: 输入

1
KB = {(GradStudent(sue),),(~GradStudent(x),Student(x)),(~Student(x),HardWorker(x)),(~HardWorker(sue),)}

则调用 ResolutionFOL(KB)后返回推理过程的列表如下:

1
2
3
4
5
6
7
1 (GradStudent(sue),)
2 (~GradStudent(x),Student(x))
3 (~Student(x),HardWorker(x))
4 (~HardWorker(sue),)
5 R[1,2a]{x=sue} = (Student(sue),)
6 R[3a,5]{x=sue} = (HardWorker(sue),)
7 R[4,6] = []

输入

1
KB = {(A(tony),),(A(mike),),(A(john),),(L(tony,rain),),(L(tony,snow),),(~A(x),S(x),C(x)),(~C(y),~L(y,rain)),(L(z,snow),~S(z)),(~L(tony,u),~L(mike,u)),(L(tony,v),L(mike,v)),(~A(w),~C(w),S(w))}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1 (A(tony),)
2 (A(mike),)
3 (A(john),)
4 (L(tony,rain),)
5 (L(tony,snow),)
6 (~A(x),S(x),C(x))
7 (~C(y),~L(y,rain))
8 (L(z,snow),~S(z))
9 (~L(tony,u),~L(mike,u))
10 (L(tony,v),L(mike,v))
11 (~A(w),~C(w),S(w))
12 R[2,11a]{w=mike} = (S(mike),~C(mike))
13 R[5,9a]{u=snow} = (~L(mike,snow),)
14 R[6c,12b]{x=mike} = (S(mike),~A(mike),S(mike))
15 R[2,14b] = (S(mike),)
16 R[8b,15]{z=mike} = (L(mike,snow),)
17 R[13,16] = []

输入

1
KB = {(On(tony,mike),),(On(mike,john),),(Green(tony),),(~Green(john),),(~On(xx,yy),~Green(xx),Green(yy))}

输出

1
2
3
4
5
6
7
8
9
10
1 (On(tony,mike),),
2 (On(mike,john),),
3 (Green(tony),),
4 (~Green(john),),
5 (~On(xx,yy),~Green(xx),Green(yy)),
6 R[4,5c]{yy=john} = (~On(xx,john),~Green(xx)),
7 R[3,5b]{xx=tony} = (~On(tony,yy),Green(yy)),
8 R[2,6a]{xx=mike} = (~Green(mike),),
9 R[1,7a]{yy=mike} = (Green(mike),),
10 R[8,9] = ()

注意

  1. 只含一个元素的 tuple类型要在末尾加 ,. 例如 ('x')是错误的写法, 而正确的写法是 ('x',).
  2. {}会被解释成空字典. 若要定义空集合请用 set().
  3. 请提交代码时只提交一个 .py代码文件, 请不要提交其他文件.
  4. 例题和作业都会进行代码测试.
  5. 上述作业的输出仅供参考。如果有不同的归结顺序,结果相同的情况,代码也算正确.

一阶逻辑归结算法

实现思路:

参考以及复用大部分前两个任务的代码来构建

ResolutionFOL()

算法思路:

  • 将KB库以子句集的方式传入到函数中进行归结处理
  • 对子句集反复进行单步归结,同时使用MGU算法进行合一处理

具体:

  • 同上面的归结算法,由于输出时需要有美观的编号,我们设置了两个记录字典–clause_dictclause_labels,前者记录子句的编号,后者记录子句中元素的编号(有多个元素的子句才需要)

  • 类似地,先将子句集的子句进行编号

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    k = 1
    clause_dict = {} # 记录子句及其编号--同时加入新增语句
    clause_labels = {} # 记录子句内元素的编号

    # 初始化子句编号,同时将子句集列举出来
    for clause in KB:
    clause_dict[clause] = k
    clause_str = format_clause(clause)
    steps.append(f"{k} {clause_str}")

    if len(clause) > 1:#有多个元素的子句编号a,b...
    clause_labels[clause] = {}
    for i, literal in enumerate(clause):
    clause_labels[clause][literal] = chr(97 + i)
    k += 1
  • 进入归结的主要步骤,需要维持一个已经归结的子句对的集合–保证子句对不会重复归结(这部分和上面的归结算法有点出入,前者只需要保证我归结过的句子不用再出现就可以了),但是由于我们可以对谓词P(x)赋不同的值,所以可以对他赋值后产生的新子句再次归结。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def ResolutionFOL(KB):
steps = []
k = 1
clause_dict = {} # 记录子句及其编号--同时加入新增语句
clause_labels = {} # 记录子句内元素的编号

# 初始化子句编号,同时将子句集列举出来
for clause in KB:
clause_dict[clause] = k
clause_str = format_clause(clause)
steps.append(f"{k} {clause_str}")

if len(clause) > 1:#有多个元素的子句编号a,b...
clause_labels[clause] = {}
for i, literal in enumerate(clause):
clause_labels[clause][literal] = chr(97 + i)
k += 1

original_size = k - 1 # 记录原始子句集的大小
#原始子句集
clauses = list(KB)
# 记录已经归结过的子句--保证不会重复归结(但问题是一阶逻辑是可以多次为量词赋值的,所有我们记录的不是归结过的单个子句,而是子句对)
processed = set()

while True:
new_resolvents = []

for i in range(len(clauses)):
for j in range(i + 1, len(clauses)):
if clauses[i] == clauses[j]: # 避免自身归结
continue
c1 = clauses[i]
c2 = clauses[j]

if (c1, c2) in processed:
continue
#这里加入子句对
processed.add((c1, c2))
processed.add((c2, c1))

resolvents = resolve(c1, c2)

for resolvent_info in resolvents:
#由于输出需要含有合一替换,所以resolve函数有一些变化
resolvent, lit1, lit2, mgu = resolvent_info

# 编号
clause1_id = str(clause_dict[c1])
clause2_id = str(clause_dict[c2])

# 处理元素的编号
if len(c1) > 1 and lit1 in clause_labels.get(c1, {}):
clause1_id += clause_labels[c1][lit1]

if len(c2) > 1 and lit2 in clause_labels.get(c2, {}):
clause2_id += clause_labels[c2][lit2]

#MGU调用
mgu_str = format_mgu(mgu)

# 查看是否产生了空集
if resolvent == ():
steps.append(f"{k} R[{clause1_id},{clause2_id}]{mgu_str} = ()")
# 找到空子句,简化归结过程
simplified_steps = Simplify(steps, original_size)
return simplified_steps

# 将新的子句加入子句集
if resolvent not in clause_dict:
clause_dict[resolvent] = k

# 编号
if len(resolvent) > 1:
clause_labels[resolvent] = {}
for idx, lit in enumerate(resolvent):
clause_labels[resolvent][lit] = chr(97 + idx)

resolvent_str = format_clause(resolvent)
steps.append(f"{k} R[{clause1_id},{clause2_id}]{mgu_str} = {resolvent_str}")
new_resolvents.append(resolvent)
k += 1

#不产生新子句,返回结果
if not new_resolvents:
return steps

# 扩展旧子句集
clauses.extend(new_resolvents)

extract_parents()

用于从归结步骤中提取父子句编号,用于筛选出真正有用的归结步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def extract_parents(step):
#从归结步骤中提取父子句编号
if "R[" not in step:
return None, None

# 提取R[]内的内容
relation_part = step[step.index("R[") + 2:step.index("]")]
parents = relation_part.split(',')

# 提取数字部分
parent1 = int(''.join(c for c in parents[0] if c.isdigit()))
parent2 = int(''.join(c for c in parents[1] if c.isdigit()))

# 提取字母后缀
parent1_suffix = ''.join(c for c in parents[0] if c.isalpha())
parent2_suffix = ''.join(c for c in parents[1] if c.isalpha())

return parent1, parent2, parent1_suffix, parent2_suffix

get_step_number(step)

获取编号,把step的第一个标号提取出来

1
2
def get_step_number(step): #获取编号
return int(step.split(' ')[0])

Simplify(steps, original_size)

简化归结过程–把又臭又长的过程删减成有用的过程

  • 记录初始子句集(也就是KB库转换的最初的子句集)
  • 同时建立一个存储有用子句的集合
  • 在旧的step的基础上进行筛选,同时创建新的编号,创建num2idx字典,用于存储编号到索引的映射
  • 为了简化过程,我们是从空子句开始回溯,找到能得到空子句的核心步骤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def Simplify(steps, original_size): #简化归结过程--把又臭又长的过程删减成有用的过程
base_process = steps[0:original_size] # 初始子句集
useful_steps = [] # 有用子句集

# 创建编号到索引的映射
number_to_index = {}
for i, step in enumerate(steps):
number = get_step_number(step)
number_to_index[number] = i

# 从空子句开始,队列中存放需要处理的子句编号
number = [get_step_number(steps[-1])] # 空子句的编号
processed = set() # 已处理的子句编号

while number:
number0 = number.pop(0) # 提取队列首元素
if number0 in processed: # 避免处理已处理的子句
continue

processed.add(number0)
step_index = number_to_index[number0]
step = steps[step_index]

if step_index >= original_size: # 非原始子句才加入useful_steps
useful_steps.append(step)

# 获取父子句编号
parent_info = extract_parents(step)
if parent_info[0] is not None:
num1, num2 = parent_info[0], parent_info[1]

# 如果不是初始子句,加入队列
if num1 > original_size and num1 not in processed:
number.append(num1)
if num2 > original_size and num2 not in processed:
number.append(num2)

# 结果需要反转--回溯
useful_steps.reverse()

# 得到新的归结过程:原始子句 + 有用子句
final_steps = base_process + useful_steps

# 创建新旧编号的映射
old_to_new = {}
for i, step in enumerate(final_steps):
old_num = get_step_number(step)
new_num = i + 1
old_to_new[old_num] = new_num

# 将归结过程重新编号
for i in range(original_size, len(final_steps)):
step = final_steps[i]
parent_info = extract_parents(step)

if parent_info[0] is not None:
old_num1, old_num2 = parent_info[0], parent_info[1]
suffix1, suffix2 = parent_info[2], parent_info[3]

# 获取新编号
new_num1 = old_to_new[old_num1]
new_num2 = old_to_new[old_num2]

# 构造新的引用字符串
old_ref = f"R[{old_num1}{suffix1},{old_num2}{suffix2}]"
new_ref = f"R[{new_num1}{suffix1},{new_num2}{suffix2}]"

# 替换引用
final_steps[i] = final_steps[i].replace(old_ref, new_ref)

# 更新步骤编号
old_step_num = get_step_number(final_steps[i])
new_step_num = i + 1
if old_step_num != new_step_num:
final_steps[i] = str(new_step_num) + final_steps[i][len(str(old_step_num)):]

return final_steps


AI-Lab2
https://pqcu77.github.io/2025/03/11/AI-lab2/
作者
linqt
发布于
2025年3月11日
许可协议