一个 LCT
Description
游戏一开始,Lostmonkey 在地上沿着一条直线摆上 $n$ 个装置,每个装置设定初始弹力系数 $k_i$,当绵羊达到第 $i$ 个装置时,它会往后弹 $k_i$ 步,达到第 $i+k_i$个装置,若不存在这个装置,则绵羊被弹飞。
绵羊想知道当它从第 $i$ 个装置起步时,被弹几次后会被弹飞。为了使得游戏更有趣,Lostmonkey 可以修改某个弹力装置的弹力系数。任何时候弹力系数均为正整数。
Solution1
我们让每个装置向弹射到的地方连一个边,那么答案就是这个点到根的路径的长度。
因为要加边、删边且一直是一棵树,我们使用 LCT 维护。
具体怎么维护呢?
我们发现,我们不需要把一段路径单独拎出来处理,只需要查询一个点到这个辅助树对应的原树上的根的距离即可。
并且不能随便换原树的根。
那好办了,回想一下我们为什么要换根:因为 $access$ 只能打通一个点到根的路径,所以当我们想查询一条路径的时候需要换根,但这个题根是固定的,不需要这么做。
当查询点 $x$ 的时候,我们直接 $access(x),splay(x)$,然后答案就是 $siz(x)$。
加边怎么办呢?我们直接 $access(x),splay(x)$,然后连一条虚边即可(实链剖分的灵活性体现出来了,这个虚边就有一点有向边的意思在里面)。
删边怎么办呢?也是一样的。
发现我们实际上少写了很多函数,代码又简洁常数又小。
Code1
#include <algorithm>
#include <cstdio>
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
const int maxn = 2e5 + 10;
struct LCT
{
int ch[maxn][2], rev[maxn], father[maxn], ans[maxn], val[maxn], siz[maxn];
inline int &ls(int x) { return ch[x][0]; }
inline int &rs(int x) { return ch[x][1]; }
inline bool get(int x) { return x == rs(father[x]); }
inline bool isroot(int x) { return x != ch[father[x]][0] && x != ch[father[x]][1]; }
inline void push_up(int x) { siz[x] = siz[ls(x)] + siz[rs(x)] + 1; }
inline void push_down(int x)
{
if (!rev[x])
return;
if (ls(x))
std::swap(ls(ls(x)), rs(ls(x))), rev[ls(x)] ^= 1;
if (rs(x))
std::swap(ls(rs(x)), rs(rs(x))), rev[rs(x)] ^= 1;
rev[x] = 0;
}
inline void update(int x)
{
if (!isroot(x))
update(father[x]);
push_down(x);
}
inline void rotate(int x)
{
int y = father[x], z = father[y], d = get(x);
if (!isroot(y))
ch[z][y == ch[z][1]] = x;
ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
father[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, father[y] = x, father[x] = z;
push_up(y), push_up(x);
}
inline void splay(int x)
{
update(x);
for (int f; f = father[x], !isroot(x); rotate(x))
if (!isroot(f))
rotate(get(x) == get(f) ? f : x);
}
inline int access(int x)
{
int p = 0;
for (; x; p = x, x = father[x])
splay(x), rs(x) = p, push_up(x);
return p;
}
} lct;
int p[maxn];
int main()
{
std::ios::sync_with_stdio(false);
int n, m;
cin >> n;
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
if (i + x > n)
continue;
else
lct.father[i] = i + x;
}
cin >> m;
while (m--)
{
int pos, x;
cin >> pos >> x;
x++;
if (pos == 1)
{
lct.access(x), lct.splay(x);
cout << lct.siz[x] << endl;
}
else
{
int k;
cin >> k;
lct.access(x), lct.splay(x);
lct.ls(x) = lct.father[lct.ls(x)] = 0;
if (x + k <= n)
lct.father[x] = x + k;
lct.push_up(x);
}
}
}
Solution2
很无脑的一个做法,我不喜欢,但有必要学。
建一个超级源点 $n+1$,弹射出去的时候在这个点和超级源点之间建边,查询的时候用 $link(x,n+1)$ ,然后答案是 $siz(n+1)-1$。
优点:高度模块化,比较无脑。
缺点:我不喜欢,且代码长,常数大。
Code2
我的实现很丑,看个乐。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#define mem(x) memset(x, 0, sizeof(x))
using std::cin;
using std::cout;
using std::endl;
const int maxn = 2e5 + 10;
inline int read()
{
char ch = getchar();
int x = 0;
while (!isdigit(ch))
ch = getchar();
while (isdigit(ch))
x = x * 10 + ch - '0', ch = getchar();
return x;
}
struct LCT
{
int ch[maxn][2], rev[maxn], father[maxn], siz[maxn], val[maxn];
inline int &ls(int x) { return ch[x][0]; }
inline int &rs(int x) { return ch[x][1]; }
inline bool get(int x) { return x == rs(father[x]); }
inline bool isroot(int x) { return x != ch[father[x]][0] && x != ch[father[x]][1]; }
inline void push_up(int x) { siz[x] = siz[ls(x)] + siz[rs(x)] + 1; }
inline void push_down(int x)
{
if (!rev[x])
return;
if (ls(x))
std::swap(ls(ls(x)), rs(ls(x))), rev[ls(x)] ^= 1;
if (rs(x))
std::swap(ls(rs(x)), rs(rs(x))), rev[rs(x)] ^= 1;
rev[x] = 0;
}
inline void update(int x)
{
if (!isroot(x))
update(father[x]);
push_down(x);
}
inline void rotate(int x)
{
int y = father[x], z = father[y], d = get(x);
if (!isroot(y))
ch[z][y == ch[z][1]] = x;
ch[y][d] = ch[x][d ^ 1];
if (ch[x][d ^ 1])
father[ch[x][d ^ 1]] = y;
ch[x][d ^ 1] = y, father[y] = x, father[x] = z;
push_up(y), push_up(x);
}
inline void splay(int x)
{
update(x);
for (int f; f = father[x], !isroot(x); rotate(x))
if (!isroot(f))
rotate(get(x) == get(f) ? f : x);
}
int access(int x)
{
int p = 0;
for (; x; p = x, x = father[x])
splay(x), rs(x) = p, push_up(x);
return p;
}
void makeroot(int x) { x = access(x), std::swap(ls(x), rs(x)), rev[x] ^= 1; }
int find(int x)
{
access(x), splay(x), push_down(x);
while (ls(x))
x = ls(x), push_down(x);
splay(x);
return x;
}
void split(int x, int y) { makeroot(x), access(y), splay(y); }
void link(int x, int y)
{
makeroot(x), splay(x);
if (find(y) == x)
return;
father[x] = y;
}
void cut(int x, int y)
{
makeroot(x);
if (find(y) == x && father[y] == x && !ls(y))
ls(x) = father[y] = 0, push_up(x);
}
} lct;
int p[maxn];
int main()
{
int n = read();
for (int i = 1; i <= n; i++)
{
int x = read();
lct.link(i, i + x > n ? n + 1 : i + x);
p[i] = i + x > n ? n + 1 - i : x;
}
int m = read();
while (m--)
{
int pos = read(), x = read();
x++;
if (pos == 1)
{
lct.split(x, n + 1);
cout << lct.siz[n + 1] - 1 << endl;
}
else
{
int k = read();
lct.cut(x, x + p[x]);
lct.link(x, x + k > n ? n + 1 : x + k);
p[x] = x + k > n ? n + 1 - x : k;
}
}
}
总结
注意题目的性质,可以考虑挖掘题目的一些性质,魔改数据结构入手,也可以考虑对数据结构进行一些扩展。