洛谷P9150 邮箱题 题解
题目链接:P9150 邮箱题
题意:
有一张 $n$ 个点和 $m$ 条边构成的有向图。每个点内都有一把另一个点的钥匙,$i$ 号点内有 $k_i$ 号点的钥匙。你能进入一个点当且仅当你有该点的钥匙。保证 $k_i$ 构成排列。
只要进入了一个点,就获得了这个点内有的钥匙。一旦获得钥匙就不会被消耗。
现在你拿到了 $i$ 号点的钥匙并到了 $i$ 号点。你需要对每个 $i$ 求出:
- 有多少点能被你到达。
- 有多少点能被你到达并返回起点 $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;
}
参考文献: