• 预测学校排名问题


    题目描述

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

    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,我们也只需要把题目给出的条件作为参数给到程序中,就可以了。

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

  • 相关阅读:
    luogu P3704 [SDOI2017]数字表格
    「雅礼集训 2018 Day4」Magic(分治NTT)
    「清华集训 2017」小 Y 和恐怖的奴隶主
    [WC2019]数树(树形dp+多项式exp)
    「FJWC2020Day5-zzq」lg (容斥)
    BoundedOptimization TopCoder
    MapGuessing TopCoder
    线性递推(Berlekamp-Massey 算法)
    杜教筛小记
    「余姚中学 2019 联测 Day 6」解码
  • 原文地址:https://www.cnblogs.com/brifuture/p/11240258.html
Copyright © 2020-2023  润新知