[环图] aw3775. 数组补全(思维+构造+CF1283C)

Posted Ypuyu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[环图] aw3775. 数组补全(思维+构造+CF1283C)相关的知识,希望对你有一定的参考价值。

1. 题目来源

链接:3775. 数组补全

推荐题解:方法一 的题解,排序填入再调整

一个图,n 个点的出度、入度都是 1,那么该图一定是由若干环组成。有无自环看具体题意。

2. 题目解析

先不谈环图,来个简单做法。

方法一: 降序填入,至多一个冲突,解决即可。

  • 统计未出现的所有数字,也就是代填数字。
  • 将所有数字从大到小排序,准备往空格位置填。
  • 由于我们是按数字的从大到小填,而数组空余位置的下标关系是从小到大的。所以只可能会有一个位置的 f[i]=i,针对这个位置,可以与其它位置做交换就行了。
  • 交换方式是:记录待插入数中第一个数 f ( 1 ) f(1) f(1)和最后一个数 f ( m ) f(m) f(m)的插入位置,记为 a a a b b b,当前冲突位置数 f ( i ) = i f(i)=i f(i)=i,根据排序后的大小关系 a < i < b a<i<b a<i<b f ( 1 ) > f ( i ) > f ( m ) f(1)>f(i)>f(m) f(1)>f(i)>f(m),那么可以让 f ( i ) f(i) f(i) 与两者任意调换一个位置即可。由于 f ( 1 ) 、 f ( m ) f(1)、f(m) f(1)f(m) 均不等于 i i i,则调换后一定会成功。当然, f ( 1 ) 、 f ( m ) f(1)、f(m) f(1)f(m) 自身也可能重复,所以需要记录两个,当边界重复的时候互相调换即可,否则每次只与 f ( 1 ) f(1) f(1) 调换即可。

方法二: 环图解法。

  • 下标从 1~n,数的范围也是从 1~n,对于每一个存在的数,即 f[i]!=0 的话,将下标到数值连一条有向边,i-->f[i],即构成环。故本题转化为一个图论问题!
  • 不存在 f[i]=i,等价于图中不存在自环。
  • 该图性质:每个点出度、入度均为 1。如果满足这两条性质,则该图一定是由若干环构成,不存在分叉,不然出度、入度将大于 1
    • 下标 i,出现且仅出现一次,即每个点的出度为 1。
    • f[i],出现且仅出现一次,即每个点的入度为 1。
    • 这样图一定由若干环构成。
  • 由若干环构成的图将其称为环图!
  • 即,本题中的环总共有两种情况,环首尾相连是完整的,环首尾相连不完整。
    • 对于完整的环来讲,就是满足答案的,不需要处理。
    • 对于不完整的环来讲,我们仅需首尾相连,连一条边,就可以将其构成完整的环。
  • 对于未填入的数字,我们仅需找到一个不完整的环之后,并找到该环的头、尾,然后将数字首尾相连加入环即可。做完之后,对于其它环就不用加入数字了,直接首尾相连即可。
  • 当然,我们可能找到的环均是完整环,那么只需要让这些未填数字构成一个完整环就行了。
  • 由于需要找到一个环的头、尾,存边关系的时候需要记录每个点的父节点是谁,便于反向查找到环头,环尾的话就一直找子节点就行了。

可能方法一更容易理解,但是方法二环图的解法在其余题中应用更广,并且是基环树、仙人掌等困难算法中的一部分,对于本题来讲就是环图的模板题,简单学习性质。


时间复杂度: O ( n ) O(n) O(n)

空间复杂度: O ( n ) O(n) O(n)


方法一: 降序填入即可,至多一个冲突就交换。

#include <bits/stdc++.h>

using namespace std;

const int N = 2e5+5;

int n;
int a[N];
int b[N], cnt;

int main() {
    int T; cin >> T; while (T -- ) {
        memset(b, 0, sizeof b), cnt = 0;
        
        cin >> n;
        unordered_set<int> S;
        for (int i = 1; i <= n; i ++ ) {
            cin >> a[i];
            S.insert(a[i]);
        }
        
        for (int i = n; i; i -- ) {
            if (!S.count(i))
                b[cnt ++ ] = i;
        }
        
        int l, r;
        for (int i = 1, j = 0; i <= n; i ++ ) {
            if (!a[i]) {
                a[i] = b[j];
                if (!j) l = i;
                else if (j == cnt - 1) r = i;
                j ++ ;
            }
        }
        
        for (int i = 1; i <= n; i ++ ) {
            if (a[i] == i) {
                if (i == l) swap(a[i], a[r]);
                else swap(a[i], a[l]);
            }
        }
        
        for (int i = 1; i <= n; i ++ ) cout << a[i] << ' ';
        cout << endl;
    }
    
    return 0;
}

