链接
对于一棵树 T T T,定义其的笛卡尔树 C ( T ) C(T) C(T) 满足:
感性理解的话, C ( T ) C(T) C(T)是把选重心改为选最大编号的点,构建出来的点分树。
先不考虑修改。对于 T T T 构造出 C ( T ) C(T) C(T) 可以发现:
性质 1 1 1 G ( T ) G(T) G(T) 中的边在 C ( T ) C(T) C(T) 中一定是反祖边。
因为横叉边对应的两个节点的 l c a C ( T ) lca_{C(T)} lcaC(T) 一定异于两点,且在两点在 T T T 的路径上,故由 G ( T ) G(T) G(T)的定义这样的边不存在。
性质 2 2 2 如果 ( u , v ) ∈ G ( T ) (u,v) \in G(T) (u,v)∈G(T) ,由性质 1 1 1 不妨设 u u u 是 v v v 的祖先,那么一定有 u = f a C ( T ) v u=fa_{C(T)}v u=faC(T)v 或者 ( f a C ( T ) v , v ) ∈ G ( T ) (fa_{C(T)}v,v) \in G(T) (faC(T)v,v)∈G(T)。
因为路径 ( f a C ( T ) v , u ) (fa_{C(T)}v,u) (faC(T)v,u) 是路径 ( u , v ) (u,v) (u,v) 和路径 ( f a C ( T ) v , v ) (fa_{C(T)}v,v) (faC(T)v,v) 的并集的子集,而两条路径都不存在大于 m i n ( f a C ( T ) v , u ) min(fa_{C(T)}v,u) min(faC(T)v,u) 的点。
性质 3 3 3 如果 ( u , v ) , ( u , x ) ∈ G ( T ) (u,v),(u,x) \in G(T) (u,v),(u,x)∈G(T) 且 v , x v,x v,x 不互为祖先,那么 u u u 一定是 l c a C ( T ) ( v , x ) lca_{C(T)}(v,x) lcaC(T)(v,x)。
由于路径 ( u , v ) , ( v , x ) (u,v),(v,x) (u,v),(v,x) 的并集包含路径 ( v , x ) (v,x) (v,x),且 l c a C ( T ) ( v , x ) lca_{C(T)}(v,x) lcaC(T)(v,x) 在路径 ( v , x ) (v,x) (v,x) 上,所以如果 u u u 不是 l c a C ( T ) ( v , x ) lca_{C(T)}(v,x) lcaC(T)(v,x) 的话,必然有一条路径不和法。
所以,如果把 G ( T ) G(T) G(T) 的边连在 C ( T ) C(T) C(T) 中,结构一定是每个点向自己子树中一条自顶向下的链中所有点连边。而链的底端一定是与这个点在 T T T 相邻的且编号小于这个点的点。这样,原问题转边成了:
给定一棵有根树 C ( T ) C(T) C(T) ,除根以外每个点为链顶有一条向子树中延伸的链,链上的所有的点向链顶的父亲连边,每次查询两点之间的最短路。
性质 4 4 4 G ( T ) G(T) G(T) 中任意两点的最短路上的编号先递增再递减,即不存在一个点编号同时小于最短路中与之相邻的点。
显然,如果存在这样的点那么可以直接删掉它让最短路变短。
设询问的最短路径为 ( u , v ) (u,v) (u,v) ,最短路中最大的点为 w w w,则 w w w 必为 u , v u,v u,v 的 l c a lca lca,最短路也等于从 u , v u,v u,v分别跳到 w w w之和。
容易想到一个贪心算法:每次跳到相邻的,深度最小的,且在 w w w 子树内的节点。
这个方法是正确的,考虑最优方案中第一次不满足这个策略节点 x x x,它一定是这个策略求出的节点 y y y 的子孙。根据性质 2 2 2,它及之后的 y y y 的子孙一定可以被替换成 y y y,这样方案一定不会变劣。
考虑快速询问。首先可以求出 u , v u,v u,v 分别跳到 w w w 的某个祖先最短步数 a , b a,b a,b,显然答案不小于 a + b a+b a+b。事实上答案不大于 a + b + 1 a+b+1 a+b+1,现在来证明这一点。
考虑 u , v u,v u,v 跳到的祖先分别是 x , y x,y x,y,不妨设 x x x 是 y y y 的祖先。设 u u u 跳到 x x x 的前一步是 l l l, l l l 显然是 w w w 的子孙,又因为 l l l 与 x x x 有边,所以 w w w 与 x x x 有边,又因为 y y y 是 x x x 的子孙,所以 y y y 与 x x x 有边。所以答案不大于 a + b + 1 a+b+1 a+b+1。
所以每次询问只需要判断答案是否等于 a + b a+b a+b 即可。设 u , v u,v u,v 分别跳了 a − 1 , b − 1 a-1,b-1 a−1,b−1 步之后,到的点为 x ′ , y ′ x',y' x′,y′,那么只需要判断 x ′ , y ′ x',y' x′,y′ 能否一步到 w w w。
建立一棵树 F ( T ) F(T) F(T) 表示每个点在 C ( T ) C(T) C(T) 中贪心向上跳的点为其在 F ( T ) F(T) F(T) 中的父亲。
考虑插入一个叶子这种修改。考虑在 v v v 处插入 u u u 对 C ( T ) C(T) C(T) 的影响:
考虑用 L C T LCT LCT维护 F ( T ) F(T) F(T):每个点如果和其 C ( T ) C(T) C(T) 上父亲在 F ( T ) F(T) F(T) 上的父亲(称为“后继”)相同,则点权为 0 0 0 且向父亲连边,否则点权为 1 1 1 且向后继连边。这样,查询时可以通过直接提取平衡树进行二分查找。修改时,在 C ( T ) C(T) C(T) 上从 u u u 暴力向上枚举(类似 L C T LCT LCT中的 a c c e s s access access操作),对于每一条覆盖到的后继比 v v v 小的链,在 L C T LCT LCT 上修改相应的链即可(被修改链余下的一段需要把连向后继)。 v v v 的后继为原来在 v v v 位置上的点的后继 w w w(由性质 2 2 2, v , w v,w v,w之间有边,而如果产生了新的链的话, v v v一定不是链底,然而 v v v 的加入不可能在 G G G中产生 v v v不是端点的边)。
在 C ( T ) C(T) C(T) 上暴力枚举相当于一棵一般的 L C T LCT LCT 上的 a c c e s s access access 操作,故总操作是 O ( n l o g n ) O(nlogn) O(nlogn) 的。总时间复杂度为 O ( n l o g 2 n + q l o g n ) O(nlog^2n+qlogn) O(nlog2n+qlogn)。
具体实现可以看代码,有比较详细的注释。
#include
using namespace std;
typedef pair<int, int> pii;
const int maxn = 100005, maxm = 500005;
int n, q;
int ti[maxn], down[maxn];
inline int gi()
{
char c = getchar();
while (c < '0' || c > '9') c = getchar();
int sum = 0;
while ('0' <= c && c <= '9') sum = sum * 10 + c - 48, c = getchar();
return sum;
}
#define mp make_pair
#define fi first
#define se second
#define pb push_back
inline void chkmax(int &a, int b) {if (a < b) a = b;}
namespace CT
{
struct edge
{
int to, next;
} e[maxn * 2];
int h[maxn], tot;
vector<int> to[maxn];
int Low[maxn], g[maxn], fa[maxn], son[maxn], top[maxn], tim, dfn[maxn], low[maxn], siz[maxn], dep[maxn];
inline void add(int u, int v)
{
e[++tot] = (edge) {v, h[u]}; h[u] = tot;
e[++tot] = (edge) {u, h[v]}; h[v] = tot;
}
int find(int x) {return g[x] == x ? x : g[x] = find(g[x]);}
void dfs1(int u)
{
static int stk[maxn], top;
while (top && ti[stk[top]] > ti[u]) down[stk[top--]] = u;
stk[++top] = u;
dfn[u] = ++tim; siz[u] = 1; dep[u] = dep[fa[u]] + 1;
for (int v : to[u]) {
fa[v] = u; dfs1(v); siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v;
}
low[u] = tim;
}
void dfs2(int u)
{
if (son[u]) top[son[u]] = top[u], dfs2(son[u]);
for (int v : to[u])
if (v != son[u]) top[v] = v, dfs2(v);
}
int lca(int x, int y) //x,y在C(T)上的lca
{
while (top[x] != top[y]) dep[top[x]] > dep[top[y]] ? x = fa[top[x]] : y = fa[top[y]];
return dep[x] < dep[y] ? x : y;
}
int jump(int x, int y) //在C(T)上,a跳到b前的最后一个点
{
while (top[x] != top[y]) if (fa[x = top[x]] == y) return x; else x = fa[x];
return son[y];
}
int go(int x, int y) //在C(T),x能否一步到y
{
int k = *(--upper_bound(to[y].begin(), to[y].end(), x, [&](int a, int b) {return dfn[a] < dfn[b];}));
return dfn[x] <= dfn[Low[k]] && dfn[Low[k]] <= low[x];
}
void pre() //离线建立C(T)
{
for (int i = 1; i <= n; ++i) g[i] = i;
for (int u = 1; u <= n; ++u)
for (int i = h[u], v; v = e[i].to, i; i = e[i].next)
if (u > find(v)) to[u].pb(find(v)), g[find(v)] = u;
dfs1(n);
top[n] = n; dfs2(n);
for (int u = 1; u <= n; ++u)
for (int i = h[u], v; v = e[i].to, i; i = e[i].next)
if (u > v) Low[jump(v, u)] = v;
}
}
namespace LCT
{
int fa[maxn], ch[maxn][2], val[maxn], sum[maxn];
int nxt[maxn], cfa[maxn]; //nxt表示点权为0的儿子
#define get(x) (ch[fa[x]][1] == x)
#define is_root(x) (ch[fa[x]][0] != x && ch[fa[x]][1] != x)
#define update(x) (sum[x] = sum[ch[x][0]] + sum[ch[x][1]] + val[x])
inline void rotate(int x)
{
int f = fa[x], gf = f[fa], k = get(x);
if (!is_root(f)) ch[gf][get(f)] = x;
ch[f][k] = ch[x][k ^ 1]; fa[ch[x][k ^ 1]] = f;
ch[x][k ^ 1] = f; fa[f] = x;
fa[x] = gf;
update(f); update(x);
}
inline void splay(int x)
{
while (!is_root(x)) {
int f = fa[x];
if (!is_root(f))
get(x) ^ get(f) ? rotate(x) : rotate(f);
rotate(x);
}
}
inline void access(int x)
{
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, update(x);
}
inline int make_top(int x)
{
int y;
splay(x);
if (!(y = ch[x][0])) return fa[x];
while (ch[y][1]) y = ch[y][1];
splay(y); ch[y][1] = 0; update(y);
return y;
}
inline int head(int x) {splay(x); while (ch[x][0]) x = ch[x][0]; return splay(x), x;}
inline int tail(int x) {splay(x); while (ch[x][1]) x = ch[x][1]; return splay(x), x;}
inline void mdf(int x, int v) {splay(x); val[x] = v; update(x);}
void add(int u, int v)
{
if (u > v) return fa[v] = cfa[v] = u, val[v] = sum[v] = 1, void();
int x, y, z, c = 0, low = u, t = 0;
//把v加入down[v]与down[v]的后继/父亲之间
y = make_top(x = down[v]);
if (nxt[y] == x) nxt[y] = v;
if (y) nxt[v] = x;
mdf(v, val[x]); mdf(x, !y); fa[v] = y; fa[x] = v;
//处理v对其它点的影响
while (u) {
splay(u); ch[u][1] = t; update(u);
if (sum[u]) { //有长度为1的边
x = u;
while (1) {
if (sum[ch[x][1]]) x = ch[x][1];
else if (val[x]) break;
else x = ch[x][0];
}
//切链
if (x > v) break;
if ((y = make_top(x)) > v) break;
//接上剩余的链
splay(low); ch[low][1] = 0;
if (z = nxt[low]) {
splay(z); fa[z] = y; mdf(z, 1);
nxt[low] = 0;
}
//把切下来的链接到一起
u = low = cfa[x];
mdf(x, 0); splay(x = tail(x));
if (c) fa[c] = x, nxt[x] = ch[x][1] = head(c);
c = x; t = 0;
} else t = u, u = fa[u];
}
cfa[v] = cfa[down[v]]; cfa[down[v]] = v;
if (c) mdf(c = head(c), 1), fa[c] = v;
}
void print(int x)
{
if (ch[x][0]) cerr << ch[x][0] << ' ' << x << endl, print(ch[x][0]);
if (ch[x][1]) cerr << ch[x][1] << ' ' << x << endl, print(ch[x][1]);
}
pii dis(int x, int y) //在平衡树上二分算距离,first是步数,second的是跳到y的祖先前最后一个点
{
if (x == y) return mp(0, 0);
access(x); splay(x);
int d = 0, p = 0, _x = x;
while (x)
if (x < y) p = x, x = ch[x][0];
else x = ch[x][1];
splay(p); x = ch[p][1];
if (!(d = sum[x])) return mp(0, _x);
while (1) {
if (sum[ch[x][0]]) x = ch[x][0];
else if (!val[x]) p = x, x = ch[x][1];
else {
if (ch[x][0]) {
x = ch[x][0];
while (ch[x][1]) x = ch[x][1];
p = x;
}
break;
}
}
return mp(d, p);
}
int query(int x, int y)
{
static pii res1, res2;
if (x == y) return 0;
if (x > y) swap(x, y);
int z = CT::lca(x, y);
if (z == y) return (res1 = dis(x, z)).fi + 1 + !CT::go(res1.se, z);
res1 = dis(x, z); res2 = dis(y, z);
return res1.fi + res2.fi + 2 + !(CT::go(res1.se, z) && CT::go(res2.se, z));
}
}
int main()
{
freopen("tree.in", "r", stdin);
freopen("tree.out", "w", stdout);
static int op[maxm], u[maxm], v[maxm];
gi(); q = gi(); n = 1;
for (int i = 1; i <= q; ++i) {
op[i] = gi(); u[i] = gi(); v[i] = gi();
chkmax(n, u[i]); chkmax(n, v[i]);
if (op[i] == 1) CT::add(u[i], v[i]), ti[v[i]] = i;
}
CT::pre();
for (int i = 1; i <= q; ++i) {
if (op[i] == 1) LCT::add(u[i], v[i]);
else printf("%d\n", LCT::query(u[i], v[i]));
}
return 0;
}