Luogu P1347 排序
题目分析与核心思路
这道题要求我们根据一系列形如 A<B 的偏序关系,判断是否能确定一个唯一的全序关系。这本质上是一个图论问题,更具体地说,是拓扑排序的经典应用。
让我们将问题进行一次抽象:
- 节点:题目中需要排序的
n个大写字母(A, B, C, ...)可以看作是图中的n个节点。 - 有向边:每个
A<B的关系可以看作是从节点A指向节点B的一条有向边。这条边的含义是,在最终的排序序列中,A必须出现在B的前面。
经过这样的转换,问题就变成了:给定一个有向图,我们需要在每增加一条边之后,判断该图的状态。具体来说,有三种可能的状态:
- 存在唯一拓扑排序:如果图的所有
n个节点可以形成一个唯一的线性序列,满足所有边的方向约束,那么排序就确定了。例如,对于n=4,关系A<B,B<C,C<D形成了一个链A -> B -> C -> D,其拓扑排序是唯一的ABCD。 - 存在环:如果图中出现了环,例如
A<B,B<C,C<A,这就构成了一个A -> B -> C -> A的环。这意味着A必须在B前面,B必须在C前面,而C又必须在A前面,这在逻辑上是矛盾的。存在环的图没有拓扑排序。 - 存在多种拓扑排序:如果图是一个无环图(DAG, Directed Acyclic Graph),但无法确定唯一的排序,说明还缺少足够的关系来约束所有节点。例如,对于
n=4,只有关系A<B和C<D,我们知道A在B前,C在D前,但A和C的关系,B和D的关系等都不确定。这时可能会有ACBD,ACDB,CABD,CADB等多种合法的排序。
因此,我们的解题策略是:每读入一个关系,就构建一次图,并尝试进行拓扑排序,然后根据排序的结果判断当前的状态。
算法详解:拓扑排序(Kahn)
拓扑排序最常用的算法之一是Kahn 算法,它基于节点的入度(In-degree)。一个节点的入度指的是有多少条边指向该节点。
Kahn 算法的步骤如下:
- 初始化:
- 计算图中所有节点的入度。
- 创建一个队列,将所有入度为 0 的节点加入队列。这些节点是排序序列的“起点”,因为没有任何节点要求必须排在它们前面。
- 排序过程:
- 当队列不为空时,从队列中取出一个节点(记为
u)。 - 将
u添加到我们的排序结果序列中。 - 遍历
u的所有邻接点(即所有u指向的节点,记为v)。 - 对于每一个
v,将其入度减 1。这相当于在图中“移除”了u以及从u出发的所有边。 - 如果
v的入度在减 1 后变为 0,说明v的所有前置依赖都已被满足(即排在了它的前面),因此将v加入队列。
- 当队列不为空时,从队列中取出一个节点(记为
- 结果判断:
- 循环结束后,检查排序结果序列中的节点数量。如果数量等于图中的总节点数,说明成功找到了一个拓扑排序。否则,说明图中存在环。
那么,如何利用拓扑排序判断三种状态?可以将 Kahn 算法应用到本题的三个输出条件上。我们每读入一个关系(第 x 个关系),就执行一次拓扑排序。
判断矛盾(Inconsistency)
- 条件:图中存在环。
- Kahn 算法体现:在拓扑排序过程中,如果最终生成的排序序列的长度小于当前已经出现过的节点总数,那么图中必定存在环。
- 原因:环中的所有节点,其入度永远不会变为 0。因为环内每个节点至少有一个来自环内其他节点的入边。当所有非环节点都被处理完后,队列会变空,但环内节点仍然存在且入度不为 0,导致它们无法进入排序序列。
- 代码实现:在拓扑排序后,比较排序出的节点数
cnt和图中出现的总节点数len(nodes)。如果cnt < len(nodes),则发现矛盾。
判断顺序确定(Sorted Sequence Determined)
- 条件:存在一个覆盖所有
n个节点的唯一拓扑排序。 - Kahn 算法体现:
- 首先,排序序列的长度必须等于
n,这保证了所有n个元素都被包含在内。 - 其次,如何保证排序是唯一的?一个关键特征是:在拓扑排序的每一步,队列中的节点数都不能超过 1。如果某一步队列中有两个或以上节点(比如
X和Y),意味着X和Y之间没有相互的顺序限制,那么...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.")
总结
本题通过将字母序问题巧妙地转化为图论中的拓扑排序问题,考察了对图的建模能力和对拓扑排序算法的深入理解。解题的关键在于,每增加一个关系,都要重新评估整个图的结构,并利用拓扑排序的性质来判断三种可能的状态:
- 有环(矛盾):排序节点数 < 图中节点数。
- 唯一长链(顺序确定):排序节点数 == n 且 最长路径 == n。
- 无环但非唯一(无法确定):遍历完所有关系后仍不满足以上两点。
通过在循环中不断执行拓扑排序并检查其结果,我们可以在满足任一确定性条件(矛盾或唯一顺序)时立即输出并终止程序,完美地解决了这道问题。

浙公网安备 33010602011771号