AcWing 343. 排序
Posted 蒟蒻豆进阶之路
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AcWing 343. 排序相关的知识,希望对你有一定的参考价值。
一、传递闭包
本题考察\\(Floyd\\)算法在传递闭包问题上的应用。给定若干对元素和若干对二元关系,并且关系具有传递性,通过传递性推导出尽量多的元素之间的关系的问题被称为传递闭包。比如\\(a < b,b < c\\),就可以推导出\\(a < c\\),如果用图形表示出这种大小关系,就是\\(a\\)到\\(b\\)有一条有向边,\\(b\\)到\\(c\\)有一条有向边,可以推出\\(a\\)可以到达\\(c\\),找出图中各点能够到达点的集合,就类似于\\(Floyd\\)算法求图中任意两点间的最短距离。\\(Floyd\\)求解传递闭包问题的代码如下:
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];
只是对原来算法在状态转移方程上略加修改 就能够求解传递闭包问题了。【套路,满满的套路,你不学习就不会的套路】
f[i][j] = 1
表示i
可以到达j
(i < j
),f[i][j] = 0
表示i
不可到达j
。只要i
能够到达k
并且k
能够到达j
,那么i
就能够到达j
,这就是上面代码的含义。
对于本题而言,给定\\(n\\)个元素和一堆二元关系,依次读取每个二元关系,在读取第\\(i\\)个二元关系后,如果可以确定\\(n\\)个元素两两间的大小关系了,就输出在几对二元关系后可以确定次序,并且次序是什么;如果出现了矛盾,就是\\(A < B\\)并且\\(B < A\\)这种情况发生了就输出多少对二元关系后开始出现矛盾;如果遍历完所有的二元关系还不能确定所有元素间的大小关系,就输出无法确定。
可以发现,题目描述要求按顺序遍历二元关系,一旦前\\(i\\)个二元关系可以确定次序了就不再遍历了,即使第\\(i + 1\\)对二元关系就会出现矛盾也不去管它了。对于二元关系的处理和之前的做法一样,\\(A < B\\),就将\\(f[0][1]\\)设为\\(1\\),题目字母只会在\\(A\\)到\\(Z\\)间,因此可以映射为\\(0\\)到\\(25\\)这\\(26\\)个元素,如果\\(f[0][1] = f[1][0] = 1\\),就可以推出f[0][0] = 1
,此时\\(A < B\\)并且\\(A > B\\)发生矛盾,因此在f[i][i]= 1
时发生矛盾。
下面详细分析下求解的步骤:首先每读取一对二元关系,就执行一遍\\(Floyd\\)算法求传递闭包,然后执行check
函数判断下此时是否可以终止遍历,如果发生矛盾或者次序全部被确定就终止遍历,否则继续遍历。在确定所有的次序后,需要输出偏序关系,因此需要执行下getorder
函数。注意这里的终止遍历仅仅是不再针对新增的二元关系去求传递闭包,循环还是要继续的,需要读完数据才能继续读下一组数据。
下面设计\\(check\\)函数和\\(getorder\\)函数。
int check()
for(int i = 0;i < n;i++)
if(f[i][i]) return 0;
for(int i = 0;i < n;i++)
for(int j = 0;j < i;j++)
if(!f[i][j] && !f[j][i]) return 1;
return 2;
如果f[i][i] = 1
就发生矛盾了,可以返回了;如果f[i][j] = f[j][i] = 0
表示i
与j
之间的偏序关系还没有确定下来,就需要继续读取下一对二元关系;如果所有的关系都确定了,就返回2。
string getorder()
char s[26];
for(int i = 0;i < n;i++)
int cnt = 0;
for(int j = 0;j < n;j++) cnt += f[i][j];
s[n - cnt - 1] = i + \'A\';
return string(s,s + n);
确定所有元素次序后如何判断元素i
在第几个位置呢?f[i][j] = 1
表示i < j
,因此计算下i
小于元素的个数cnt
,就可以判定i
是第cnt + 1
大的元素了。
总的代码如下:
#include <bits/stdc++.h>
// Floyd解决传送闭包问题
using namespace std;
const int N = 27;
int n; // n个变量
int m; // m个不等式
int g[N][N]; //原始关系
int f[N][N]; //推导的关系
void floyd()
//复制出来f
memcpy(f, g, sizeof g);
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
// i可以到达k,k可以到达j,那么i可以到达j
//为了防止枚举其它k\'时,导致被覆盖成0,所以写成“或”的形式
f[i][j] |= f[i][k] & f[k][j];
// 1:可以确定两两之间的关系,2:矛盾,3:不能确定两两之间的关系
int check()
//如果i<i,那么就是出现了矛盾
for (int i = 0; i < n; i++)
if (f[i][i]) return 2;
//存在还没有识别出关系的两个点i,j,还要继续读入
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!f[i][j] && !f[j][i]) return 3;
return 1;
//升序输出所有变量
string getorder()
char s[26];
for (int i = 0; i < n; i++)
//这个思路很牛X!
int cnt = 0;
// f[i][j] = 1表示i可以到达j (i< j)
for (int j = 0; j < n; j++) cnt += f[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字符数组为字符串
return string(s, s + n);
int main()
// n个变量,m个不等式
// 当输入一行 0 0 时,表示输入终止
while (cin >> n >> m, n || m)
string str;
int type = 3; // 3:不能确定两两之间的关系
//初始化原始关系,准备读入数据
memset(g, 0, sizeof g);
// m条边,下面需要输出在第几个输入后有问题,所以需要用for循环
for (int i = 1; i <= m; i++)
cin >> str;
//如果不是待确定,就表示是已确定或者出现了矛盾,就没有必要再处理了
//但是,还需要耐心的读取完毕,因为可能还有下一轮,不读入完耽误下一轮
if (type != 3) continue;
//变量只可能为大写字母A~Z,映射到0~25
int a = str[0] - \'A\', b = str[2] - \'A\';
g[a][b] = 1; //记录a<b
//跑一遍最短路,传递闭包
floyd();
//检查一下现在的情况,是不是已经可以判定了
type = check();
//出现的矛盾
if (type == 2)
printf("Inconsistency found after %d relations.\\n", i);
else if (type == 1) //可以确定了
//输出升序排列的所有变量
string ans = getorder();
printf("Sorted sequence determined after %d relations: %s.\\n", i, ans.c_str());
//所有表达式都输入了,仍然定不下来关系
if (type == 3) printf("Sorted sequence cannot be determined.\\n");
return 0;
二、优化算法
分析上面的代码可以发现,每读取一对二元关系就去执行一次\\(floyd\\)算法,时间复杂度是\\(O(m*n^3)\\),显然冗余度很高,新增了\\(a\\)与\\(b\\)的大小关系,只需要更改由这条边可传递下去的关系即可,比如之前执行\\(floyd\\)已经确定\\(A < C\\),新增了\\(B < D\\),完全没必要再去求解\\(A\\)与\\(C\\)的大小关系了。因此,如果新读入的二元关系f[a][b]
已经是1了,表示之前的算法已经使用了a
与b
这条边了,就不需要再执行传递闭包算法了,如果f[a][b] = 0
,也只需要更新与a
、b
有关点的关系。
如上图所示,加入a
与b
这条边后,我们只需要遍历能够到达a
的所有点x
以及b
能够到达所有的点y
,用平方级复杂度就可以完成加一条边后关系的更新。f[a][b] = 1
,首先我们需要更新f[x][b]
与f[a][y]
的值为1
,表示x
可以到达b
了,a
可以到达y
了,最后再更新f[x][y] = 1
。注意这里的x
与y
都是泛指,x
指的是能够到达a
的点,不一定是图中标的与a
直接相连的那个点,也可能是图中x
的上一点,也是可以到达a
的。这样一来每读入一条边最多只要平方级别复杂度就可以完成更新,总的时间复杂度为\\(O(m*n^2)\\),效率也大幅提升了。这种方法的代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 27;
int n, m, f[N][N];
// 1:可以确定两两之间的关系,2:矛盾,3:不能确定两两之间的关系
int check()
//如果i<i,那么就是出现了矛盾
for (int i = 0; i < n; i++)
if (f[i][i]) return 2;
//存在还没有识别出关系的两个点i,j,还要继续读入
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!f[i][j] && !f[j][i]) return 3;
return 1;
//升序输出所有变量
string getorder()
char s[26];
for (int i = 0; i < n; i++)
//这个思路很牛X!
int cnt = 0;
// f[i][j] = 1表示i可以到达j (i< j)
for (int j = 0; j < n; j++) cnt += f[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字符数组为字符串
return string(s, s + n);
int main()
// n个变量,m个不等式
// 当输入一行 0 0 时,表示输入终止
while (cin >> n >> m, n || m)
string str;
int type = 3;
memset(f, 0, sizeof f);
for (int i = 1; i <= m; i++)
cin >> str;
if (type != 3) continue;
int a = str[0] - \'A\', b = str[2] - \'A\';
if (!f[a][b]) //如果a和b还没有确定关系
f[a][b] = 1; //记录a<b
for (int x = 0; x < n; x++) //枚举所有节点
if (f[x][a]) f[x][b] = 1; //如果x<a,那么x一定小于b
if (f[b][x]) f[a][x] = 1; //如果b<x,那么a一定小于x
//外层循环找出所有小于a的点x,内层循环找出所有大于b的点y
//记录x,y的关系为x<y
for (int y = 0; y < n; y++)
if (f[x][a] && f[b][y]) f[x][y] = 1;
//检查一下现在的情况,是不是已经可以判定了
type = check();
//出现的矛盾
if (type == 2)
printf("Inconsistency found after %d relations.\\n", i);
else if (type == 1) //可以确定了
//输出升序排列的所有变量
string ans = getorder();
printf("Sorted sequence determined after %d relations: %s.\\n", i, ans.c_str());
//所有表达式都输入了,仍然定不下来关系
if (type == 3) printf("Sorted sequence cannot be determined.\\n");
return 0;
以上是关于AcWing 343. 排序的主要内容,如果未能解决你的问题,请参考以下文章
2022下半年 Acwing 第四篇:AcWing 787. 归并排序
2022下半年 Acwing 第四篇:AcWing 787. 归并排序