【笔记】线段树合并

有了动态开点线段树,就有了这种东西

你在树上跑,是不是经常很想用权值线段树维护一些东西
然后马上觉得不行,你从这个子树下去对线段树一通操作,那你等会去另一个子树时被影响到了怎么办
然后你又想,如果每个子树(的根节点)都有一个线段树,然后合并起来不好吗
那这不是在扯淡吗,线段树开多大,合并的话还得全走一遍那怎么过的去
但是,
如果是动态开点的线段树的话
如果只进行点修改的话
如果修改规模等同于 N 的话
那就可以了
为什么?
首先是点修改:

void update(int &x, int l, int r, int p) {
	if (!x) x = ++tot;
	if (l==r) { sum[x]++; }
	else {
		int mid = (l + r) >> 1;
		if (mid>=p) update(lson, l, mid, p);
		if (mid< p) update(rson, mid+1, r, p);
		pushup(x);
	}
}

(这个写法比我原来的更好呢, &x 这个地方)
一次点修改是 log 的复杂度, k 个节点最坏就是 klogk
对于合并,我们把子节点 v 的线段树合并到父节点 u 上去:

ST.merge(rt[u], rt[v], 1, N);
int merge(int x, int y, int l, int r) {
	if (!x) return y;
	if (!y) return x;
	if (l==r) { sum[x] += sum[y]; return x; }
	else {
		int mid = (l + r) >> 1;
		ls[x] = merge(ls[x], ls[y], l, mid);
		rs[x] = merge(rs[x], rs[y], mid+1, r);
		pushup(x);
		return x;
	}
}

合并过程中,如果其中一个的子节点是 null ,那就直接连到另一个的儿子上去
如果不是,那么合二为一(要求维护信息可以合并),意味着我们删掉了一个节点,然后继续递归子区间做下去
那么最后的时空复杂度呢?
点修改次数 M , M 与 N 相当,那么空间复杂度最坏是 NlogN ,这里注意一下修改带的常数,比如树上差分什么的
考虑合并带来的复杂度,对于每个节点,它被“删”了就再也不会出现了;如果没有“删”,也就是出现了null,那就停住不走,所以说,我们可以大致把不删看成是一系列“删”的尾声,这是个常数;而“删”显然就是 NlogN
所以复杂度是优秀的 NlogN

一般的,我们都用值域线段树来合并,维护东西也比较有用

luoguP4556 【模板】线段树合并 :就是板子题

struct segmentTree {
	#define lson ls[x]
	#define rson rs[x]
	int tot, maxv[MAXN<<2], val[MAXN<<2], ls[MAXN<<2], rs[MAXN<<2];
	segmentTree() {
		memset(maxv, 0, sizeof(maxv));
		memset(val, 0, sizeof(val));
	}
	void pushup(int x) {
		if (maxv[lson]> maxv[rson]) {
			maxv[x] = maxv[lson], val[x] = val[lson];
		}
		if (maxv[lson]< maxv[rson]) {
			maxv[x] = maxv[rson], val[x] = val[rson];
		}
		if (maxv[lson]==maxv[rson]) {
			maxv[x] = maxv[lson], val[x] = val[lson];
		}
	}
	void update(int &x, int l, int r, int p, int v) {
		if (!x) x = ++tot;
		if (l==r) {
			maxv[x] += v, val[x] = l;
		} else {
			int mid = (l + r) >> 1;
			if (mid>=p) update(lson, l, mid, p, v);
			if (mid< p) update(rson, mid+1, r, p, v);
			pushup(x);
		}
	}
	int merge(int x, int y, int l, int r) {
		if (!x) return y;
		if (!y) return x;
		if (l==r) {
			maxv[x] += maxv[y], val[x] = l;
			return x;
		} else {
			int mid = (l + r) >> 1;
			ls[x] = merge(ls[x], ls[y], l, mid);
			rs[x] = merge(rs[x], rs[y], mid+1, r); // r not y! fuck!
			pushup(x); // wtf
			return x;
		}
	}
	void debug(int x, int l, int r) {
		printf("  (%d, %d), maxv=%d, val=%d\n", l, r, maxv[x], val[x]);
		if (l==r) return;
		else {
			int mid = (l + r) >> 1;
			if (lson) debug(lson, l, mid);
			if (rson) debug(rson, mid+1, r);
		}
	}
} ST;

另外我们一般需要用一个 rt[] 数组来记录树上每个节点自己的线段树的根节点是哪个,一般来说我们可以直接钦定:

for (int i=1; i<=N; i++) rt[i] = i; ST.tot = N;

你要一边遍历树一边钦定也行

例题
[POI2011]ROT-Tree Rotations :维护子树逆序对数,你要是知道线段树合并,逆序对怎么求自然就知道了

上一篇:1479D.Odd Mineral Resource(可持久化线段树+树上差分+随机算法)


下一篇:043-PHP简单获得一个类对应的反射信息