基本线段树及动态开点

前言

线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。

线段树可以在 O ( l o g N ) O(logN) O(logN) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。

简介

线段树将每个长度不为 1 1 1 的区间划分成左右两个区间递归求解,把整个线段划分为一个树形结构,通过合并左右两区间信息来求得该区间的信息。这种数据结构可以方便的进行大部分的区间操作。

线段树的结构如下:

基本线段树及动态开点_第1张图片

简单的来讲,对于线段树内任意一颗非叶子结点而言,他表示的区间是 [ l , r ] [l,r] [l,r] ,其左儿子表示的区间是 [ l , m i d ] [l, mid] [l,mid] ,其右儿子表示的区间是 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] ,其中 m i d = ⌊ l + r 2 ⌋ mid=\lfloor\frac{l+r}2\rfloor mid=2l+r

堆式存储

空间

如果 n n n 为叶子结点数量,那么数组要开 2 l o g ( n ) + 1 2^{log(n)+1} 2log(n)+1 这么大,如果懒得算就开直接 4n。

建树

考虑递归建树,dfs遍历一遍即可,要在回溯时pushup(即维护当前区间的值,因为下面更改了)。

区间修改

单点修改相当于 l = r l = r l=r 的情况 , 我就不啰嗦了。

可以考虑直接修改表示区间被 [ l , r ] [l,r] [l,r] 包含的所有节点。但如果这样做时间肯定爆,所以我们采用 l a z y t a g lazytag lazytag 技术。

就是懒标记拉。我们令 t a g [ k ] tag[k] tag[k] 表示节点 k k k 欠下其子树的更新值,区间修改时点到即止(即不去更新被完全包含的结点的子树),然后呢什么时候访问到这个结点再下传标记给它的子节点(注意修改时也要下传,因为如果不下传标记,子节点会pushup一个错误额值)。

我们可以记下传标记为pushdown,这个操作实在接着向下递归之前。

区间查询

直接将所有被 [ l , r ] [l,r] [l,r] 包含的区间合并起来即可,记得下传标记。

模板(区间和)

struct SegamentTree {
	static const int MAX = 1e5;
	struct TreeNode {
		ll val, tag;
	}tr[MAX<<2];
	inline void pushup(int &k) {
		tr[k].val = tr[k<<1].val + tr[k<<1|1].val;
	}
	inline void pushdown(int &k, int &l, int &r, int &mid) {
		if(tr[k].tag) {
			tr[k<<1].val += tr[k].tag * (mid-l+1), tr[k<<1|1].val += tr[k].tag * (r-mid);
			if(l < mid) tr[k<<1].tag += tr[k].tag, tr[k<<1|1].tag += tr[k].tag; // 叶子结点不需要标记
			tr[k].tag = 0;
		}
	}
	void build(int k, int l, int r) { 
    // k:结点编号 [l, r] 结点k的表示的区间。
		if(l == r) {
			tr[k].val = a[l];
			return;
		}
		int mid = l + (r - l >>1);
		build(k<<1, l, mid);
		build(k<<1|1, mid+1, r);
		pushup(k);
	}
	ll query(int k, int l, int r, int x, int y) { 
    // k, l, r 同上 [x, y] 查询区间
		if(x <= l && r <= y) // 如果被包含
			return tr[k].val;
		int mid = l + (r - l >> 1); ll res = 0;
		pushdown(k, l, r, mid);
		if(x <= mid) res = query(k<<1, l, mid, x, y); // 左区间与查询区间有交集
		if(y > mid) res += query(k<<1|1, mid+1, r, x, y); // 右区间与查询区间有交集
		return res;
	}
	void update(int k, int l, int r, int x, int y, int t) { 
    // k, l, r, x, y 同上 t 表示增加值
		if(x <= l && r <= y) { 
			tr[k].val += (r - l + 1) * t;
			if(l != r) tr[k].tag += t;
			return;
		}
		int mid = l + (r - l >> 1);
		pushdown(k, l, r, mid);
		if(x <= mid) update(k<<1, l, mid, x, y, t); 
		if(y > mid) update(k<<1|1, mid+1, r, x, y, t);
		pushup(k);
	}
};

标记永久化

标记永久化就是不上下传递标记,在不爆的前提下可以优化常数,也更加方便使用。

修改时,把所有不被修改区间完全包含和被修改区间一级包含(即第一个被枚举到的完全包含区间)结点累计上他总共能获得的贡献,然后在一级包含结点上打标记。

查询时,累计枚举到一级包含结点前的所有tag,返回 t a g s u m ∗ l e n tagsum*len tagsumlen ,其中 t a g s u m tagsum tagsum 指标记和, l e n len len 指的是这一节点表示的区间的长度。

Code

ll query(int k, int l, int r, int x, int y, ll sum) {
		if(x <= l && r <= y)
			return tr[k].val + sum * (r - l + 1);
		sum += tr[k].tag;
		int mid = l + (r - l >> 1); ll res = 0;
		if(x <= mid) res = query(k<<1, l, mid, x, y, sum);
		if(y > mid) res += query(k<<1|1, mid+1, r, x, y, sum);
		return res;
	}
	void update(int k, int l, int r, const int& x, const int& y, const int& t) {
		tr[k].val += (std::min(y, r) - std::max(x, l) + 1) * t; // 他应得的贡献
		if(x <= l && r <= y) {
			tr[k].tag += t;
			return;
		}
		int mid = l + (r - l >> 1);
		if(x <= mid) update(k<<1, l, mid, x, y, t);
		if(y > mid) update(k<<1|1, mid+1, r, x, y, t);
	}

动态开点

动态开点就是不一下开满整一棵树,用到哪开到哪。可以在结构体内建立ls和rs表示左右儿子分别存在哪,这里就不贴代码了。

你可能感兴趣的:(算法)