专栏文章

不辛苦,命苦。

AT_abc417_g题解参与者 4已保存评论 4

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
4 条
当前快照
1 份
快照标识符
@miok1qtb
此快照首次捕获于
2025/12/02 20:29
3 个月前
此快照最后确认于
2025/12/02 20:29
3 个月前
查看原文
ABC417G 的题解。DAG 链剖分做法,可持久化平衡树不会。
首先考虑树的情况怎么做。
也就是说,每个点只作为至多一个点的 L,RL,R,这当然不可能,但是你不妨假设初始不是只有 S0S_0S1S_1,而是有很多个长度为 11 的字符串。
你发现这个东西肯定是个二叉树森林。每次就是问你一个点 uu 子树里的第 xx 个叶子是啥。
你一个自然的想法当然是从上往下二分,假设当前节点是 uu,如果 x>Left(u)x>\mathrm{Left}(u)xxLeft(u)x\leftarrow x-\mathrm{Left}(u),然后 uRson(u)u\leftarrow \mathrm{Rson}(u),否则 uLson(u)u\leftarrow \mathrm{Lson}(u)。这里 Left(u)\mathrm{Left}(u) 表示 uu 左儿子子树中的叶子数。
答案肯定是对的,但是你发现树不平衡,所以复杂度爆了。那一个自然的想法当然是想办法一次往下跳很多步。
这要求我们每个节点有一个后继,不构造出链状结构这个想法就没法成立。我们先起手一个 HLD 试试看,当然这里看重儿子的依据不是子树大小,而是叶子个数,复杂度也就差常数倍所以没关系。
HLD 完了那肯定就是二分 / 倍增。倍增好写一点,考虑倍增。对每个点 uu 维护 jump(u,k)\mathrm{jump}(u,k) 表示从 uu 出发,在重链上往下走 2k2^k 步到的节点,再维护一个 skip(u,k)\mathrm{skip}(u,k),表示这 2k2^k 步之中,所有下一步向右的节点 uu 的左子树叶子个数之和。
然后倍增跳就行了,注意能跳的 kk 必须满足
x>skip(u,k)xskip(u,k)+f(jump(u,k))x>\mathrm{skip}(u,k)\land x\le \mathrm{skip}(u,k)+\mathrm{f}(\mathrm{jump}(u,k))
其中 f(u)\mathrm{f}(u) 的意思是 uu 子树的叶子个数。这很容易理解,xskip(u,k)x-\mathrm{skip}(u,k) 这个叶子肯定要在跳完的 jump(u,k)\mathrm{jump}(u,k) 这个子树内。在每轮结束之后在手动把 uu 往左子树 / 右子树移一位。
然后就是分析复杂度。我们肯定是分析手动移位的个数,因为每轮倍增的代价是 O(logq)\mathcal O(\log q) 的。分析 xx 的变化,我们分成四类讨论:
  1. 手动向左,左侧为轻边。
  2. 手动向右,右侧为轻边。
这两类加起来肯定是 O(logn)\mathcal O(\log n)
  1. 手动向左,左侧为重边。
  2. 手动向右,右侧为重边。
