题目大意
给你一个 \(n(1\leq n\leq 10^5)\) 个数的排列 \(p\),你需要维护以下两种操作:
-
1 x y
:交换 \(p_x\) 和 \(p_y\)。 -
2 i k
:令 \(i:=p_i\),\(k\)次后输出\(i\) 。
操作数量小于等于 \(10^5\)。
题解
首先老套路对于排列 \(p\),从 \(i\) 向 \(p_i\) 连边,可以形成若干个有向环。然后操作1交换 \(p_x\) 和 \(p_y\) 实际上是改变环上两个指针的指向,可以导致一个环拆成两个环,或者两个环合并成一个环。所以数组模拟链表可以很容易建出这些有向环,并且很容易维护操作1。但是操作2比较难维护,因为链表无法支持随机访问。然后 \(O(n\log n)\) 好像没有什么好做法,于是就可以想 \(O(n\sqrt n)\) 的做法了。设 \(link[u]\) 表示从 \(u\) 点出发,往下跳 \(\sqrt n\) 次所跳到的点。那么相比于直接暴力跳 \(k\) 次,我们只需要跳 \(u:=link[u]\) 最多 \(\sqrt n\) 次,最后剩下的步数一定是小于 \(\sqrt n\) 的,可以暴力跳。于是单次查询操作的时间复杂度是 \(O(\sqrt n)\)。对于修改操作,因为交换两个指针可能导致 \(link[u]\) 发生变化,所以对于要修改指针的结点可以暴力回退 \(\sqrt n\) 的距离来更新 \(link[u]\)。于是本题的时间复杂度为 \(O(n\sqrt n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
template<typename elemType>
inline void Read(elemType& T) {
elemType X = 0, w = 0; char ch = 0;
while (!isdigit(ch)) { w |= ch == '-';ch = getchar(); }
while (isdigit(ch)) X = (X << 3) + (X << 1) + (ch ^ 48), ch = getchar();
T = (w ? -X : X);
}
int nxt[100010][2], link[100010];
int n, m, q;
void update_link(int u) {
int v = u;
for (int i = 1;i <= m;++i) v = nxt[v][0];
for (int i = 1;i <= m;++i) { link[u] = v; u = nxt[u][1]; v = nxt[v][1]; }
}
void update(int u, int v) {
int x = nxt[u][0], y = nxt[v][0];
nxt[x][1] = v; nxt[y][1] = u;
nxt[u][0] = y; nxt[v][0] = x;
update_link(u);
update_link(v);
}
int query(int u, int k) {
int step = 0;
while (step + m <= k) { u = link[u]; step += m; }
while (step < k) { u = nxt[u][0]; ++step; }
return u;
}
int main() {
Read(n);Read(q);
m = sqrt(n);
for (int i = 1;i <= n;++i)
Read(nxt[i][0]);
for (int i = 1;i <= n;++i)
nxt[nxt[i][0]][1] = i;
for (int i = 1;i <= n;++i)
if (!link[i]) update_link(i);
while (q--) {
int opt, x, y;
Read(opt); Read(x); Read(y);
if (opt == 1) update(x, y);
else printf("%d\n", query(x, y));
}
return 0;
}