## [$AcWing$ $343$. 排序](https://www.acwing.com/problem/content/345/) ### 一、题目描述 给定 $n$ 个变量和 $m$ 个不等式。其中 $n$ 小于等于 $26$,变量分别用前 $n$ 的大写英文字母表示。 不等式之间具有传递性,即若 $A>B$ 且 $B>C$,则 $A>C$。 请从前往后遍历每对关系,每次遍历时判断: * 如果能够确定全部关系且无矛盾,则结束循环,输出确定的次序; * 如果发生矛盾,则结束循环,输出有矛盾; * 如果循环结束时没有发生上述两种情况,则输出无定解。 **输入格式** 输入包含多组测试数据。 每组测试数据,第一行包含两个整数 $n$ 和 $m$。 接下来 $m$ 行,每行包含一个不等式,不等式全部为 **小于** 关系。 当输入一行 `0 0` 时,表示输入终止。 **输出格式** 每组数据输出一个占一行的结果。 结果可能为下列三种之一: * 如果可以确定两两之间的关系,则输出 `Sorted sequence determined after t relations: yyy...y.`,其中`t`指 **迭代次数**,`yyy...y`是指 **升序排列** 的所有变量。 * 如果有矛盾,则输出: `Inconsistency found after t relations.`,其中`t`指迭代次数。 * 如果没有矛盾,且不能确定两两之间的关系,则输出 `Sorted sequence cannot be determined.`。 **数据范围** $2≤n≤26$,变量只可能为大写字母 $A$∼$Z$。 **输入样例$1$**: ```cpp {.line-numbers} 4 6 A**解释**:比如$a < b,b < c$,就可以推导出$a < c$,如果用图形表示出这种大小关系,就是$a$到$b$有一条有向边 【小的向大的连一条边】,$b$到$c$有一条有向边,可以推出$a$可以到达$c$,找出图中各点能够到达点的集合,**类似** 于$floyd$算法求图中任意两点间的最短距离 【魔改版的$floyd$】。 **模板** ```cpp {.line-numbers} //传递闭包 void floyd(){ for(int k = 0;k < n;k++) for(int i = 0;i < n;i++) for(int j = 0;j < n;j++) f[i][j] |= f[i][k] & f[k][j]; } // 原始版本 /* for (int k = 0; k < n; k++) for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); */ ``` **回到本题** - 题目描述要求按顺序遍历二元关系,一旦前$i$个二元关系可以确定次序了就不再遍历了,即使第$i + 1$对二元关系就会出现矛盾也不去管它了。 - 题目字母只会在$A$到$Z$间,因此可以映射为$0$到$25$这$26$个元素 - $A < B$,则$f[0][1]=1$。如果$f[0][1] = f[1][0] = 1$,推出$f[0][0] = 1$,此时$A < B$并且$B < A$发生矛盾,即$f[i][i]= 1$时表示发生矛盾。 **算法步骤** 每读取一对二元关系,就执行一遍$floyd$算法求 **传递闭包**,然后执行$check$函数判断: * ① 如果发生矛盾终止遍历 * ② 如果次序全部被确定终止遍历 * ③ 两者都没有,继续遍历 在确定所有的次序后,需要 **输出大小关系**,需要一个$getorder$函数。 >**注意**: 终止遍历仅仅是不再针对新增的二元关系去求传递闭包,循环还是要继续的,需要读完数据才能继续读下一组数据。 下面设计$check$函数和$getorder$函数。 ```cpp {.line-numbers} // 1:可以确定两两之间的关系,2:矛盾,3:不能确定两两之间的关系 int check() { // 如果i **解释**:确定所有元素次序后如何判断元素`i`在第几个位置呢?`f[i][j] = 1`表示`i < j`,因此计算下`i`小于元素的个数`cnt`,就可以判定`i`是第`cnt + 1`大的元素了 #### $Code$ $O(N^3)$ ```cpp {.line-numbers} #include using namespace std; const int N = 26; int n, m; int g[N][N]; bool st[N]; // 求传递闭包 void floyd() { for (int k = 0; k < n; k++) for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) g[i][j] |= g[i][k] && g[k][j]; } int check() { for (int i = 0; i < n; i++) if (g[i][i]) return 2; // 矛盾 for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) if (!g[i][j] && !g[j][i]) // 待继续 return 0; return 1; // 找到顺序 } string getorder() { // 升序输出所有变量 char s[26]; for (int i = 0; i < n; i++) { int cnt = 0; // f[i][j] = 1表示i可以到达j (i< j) for (int j = 0; j < n; j++) cnt += g[i][j]; // 比i大的有多少个 // 举个栗子:i=0,表示字符A // 比如比i大的有5个,共6个字符:ABCDEF // n - cnt - 1 = 6-5-1 = 0,也就是A放在第一个输出的位置上, 之所以再-1,是因为下标从0开始 s[n - cnt - 1] = i + 'A'; } // 转s字符数组为字符串 string res; for (int i = 0; i < n; i++) res = res + s[i]; return res; } int main() { while (cin >> n >> m, n || m) { memset(g, 0, sizeof g); // 邻接矩阵 int type = 0, t; // type: 0=还需要继续给出条件 1=找到了顺序 2=存在冲突 // t:在第几次输入后找到了顺序,不能中间break,因为那样会造成数据无法完成读入,后续的操作无法进行,只能记录下来当时的i for (int i = 1; i <= m; i++) { char s[5]; cin >> s; int a = s[0] - 'A', b = s[2] - 'A'; // A->0,B->1,...,Z->25完成映射关系 if (!type) { // 如果不存在矛盾,就尝试找出大小的顺序 g[a][b] = 1; // 有边 floyd(); // 求传递闭包 type = check(); // 检查是不是存在矛盾,或者找到了完整的顺序 if (type > 0) t = i; // 如果找到了顺序,或者发现了矛盾,记录是第几次输入后发现的 } // 即使存在矛盾,也需要继续读入,直到本轮数据读入完成 } if (!type) puts("Sorted sequence cannot be determined."); else if (type == 2) printf("Inconsistency found after %d relations.\n", t); else { string ans = getorder(); // 输出升序排列的所有变量 printf("Sorted sequence determined after %d relations: %s.\n", t, ans.c_str()); } } return 0; } ``` ### 三、优化版本 $O(N^2)$ 其实,由于每次新增加的一对$(a,b)$,只会更新与$a,b$有边连接的点,其它的无关点是没有影响的,如果加上一对$(a,b)$就去全新计算,无疑是存在浪费的,可以优化的。 怎么优化呢?核心思路就是$(a,b)$做为$floyd$算法的中继点即可,其它点不再被遍历做为中继点。 说人话就是: ① 遍历所有节点,找出所有小于$a$的节点$x$,那么$x$一定小于$b$。 ② 遍历所有节点,找出所有大于$b$的节点$x$,那么$a$一定小于$x$。 ③ 遍历所有节点,如果$x using namespace std; const int N = 26; int n, m; bool g[N][N]; bool st[N]; int check() { for (int i = 0; i < n; i++) if (g[i][i]) return 2; // 矛盾 for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) if (!g[i][j] && !g[j][i]) // 待继续 return 0; return 1; // 找到顺序 } string getorder() { // 升序输出所有变量 char s[26]; for (int i = 0; i < n; i++) { int cnt = 0; // f[i][j] = 1表示i可以到达j (i< j) for (int j = 0; j < n; j++) cnt += g[i][j]; // 比i大的有多少个 // 举个栗子:i=0,表示字符A // 比如比i大的有5个,共6个字符:ABCDEF // n - cnt - 1 = 6-5-1 = 0,也就是A放在第一个输出的位置上, 之所以再-1,是因为下标从0开始 s[n - cnt - 1] = i + 'A'; } // 转s字符数组为字符串 string res; for (int i = 0; i < n; i++) res = res + s[i]; return res; } int main() { while (cin >> n >> m, n || m) { memset(g, 0, sizeof g); int type = 0, t; for (int i = 1; i <= m; i++) { char str[5]; cin >> str; int a = str[0] - 'A', b = str[2] - 'A'; // a