【SD集训】20230417 T2 b(b) 【大联盟】20230531 T3 树上查询(tree)

b

两个密码不知道设哪个?那就不设了吧,还据说是原题。

题目描述

给定 \(n\) 个顶点的树,顶点编号为 \(1,2,\cdots,n\),给定长度 \(n_0\) 的序列 \(a_1,a_2,\cdots, a_{n_0}\),共 \(m\) 次查询,每次查询给定 \(l, r, x\),问树的顶点 \(x\),依次向 \(a_l,a_{l+1},\cdots,a_r\) 移动一步,到达的顶点。

\(x=y\),则从顶点 \(x\)\(y\) 移动一步到达 \(y\),否则到达与 \(y\) 在树上相邻且距离 \(y\) 最近的位置。

数据范围:\(1\le n,n_0,m\le 10^5\)

题解

很 lxl 的题。

解法 \(1\):官方做法

首先对操作序列分块,块长为 \(B\)。考虑预处理出 \(f_{i,j}\) 表示经过第 \(i\) 个块内的操作,本来位于 \(j\) 的点,之后在哪里。

如何求呢?首先,由于一个块内操作序列的点只有 \(B\) 个,所以对这 \(B\) 个点建出虚树。不难发现,每次操作同一条虚边上的点移动方向相同,而且是整天向上、向下移动,于是考虑用 deque 维护,@帅气yuyue(雾。

考虑这若干点可能移动到同一个点上,于是需要快速支持合并,可以用链表来维护,即 deque <list <int>>

当然,并不是所有点都初始在虚树上,所以,以所以虚树上的点为源点跑 bfs,算出每个点距离最近的虚树上的点的距离,以及对应的点。

所以,预处理的复杂度就能做到 \(O(\frac{n n_0}{B}+nB)\)

考虑查询,首先散块的暴力查询可以使用长链剖分 \(O(1)\) 求树上 \(k\) 级祖先来求;而整块用预处理的值。

所以,查询的复杂度就能做到 \(O(m(B+\frac{n_0}{B}))\)

\(n,n_0,m\) 同阶,时间复杂度为 \(O(n\sqrt{n})\)

注意:list merge 的复杂度是 \(O(|A|+|B|)\)splice 的复杂度是 \(O(1)\) 但是常数巨大。所以 list 需要手写来卡常。

至于 deque 倒没有必要手写就能过了。

代码

记录

#include <bits/stdc++.h>
#define SZ(x) (int) x.size() - 1
#define all(x) x.begin(), x.end()
#define ms(x, y) memset(x, y, sizeof x)
#define F(i, x, y) for (int i = (x); i <= (y); i++)
#define DF(i, x, y) for (int i = (x); i >= (y); i--)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) { x = max(x, y); }
template <typename T> void chkmin(T &x, T y) { x = min(x, y); }
template <typename T> void read(T &x) {
	x = 0; int f = 1; char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
const int N = 1e5 + 10, B = 320;
int n, n0, m, a[N], dfn[N], rdfn[N], t[N], tot, fa[21][N], mx[N], son[N], dd[N], st[21][N], lg[N], dep[N], f[N], num[N], g[N], ff[N], pre[N / B + 5][N], id[N];
vector <int> v[N], vv[N], line[N];
int qq(int x, int y) {
	return dep[x] < dep[y] ? x : y;
}
void dfs(int x) {
	mx[x] = dep[x] = dep[fa[0][x]] + 1;
	for (int i: v[x]) {
		dfs(i);
		if (mx[i] > mx[son[x]]) {
			son[x] = i;
			mx[x] = mx[i];
		}
	}
}
void dfs3(int x) {
	dd[dfn[x] = ++tot] = x;
	st[0][tot] = fa[0][x];
	if (son[x]) dfs3(son[x]);
	for (int i: v[x])
		if (i != son[x]) dfs3(i);
	rdfn[x] = tot;
}
int lca(int x, int y) {
	if (x == y) return x;
	x = dfn[x]; y = dfn[y];
	if (x > y) swap(x, y);
	int k = lg[y - x++];
	return qq(st[k][x], st[k][y - (1 << k) + 1]);
}
queue <int> q;
int listval[N], nxt[N], listtot;
struct List {
	int l, r;
	List(int L = 0, int R = 0) {
		l = L; r = R;
	}
	void merge(List x) {
		if (!x.l) return;
		if (!l) {
			l = x.l;
			r = x.r;
		} else {
			nxt[r] = x.l;
			r = x.r;
		}
	}
};
List nwlist(int x) {
	listtot++; nxt[listtot] = 0; listval[listtot] = x;
	return {listtot, listtot};
}
deque <List> s[B * 2 + 5];
List emp, tanp[B * 2 + 5];
vector <pair <int, int>> bru[B + 5];
int vis[N];
void dfs2(int x) {
	for (int i: vv[x]) {
		dfs2(i);
		s[id[i]].resize(dep[i] - dep[x]);
		line[i].clear();
		int k = i;
		while (k != x) {
			g[k] = i, f[k] = 0, num[k] = k, q.push(k);
			line[i].push_back(k);
			k = fa[0][k];
		} reverse(all(line[i]));
	}
}
int ask(int x, int k) {
	if (!k) return x;
	int t = lg[k];
	x = fa[t][x]; k -= (1 << t);
	return fa[t][dd[dfn[x] + (1 << t) - k]];
}
int jump(int x, int y) {
	if (x == y) return x;
	if (dfn[x] <= dfn[y] && dfn[y] <= rdfn[x]) return ask(y, dep[y] - dep[x] - 1);
	return fa[0][x];
}
signed main() {
	read(n); read(n0); read(m);
	F(i, 2, n) lg[i] = lg[i >> 1] + 1;
	F(i, 2, n) {
		read(fa[0][i]);
		v[fa[0][i]].push_back(i);
	} dfs(1);
	dfs3(1);
	F(i, 1, 20)
		F(j, 1, n)
			fa[i][j] = fa[i - 1][fa[i - 1][j]];
	F(i, 1, lg[n])
		for (int j = 1; j + (1 << i) - 1 <= n; j++)
			st[i][j] = qq(st[i - 1][j], st[i - 1][j + (1 << (i - 1))]);
	F(i, 1, n0) read(a[i]);
	line[1].push_back(1);
	g[1] = num[1] = 1;
	for (int l = 1, r, id = 1; l <= n0; l = r + 1, id++) {
		listtot = 0;
		r = min(l + B - 1, n0);
		int len = r - l + 1;
		F(i, l, r) t[i - l + 1] = a[i];
		t[++len] = 1;
		sort(t + 1, t + len + 1, [&] (int x, int y) {
			return dfn[x] < dfn[y];
		});
		int ttmp = len;
		F(i, 1, ttmp - 1) t[++len] = lca(t[i], t[i + 1]);
		sort(t + 1, t + len + 1, [&] (int x, int y) {
			return dfn[x] < dfn[y];
		});
		len = unique(t + 1, t + len + 1) - t - 1;
		F(i, 1, len) s[i].clear(), vv[t[i]].clear(), ::id[t[i]] = i;
		F(i, 1, len - 1) {
			int k = lca(t[i], t[i + 1]);
			vv[k].push_back(t[i + 1]);
			ff[t[i + 1]] = k;
		}
		s[1].resize(1);
		q.push(1);
		ms(f, 0x3f);
		f[1] = 0;
		dfs2(1);
		while (q.size()) {
			int x = q.front(); q.pop();
			for (int i: v[x])
				if (f[x] + 1 < f[i]) {
					f[i] = f[x] + 1;
					num[i] = num[x];
					q.push(i);
				}
		}
		F(i, 1, n)
			if (f[i] <= r - l) bru[f[i]].emplace_back(num[i], i);
			else pre[id][i] = ask(i, r - l + 1);
		F(i, l, r) {
			for (auto [j, k]: bru[i - l]) s[::id[g[j]]][dep[j] - dep[ff[g[j]]] - 1].merge(nwlist(k));
			bru[i - l].clear();
			int tt = a[i];
			while (tt) {
				vis[ff[tt]] = tt;
				tt = ff[tt];
			}
			F(j, 1, len)
				if (vis[t[j]] || t[j] == a[i]) {
					tanp[j] = s[::id[t[j]]].back();
					s[::id[t[j]]].pop_back();
					s[::id[t[j]]].push_front(emp);
				} else {
					tanp[j] = s[::id[t[j]]].front();
					s[::id[t[j]]].pop_front();
					s[::id[t[j]]].push_back(emp);
				}
			F(j, 1, len)
				if (vis[t[j]]) s[::id[vis[t[j]]]].front().merge(tanp[j]), vis[t[j]] = 0;
				else {
					if (t[j] == a[i]) s[::id[a[i]]].back().merge(tanp[j]);
					else s[::id[ff[t[j]]]].back().merge(tanp[j]);
				}
		}
		F(i, 1, len)
			F(j, 0, dep[t[i]] - dep[ff[t[i]]] - 1) {
				int kk = s[::id[t[i]]][j].l;
				while (kk) {
					pre[id][listval[kk]] = line[t[i]][j];
					kk = nxt[kk];
				}
			}
	}
	while (m--) {
		int l, r, x; read(l); read(r); read(x);
		int idl = (l - 1) / B + 1, idr = (r - 1) / B + 1;
		if (idl == idr) {
			F(i, l, r) x = jump(x, a[i]);
		} else {
			F(i, l, idl * B) x = jump(x, a[i]);
			F(i, idl + 1, idr - 1) x = pre[i][x];
			F(i, (idr - 1) * B + 1, r) x = jump(x, a[i]);
		} cout << x << '\n';
	}
	return 0;
}

解法 \(2\):1kri 的做法

mwr /bx

先考虑一条链的做法,我们记录一个询问现在到了哪个点,在 \(l\) 处插入询问,\(r\) 后单点查询。

每次是将所有编号 \(<a\) 的询问点编号 \(+1\)\(>a\)\(-1\)

可以平衡树分裂后打加法标记

拓展到树,考虑树剖。我们是将a祖先的询问点向下拉,其他点向上拉。

先考虑向上拉的过程,除了所有树链链顶节点,其他的都是将 \(dfn\)\(-1\)。我们对于这些点暴力。

再考虑向下拉的过程,除了 \(a\) 向上的 \(\log\) 条树链链顶的父亲,其他都是将 \(dfn\)\(+1\)。我们对于这些点暴力。

我们分析下复杂度:除了暴力的部分,每次是划分成 \(\log\)\(dfn\) 区间进行修改,而暴力的复杂度可以均摊,我们设势能函数为所有点上方轻边数之和。一次暴力上拉使势能 \(-1\),一次下拉使势能 \(-1\),但对于每个 \(a\) 只会下拉 \(\log\) 次。

下拉时要注意将一个点上挂的所有询问并查集合并,只留一个询问,要么势能变化量不是 \(1\)

总势能 \(O(n\log n)\),每次都是 \(O(\log n)\) 的平衡树操作,所以总时间复杂度 \(O(n\log ^ 2 n)\)

代码

太难写了,先咕了。

posted @ 2023-04-18 18:36  zhaohaikun  阅读(69)  评论(1)    收藏  举报