嘘~ 正在从服务器偷取页面 . . .

洛谷P9150 邮箱题 题解


洛谷P9150 邮箱题 题解

题目链接:P9150 邮箱题

题意

有一张 \(n\) 个点和 \(m\) 条边构成的有向图。每个点内都有一把另一个点的钥匙,\(i\) 号点内有 \(k_i\) 号点的钥匙。你能进入一个点当且仅当你有该点的钥匙。保证 \(k_i\) 构成排列。

只要进入了一个点,就获得了这个点内有的钥匙。一旦获得钥匙就不会被消耗。

现在你拿到了 \(i\) 号点的钥匙并到了 \(i\) 号点。你需要对每个 \(i\) 求出:

  1. 有多少点能被你到达。
  2. 有多少点能被你到达并返回起点 \(i\)

请注意:给出的边均是有向边!

输入格式

本题有多组数据。

第一行,一个正整数 \(T\),表示数据的组数。对于每组数据:

第一行,两个整数 \(n,m\),表示图的点数和边数。

第二行,\(n\) 个整数 \(k_1, k_2, \ldots, k_n\),表示 \(i\) 号点内有 \(k_i\) 号点的钥匙。保证 \(k_i\) 构成排列。

接下来 \(m\) 行,每行两个整数 \(x, y\),表示图上的一条从 \(x\) 指向 \(y\) 的有向边。保证不含重边或自环。

输出格式

对于每组数据,输出 \(n\) 行,每行两个整数,第 \(i\) 行的整数分别表示从 \(i\) 号点出发,能到达的点数和能到达并返回起点的点数。

数据范围

保证图中不含重边或自环。

满足 \(n \ge 3\)\(m\ge 0\)\(\sum n\le 1.5\times{10}^6\)\(\sum m\le 3\times{10}^6\)\(1 \le T\le 2\times{10}^4\)\(1 \le x, y \le n\)

由于每个点只有一把钥匙,且钥匙的数量不会超过进入的点的数量 \(+1\) ,所以其实下一个能进入的点是固定的。

因此,如果从 \(i\) 号点出发,只需在已经进入的点的基础上,一次考虑 \(k_i, k_{k_i},\cdots\) 是否可达即可。

考虑当前点 \(j\) 和从 \(i\) 不断跳 \(k_i\)\(j\) 的序列 \(a = \{i,k_i,k_{k_i},\cdots,j\}\) ,探究 \(k_j\) 可达的充要条件:

  • \(k_j \not\in a\) 。这等价于 \(k_j \ne i\) ,因为 \(k\) 形成排列,而除了 \(i\) 都不可能有更多到达的边了。
  • 考虑路径上最大的 \(p\) 的使得 \(a_p\)\(k_j\) 存在边 \(a_p \to k_j\)\(a_p\) 应当存在且 \(j\) 可以回到 \(a_p\)

关于条件二的判断,因为 \(a_i\) 可以到达 \(a_{i+1}\) ,所以只需在每次新进入点 \(j\) 时,考虑其所有的返祖边 \(j \to a_q\)

\(a_q\)\(j\) 的路径上所有点强连通。枚举 \(k_j\) 的入边 \(u \to k_j\) ,若 \(u \in a\)\(u\)\(j\) 强连通,则 \(k_j\) 可达。

用并查集维护强连通分量的合并过程,我们可以得到一个 \(\mathcal{O}(n^2\alpha(n))\) 的暴力。

至此我们还没有很好的利用 \(k\) 形成排列这一性质,因此我们把 \(k\) 上所有的环拎出来单独探究其特点。

首先断环成链,也就是把环再复制一份拼到后面,记为序列 \(c\)

则每个点 \(c_j\) 的前一个 \(c_{j-1}\) 在链上的答案就是它在环上的答案。

因为从后继(指 \(k\) 值)的点出发,不会到达之前的点,所以我们从后往前加入计算每个点的答案

