线段树

关于动态区间询问问题的通解

某些相关类型的算法既然都能存在,那么一定有它自己的优势

线段树最大的劣势:1. 常数消耗 2.空间消耗

ST 表的核心问题在于不能修改

假设现在有 10w10w 个数字,如果只需要修改一个数字,在某些情况下我可能真的需要将整个 dp[100000][20]dp[100000][20] 重新进行计算

假设现在需要修改的数字为 a[i]a[i]
其实只需要修改包含了 a[i]a[i] 的区间即可
首先包含 a[i]a[i] 的区间只有可能出现在 a[i]a[i] 之前
dp[i][020]dp[i][0 \sim 20]
dp[i1][120]dp[i-1][1 \sim 20]
dp[i2][220]dp[i-2][2 \sim 20]
dp[i3][220]dp[i-3][2 \sim 20]
dp[i4][320]dp[i-4][3 \sim 20]
dp[i5][320]dp[i-5][3 \sim 20]
数量大概在 nlognnlogn 量级

为什么ST表修改很慢?因为涉及到 a[i]a[i] 的区间太多了
核心问题:区间重叠面积很大

也就是说我们希望让区间尽可能的 独立

倍增思想带来的另一个思想:分治 -> 二分

// 以倍增的思想逼近答案
int now = 0;
for (int i = 30; i >= 0; --i){
	if (check(now + (1 << i))){
		now += 1 << i;
		ans += value;	
	}	
}

将所有区间对半切成两个小区间,进行存储
按照这种方式进行区间的分割,一个数字 a[i]a[i] 最多影响 lognlogn 区间?

123

以这种形式构造区间,当需要修改 a[i]a[i] 的时候,实际需要修改的就是 [i,i][i,i] 的所有祖先,也就是从 [i,i][i,i] 区间一直到根节点的路径上所有节点

而由于这种建树方式,我们的树最多只有 lognlogn 层,那么单次修改的复杂度就是 O(logn)O(logn)

由于查询的区间必然是连续的
那么在树上的同一层中,必然最多只会选择两个节点
反证法:如果存在一层中出现选择了三个区间的情况,说明此时必然有一个区间在划分的时候,左右子区间都被选了,但是这种情况不如直接选择父区间
那么也就是说,一次询问最多可能包含的区间数是 <2logn<2logn,所以一次查询的复杂度就是 O(logn)O(logn)

树形结构一般怎么存?

  1. 当成图来存储 vector<int>G[maxn]vector<int>G[maxn]
  2. fatherfather 存树
  3. 按照标准数据结构的存法,对于一个节点,存储 lson,rson,fatherlson,rson,father
  4. 对于 二叉树 而言,有一种特殊的存法:把任意一棵二叉树当成完全二叉树进行存储,因为在完全二叉树中, ii 的孩子分别是 i2i*2i2+1i*2+1,所以此时可以直接用数组进行存储,a[i]a[i] 表示 ii 节点的信息

请问空间消耗?

int tree[4 * maxn]
2^0
2^1
2^2
2^3
2^4
...
2^(logn + 1)
求和 = 2^(logn + 2) - 1 
	= 2^2 * 2^(logn) - 1
	= 4 * 2^logn - 1
	= 4 * n - 1

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 100010;
const int INF = 0x3f3f3f3f;
int a[maxn], n, m;
int sumv[4 * maxn];
int minv[4 * maxn];
int maxv[4 * maxn];
// 当前区间编号为 id,对应区间 [l,r]
void build(int id, int l, int r){
	//递归到叶子结点结束
	if (l == r){
		sumv[id] = a[l];
		minv[id] = a[l];
		maxv[id] = a[l];
		return;
	}
	//大区间分成两个区间
	int mid = (l + r) / 2;
	//拆分左右两个子区间
	build(id * 2, l, mid); //左区间
	build(id * 2 + 1, mid + 1, r);//右区间
	sumv[id] = sumv[id * 2] + sumv[id * 2 + 1];
	minv[id] = min(minv[id * 2], minv[id * 2 + 1]);
	maxv[id] = max(maxv[id * 2], maxv[id * 2 + 1]);
}
//当前区间编号为 id,对应区间 [l,r]
//当前查询区间为 [x,y]
int query(int id, int l, int r, int x, int y){
	//如果当前区间整个被需要查询的区间包含
	//则直接返回区间信息
	if (x <= l && r <= y){
		return minv[id];
		return sumv[id];
		return maxv[id];
	}
	//如果当前区间没有被包含
	//则需要考虑左右子区间是否需要查询
	int mid = (l + r) / 2;
	int ans = INF;
	// 左区间 [l, mid]
	if (x <= mid){
		ans = min(ans, query(id * 2, l, mid, x, y));	
	}
	// 右区间 [mid + 1, r]
	if (y >= mid + 1){
		ans = min(ans, query(id * 2 + 1, mid + 1, r, x, y));	
	}
	return ans;
}
// 当前编号 id,对应区间 [l,r]
// 需要修改节点 x,修改值为 v
void update(int id, int l, int r, int x, int v){
	//更新到叶子结点结束
	if (l == r){
		a[l] = v;
		sumv[id] = v;
		minv[id] = v;
		maxv[id] = v;
		return ;	
	}
	//还没到叶子则分左右子区间更新
	//挑其中一个更新即可
	int mid = (l + r) / 2;
	if (x <= mid){
		update(id * 2, l, mid, x, v);	
	} else {
		update(id * 2 + 1, mid + 1, r, x, v);	
	}
	sumv[id] = sumv[id * 2] + sumv[id * 2 + 1];
	minv[id] = min(minv[id * 2], minv[id * 2 + 1]);
	maxv[id] = max(maxv[id * 2], maxv[id * 2 + 1]);
}
int main(){
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i){
		scanf("%d", &a[i]);	
	}
	build(1, 1, n);
	while (m--){
		int op, x, y;
		scanf("%d%d%d", &op, &x, &y);
		if (op == 1){
			printf("%d ", query(1, 1, n, x, y));	
		} else {
			update(1, 1, n, x, y);	
		}
	}
	return 0;
}