Luogu P1347 排序

题目分析与核心思路

这道题要求我们根据一系列形如 A<B 的偏序关系,判断是否能确定一个唯一的全序关系。这本质上是一个图论问题,更具体地说,是拓扑排序的经典应用。

让我们将问题进行一次抽象:

  1. 节点:题目中需要排序的 n 个大写字母(A, B, C, ...)可以看作是图中的 n 个节点。
  2. 有向边:每个 A<B 的关系可以看作是从节点 A 指向节点 B 的一条有向边。这条边的含义是,在最终的排序序列中,A 必须出现在 B 的前面。

经过这样的转换,问题就变成了:给定一个有向图,我们需要在每增加一条边之后,判断该图的状态。具体来说,有三种可能的状态:

  1. 存在唯一拓扑排序:如果图的所有 n 个节点可以形成一个唯一的线性序列,满足所有边的方向约束,那么排序就确定了。例如,对于 n=4,关系 A<B, B<C, C<D 形成了一个链 A -> B -> C -> D,其拓扑排序是唯一的 ABCD
  2. 存在环:如果图中出现了环,例如 A<B, B<C, C<A,这就构成了一个 A -> B -> C -> A 的环。这意味着 A 必须在 B 前面,B 必须在 C 前面,而 C 又必须在 A 前面,这在逻辑上是矛盾的。存在环的图没有拓扑排序。
  3. 存在多种拓扑排序:如果图是一个无环图(DAG, Directed Acyclic Graph),但无法确定唯一的排序,说明还缺少足够的关系来约束所有节点。例如,对于 n=4,只有关系 A<BC<D,我们知道 AB 前,CD 前,但 AC 的关系,BD 的关系等都不确定。这时可能会有 ACBD, ACDB, CABD, CADB 等多种合法的排序。

因此,我们的解题策略是:每读入一个关系,就构建一次图,并尝试进行拓扑排序,然后根据排序的结果判断当前的状态

算法详解:拓扑排序(Kahn)

拓扑排序最常用的算法之一是Kahn 算法,它基于节点的入度(In-degree)。一个节点的入度指的是有多少条边指向该节点。

Kahn 算法的步骤如下:

  1. 初始化
    • 计算图中所有节点的入度。
    • 创建一个队列,将所有入度为 0 的节点加入队列。这些节点是排序序列的“起点”,因为没有任何节点要求必须排在它们前面。
  2. 排序过程
    • 当队列不为空时,从队列中取出一个节点(记为 u)。
    • u 添加到我们的排序结果序列中。
    • 遍历 u 的所有邻接点(即所有 u 指向的节点,记为 v)。
    • 对于每一个 v,将其入度减 1。这相当于在图中“移除”了 u 以及从 u 出发的所有边。
    • 如果 v 的入度在减 1 后变为 0,说明 v 的所有前置依赖都已被满足(即排在了它的前面),因此将 v 加入队列。
  3. 结果判断
    • 循环结束后,检查排序结果序列中的节点数量。如果数量等于图中的总节点数,说明成功找到了一个拓扑排序。否则,说明图中存在环。

那么,如何利用拓扑排序判断三种状态?可以将 Kahn 算法应用到本题的三个输出条件上。我们每读入一个关系(第 x 个关系),就执行一次拓扑排序。

判断矛盾(Inconsistency)

  • 条件:图中存在环。
  • Kahn 算法体现:在拓扑排序过程中,如果最终生成的排序序列的长度小于当前已经出现过的节点总数,那么图中必定存在环。
  • 原因:环中的所有节点,其入度永远不会变为 0。因为环内每个节点至少有一个来自环内其他节点的入边。当所有非环节点都被处理完后,队列会变空,但环内节点仍然存在且入度不为 0,导致它们无法进入排序序列。
  • 代码实现:在拓扑排序后,比较排序出的节点数 cnt 和图中出现的总节点数 len(nodes)。如果 cnt < len(nodes),则发现矛盾。

判断顺序确定(Sorted Sequence Determined)

  • 条件:存在一个覆盖所有 n 个节点的唯一拓扑排序。
  • Kahn 算法体现
    • 首先,排序序列的长度必须等于 n,这保证了所有 n 个元素都被包含在内。
    • 其次,如何保证排序是唯一的?一个关键特征是:在拓扑排序的每一步,队列中的节点数都不能超过 1。如果某一步队列中有两个或以上节点(比如 XY),意味着 XY 之间没有相互的顺序限制,那么 ...XY......YX... 都是合法的,排序便不唯一。
    • 一个更巧妙的判断方法(参考代码中使用):如果一个包含 n 个节点的有向无环图,其最长路径的长度恰好为 n,那么这个图必然是一条简单的链(如 A -> B -> C -> ...),其拓扑排序是唯一的。我们可以通过在拓扑排序的同时计算最长路径长度来判断。
  • 代码实现
    • 在拓扑排序后,检查排序出的节点数 cnt 是否等于 n
    • 同时,在排序过程中记录每个节点所在的“层级”或路径长度。初始入度为0的节点在第1层。当从节点 u(在 path_len 层)更新到邻居 v 时,v 的层级就是 path_len + 1
    • 排序结束后,找到所有节点层级的最大值 length。如果 length == n,则说明形成了一条包含所有 n 个节点的长链,顺序唯一。