显然如果 \(x\) 可达 \(y\)\(y\) 可达 \(z\) ,则 \(x\) 可达 \(z\) ,也就是具有传递性。

考虑设已知 \(c_{i+1}\)\(c_{L}\) 的答案,如何计算 \(c_i\) 的答案。那么这整个过程相当于不断尝试合并第一条链和第二条链

设第一条链的末尾为 \(c_p\) ,第二条链的开头为 \(c_{p+1}\) ,考虑 \(c_{p+1}\) 的最大 \(u\) 满足 \(i \le u \le p\)

\(c_u\) 存在且和 \(c_p\) 强连通,则可以合并,反之则无法合并。

用两个并查集分别维护这个表示可达性的链和强连通分量,并预处理每个点的那个最大 \(u\)

那么怎么合并呢?如果两条链可以合并,那么第一条链的开头,即当前 \(c_i\) 一定产生了贡献

否则在加入 \(c_i\) 之前这两条链就已经可以合并了;而如果 \(c_i\) 要产生贡献,

它一定将第一条链的末尾 \(c_p\) 所在强连通分量扩大了,这说明 \(c_i\)\(c_p\) 在同一强连通分量。

这样对于每条链,只需维护 \(c\) 上编号最大的有还未统计过的返祖边的节点。

时间复杂度 \(\mathcal{O}(n)\)

代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
void up(int &x,int y) { x < y ? x = y : 0; }
void down(int &x,int y) { x > y ? x = y : 0; }
#define N ((int)(3e6 + 15))

char vis[N];
int n,m,c,k[N],a1[N],a2[N],cyc[N],in[N],val[N],pre[N];
vector<int> e[N];
struct dsu
{
    int fa[N];
    int find(int x){ return fa[x] == x ? x : fa[x] = find(fa[x]); }
    void merge(int u,int v) { fa[find(v)] = find(u); }
}c1, c2;
void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) e[i].clear();
    memset(vis, 0, sizeof(char) * (n + 5));
    for(int i = 1; i <= n; i++) cin >> k[i];
    for(int i = 1, u,v; i <= m; i++) {
        cin >> u >> v; e[v].push_back(u);
    }
    for(int i = 1; i <= n; i++)
    {
        if(vis[i]) continue;
        int p = i;
        while(!in[p]) { cyc[++c] = p; in[p] = c; p = k[p]; }
        for(int j = c * 2; j; j--)
        {
            c1.fa[j] = c2.fa[j] = j;
            val[j] = pre[j] = 0;
            int id = cyc[j > c ? j - c : j];
            for(int t : e[id]) {
                if(!in[t]) continue; else t = in[t];
                if(t + c < j) t += c; if(t > j) t -= c;
                up(pre[j], t); 
                if(t < j) t += c; if(t <= c * 2) up(val[c2.find(t)], c1.find(t));
            }
            while(true)
            {
                while(true)
                {
                    int x = c1.find(j), y = c2.find(j);
                    if(x < val[y]) c1.merge(x + 1, x); else break;
                }
                int x = c1.find(j), y = c2.find(j);
                val[y] = 0;
                if(y == c * 2 || x != y || pre[y + 1] < j) break;
                c2.merge(y + 1, y);
            }
            a1[id] = min(c, c2.find(j) - j + 1);
            a2[id] = min(c, c1.find(j) - j + 1);
        }
        for(int j = 1; j <= c; j++){ in[cyc[j]] = 0; vis[cyc[j]] = 1; }
        c = 0;
    }
    for(int i = 1; i <= n; i++) cout << a1[i] << ' ' << a2[i] << '\n';
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    // freopen("check.in","r",stdin);
    // freopen("check.out","w",stdout);
    int qwq; cin >> qwq; while(qwq--) solve();
    return 0;
}

参考文献

[1] https://www.luogu.com.cn/blog/AlexWei/solution-p9150


文章作者: q779
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 q779 !
评论
  目录