题目描述
有A、B、C、D、E 5所学校,在一次检查评比中,已知E校肯定不是第2名或第3.他们相互进行推测。
A校有人说,E校一定是第1名。
B校有人说,我校可能是第2名。
C校有人说,A校最差。
D校有人说,C校不是最好的。
E校有人说,D校会获得第1名。
结果只有第1名和第2名学校的人猜对了,编程指出这5所学校的名次。
思考
每个人的猜测结果都可能对,但只有两个人猜对了。可以直接取猜对的两个人的猜测结果,以及另外三个猜错的结果的反面,与已知条件进行对比,若不存在矛盾选项,且每个学校的排名能够唯一确定,认为选取的猜对的人是正确的。
例如,已知有 A、B、C、D、E 5 所学校及其相应的猜测结果,我们可以得到类似这样的数据结构:
schools = ('A', 'B', 'C', 'D', 'E')
pred = [
{
"Pos": ('E', (1, ), "D"),
"Neg": ('E', (4, 5)) # E 不会是 2 3
},
# ...
]
pred 是一个数组,数组中有 5 个元素,表示 5 个学校的预测结果,每个元素(预测结果)是一个字典。用 “Pos” 表示该学校猜对的结果,“Neg” 表示相反的结果。
将“A校有人说,E校一定是第1名。” 转化为数据结构就是 { "Pos": ('E', (1, ), "D") }
,Pos 元祖第 1 个元素 “E” 表示对 E 校的猜测,第 2 个元素 (1, ) 表示猜测的排名,为了统一,第 2 个用一个 tuple 表示,第 3 个元素 “D” 表示 A 的猜测是肯定
的。而 B 校的推测是 “可能”,因此用 “M” 表示。
题目中还给了一个条件,用数组表示:
# 猜对的人的学校排名是 1 或 2
RankFromRightPeople = [1, 2]
# 猜错的人的学校排名是 3 4 5
RankFromWrongPeople = [3, 4, 5]
当对某个学校的排名进行可能性分析时,实际上是利用已知的条件,对不同人给出的猜测或已知条件中的可能性做一个交集的操作。
然后我们开始尝试遍历所有可能的正确的学校,得到唯一的且不矛盾的结果即可。
编程
我们对学校的排名预测中会用到交集和差集,这里利用 python set 的 API 进行交集或差集运算:
def union(A, B) :
""" A + B
"""
return list(set(A).intersection(B))
def diff(A, B):
""" A - B
"""
return list(set(A).difference(set(B)))
接下来,对某种可能的排名情况进行分析。
根据给定的条件,我们可以遍历前两名的学校的可能情况,由于总共有 5 所学校,所以可能的情况有 10 种。
每次遍历的时候,从 pred 数组中取出各个学校对于排名的描述,将排名的描述(python 中的数组来表示)放入 predictSchool 数组中:
predictSchool = [0] * len(schools)
# 收集结果集
for index, p in enumerate(pred):
if si == index or sj == index:
predictSchool[schools.index(p["Pos"][0])] = list(p["Pos"][1])
else:
predictSchool[schools.index(p["Neg"][0])] = list(p["Neg"][1])
假设 A 和 B 学校在前两名,那么对 A 和 B 的描述中就要加上交集 (1, 2),另外 3 所学校的描述中要加上交集 (3, 4, 5):
# 取交集 union
for i in range(len(schools)):
if i == si or i == sj:
predictSchool[i] = union(predictSchool[i], RankFromRightPeople)
else:
predictSchool[i] = union(predictSchool[i], RankFromWrongPeople)
此时 predictSchool 应该是一个二维数组,它的数组类似于:
[
[1, 2, 3],
[1, 2, 5],
[3, 4],
[3, 5],
[3, 4, 5]
]
接下来,对每个学校的排名描述做差集处理,当 predictSchool 中出现某一项的元素为 0,说明描述出现矛盾,我们假设的第 1 名、第 2 名是错误的,可以直接停止分析:
for _ in range(len(schools) * 2):
# 只会执行一定次数循环
s = set()
# print("Before", predictSchool)
for pr in predictSchool:
lpr = len(pr)
if lpr == 0:
return []
elif lpr == 1:
if pr[0] in s:
# print("Already definite.")
return []
s.add(pr[0])
# print("Set 2 list", s)
for i, pr in enumerate(predictSchool):
lpr = len(pr)
if lpr > 1:
predictSchool[i] = diff(pr, s)
当 predictSchool 中出现某一项的元素数目为 1 时,表明这个学校的排名当前可以确定下来,接下来将 prediectSchool 中其他项的数组中去除掉这个排名的描述,最后循环到 predictSchool 中的每一项都只有一个元素时,说明我们猜测的第 1 名和第 2 名学校是可能的。
接下来只需要遍历所有可能的 1 2 名的情况即可:
def startPredict():
for i in range(5):
for j in range(i+1, 5):
pr = predictRank(i, j)
if len(pr) > 0:
for k, p in enumerate(pr):
pr[k] = [schools[k], p[0]]
dpr = dict(pr)
npr = sorted(dpr.items(), key = lambda item: item[1])
print(npr)
# 这里其实可以停止循环了
# return
# print(pr, i, j, '
---------')
startPredict()
结果
运行我们的 Python 程序,可以得到如下结果:
[('C', 1), ('B', 2), ('D', 3), ('E', 4), ('A', 5)]
完整的 python 程序可以从 我的 github 仓库 中获取。
可扩展性的讨论
至此为止,这个 Python 程序已经可以找到正确的排名了,但它还有改进的空间。还记得我们定义 pred 数组的时候是怎么样的吗:
schools = ('A', 'B', 'C', 'D', 'E')
pred = [
{
"Pos": ('E', (1, ), "D"),
"Neg": ('E', (4, 5)) # E 不会是 2 3
},
# ...
]
pred 数组中的 "Pos" 项就是题目所描述的,然而 pred 数组中的 "Neg" 项是我们从程序中获取并经过了处理的,但是实际上它并不需要人为的处理,我们可以写一个简单的函数对 pred 的 "Pos" 项做一个预处理,将其转化为 "Neg" 项。
需要注意的是,题目有个前置条件,E 学校不是第 2 名,也不是第 3 名。预处理的过程如果有对 E 学校的描述时,要与这个条件求交集
我已经给它留好了扩展的空间,"Pos" 对应 value 的第 3 个元素表示猜测的可能性,我们可以根据第 3 个元素和第 2 个元素推导出 "Neg":
- 如果第 3 个元素是 "D",那么对 Pos 中的第 2 项取差集(当然,总集合是 schools 数组所对应的的 set)。
- 如果第 3 个元素是 "M",那么 Pos 中的第 2 项和 Neg 中的第 2 项应该是一样的。因为 A 可能是第 1 名,和它的反面 A 可能不是第 1 名的可能情况数是一样的。(当然你可能认为相反是另外一种定义,比如 A 可能是第 1 名,那么它的反面是 “A 不可能是第 1 名”,这样的话, Pos 项中的 D 在 Neg 项中就变成了 M,这显然不是我们想要的结果)
加上这样的预处理后,实际上解决这类问题就不用修改太多代码,而且也不需要人为的计算出每个学校的 Pos 项和 Neg 项了。就是说这个程序的扩展性还是比较好的。
如果学校变多了,比如从 A 到 G,我们也只需要把题目给出的条件作为参数给到程序中,就可以了。
关于这个程序的执行效率问题,实际上程序就是靠遍历找到可能的排名情况,因此效率并不会太高。