判断无法确定(Sorted sequence cannot be determined)

  • 条件:遍历完所有 m 个关系后,既没有发现矛盾,也没有确定唯一顺序。
  • 体现:循环 m 次,每次都进行拓扑排序和判断。如果循环结束程序仍未退出,就说明属于这种情况。这通常意味着图是无环的,但没有足够的边来约束所有节点形成唯一序列(例如,图不连通,或者存在多个入度为0的节点等情况)。

代码逐行讲解

from collections import deque

# 1. 初始化
n, m = map(int, input().split())
# edge: 邻接表,edge[i] 存储节点 i 指向的所有节点
# ord('A') 是 65, 字母 A-Z 对应 0-25
edge = [[] for _ in range(30)] 
# in_degree: 存储每个节点的入度
in_degree = [0] * 30
# nodes: 一个集合,用来存储所有在关系中出现过的节点。
# 使用集合可以自动去重,并且方便地获取当前涉及的节点总数。
nodes = set()

# 2. 主循环:逐个处理 m 个关系
for _ in range(1, m + 1):
    # 读入当前关系,例如 "A<B"
    cur_relation = input()
    u = ord(cur_relation[0]) - ord('A') # 起点,例如 A -> 0
    v = ord(cur_relation[2]) - ord('A') # 终点,例如 B -> 1

    # 3. 构建图(更新边和入度)
    # 避免重复添加边,虽然在本题逻辑中不影响正确性,但这是好习惯
    if v not in edge[u]:
        edge[u].append(v)
        in_degree[v] += 1
    
    # 将出现过的节点加入集合
    nodes.add(u)
    nodes.add(v)

    # 4. 准备进行拓扑排序
    q = deque()
    # 关键:每次拓扑排序都在一个临时副本上进行,以防影响下一次循环
    # cur_in_degree 存储本次拓扑排序中各节点的动态入度
    cur_in_degree = list(in_degree) 
    
    # 初始化队列:找到所有当前图中入度为0的节点
    # 注意:只在 `nodes` 集合中出现的节点里寻找,未出现的节点不参与排序
    for i in range(n): # 题目规定了元素范围是前n个字母
        if i in nodes and cur_in_degree[i] == 0:
            # 队列中存储一个元组 (节点, 路径长度)
            # 初始节点的路径长度/层级为 1
            q.append((i, 1))
    
    # 5. 执行拓扑排序 (Kahn's Algorithm)
    # sorted_seq: 存储拓扑排序的结果
    # cnt: 记录排序序列中的节点数量
    # max_path_len: 记录最长路径的长度
    sorted_seq, cnt, max_path_len = [], 0, 0
    
    # temp_q_for_uniqueness_check: 用于检查唯一性
    # 如果在某一步,队列 q 的大小大于1,说明排序不唯一
    # 这里我们用另一种方法(最长路径)判断,但检查队列大小是更通用的方法
    
    while q:
        # 如果在某一步队列中有多个元素,说明有多个起点,排序不唯一
        # if len(q) > 1: is_unique = False (这是另一种判断唯一性的方法)
        
        curr_u, path_len = q.popleft()
        cnt += 1
        max_path_len = max(max_path_len, path_len)
        sorted_seq.append(chr(curr_u + ord('A')))
        
        # 遍历当前节点的邻居,更新它们的入度
        for neighbor_v in edge[curr_u]:
            cur_in_degree[neighbor_v] -= 1
            if cur_in_degree[neighbor_v] == 0:
                # 新的入度为0的节点入队,路径长度加1
                q.append((neighbor_v, path_len + 1))

    # 6. 根据拓扑排序结果进行判断
    
    # 判断条件1:发现矛盾(存在环)
    # 如果排序出的节点数小于图中实际出现的节点数,说明有环
    if cnt < len(nodes):
        print(f"Inconsistency found after {_} relations.")
        exit(0) # 找到答案,直接退出程序

    # 判断条件2:确定唯一顺序
    # 排序出的节点数必须等于 n,且最长路径也等于 n
    # 这确保了所有 n 个节点都被包含,并且形成了一个单链
    if cnt == n and max_path_len == n:
        print(f"Sorted sequence determined after {_} relations: {''.join(sorted_seq)}.")
        exit(0) # 找到答案,直接退出程序

# 7. 循环结束仍未确定
# 如果 m 个关系全部处理完,程序还没退出,说明无法确定顺序
print("Sorted sequence cannot be determined.")

总结

本题通过将字母序问题巧妙地转化为图论中的拓扑排序问题,考察了对图的建模能力和对拓扑排序算法的深入理解。解题的关键在于,每增加一个关系,都要重新评估整个图的结构,并利用拓扑排序的性质来判断三种可能的状态:

  1. 有环(矛盾):排序节点数 < 图中节点数。
  2. 唯一长链(顺序确定):排序节点数 == n 且 最长路径 == n。
  3. 无环但非唯一(无法确定):遍历完所有关系后仍不满足以上两点。

通过在循环中不断执行拓扑排序并检查其结果,我们可以在满足任一确定性条件(矛盾或唯一顺序)时立即输出并终止程序,完美地解决了这道问题。

posted @ 2025-08-09 16:58  AFewMoon  阅读(12)  评论(0)    收藏  举报