预测学校排名问题

题目描述

有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, '\n---------')

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":

  1. 如果第 3 个元素是 "D",那么对 Pos 中的第 2 项取差集(当然,总集合是 schools 数组所对应的的 set)。
  2. 如果第 3 个元素是 "M",那么 Pos 中的第 2 项和 Neg 中的第 2 项应该是一样的。因为 A 可能是第 1 名,和它的反面 A 可能不是第 1 名的可能情况数是一样的。(当然你可能认为相反是另外一种定义,比如 A 可能是第 1 名,那么它的反面是 “A 不可能是第 1 名”,这样的话, Pos 项中的 D 在 Neg 项中就变成了 M,这显然不是我们想要的结果)

加上这样的预处理后,实际上解决这类问题就不用修改太多代码,而且也不需要人为的计算出每个学校的 Pos 项和 Neg 项了。就是说这个程序的扩展性还是比较好的。
如果学校变多了,比如从 A 到 G,我们也只需要把题目给出的条件作为参数给到程序中,就可以了。

关于这个程序的执行效率问题,实际上程序就是靠遍历找到可能的排名情况,因此效率并不会太高。

posted @ 2019-07-24 19:14  brifuture  阅读(469)  评论(0编辑  收藏  举报