方法二: 环图解法,代码细节比较多。

如果一个点,没在环中出现,等价于该点没有出边和入边,在此两个都需要判断,否则如第三个输入 n=7, 7 4 0 3 0 5 1,这个情况,6 指向 5,但是 5 的出边是 0,那么在考虑 2 的出边时应该是什么时,就不能仅选择出边为 0 的 5,因为 5 已经有入边了,入边为 6,所以在此真正出边、入边均为 0 的才是要加入环中的点,且在别的环中也没出现过。

而在最后的话,均是完整环,在判断局外点,顺序判断,只要出边是 0,那就是局外点,因为最后这些局外点要构成一个环,那么初始时他们的出度、入度都是 0,就不需要在判断出边的同时再判断入边是 0 了。


代码写的还是蛮细节的,思维难度并不大,关键是编程思维,能否将其实现成代码。

#include <bits/stdc++.h>

using namespace std;

const int N = 2e5+5;

int n;
int p[N], q[N];     // 边i-->f[i],反向边f[i]-->i
bool st[N];

int main() {
    int T; cin >> T; while (T -- ) {
        memset(p, 0, sizeof p);
        memset(q, 0, sizeof q);
        memset(st, 0, sizeof st);
        
        cin >> n;
        for (int i = 1; i <= n; i ++ ) {
            cin >> p[i];
            q[p[i]] = i;
        }
        
        bool flag = false;                      // 有无不完整环
        for (int i = 1; i <= n; i ++ ) {
            if (st[i] || !p[i]) continue;       // 如果被处理过,或者无出边(f[i]=0 的情况)
            st[i] = true;
            int x = i, y = i;                   // 环头、尾
            while (p[x] && !st[p[x]]) {         // 如果头的下一个有点,并且未被处理过,则向下一个走,并标记
                x = p[x];
                st[x] = true;
            }
            
            while (q[y] && !st[q[y]]) {         // 如果尾的上一个有点,并且未被处理过,则向上一个走,并标记
                y = q[y];
                st[y] = true;
            }
            
            // 至此,i 这个点所在的环就被拓展完毕了。y 是所在环尾的点,p[x] 是环头指向的下一个
            if (p[x] == y) continue;            // 如果环已经连通,即是完整环
            
            if (!flag) {                        // 该环没有处理过,是第一个不完整的环,则添加所有待添加数即可
                flag = true;
                for (int j = 1; j <= n; j ++ ) 
                    if (!p[j] && !q[j]) {       // 该点没有出边、没有入边,说明就是完全的局外点
                        p[x] = j;               // 这些局外点加入这个完整环即可
                        x = j;
                        st[j] = true;
                    }
            }
            
            p[x] = y;
        }
        
        // 如果均是完整环,那么将局外点自己组成一个环即可
        if (!flag) {
            int x = 0, y = 0;                   // 头、尾,初始化为 0,都还没有
            for (int i = 1; i <= n; i ++ ) {
                if (!p[i]) {                    // 如果该点还没出边,则就是局外点了
                    if (!x && !y) x = y = i;
                    else {
                        p[x] = i;
                        x = i;
                    }
                }
            }
            
            p[x] = y;
        }
        
        // 输出所有点
        for (int i = 1; i <= n; i ++ ) cout << p[i] <<  ' ';
        cout << endl;
        
    }
    
    return 0;
}

以上是关于[环图] aw3775. 数组补全(思维+构造+CF1283C)的主要内容,如果未能解决你的问题,请参考以下文章

[构造] aw3672. 数组重排(构造+推公式+思维)

[拓扑排序] aw3696. 构造有向无环图(拓扑排序+memset使用坑点+aw周赛004_3)

[构造] aw3763. 数字矩阵(构造+模拟+思维)

[思维] aw3779. 相等的和(思维+构造+题意理解+aw周赛009_2)

[思维] aw3577. 选择数字(思维+脑筋急转弯+aw周赛009_1)

[构造] aw3679. 素数矩阵(构造+思维)