显然,这两类根本不可能。因为如果我们还能在重链上往下走,刚才在倍增的时候这条边肯定就走过了。
综上,复杂度 O(qlog2q)\mathcal O(q\log^2 q)
然后考虑原问题,这实际上是一模一样的。
接下来说的所有路径,指的都是当前点出发,到达 0\texttt{0}1\texttt{1} 这两个初始字符串的路径。
终点指的也是这两个初始字符串。
我们要找到从 uu 出发,从第 xx 条小的到 0\texttt{0} 或到 1\texttt{1} 的路径。
这里的意思就是说,我们维护一个空的字符串 ss,每次往左走就在 ss 后增加一个 a\texttt{a},往右走就在 ss 后增加一个 b\texttt{b}。然后路径 P1\mathcal P_1 比路径 P2\mathcal P_2 小就是 P1\mathcal P_1 对应的字符串,字典序小于 P2\mathcal P_2 的字符串。
就是,左侧的所有路径小于右侧的所有路径。
f(u),Left(u)\mathrm{f}(u),\mathrm{Left}(u) 之类的都类似改成路径数就行了。
欸,问题是这个重链剖分咋办来着?注意到询问的 Xi1018X_i\le 10^{18},所以只要点 uu 的左后继 Lson(u)\mathrm{Lson}(u)f\mathrm{f}101810^{18} 大,我们就可以删掉 uu 的右后继了,它根本没用。
从而,每个点出发的路径总数就是 101810^{18} 的量级,而不是指数级。那当然就是把 DAG 链剖分拿出来了!我们把点 uu 的后继中 f\mathrm{f} 较大的那个称为重后继,从而不难发现每次走向一个轻后继f\mathrm{f} 至少减半。所有从每个点 uu 出发到达终点也至多会经过 O(logWi)\mathcal O(\log W_i) 条轻边。
那我们直接把上面树上的做法搬下来即可。附代码:
CPP
// 望んだ その星 塔の頂上で
// あなたの目を焼くのはひかり
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;

//{{{
template<typename Ta, typename Tb>
inline void chkmax(Ta &a, const Tb &b) {if (a < b)	a = b;}
template<typename Ta, typename Tb>
inline void chkmin(Ta &a, const Tb &b) {if (a > b)	a = b;}
constexpr int MEMORY_POOL = 1 << 20;
char BUFFER[MEMORY_POOL], *PS, *PT;
inline char gc() {
	if (PS == PT)	PT = (PS = BUFFER) + fread(BUFFER, 1, MEMORY_POOL, stdin);
	return PS == PT ? EOF : *PS++;
}
template<typename T>
inline void read(T &ans) {
	ans = 0;
	T w = 1;
	char c = gc();
	while (!isdigit(c)) {
		if (c == '-')	w = -1;
		c = gc();
	}
	while (isdigit(c)) {
		ans = (ans << 3) + (ans << 1) + (c ^ 48);
		c = gc();
	}
	ans *= w;
}
template<typename T, typename ...Ts>
void read(T &a, Ts&... other) {
	read(a);
	read(other...);
}
//}}}

constexpr int N = 5e5 + 10;
constexpr int K = 20;
constexpr u64 MAXV = 1e18 + 200;
std::array<int, 2> ch[N];
bool vis[N];
int jump[N][K];
u64 skip[N][K];
u64 f[N];
int n, q;

int main() {
#ifdef HeratinoNelofus
	freopen("input.txt", "r", stdin);
#endif
	f[1] = f[2] = 1;
	read(q);
	n = q + 2;
	for (int i = 3; i <= n; i++) {
		int l, r;
		i64 x;
		read(l, r, x);
		l += 1, r += 1;
		int u = i;
		if (f[l] >= MAXV)
			r = 0;
		ch[u][0] = l, ch[u][1] = r;
		f[u] = std::min(f[l] + f[r], MAXV);
		if (f[l] < f[r])
			jump[u][0] = r, skip[u][0] = f[l];
		else
			jump[u][0] = l;
		for (int k = 1; k < K; k++) {
			jump[u][k] = jump[jump[u][k - 1]][k - 1];
			skip[u][k] = skip[u][k - 1] + skip[jump[u][k - 1]][k - 1];
			chkmin(skip[u][k], MAXV);
		}

		while (u != 1 && u != 2) {
			for (int k = K - 1; k >= 0; k--)
				if (jump[u][k] && skip[u][k] < x)
					if (f[jump[u][k]] + skip[u][k] >= x)
						x -= skip[u][k], u = jump[u][k];
			if (u == 1 || u == 2)
				break;
			if (x <= f[ch[u][0]])
				u = ch[u][0];
			else
				x -= f[ch[u][0]], u = ch[u][1];
		}
		assert(x == 1);
		std::cout << u - 1 << '\n';
	}
}
其实真实情况是先条件反射 DAG 链剖,然后条件反射倍增,再想的树上的情况,就做完了。为啥写得这么长我也不懂,可能是退役若干个月完全忘记怎么说 OI 话了。

评论

4 条评论,欢迎与作者交流。

正在加载评论...