序列元素在线段树上的深度 感悟

0x01 前言

想法源于一道你谷的毒瘤题目。

这个方面的知识点好像挺新颖的。

于是和 JC 一起想出了该命题的 (O(n)) 解法。


0x02 算法本身

总所周知,线段树上的节点都对应表示的原序列里的一些结点。

而我们现在需要解决的问题就是:在极快的时间复杂度内求到每个原序列里的元素对应的元区间在线段树中的深度。

也就是求每个叶子节点的深度。

用线段树建树的朴素做法显然是:(O(nlogn))

但有些题目会比较恶心,于是我们考虑一种新的做法。

首先明确线段树的一个性质,如果树上有两个节点,且这两个结点表示区间长度相同,则处于相对位置相同的两个分别在这两个节点表示的区间中的原序列中的元素表示的元区间分别到这两个节点的距离相等。(好抽象 www。

于是我们将其剥离出来。

即有两个结点 (p,q),其中 (p) 表示区间 (A)(q) 表示区间 (B),且区间 (A) 的右端点为 (A_l),区间 (B) 的右端点为 (B_l),且记 (f(x)) 表示 (x) 为当前所在区间的第几个元素。

则对于任意两点 (m,n)(m in A,n in B, f(m) = f(n)),一定有 (p) 到表示 (m) 的元区间的叶子节点的距离等于一定有 (q) 到表示 (n) 的元区间的叶子节点的距离。

这其实很显然吧。。因为对于每个表示区间长度的节点,我们线段树往下划分的方式是不变的。

接下来,我们记 (dep(x)) 表示 (x) 这个元区间到根节点的距离,即表示 (x) 这个元区间的叶子节点的深度。(Dep(x)) 表示 (x) 这个节点的深度。

那么如果我们现在遍历到了一个节点 (Q),它表示的区间长度为 (len),而我们之前也遍历过一个表示区间长度为 (len) 的节点 (P),则定会有 (dep(x) = dep(y) - Dep(P) + Dep(Q) (x in Q,y in P, f(x) = f(y)))

这是因为我们有刚刚那个性质嘛,(x) 这个元区间对应的叶子节点的深度可以分解为这个节点到 (Q) 的距离和 (Q) 的深度。因为 (y) 的深度也可以同样分解,所以前者就等于 (dep(y) - Dep(p))

那么我们可以利用一个 dfs,遍历线段树上的节点,如果遇到一个节点且之前遇到过表示区间长度相同的节点,则我们可以直接用之前那个点对当前节点表示区间内的所有元素进行深度转移,然后这个分支就可以结束了。

因为有记忆化,且你会发现每个节点我们只会更新一次,于是这就是个类 (O(n)) 算法。


0x03 部分实现

// q 是一个结构体。
// flag 表示之前是否访问过。
// l 表示上一个访问过的表示区间的长度和当前的一样的节点表示的区间的左端点。
// x 表示上一个访问过的表示区间的长度和当前的一样的节点的深度。
// 按照刚刚推的式子模拟即可。
void Get_Dep(int l, int r, int cnt) {
	if(q[r - l + 1].flag) {
		for(int i = l; i <= r; i++)
			dep[i] = dep[i - l + q[r - l + 1].l] - q[r - l + 1].x + cnt;
		return ;
	}
	if(l == r) {
		dep[l] = cnt;
		return ;
	}
	int mid = (l + r) >> 1;
	Get_Dep(l, mid, cnt + 1);
	Get_Dep(mid + 1, r, cnt + 1);
	q[r - l + 1].flag = true;
	q[r - l + 1].l = l;
	q[r - l + 1].x = cnt;
}

0x04 应用场景

题目来源

这道题就可以用我们的思路进行预处理。

首先此题是求在线段树中从根到某一叶子节点经过路径权值和的期望。

朴素期望公式:一颗维护区间和的线段树,答案为每个节点表示的权值乘上每个节点的深度,然后在将它们全部加起来。

于是我们将每个节点的权值再返回到原序列中。

设原序列中元素 (x) 表示的元区间的深度为 (g(x)),其表示的数为 (v(x))

则原序列的每个元素会对答案产生的贡献为:(v(x) imes sum_{i = 1}^{g(x)} 2^i)

很显然当前这个元素在线段树种,会在其元区间到根的每一个节点表示的区间里出现。

其中 (sum_{i = 1}^{g(x)} 2^i) 显然可以用上述算法预处理出来。

那么考虑区修。设所改区间为 ([l, r])。增加量为 (x)

则这次修改对答案产生的贡献就是 (Δ imes sum_{x = l}^rsum_{i = 1}^{g(x)} 2^i)

那么再维护一个 (sum_{i = 1}^{g(x)} 2^i) 的前缀和不就结了吗?

(注,此题若用 double 会错掉一个点,可能与数据精度有关,建议直接使用 long long

#include <cstdio>

typedef long long LL;
int read() {
    int k = 1, x = 0;
    char s = getchar();
    while (s < '0' || s > '9') {
        if (s == '-')
            k = -1;
        s = getchar();
    }
    while (s >= '0' && s <= '9') {
        x = (x << 3) + (x << 1) + s - '0';
        s = getchar();
    }
    return x * k;
}

const int MAXN = 1e6 + 5;
int a[MAXN], dep[MAXN];
LL w[MAXN], sum[MAXN];

struct node {
	bool flag;
	int l, x;
	node() {}
	node(bool Flag, int L, int X) {
		flag = Flag;
		l = L;
		x = X;
	}
} q[MAXN];

void Get_Dep(int l, int r, int cnt) {
	if(q[r - l + 1].flag) {
		for(int i = l; i <= r; i++)
			dep[i] = dep[i - l + q[r - l + 1].l] - q[r - l + 1].x + cnt;
		return ;
	}
	if(l == r) {
		dep[l] = cnt;
		return ;
	}
	int mid = (l + r) >> 1;
	Get_Dep(l, mid, cnt + 1);
	Get_Dep(mid + 1, r, cnt + 1);
	q[r - l + 1].flag = true;
	q[r - l + 1].l = l;
	q[r - l + 1].x = cnt;
}

int main() {
	int n = read(), m = read(), qwq = read();
	Get_Dep(1, n, 1);	
	w[1] = qwq;
	for(int i = 2; i <= 23; i++) 
		w[i] = w[i - 1] + (qwq >> (i - 1));
	for(int i = 1; i <= n; i++) 
		sum[i] = sum[i - 1] + w[dep[i]];	
	LL ans = 0;	
	for(int i = 1; i <= n; i++) {
		a[i] = read();
		ans += (a[i] * (sum[i] - sum[i - 1]));		
	}	
	for(int i = 1; i <= m; i++) {
		int l = read(), r = read(), x = read();
		ans += ((sum[r] - sum[l - 1]) * x);
		printf("%lld
", ans);
	}
	return 0;
}