「LOJ2474」「2018 集训队互测」北校门外的未来-笛卡尔树及其扩展+LCT

Description

链接

Solution

对于一棵树 T T T,定义其的笛卡尔树 C ( T ) C(T) C(T) 满足:

  • 堆性质,即祖先的权值(本题中为标号)一定大于子孙的权值。
  • 搜索树性质,即任意子树对应的节点在 T T 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 a1,b1 步之后,到的点为 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) 的影响:

  • u < v uu<v 的情况非常简单: v v v G G G 中只有一条向 u u u 的边。
  • 考虑 u > v u>v u>v 的情况, v v v 被插入到了 v v v 的祖先中编号大小合适的位置。原来 C ( T ) C(T) C(T) 的链的两端不受影响,新增了一条从 u u u v v v 的链。

考虑用 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;
}

你可能感兴趣的:(数据结构——动态树/LCT,文章类型——题解)