线段树
1.线段树的概念
线段树是擅长处理区间的。线段树是一个完美的二叉树(所有的叶子的深度都相同,并且每个节点要么是叶子要么有2个儿子的树),树上的每个节点都维护一个区间。根维护的是整个区间,每个节点维护的是父亲的区间二等分后的其中一个子区间。当有n个元素时,对区间的操作可以在O(logn)的时间内完成。
根据节点中维护的数据的不同,线段树可以提供不同的功能。
2.基于线段树的RMQ的结构
下面要建立的线段树在给定数列a0,a1,….,an-1的情况下,可以在O(logn)时间内完成如下两种操作
*给定s和t,求as,as+1,…,at的最小值
*给定i和x,把ai的值改成x
线段树的每个节点维护对应区间的最小值。在建树时,只需要按照从上到下的顺序分别取左右儿子的值中的较小者就可以了。
3.基于线段树的RMQ的查询
像这样,即使查询的是一个比较大的区间,由于较靠上的节点对应较大的区间,通过这些区间就可以知道大部分值的最小值,从而只需要访问很少的节点就可以求得最小值。
要求某个区间的最小值,像下面这样递归处理就可以了。
*如果所查询的区间和当前节点对应的区间完全没有交集,那么就返回一个不影响答案的值(例如INT_MAX)。
*如果所查询的区间完全包含了当前节点对应的区间,那么久返回当前节点的值。
*以上两种情况都不满足的话,就对两个儿子递归处理,返回两个结果中的较小者。
4.基于线段树的RMQ的值的更新
在更新ai的值时,需要对包含i的所有区间对应的节点的值重新进行计算。在更新时,可以从下面的节点开始向上不断更新,把每个节点的值更新为左右两个儿子的值得较小者就可以了。
5.基于线段树的RMQ的实现
为了简单起见,在建立线段树时,把数列所有的值都初始化为INT_MAX。此外,querey的参数中不止传入节点的编号,还传入了节点对应的区间。
虽然从节点的编号也可以计算出对应的区间,但是把区间作为参数传入就可以节省这一步计算,为了简单起见,我们在实现中传入了对应的区间。
const int maxn = 1 << 17;
//存储线段树的全局数组
int n, dat[2 * maxn - 1];
//初始化
void init(int n_)
{
//为了简单起见,把元素个数扩大到2的幂
n = 1;
while (n < n_)
n *= 2;
//把所有的值都设为INT_MAX
for (int i = 0; i < 2 * n - 1; i++)
dat[i] = INT_MAX;
}
//把第k个值(0 - indexed)更新为a
void update(int k, int a)
{
//叶子节点
k += n - 1;
dat[k] = a;
//向上更新
while (k > 0){
k = (k - 1) / 2;
dat[k] = min(dat[k * 2 + 1], dat[k * 2 + 2]);
}
}
//求[a, b)的最小值
//后面的参数是为了计算起来方便而传入的
//k是节点的编号,l, r表示这个节点对应的是[l, r)区间
//在外部调用时,用query(a, b, 0, 0, n)
int query(int a, int b, int l, int r)
{
//如果[a, b)和[l, r)不相交,则返回INT_MAX
if (r <= a || b <= l)
return INT_MAX;
//如果[a, b)完全包含[l, r),则返回当前节点的值
if (a <= l && r <= b)
return dat[k];
else{
//否则返回两个儿子中值得较小者
int vl = query(a, b, k * 2 + 1, l, (l + r) / 2);
int vr = query(a, b, k * 2 + 2, (l + r) / 2, r);
return min(vl, vr);
}
}