Link Cut Tree
Link Cut Tree
LCT(Link-Cut-Tree)常用于解决动态树问题,可以做到均摊时间复杂度 \(O(n \log n)\) ,自带大常数。
动态树问题:维护一个森林,支持加入、删除某条边,并保证加边、删边之后仍是森林,需要维护这个森林的一些信息。
LCT 基于实链剖分,用 Splay 维护每一条深度递增的实链,而虚边总是从一颗 Splay 的根指向该 Splay 中深度最浅的点的父亲。
实链剖分:对于一个点连向它所有儿子的边,自行选择一条边进行剖分,称被选择的边为实边,其他边则为虚边。
基本操作
access
含义:拉一条从根节点到指定节点的实链。
流程:
- 新建一个空结点 \(y\),把 \(x\) Splay 到根。
- 将 \(x\) 的右儿子改为 \(y\)(相当于删去 \(x\) 的实儿子,并把 \(y\) 的实链接了上去 )。
- 更新信息。
- 令
y = x, x = fa[x]
(跳到更上面的实链,继续把之前拼成的实链与上面的链拼接)。 - 重复第一步直到走到原树的根。
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
makeroot
含义:换根操作,指定某点为树的根。
注意到 access(x)
后 \(x\) 一定是 Splay 中中序遍历最后的点,即 splay(x)
后,\(x\) 在 Splay 中没有右子树。
于是翻转整个 Splay, \(x\) 就没了左子树,成了深度最小的点,即根节点。
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
split
含义:拉出 \(x-y\) 的路径成为一个 Splay 。
将根换为 \(x\) 之后拉一条 \(x - y\) 的实链即可。
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
findroot
含义:找到某个点所在树的根。
主要用来判断两点之间的连通性,两点在原树中的根相同就代表他们连通。
思想不难理解,拉一条到根的链之后找深度最小的点即可。
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
link
含义:连一条 \(x \to y\) 的边。
换根完直接连即可。
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(y), fa[y] = x;
}
cut
含义:断开 \(x - y\) 的边。
先判断两点是否连通,再判断 \(x, y\) 是否直接连边,即不存在深度介于 \(x, y\) 之间的点,最后更新信息即可。
inline void cut(int x, int y) {
if (findroot(x) != findroot(y))
return;
split(x, y);
if (ch[y][0] == x && !ch[x][1])
fa[x] = ch[y][0] = 0, pushup(y);
}
应用
动态维护链上信息
P3690 【模板】动态树(Link Cut Tree)
给定 \(n\) 个点以及每个点的权值,\(m\) 次操作:
0 x y
:询问从 \(x\) 到 \(y\) 的路径上的点的权值的 \(\text{xor}\) 和。1 x y
:连一条 \((x, y)\) 的边。2 x y
:删除边 \((x,y)\) 。3 x y
:将点 \(x\) 上的权值变成 \(y\) 。\(n \leq 10^5\) ,\(m \leq 3 \times 10^5\)
模板题,下给出参考代码。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int n, m;
namespace LCT {
int ch[N][2], fa[N], rev[N], sta[N], val[N], s[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
s[x] = val[x];
if (ch[x][0])
s[x] ^= s[ch[x][0]];
if (ch[x][1])
s[x] ^= s[ch[x][1]];
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(x), fa[x] = y;
}
inline void cut(int x, int y) {
if (findroot(x) != findroot(y))
return;
split(x, y);
if (ch[y][0] == x && !ch[x][1])
fa[x] = ch[y][0] = 0, pushup(y);
}
inline void update(int x, int k) {
makeroot(x), val[x] = k, pushup(x);
}
inline int query(int x, int y) {
return split(x, y), s[y];
}
} // namespace LCT
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", LCT::val + i), LCT::s[i] = LCT::val[i];
while (m--) {
int op, x, y;
scanf("%d%d%d", &op, &x, &y);
if (!op)
printf("%d\n", LCT::query(x, y));
else if (op == 1)
LCT::link(x, y);
else if (op == 2)
LCT::cut(x, y);
else
LCT::update(x, y);
}
return 0;
}
P3203 [HNOI2010]弹飞绵羊
有 \(n\) 个装置,每个装置设定初始弹力系数 \(k_i\) ,当绵羊达到第 \(i\) 个装置时它会达到第 \(i+k_i\) 个装置,若不存在第 \(i+k_i\) 个装置则会弹飞。\(m\) 次操作:
1 x
:求从 \(x\) 开始要几次才会被弹飞。2 x k
:将 \(x\) 的弹力系数修改为 \(k\) 。\(n \leq 2 \times 10^5\) ,\(m \leq 10^5\)
对于每个装置,有且仅有一个位置可以弹到或是被弹飞,可以把这种关系视为边。
因为每个装置都会往后弹,所以不存在环,可以构建出一个森林。
修改弹力值的操作,我们用只要动态增减边即可。
查询弹飞的步数,就是查询该点到其所属原树中根节点的路径的长度。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
int a[N];
int n, m;
namespace LCT {
int ch[N][2], fa[N], rev[N], sta[N], siz[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
siz[x] = 1;
if (ch[x][0])
siz[x] += siz[ch[x][0]];
if (ch[x][1])
siz[x] += siz[ch[x][1]];
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
} // namespace LCT
signed main() {
scanf("%d", &n);
fill(LCT::siz + 1, LCT::siz + n + 1, 1);
for (int i = 1; i <= n; ++i) {
int k;
scanf("%d", &k);
if (i + k <= n)
LCT::fa[i] = i + k;
}
scanf("%d", &m);
while (m--) {
int op, x;
scanf("%d%d", &op, &x);
LCT::access(++x), LCT::splay(x);
if (op == 1)
printf("%d\n", LCT::siz[x]);
else {
int k;
scanf("%d", &k);
LCT::ch[x][0] = LCT::fa[LCT::ch[x][0]] = 0;
LCT::pushup(x);
if (x + k <= n)
LCT::fa[x] = x + k;
}
}
return 0;
}
P1501 [国家集训队]Tree II
给出一棵树,每个点初始点权为 \(1\) ,\(q\) 次操作:
+ u v k
:将 \(u, v\) 路径上的点权 \(+k\) 。- u v x y
:删除边 \((u, v)\) ,加入边 \((x, y)\) ,保证操作完之后仍然是一棵树。* u v k
:将 \(u, v\) 路径上的点权 \(\times k\) 。/ u v
:查询 \(u, v\) 路径上的点权和。\(n, q \leq 10^5\)
维护加法、乘法标记与每个 Splay 的大小,下放标记类似于线段树,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 51061;
const int N = 1e5 + 7;
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline char readc() {
char c = getchar();
while (c != '+' && c != '-' && c != '*' && c != '/')
c = getchar();
return c;
}
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
namespace LCT {
struct Node {
int siz, val, s, ta, tm;
inline Node() {
siz = 1, val = s = 1, ta = 0, tm = 1;
}
inline void spread(int ka, int km) {
val = add(1ll * val * km % Mod, ka);
s = add(1ll * s * km % Mod, 1ll * ka * siz % Mod);
ta = add(1ll * ta * km % Mod, ka), tm = 1ll * tm * km % Mod;
}
} nd[N];
int ch[N][2];
int fa[N], rev[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void reverse(int x) {
rev[x] ^= 1, swap(ch[x][0], ch[x][1]);
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
if (nd[x].ta || nd[x].tm != 1) {
if (ch[x][0])
nd[ch[x][0]].spread(nd[x].ta, nd[x].tm);
if (ch[x][1])
nd[ch[x][1]].spread(nd[x].ta, nd[x].tm);
nd[x].ta = 0, nd[x].tm = 1;
}
}
inline void pushup(int x) {
nd[x].siz = 1, nd[x].s = nd[x].val;
if (ch[x][0])
nd[x].siz += nd[ch[x][0]].siz, nd[x].s = add(nd[x].s, nd[ch[x][0]].s);
if (ch[x][1])
nd[x].siz += nd[ch[x][1]].siz, nd[x].s = add(nd[x].s, nd[ch[x][1]].s);
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
stack<int> sta;
sta.emplace(x);
for (int cur = x; !isroot(cur); cur = fa[cur])
sta.emplace(fa[cur]);
while (!sta.empty())
pushdown(sta.top()), sta.pop();
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return splay(x), x;
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(x), fa[x] = y;
}
inline void cut(int x, int y) {
if (findroot(x) != findroot(y))
return;
split(x, y);
if (ch[y][0] == x && !ch[x][1])
fa[x] = ch[y][0] = 0, pushup(y);
}
inline void update(int x, int y, int ka, int km) {
split(x, y), nd[y].spread(ka, km);
}
inline int query(int x, int y) {
return split(x, y), nd[y].s;
}
} // namespace LCT
signed main() {
n = read(), m = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
LCT::link(u, v);
}
while (m--) {
char op = readc();
if (op == '+') {
int x = read(), y = read(), k = read();
LCT::update(x, y, k, 1);
} else if (op == '-') {
int u = read(), v = read(), x = read(), y = read();
LCT::cut(u, v), LCT::link(x, y);
} else if (op == '*') {
int x = read(), y = read(), k = read();
LCT::update(x, y, 0, k);
} else {
int x = read(), y = read();
printf("%d\n", LCT::query(x, y));
}
}
return 0;
}
P2486 [SDOI2011] 染色
给定一棵 \(n\) 个节点的无根树,共有 \(m\) 个操作,操作分为两种:
- 将节点 \(a\) 到节点 \(b\) 的路径上的点都染成颜色 \(c\) 。
- 询问节点 \(a\) 到节点 \(b\) 的路径上的颜色段数量。
\(n, m \leq 10^5\)
对每个点维护颜色段数量、最左端颜色和最右端颜色,就容易合并信息了,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int a[N];
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline char readc() {
char c = getchar();
while (c != 'C' && c != 'Q')
c = getchar();
return c;
}
namespace LCT {
struct Node {
int col, lcol, rcol, s, tag;
inline void spread(int k) {
col = lcol = rcol = tag = k, s = 1;
}
} nd[N];
int ch[N][2];
int fa[N], val[N], rev[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
nd[x].lcol = nd[x].rcol = nd[x].col, nd[x].s = 1;
if (ch[x][0]) {
nd[x].lcol = nd[ch[x][0]].lcol;
nd[x].s += nd[ch[x][0]].s - (nd[ch[x][0]].rcol == nd[x].col);
}
if (ch[x][1]) {
nd[x].rcol = nd[ch[x][1]].rcol;
nd[x].s += nd[ch[x][1]].s - (nd[ch[x][1]].lcol == nd[x].col);
}
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), swap(nd[x].lcol, nd[x].rcol), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
if (nd[x].tag) {
if (ch[x][0])
nd[ch[x][0]].spread(nd[x].tag);
if (ch[x][1])
nd[ch[x][1]].spread(nd[x].tag);
nd[x].tag = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
stack<int> sta;
sta.emplace(x);
for (int cur = x; !isroot(cur); cur = fa[cur])
sta.emplace(fa[cur]);
while (!sta.empty())
pushdown(sta.top()), sta.pop();
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(y), fa[y] = x;
}
inline void update(int x, int y, int k) {
split(x, y), nd[y].spread(k);
}
inline int query(int x, int y) {
return split(x, y), nd[y].s;
}
} // namespace LCT
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; ++i)
LCT::nd[i].col = LCT::nd[i].lcol = LCT::nd[i].rcol = read(), LCT::nd[i].s = 1;
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
LCT::link(u, v);
}
while (m--) {
if (readc() == 'C') {
int x = read(), y = read(), k = read();
LCT::update(x, y, k);
} else {
int x = read(), y = read();
printf("%d\n", LCT::query(x, y));
}
}
return 0;
}
P4332 [SHOI2014] 三叉神经树
给定一个 \(3n+1\) 个结点的三叉树,除了叶子节点外的点都有三个儿子,给定叶子节点的值 \(\in \{ 0, 1 \}\) ,非叶子节点的值为三个儿子的值中较多者。\(m\) 次操作,每次将一个叶子节点的值取反,求取反后根节点的值。
\(n, q \leq 5 \times 10^5\)
为了方便设点的值为儿子中 \(1\) 的数量,范围为 \(0 \sim 3\) 。
可以发现一次修改影响的范围是自底向上的一条链,叶子 \(0 \to 1\) 时会改变一段值为 \(1\) 的链,叶子 \(1 \to 0\) 时会改变一段值为 \(2\) 的链。
对于每个点维护最底端值不为 \(1 / 2\) 的点,每次直接伸展上来做修改即可,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1.5e6 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int n, m, ans;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
namespace LCT {
int ch[N][2], low[N][2];
int fa[N], val[N], sum[N], tag[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
low[x][0] = low[x][1] = 0;
for (int i = 0; i <= 1; ++i) {
if (ch[x][1] && low[ch[x][1]][i])
low[x][i] = low[ch[x][1]][i];
else if (sum[x] != i + 1)
low[x][i] = x;
else if (ch[x][0] && low[ch[x][0]][i])
low[x][i] = low[ch[x][0]][i];
else
low[x][i] = 0;
}
}
inline void spread(int x, int k) {
sum[x] += k, swap(low[x][0], low[x][1]), tag[x] += k;
}
inline void pushdown(int x) {
if (tag[x]) {
if (ch[x][0])
spread(ch[x][0], tag[x]);
if (ch[x][1])
spread(ch[x][1], tag[x]);
tag[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
stack<int> sta;
sta.emplace(x);
for (int cur = x; !isroot(cur); cur = fa[cur])
sta.emplace(fa[cur]);
while (!sta.empty())
pushdown(sta.top()), sta.pop();
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void solve(int x, int c) {
x = fa[x];
access(x), splay(x);
int cur = low[x][c], k = (c ? -1 : 1);
if (cur) {
splay(cur);
if (ch[cur][1])
spread(ch[cur][1], k), pushup(ch[cur][1]);
sum[cur] += k, pushup(cur);
} else
spread(x, k), ans ^= 1, pushup(x);
}
} // namespace LCT
void dfs(int u) {
for (int v : G.e[u])
dfs(v), LCT::sum[u] += LCT::val[v];
if (u <= n)
LCT::val[u] = (LCT::sum[u] >= 2);
}
signed main() {
n = read();
for (int i = 1; i <= n; ++i) {
G.e[i].resize(3);
for (int &it : G.e[i])
LCT::fa[it = read()] = i;
}
for (int i = n + 1; i <= n * 3 + 1; ++i)
LCT::val[i] = read();
dfs(1), ans = LCT::val[1];
m = read();
while (m--) {
int x = read();
LCT::solve(x, LCT::val[x]), LCT::val[x] ^= 1;
printf("%d\n", ans);
}
return 0;
}
P5354 [Ynoi Easy Round 2017] 由乃的 OJ
给定一棵树,每个点有 \([0, 2^k - 1]\) 范围内的整数权值和一个运算符(按位与、按位或、按位异或之一)。\(m\) 次操作,操作有:
- 单点修改权值与运算符。
- 查询初始值 \(\in [0, z]\) 时,依次经过 \(x \to y\) 路径上每个点的变换后(原数与点权做点上的运算)得到结果的最大值。
\(n, m \leq 10^5\) ,\(k \leq 64\)
首先可以发现每一位变换都是独立的,因此可以用 \(k\) 个 LCT 维护每一位变换后的结果。
套路地考虑按位从高到低贪心,可以做到 \(O(n + mk \log n)\) ,无法通过。
注意到每个 LCT 只维护了 \(0\) 和 \(1\) 的信息,比较浪费。由于每一位变换独立,因此直接用 unsigned long long
同时维护每一位的信息即可,时间复杂度 \(O(n + m(\log n + k))\) 。
#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int N = 1e5 + 7;
pair<ull, ull> trans[N];
int n, m, k;
inline pair<ull, ull> operator + (pair<ull, ull> a, pair<ull, ull> b) {
return make_pair((~a.first & b.first) | (a.first & b.second), (~a.second & b.first) | (a.second & b.second));
}
namespace LCT {
pair<ull, ull> tl[N], tr[N];
int ch[N][2], fa[N], rev[N], sta[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
tl[x] = tr[x] = trans[x];
if (ch[x][0])
tl[x] = tl[ch[x][0]] + tl[x], tr[x] = tr[x] + tr[ch[x][0]];
if (ch[x][1])
tl[x] = tl[x] + tl[ch[x][1]], tr[x] = tr[ch[x][1]] + tr[x];
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), swap(tl[x], tr[x]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline void link(int x, int y) {
makeroot(y), fa[y] = x;
}
inline void update(int x, int y, ull z) {
makeroot(x), trans[x] = (y == 1 ? make_pair(0ull, z) :
(y == 2 ? make_pair(z, ~0ull) : make_pair(z, ~z))), pushup(x);
}
inline ull query(int x, int y, ull z) {
split(x, y);
ull res = 0;
for (int i = k - 1; ~i; --i) {
if (tl[y].first >> i & 1)
res |= 1ull << i;
else if ((tl[y].second >> i & 1) && (1ull << i) <= z)
res |= 1ull << i, z -= 1ull << i;
}
return res;
}
} // namespace LCT
signed main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n; ++i) {
int op;
ull k;
scanf("%d%llu", &op, &k);
LCT::tl[i] = LCT::tr[i] = trans[i] = (op == 1 ? make_pair(0ull, k) :
(op == 2 ? make_pair(k, ~0ull) : make_pair(k, ~k)));
}
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
LCT::link(u, v);
}
while (m--){
int op, x, y;
ull z;
scanf("%d%d%d%llu", &op, &x, &y, &z);
if (op == 1)
printf("%llu\n", LCT::query(x, y, z));
else
LCT::update(x, y, z);
}
return 0;
}
P9340 [JOISC 2023] Tourism (Day3)
给定一棵大小为 \(n\) 的树和序列 \(a_{1 \sim m}\) ,其中 \(a_i \in [1, n]\) 。\(q\) 次询问,每次给定 \(l, r\) ,求包含 \(a_{l \sim r}\) 所有点的极小连通块大小。
\(n, m, q \leq 10^5\)
对于一个询问 \([l, r]\) ,若将 \(a_{l \sim r}\) 的点都染黑,则答案为子树内有黑点的点的数量减去 \(dep_{LCA(a_{l \sim r})}\) ,后者只要查 dfn 最小和最大的两个点的 LCA 即可。
考虑维护前者,当右端点固定时,一个点 \(u\) 对前者有贡献当且仅当 \(l \in [1, mx_u]\) ,其中 \(mx_u\) 表示 \([1, r]\) 中最靠后的子树内有 \(u\) 的位置。
因此考虑对 \(r\) 扫描线,动态维护 \(mx_u\) 与所有左端点的答案。观察到当 \(r - 1 \to r\) 时,\(a_r\) 到根的路径上的点都会变得有贡献,直接用 LCT 的 access 操作拉出这条链,用 BIT 动态维护修改、查询操作即可。
一次操作相当于在 \(O(\log n)\) 条实链上颜色段均摊,时间复杂度 \(O(n \log^2 n + q \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, LOGN = 17;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<pair<int, int> > qry[N];
int fa[N][LOGN], a[N], dep[N], dfn[N], id[N], ans[N];
int n, m, q, dfstime;
void dfs(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1, id[dfn[u] = ++dfstime] = u;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
if (v != f)
dfs(v, u);
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int h = dep[x] - dep[y]; h; h &= h - 1)
x = fa[x][__builtin_ctz(h)];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
namespace ST {
int mn[LOGN][N], mx[LOGN][N];
inline void prework() {
for (int i = 1; i <= m; ++i)
mn[0][i] = mx[0][i] = dfn[a[i]];
for (int j = 1; j <= __lg(m); ++j)
for (int i = 1; i + (1 << j) - 1 <= m; ++i) {
mn[j][i] = min(mn[j - 1][i], mn[j - 1][i + (1 << (j - 1))]);
mx[j][i] = max(mx[j - 1][i], mx[j - 1][i + (1 << (j - 1))]);
}
}
inline int querymin(int l, int r) {
int k = __lg(r - l + 1);
return min(mn[k][l], mn[k][r - (1 << k) + 1]);
}
inline int querymax(int l, int r) {
int k = __lg(r - l + 1);
return max(mx[k][l], mx[k][r - (1 << k) + 1]);
}
} // namespace ST
namespace BIT {
int c[N];
inline void modify(int x, int k) {
for (; x <= m; x += x & -x)
c[x] += k;
}
inline void update(int l, int r, int k) {
modify(l, k), modify(r + 1, -k);
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} // namespace BIT
namespace LCT {
int ch[N][2], fa[N], sta[N], val[N], cov[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void spread(int x, int k) {
val[x] = cov[x] = k;
}
inline void pushdown(int x) {
if (cov[x]) {
if (ch[x][0])
spread(ch[x][0], cov[x]);
if (ch[x][1])
spread(ch[x][1], cov[x]);
cov[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x, int k) {
int y = 0;
for (; x; y = x, x = fa[x])
splay(x), BIT::update(val[x] + 1, k, dep[x] - dep[fa[x]]), ch[x][1] = y;
spread(y, k);
}
} // namespace LCT
signed main() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0);
for (int i = 2; i <= n; ++i)
LCT::fa[i] = fa[i][0];
for (int i = 1; i <= m; ++i)
scanf("%d", a + i);
ST::prework();
for (int i = 1; i <= q; ++i) {
int l, r;
scanf("%d%d", &l, &r);
qry[r].emplace_back(l, i);
}
for (int i = 1; i <= m; ++i) {
LCT::access(a[i], i);
for (auto it : qry[i]) {
int l = it.first;
ans[it.second] = BIT::query(l) - dep[LCA(id[ST::querymin(l, i)], id[ST::querymax(l, i)])] + 1;
}
}
for (int i = 1; i <= q; ++i)
printf("%d\n", ans[i]);
return 0;
}
动态维护连通性
P3950 部落冲突
给出一棵树,\(m\) 次操作:
Q x y
:求 \(x, y\) 是否连通。C x y
:删掉边 \((x, y)\) 。U x
:恢复第 \(x\) 次操作删掉的边。\(n, m \leq 3 \times 10^5\)
不难用 LCT 的 findroot 直接维护做到 \(O(n \log n)\) 。
这个也是板子:P2147 [SDOI2008] 洞穴勘测
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 7;
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline char readc() {
char c = getchar();
while (c != 'C' && c != 'Q' && c != 'U')
c = getchar();
return c;
}
namespace LCT {
int ch[N][2];
int fa[N], rev[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
}
inline void splay(int x) {
stack<int> sta;
sta.emplace(x);
for (int cur = x; !isroot(cur); cur = fa[cur])
sta.emplace(fa[cur]);
while (!sta.empty())
pushdown(sta.top()), sta.pop();
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y;
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(y), fa[y] = x;
}
inline void cut(int x, int y) {
if (findroot(x) != findroot(y))
return;
split(x, y);
if (ch[y][0] == x && !ch[x][0])
fa[x] = ch[y][0] = 0;
}
} // namespace LCT
signed main() {
n = read(), m = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
LCT::link(u, v);
}
vector<pair<int, int> > war;
while (m--) {
char op = readc();
if (op == 'Q') {
int x = read(), y = read();
puts(LCT::findroot(x) == LCT::findroot(y) ? "Yes" : "No");
} else if (op == 'C') {
int x = read(), y = read();
LCT::cut(x, y), war.emplace_back(x, y);
} else {
int x = read() - 1;
LCT::link(war[x].first, war[x].second);
}
}
return 0;
}
P2173 [ZJOI2012] 网络
给出一张无向图 \(G\) ,每个点有权值,每条边有颜色 \(c_i \in [1, C]\) ,满足两个条件:
- 任意点的出边中,相同颜色的边不超过两条。
- 图中不存在同色的环。
\(m\) 次操作,操作有:
- 修改一个点的权值。
- 修改一条边的颜色。
- 若修改时边不存在或不满足给定的两个条件,则不做修改,并输出相应信息。
- 查询只保留颜色 \(c\) 的边时,所有可能在 \(u \to v\) 路径上的点的点权最大值。
\(n \leq 10^4\) ,\(m \leq 10^5\) ,\(C \leq 10\)
考虑对每个颜色都维护一个森林,不难用 LCT 做到 \(O(cm \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7, C = 11;
int cnt[N][C];
int n, m, c, k;
struct LCT {
int ch[N][2], fa[N], rev[N], sta[N], val[N], mx[N];
inline bool isroot(int x) {
return ch[fa[x]][0] != x && ch[fa[x]][1] != x;
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
mx[x] = val[x];
if (ch[x][0])
mx[x] = max(mx[x], mx[ch[x][0]]);
if (ch[x][1])
mx[x] = max(mx[x], mx[ch[x][1]]);
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return splay(x), x;
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline bool link(int x, int y) {
if (findroot(x) != findroot(y))
return makeroot(x), fa[x] = y, true;
else
return false;
}
inline bool cut(int x, int y) {
split(x, y);
if (ch[y][0] == x && !ch[x][1])
return ch[y][0] = fa[x] = 0, pushup(y), true;
else
return false;
}
inline void update(int x, int k) {
makeroot(x), val[x] = k, pushup(x);
}
inline int query(int x, int y) {
return findroot(x) == findroot(y) ? split(x, y), mx[y] : -1;
}
} lct[C];
inline void update(int x, int val) {
for (int i = 0; i < c; ++i)
lct[i].update(x, val);
}
inline void modify(int u, int v, int w) {
for (int i = 0; i < c; ++i)
if (lct[i].cut(u, v)) {
--cnt[u][i], --cnt[v][i];
if (cnt[u][w] == 2 || cnt[v][w] == 2)
puts("Error 1."), lct[i].link(u, v), ++cnt[u][i], ++cnt[v][i];
else if (lct[w].link(u, v))
puts("Success."), ++cnt[u][w], ++cnt[v][w];
else
puts("Error 2."), lct[i].link(u, v), ++cnt[u][i], ++cnt[v][i];
return;
}
puts("No such edge.");
}
signed main() {
scanf("%d%d%d%d", &n, &m, &c, &k);
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
update(i, x);
}
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
lct[w].link(u, v), ++cnt[u][w], ++cnt[v][w];
}
while (k--) {
int op;
scanf("%d", &op);
if (!op) {
int x, k;
scanf("%d%d", &x, &k);
update(x, k);
} else if (op == 1) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
modify(u, v, w);
} else {
int c, u, v;
scanf("%d%d%d", &c, &u, &v);
printf("%d\n", lct[c].query(u, v));
}
}
return 0;
}
P5489 EntropyIncreaser 与 动态图
有 \(n\) 个点,\(m\) 次操作:
- 加入一条边。
- 询问两点路径上割边数量。
- 询问两点路径上割点数量。
\(n \leq 10^5\) ,\(q \leq 3 \times 10^5\) ,强制在线
先考虑求割边。可以发现 \(u \to v\) 路径上的桥边等于将所有边双缩点后 \(u \to v\) 路径上的点的数量 \(-1\) 。
加入一条边时,如果两点原来不连通,则在 LCT 上连接两点;否则提取出加这条边之前 LCT 上这两点之间的路径,遍历这条路径,将这些点合并,利用并查集维护合并的信息,用合并后并查集的代表元素代替原来树上的路径即可。
再考虑求割点,考虑用 LCT 动态维护圆方树。若该次加边后出现了一个环,则新建一个方点,暴力删除环上所有边,并将环上所有点连向方点。查询时只要查路径上所有圆点的数量即可。
时间复杂度 \(O(n \log n)\) 。
还有两个模板:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
int n, m;
namespace LCT_dot {
namespace LCT {
int ch[N][2], fa[N], s[N], rev[N], sta[N];
int ext;
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
s[x] = (x <= n);
if (ch[x][0])
s[x] += s[ch[x][0]];
if (ch[x][1])
s[x] += s[ch[x][1]];
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
void dfs(int x, int r) {
pushdown(x);
if (ch[x][0])
dfs(ch[x][0], r);
if (ch[x][1])
dfs(ch[x][1], r);
LCT::fa[x] = ext, LCT::ch[x][0] = LCT::ch[x][1] = 0, LCT::pushup(x);
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(y), fa[y] = x;
else
LCT::split(x, y), LCT::dfs(y, ++ext);
}
} // namespace LCT
inline void prework() {
fill(LCT::s + 1, LCT::s + n + 1, 1), LCT::ext = n;
}
inline void link(int x, int y) {
if (x != y)
LCT::link(x, y);
}
inline int query(int x, int y) {
if (LCT::findroot(x) != LCT::findroot(y))
return -1;
else
return LCT::split(x, y), LCT::s[y];
}
} // namespace LCT_dot
namespace LCT_edge {
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
namespace LCT {
int ch[N][2], fa[N], siz[N], rev[N], sta[N];
inline bool isroot(int x) {
return x != ch[dsu.find(fa[x])][0] && x != ch[dsu.find(fa[x])][1];
}
inline int dir(int x) {
return x == ch[dsu.find(fa[x])][1];
}
inline void pushup(int x) {
siz[x] = 1;
if (ch[x][0])
siz[x] += siz[ch[x][0]];
if (ch[x][1])
siz[x] += siz[ch[x][1]];
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = dsu.find(fa[x]), z = dsu.find(fa[y]), d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = dsu.find(fa[x]))
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
void dfs(int x) {
pushdown(x);
if (ch[x][0])
dfs(ch[x][0]), dsu.merge(x, ch[x][0]);
if (ch[x][1])
dfs(ch[x][1]), dsu.merge(x, ch[x][1]);
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(y), fa[y] = x;
else
split(x, y), dfs(y), ch[y][0] = ch[y][1] = 0, pushup(y);
}
} // namespace LCT
inline void prework() {
dsu.prework(n), fill(LCT::siz + 1, LCT::siz + n + 1, 1);
}
inline void link(int x, int y) {
if (x != y)
LCT::link(dsu.find(x), dsu.find(y));
}
inline int query(int x, int y) {
x = dsu.find(x), y = dsu.find(y);
if (LCT::findroot(x) != LCT::findroot(y))
return -1;
else
return LCT::split(x, y), LCT::siz[y] - 1;
}
} // namespace LCT_edge
signed main() {
scanf("%d%d", &n, &m);
LCT_dot::prework(), LCT_edge::prework();
int lstans = 0;
while (m--) {
int op, x, y;
scanf("%d%d%d", &op, &x, &y);
x ^= lstans, y ^= lstans;
if (op == 1)
LCT_dot::link(x, y), LCT_edge::link(x, y);
else {
int ans = (op == 2 ? LCT_edge::query(x, y) : LCT_dot::query(x, y));
printf("%d\n", ans);
if (~ans)
lstans = ans;
}
}
return 0;
}
动态维护边权
LCT 并不能直接处理边权,此时需要对每条边建立一个对应点,方便查询链上的边信息。利用这一技巧可以动态维护生成树。
P3366 【模板】最小生成树
求给定无向图的 MST 。
\(n \leq 5000\) ,\(m \le 2 \times 10^5\)
每次连一条边,若其构成环,则断掉环上最长边,不难发现这样操作后仍为森林。
由于 LCT 上没有固定的父子关系,所以不能将边权下放到点权中。为了记录树链上的边的信息,考虑拆边,对每条边建立一个对应的点,从这条边向其两个端点连接一条边,原先的连边与删边操作都变成两次操作。然后这个点的点权为其边权,连边时若需要替换掉环上的边,则将最大边代表的点转到根,然后删去即可。
时间复杂度 \(O(m \log (n + m))\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 7;
int val[N];
int n, m, ext;
namespace LCT {
struct Node {
int mx, id;
inline bool operator < (const Node &rhs) const {
return mx == rhs.mx ? id < rhs.id : mx < rhs.mx;
}
} nd[N];
int ch[N][2], fa[N], rev[N], sta[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
nd[x] = (Node){val[x], x};
if (ch[x][0])
nd[x] = max(nd[x], nd[ch[x][0]]);
if (ch[x][1])
nd[x] = max(nd[x], nd[ch[x][1]]);
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(y), fa[y] = x;
}
} // namespace LCT
signed main() {
scanf("%d%d", &n, &m), ext = n;
int ans = 0, cnt = 0;
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
if (LCT::findroot(u) != LCT::findroot(v)) {
val[++ext] = w, LCT::link(u, ext), LCT::link(ext, v);
++cnt, ans += w;
} else {
LCT::split(u, v);
int cur = LCT::nd[v].id;
if (val[cur] <= w)
continue;
ans += w - val[cur];
LCT::splay(cur), LCT::fa[LCT::ch[cur][0]] = LCT::fa[LCT::ch[cur][1]] = 0;
val[++ext] = w, LCT::link(u, ext), LCT::link(ext, v);
}
}
if (cnt != n - 1)
puts("orz");
else
printf("%d", ans);
return 0;
}
P4234 最小差值生成树
给出一张无向图,求边权最大值与最小值的差值最小的生成树,图可能存在自环。
\(n \leq 5 \times 10^4\) ,\(m \leq 2 \times 10^5\)
将边按照边权从小到大排序,枚举选择的最大边,要得到最优解,需要使边权最小边的边权最大。
每次按照顺序添加边,如果将要连接的这两个点已经连通,则删除这两点之间边权最小的一条边。如果整个图已经连通成了一棵树,则用当前边权减去最小边权更新答案。
最小边权可用双指针维护,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 3e5 + 7;
struct Edge {
int u, v, w;
inline bool operator < (const Edge &rhs) const {
return w < rhs.w;
}
} e[N];
int val[N];
bool used[N];
int n, m, ext;
namespace LCT {
struct Node {
int mn, id;
inline bool operator < (const Node &rhs) const {
return mn == rhs.mn ? id < rhs.id : mn < rhs.mn;
}
} nd[N];
int ch[N][2], fa[N], rev[N], sta[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
nd[x] = (Node){val[x], x};
if (ch[x][0])
nd[x] = min(nd[x], nd[ch[x][0]]);
if (ch[x][1])
nd[x] = min(nd[x], nd[ch[x][1]]);
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(y), fa[y] = x;
}
} // namespace LCT
signed main() {
scanf("%d%d", &n, &m), ext = n;
for (int i = 1; i <= m; ++i)
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
sort(e + 1, e + m + 1);
int ans = inf, cnt = 0;
memset(val + 1, inf, sizeof(int) * n);
for (int i = 1, j = n + 1; i <= m; ++i) {
int u = e[i].u, v = e[i].v, w = e[i].w;
if (u == v)
continue;
if (LCT::findroot(u) != LCT::findroot(v))
++cnt;
else {
LCT::split(u, v);
int cur = LCT::nd[v].id;
used[cur] = false, LCT::splay(cur), LCT::fa[LCT::ch[cur][0]] = LCT::fa[LCT::ch[cur][1]] = 0;
}
val[++ext] = w, LCT::link(u, ext), LCT::link(ext, v), used[ext] = true;
while (!used[j])
++j;
if (cnt == n - 1)
ans = min(ans, w - val[j]);
}
printf("%d", ans);
return 0;
}
P2387 [NOI2014] 魔法森林
给出一张无向图,每条边有权值 \(a, b\) ,求一条 \(1 \to n\) 的路径使得 \((\max a) + (\max b)\) 最小。
\(n \leq 5 \times 10^4\) ,\(m \leq 10^5\)
直接处理这个值不好做,因为 \(a, b\) 会互相影响。考虑固定 \(\max a\) ,这个可以通过将 \(a\) 排序后动态加边处理,然后对于 \(b\) 只要维护一棵 MST 即可,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
struct Edge {
int u, v, a, b;
inline bool operator < (const Edge &rhs) const {
return a == rhs.a ? b < rhs.b : a < rhs.a;
}
} e[N];
int val[N];
int n, m;
namespace LCT {
struct Node {
int mx, id;
inline bool operator < (const Node &rhs) const {
return mx == rhs.mx ? id < rhs.id : mx < rhs.mx;
}
} nd[N];
int ch[N][2], fa[N], rev[N], sta[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
nd[x] = (Node){val[x], x};
if (ch[x][0])
nd[x] = max(nd[x], nd[ch[x][0]]);
if (ch[x][1])
nd[x] = max(nd[x], nd[ch[x][1]]);
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(y), fa[y] = x;
}
} // namespace LCT
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].a, &e[i].b);
sort(e + 1, e + m + 1);
int ans = 1e9, ext = n;
for (int i = 1; i <= m; ++i) {
int u = e[i].u, v = e[i].v, a = e[i].a, b = e[i].b;
if (LCT::findroot(u) != LCT::findroot(v))
val[++ext] = b, LCT::link(u, ext), LCT::link(ext, v);
else {
LCT::split(u, v);
int cur = LCT::nd[v].id;
if (val[cur] > b) {
LCT::splay(cur), LCT::fa[LCT::ch[cur][0]] = LCT::fa[LCT::ch[cur][1]] = 0;
val[++ext] = b, LCT::link(u, ext), LCT::link(ext, v);
}
}
if (LCT::findroot(1) == LCT::findroot(n))
LCT::split(1, n), ans = min(ans, a + val[LCT::nd[n].id]);
}
printf("%d", ans == 1e9 ? -1 : ans);
return 0;
}
动态维护子树信息
LCT 由于“认父不认子“的特性,不擅长维护子树信息。分别统计一个点的所有实子树、虚子树信息,就可以求得整棵树的信息。
注意一些函数写法上有些差异,需要实时更新虚实儿子的信息统计。
UOJ207. 共价大爷游长沙
动态维护一棵树和二元组集合,需要支持:
- 删边并加边,保证图仍为树。
- 加入/删除某个二元组。
- 给出一条边 \(e\) ,判定是否对于集合中的每个二元组 \((s, t)\) ,\(e\) 都在 \(s, t\) 的路径上。
\(n \leq 10^5\) ,\(m \leq 3 \times 10^5\)
首先不难想到用 LCT 动态维护树的形态,考虑对每条边用哈希维护经过这条边的二元组集合。
给每个二元组随机赋上一个权值,这样一条边的哈希值就可以定义为子树异或和,加入/删除某个二元组就是单点异或。
时间复杂度 \(O((n + m) \log n)\) 。
#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int N = 1e5 + 7, M = 3e5 + 7;
struct Node {
ull h;
int x, y;
} nd[M];
mt19937_64 myrand(time(0));
int testid, n, m, tot;
namespace LCT {
ull val[N], s[N], s2[N];
int ch[N][2], fa[N], rev[N], sta[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
s[x] = val[x] ^ s2[x];
if (ch[x][0])
s[x] ^= s[ch[x][0]];
if (ch[x][1])
s[x] ^= s[ch[x][1]];
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), s2[x] ^= s[y] ^ s[ch[x][1]], ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline int findroot(int x) {
access(x), splay(x);
while (ch[x][0])
x = ch[x][0];
return x;
}
inline void link(int x, int y) {
if (findroot(x) != findroot(y))
makeroot(x), makeroot(y), fa[x] = y, s2[y] ^= s[x], pushup(y);
}
inline void cut(int x, int y) {
if (findroot(x) != findroot(y))
return;
split(x, y);
if (ch[y][0] == x && !ch[x][1])
fa[x] = ch[y][0] = 0, pushup(y);
}
inline void update(int x, ull k) {
makeroot(x), val[x] ^= k, pushup(x);
}
inline ull query(int x, int y) {
return split(x, y), s2[y] ^ val[y];
}
} // namespace LCT
signed main() {
scanf("%d%d%d", &testid, &n, &m);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
LCT::link(u, v);
}
ull all = 0;
while (m--) {
int op;
scanf("%d", &op);
if (op == 1) {
int x, y, u, v;
scanf("%d%d%d%d", &x, &y, &u, &v);
LCT::cut(x, y), LCT::link(u, v);
} else if (op == 2) {
all ^= (nd[++tot].h = myrand());
scanf("%d%d", &nd[tot].x, &nd[tot].y);
LCT::update(nd[tot].x, nd[tot].h), LCT::update(nd[tot].y, nd[tot].h);
} else if (op == 3) {
int x;
scanf("%d", &x);
all ^= nd[x].h, LCT::update(nd[x].x, nd[x].h), LCT::update(nd[x].y, nd[x].h);
} else {
int x, y;
scanf("%d%d", &x, &y);
puts(LCT::query(x, y) == all ? "YES" : "NO");
}
}
return 0;
}
P4299 首都
有 \(n\) 个孤立点,\(m\) 次操作,操作有三种:
- 连接两个点,保证两点不连通。
- 查询某个点所在树的重心,若这棵树存在两个重心,则规定重心为编号较小者。
- 查询每棵树重心的编号异或和。
\(n \leq 10^5\) ,\(m \leq 2 \times 10^5\)
合并两棵树时,有结论:新树的重心一定在连接原来两棵树的重心的路径上。
因此合并两棵子树时直接在两个重心之间的路径上二分即可,判定就是找最大子树大小不超过总大小一半的点。
显然对于重心的领域点,最大子树均为包含重心的子树,因此只要判断包含两个重心的子树大小较大值是否合法即可。
用并查集维护重心关系,将重心设为该连通块的根,时间复杂度 \(O(n \log n + m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
int n, m;
namespace LCT {
int ch[N][2], fa[N], rev[N], sta[N], siz[N], lsiz[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void pushup(int x) {
siz[x] = siz[ch[x][0]] + siz[ch[x][1]] + lsiz[x] + 1;
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), lsiz[x] += siz[ch[x][1]] - siz[y], ch[x][1] = y, pushup(x);
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void split(int x, int y) {
makeroot(x), access(y), splay(y);
}
inline void link(int x, int y) {
makeroot(x), makeroot(y);
fa[x] = y, lsiz[y] += siz[x], pushup(y);
}
inline int query(int x, int y) {
split(y, x);
int cur = n + 1, lim = siz[x] / 2, lsum = 0, rsum = 0;
while (x) {
pushdown(x);
if (max(lsum + siz[ch[x][0]], rsum + siz[ch[x][1]]) <= lim)
cur = min(cur, x);
if (lsum + siz[ch[x][0]] < rsum + siz[ch[x][1]])
lsum += siz[ch[x][0]] + lsiz[x] + 1, x = ch[x][1];
else
rsum += siz[ch[x][1]] + lsiz[x] + 1, x = ch[x][0];
}
return splay(cur), cur;
}
} // namespace LCT
signed main() {
scanf("%d%d", &n, &m);
dsu.prework(n);
int ans = 0;
for (int i = 1; i <= n; ++i)
LCT::siz[i] = 1, ans ^= i;
while (m--) {
char str[5];
scanf("%s", str);
if (str[0] == 'X')
printf("%d\n", ans);
else if (str[0] == 'A') {
int x, y;
scanf("%d%d", &x, &y);
LCT::link(x, y);
int z = LCT::query(x = dsu.find(x), y = dsu.find(y));
ans ^= x ^ y ^ z, dsu.fa[x] = dsu.fa[y] = dsu.fa[z] = z;
} else if (str[0] == 'Q') {
int x;
scanf("%d", &x);
printf("%d\n", dsu.find(x));
}
}
return 0;
}
动态维护 LCA
P3379 【模板】最近公共祖先(LCA)
给出一棵树,\(m\) 次询问两点 LCA 。
\(n, m \leq 5 \times 10^5\)
LCT 支持任意换根下的 LCA 查询。
询问 \(x, y\) 的 LCA 时,可以发现 access(x)
后 \(x\) 、LCA 、根在同一个 Splay 中,且 \(y\) 到 LCA 一定是一条虚边。
于是再 access(y)
,记录跳过的虚边是跳到了谁那里,LCA 即为最后一次跑虚边的父亲。
单次询问时间复杂度 \(O(\log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;
int n, m, rt;
namespace LCT {
int ch[N][2], fa[N], rev[N], sta[N];
inline bool isroot(int x) {
return x != ch[fa[x]][0] && x != ch[fa[x]][1];
}
inline int dir(int x) {
return x == ch[fa[x]][1];
}
inline void reverse(int x) {
swap(ch[x][0], ch[x][1]), rev[x] ^= 1;
}
inline void pushdown(int x) {
if (rev[x]) {
if (ch[x][0])
reverse(ch[x][0]);
if (ch[x][1])
reverse(ch[x][1]);
rev[x] = 0;
}
}
inline void rotate(int x) {
int y = fa[x], z = fa[y], d = dir(x);
if (!isroot(y))
ch[z][dir(y)] = x;
fa[x] = z, ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
fa[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, fa[y] = x;
}
inline void splay(int x) {
int top = 0;
sta[++top] = x;
for (int cur = x; !isroot(cur); cur = fa[cur])
sta[++top] = fa[cur];
while (top)
pushdown(sta[top--]);
for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
if (!isroot(f))
rotate(dir(f) == dir(x) ? f : x);
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y;
}
inline void makeroot(int x) {
access(x), splay(x), reverse(x);
}
inline void link(int x, int y) {
makeroot(y), fa[y] = x;
}
inline int querylca(int x, int y) {
access(x);
int lca = y;
for (; y; lca = y, y = fa[y])
splay(y), ch[y][1] = lca;
return lca;
}
} // namespace LCT
signed main() {
scanf("%d%d%d", &n, &m, &rt);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
LCT::link(u, v);
}
LCT::makeroot(rt);
while (m--) {
int x, y;
scanf("%d%d", &x, &y);
printf("%d\n", LCT::querylca(x, y));
}
return 0;
}