逐条加边维护位图传递闭包:自环位既用于 O(1) 判矛盾,又让每个点的可达点数成为排名,n 个可达数互不相同即全序唯一。
OJ: roj
题目 ID: 3182
难度:普及+/提高-
标签:图论传递闭包Floyd拓扑排序位运算python
创建: 2026-10-01 23:58
更新: 2026-10-10 13:00
形式化题目
给定 x<y。
把变量看作点、把 x<y 看作有向边
按输入顺序逐条加入关系,每加一条后立即检查:
- 若
个变量两两之间的大小都已确定(即 是全序),停止,输出 Sorted sequence determined after t relations: yyy...y.,其中是已处理的关系条数, yyy...y是由小到大的变量序列; - 若出现矛盾(能推出某个
),停止,输出 Inconsistency found after t relations.; - 若
条关系全部处理完仍不满足前两条,输出 Sorted sequence cannot be determined.。
矛盾优先于定序;两种情况一旦触发,后面的关系不再影响答案。多组数据以 0 0 结束。
样例 4 6(DCAB,
即输出 Sorted sequence determined after 6 relations: DCAB.。
正解
思路
朴素做法
最直接的想法是:每读入一条关系就存进图里,然后从每个点各跑一次 BFS/DFS(或跑一次三重循环 Floyd)求出全部可达性,再检查"是否有环"和"是否所有点对都可比"。
这正是为什么需要更省事的实现:下面用一点小技巧把"补闭包"和"两个判定"都压到一行位运算里。
观察一:加边只让关系变多,闭包可以增量维护
传递闭包是单调的:加入
观察二:矛盾 自环
有向图有环
观察三:无环时,可达点数就是排名
无环时
-
全序
互不相同:把点按从小到大排成 , 则 ,取遍 。 -
互不相同 全序: 个互不相同的取值只能是 ,于是 。另一方面, 把每个点对 ( ) 数了一次,其中 个是 ,其余是严格可比的点对,而严格可比的点对最多 个,所以 上式取等当且仅当每一对点都可比,即
是全序。
所以"是否全序"等价于"
观察四:一行一个整数位图,Floyd 内层化成一次位或
把 bits[i],第
if R[i][k]: R[i][j] |= R[k][j] (对所有 j)正好等于 if bits[i] >> k & 1: bits[i] |= bits[k]:bits[i].bit_count()——观察三的判定也顺手完成了。
算法
- 每个点
初始 bits[i] = 1 << i(自环位,既表示"可达自己"又充当已访问标记)。 - 逐条读入关系
: - 若
bits[hi] >> lo & 1为真,说明此前已推出,再加上 即成环 输出 Inconsistency,结束本组; - 否则
bits[lo] |= 1 << hi,再跑一遍 Floyd 式闭包补全传递关系; - 若此时
个 bits[i].bit_count()互不相同,说明已是全序,按可达数降序输出序列,结束本组。
- 若
条边都处理完仍没触发前两种,输出 cannot be determined。
下面这张图是"传递"这一层的示例:输入
digraph G {
rankdir=LR;
node [shape=circle, fontsize=14];
A -> B;
B -> C;
C -> D;
D -> E;
A -> C [style=dashed];
A -> D [style=dashed];
A -> E [style=dashed];
B -> D [style=dashed];
B -> E [style=dashed];
C -> E [style=dashed];
}
正因为虚线边也被算进
样例走查
对样例 4 6,逐条加边后各点的
| 第几条 | 加入的关系 | 判定 | ||||
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 2 | 有并列,继续 | |
| 2 | 1 | 1 | 1 | 3 | 有并列,继续 | |
| 3 | 1 | 1 | 1 | 4 | 有并列,继续 | |
| 4 | 1 | 1 | 2 | 4 | 有并列,继续 | |
| 5 | 1 | 1 | 3 | 4 | 有并列,继续 | |
| 6 | 2 | 1 | 3 | 4 | 互不相同 → DCAB |
前 DCAB——
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 13:00
* update_at: 2026-10-10 13:00
*/
// 排序:位图传递闭包,逐条加入关系并检查是否成环 / 是否已全序
#include <cstdio>
using namespace std;
typedef long long ll;
const int A_CODE = 65; // 变量编号 = 字母 - A_CODE
const int MAXN = 32;
unsigned int bits[MAXN]; // 第 i 个 bit 为 1 表示已知 i < j(含自环位)
// 把邻接位图原地扩成传递闭包;逐个把点 k 当一次中转站
void closure(int n) {
for (int k = 0; k < n; k++) {
unsigned int bit = 1u << k;
for (int i = 0; i < n; i++) {
if (bits[i] & bit) {
bits[i] |= bits[k];
}
}
}
}
int popcount32(unsigned int x) {
int c = 0;
while (x) {
x &= x - 1;
c++;
}
return c;
}
// 位图构成全序时输出序号表(按可达点数从大到小),否则返回 0
int ordered(int n, char* out) {
int pc[MAXN];
for (int i = 0; i < n; i++) {
pc[i] = popcount32(bits[i]);
}
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (pc[i] == pc[j]) {
return 0; // 有并列说明还存在不可比的两点
}
}
}
// n 个互不相同的可达点数 ⇒ 排序唯一,按可达点数降序输出
for (int t = 0; t < n; t++) {
int pick = -1;
for (int i = 0; i < n; i++) {
if (pc[i] >= 0 && (pick < 0 || pc[i] > pc[pick])) {
pick = i;
}
}
out[t] = (char)(A_CODE + pick);
pc[pick] = -1;
}
out[n] = 0;
return 1;
}
int main() {
int printed = 0;
while (true) {
int n;
if (scanf("%d", &n) != 1) {
break;
}
if (n == 0) {
break;
}
int m;
scanf("%d", &m);
char rel_lo[512], rel_hi[512];
for (int t = 0; t < m; t++) {
char tok[8];
scanf("%7s", tok); // 兼容 "A<B" 与拆成 "A" "<" "B" 的写法
if (tok[1] == 0) {
char mid[8], rhs[8];
scanf("%7s %7s", mid, rhs);
rel_lo[t] = tok[0];
rel_hi[t] = rhs[0];
} else {
rel_lo[t] = tok[0];
rel_hi[t] = tok[2];
}
}
for (int i = 0; i < n; i++) {
bits[i] = 1u << i; // 先只保留自环
}
bool finished = false;
for (int step = 1; step <= m; step++) {
int lo = rel_lo[step - 1] - A_CODE;
int hi = rel_hi[step - 1] - A_CODE;
if ((bits[hi] >> lo) & 1) { // 已知 hi < lo,再加 lo < hi 就成环
printf("Inconsistency found after %d relations.\n", step);
printed++;
finished = true;
break;
}
bits[lo] |= 1u << hi;
closure(n);
char seq[MAXN];
if (ordered(n, seq)) {
printf("Sorted sequence determined after %d relations: %s.\n", step, seq);
printed++;
finished = true;
break;
}
}
if (!finished) {
printf("Sorted sequence cannot be determined.\n");
printed++;
}
}
if (printed == 0) {
printf("\n");
}
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 23:58
# update_at: 2026-10-01 23:58
import sys
A_CODE = 65 # 'A' 的 ASCII 码,变量编号 = 字母 - A_CODE
def closure(bits: list[int], n: int) -> list[int]:
"""把邻接位图原地扩成传递闭包:bits[i] 第 j 位为 1 表示已知 i < j(含 i = j)。
逐个把点 k 当一次中转站:凡是能到 k 的点,把 k 的出边整条并进来。
自环位一开始就置上,所以补完闭包后"是否成环"与"每个点能到达几个点"都能直接读出来。
"""
for k in range(n):
bit = 1 << k
for i in range(n):
if bits[i] & bit:
bits[i] |= bits[k]
return bits
def ordered(bits: list[int], n: int) -> str | None:
"""位图构成全序时返回输出串(由小到大),否则返回 None。
无环时"i 能到达几个点"就是 i 的排名:全序时这 n 个数只能是 1..n;
反之只要有并列,就说明还存在不可比的两点,尚未定序。
"""
if len({b.bit_count() for b in bits}) != n:
return None
return ''.join(chr(A_CODE + i) for i in sorted(range(n), key=lambda i: -bits[i].bit_count()))
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
out = []
while (n := int(next(data))): # 读到 "0 0" 就结束
m = int(next(data))
relations: list[bytes] = []
while len(relations) < m: # 兼容 "A<B" 与拆成 "A" "<" "B" 的写法
token = next(data)
if len(token) == 1:
next(data) # 跳过单独出现的 "<"
token += b'<' + next(data)
relations.append(token)
bits = [1 << i for i in range(n)] # 位图关系,先只保留自环
for step, relation in enumerate(relations, 1):
lo, hi = relation[0] - A_CODE, relation[2] - A_CODE
if bits[hi] >> lo & 1: # 已知 hi < lo,再加 lo < hi 就成环
out.append(f"Inconsistency found after {step} relations.")
break
bits[lo] |= 1 << hi # 不等式统一成"小字母 → 大字母"的有向边
closure(bits, n)
if (seq := ordered(bits, n)): # 两两关系都定下来了
out.append(f"Sorted sequence determined after {step} relations: {seq}.")
break
else:
out.append("Sorted sequence cannot be determined.")
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:每条关系一次判环
、一次补闭包 (外层枚举 个中转点、内层 行位或)、 一次全序判定 ,共 。 时单条关系约 次位运算, 位运算把常数压得很低;朴素"每条边后重跑一次 Floyd"则需要 。 - 空间复杂度:
bits为个 位整数,即 位;读入的 token 为 。
实测:200 组 p1.in 约
总结
- 把"逐条加边、逐次判定"的形式化成有向图的增量传递闭包:加边单调,所以只需在旧闭包上补一次。
- 两个判定都从同一张位图读出:对角线有自环即矛盾;无环时
个可达点数互不相同即全序, 且可达数从大到小就是升序序列。 - 实现要点:每行一个整数位图,Floyd 的内层
循环退化为一次位或, bit_count()免费给出排名。 - 易错点:矛盾优先级高于定序;
A<A、重边、以及"序已定死但后面还有反向边"都要按题面提前结束。
图示解析
每加入一条关系后的判定顺序固定为三步,下图是代码主循环与它们的对应关系。
flowchart TD
start["读入一条关系 lo<hi"] --> cyc{"bits[hi] 第 lo 位为 1?"}
cyc -->|"是"| incons["Inconsistency found after t relations."]
cyc -->|"否"| add["bits[lo] 置第 hi 位,并补闭包"]
add --> full{"n 个 bit_count 互不相同?"}
full -->|"是"| sorted["Sorted sequence determined after t relations: yyy...y."]
full -->|"否"| more{"还有下一条关系?"}
more -->|"有"| start
more -->|"没有"| cant["Sorted sequence cannot be determined."]
流程图的左侧分支(成环)优先级最高,一触发就结束;中间是唯一的"计算"步骤——一次位或加一遍闭包;
右侧则说明"全序"是恰好