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表示ij之间的偏序关系还没有确定下来,就需要继续读取下一对二元关系;如果所有的关系都确定了,就返回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 
// 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> 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

二、优化算法

分析上面的代码可以发现,每读取一对二元关系就去执行一次\(floyd\)算法,时间复杂度是\(O(m*n^3)\),显然冗余度很高,新增了\(a\)\(b\)的大小关系,只需要更改由这条边可传递下去的关系即可,比如之前执行\(floyd\)已经确定\(A < C\),新增了\(B < D\),完全没必要再去求解\(A\)\(C\)的大小关系了。因此,如果新读入的二元关系f[a][b]已经是1了,表示之前的算法已经使用了ab这条边了,就不需要再执行传递闭包算法了,如果f[a][b] = 0,也只需要更新与ab有关点的关系。

如上图所示,加入ab这条边后,我们只需要遍历能够到达a的所有点x以及b能够到达所有的点y,用平方级复杂度就可以完成加一条边后关系的更新。f[a][b] = 1,首先我们需要更新f[x][b]f[a][y]的值为1,表示x可以到达b了,a可以到达y了,最后再更新f[x][y] = 1。注意这里的xy都是泛指,x指的是能够到达a的点,不一定是图中标的与a直接相连的那个点,也可能是图中x的上一点,也是可以到达a的。这样一来每读入一条边最多只要平方级别复杂度就可以完成更新,总的时间复杂度为\(O(m*n^2)\),效率也大幅提升了。这种方法的代码如下:

#include 
using namespace std;
const int N = 27;
int n, m, f[N][N];
//  1:可以确定两两之间的关系,2:矛盾,3:不能确定两两之间的关系
int check() {
    //如果i> 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

其实这种优化算法就和#Floyd#没有关系了。也可以认为是一种基于\(Floyd\)思想的优化版本,什么是\(Floyd\)思想呢?似乎就是完全松弛,不怕费时间吧。