线段树算法学习
http://www.cnblogs.com/TenosDoIt/p/3453089.html#b
线段树的思想是 将一整个区间二分拆成多个不重合的2段区间,知道不能拆分为止。
例如对于数组[2, 5, 1, 4, 9, 3]可以构造如下的二叉树(背景为白色表示叶子节点,非叶子节点的值是其对应数组区间内的最小值,例如根节点表示数组区间arr[0...5]内的最小值是1):
空间消耗: 如果假定原数组的长度为n,那么线段树的节点数就设为4*n。 原理:假设n=2^h,则 从第0行(i=0)开始,第i行有2^i个节点,一共有h行,所以节点总数为1+2+4+8+...+2^h=2^(h+1)-1=2*2^h - 1 等于2n-1 , 这时候难道线段树的空间复杂度就是O(2n-1)吗? 不是的。 这里我们假设了n=2^h,但是题目中的n可没说一定是2的幂次,这导致最后一行有一些节点没有用到(如上图最后一行所示),不过这没有关系,我们可以多开辟一些空间,这不影响
这时我们可以找一个最小且满足n<=2^h 的h,这时候就可以说线段数的空间复杂度是(2*2^h - 1)了。
但是常用的并不是这个空间复杂度,而是4*n,这是为什么呢?
因为开辟空间的时候去判断最大的h为多少略显麻烦,事实上我们的空间不用这么精打细算,可不可以找到一个方便得到的“可行”的空间复杂度呢?
可以这样考虑:当我们找到一个最小且满足n<=2^h 的h时,即 2^(h-1) <= n <= 2^h , 所以我们得到 2^h <= 2n , 所以接着上面的结论 复杂度(2*2^h - 1)可以拓展成(2*2n - 1) 即(4*n)。
算法分几个部分:
const int maxn=2e2+9;
int arr[maxn];
struct seg //segtree下标从1开始
{
int val;
}segTree[4*maxn]; //4倍空间
1、build建树
void build(int node, int istart, int iend)
{
if(istart == iend)
segTree[node].val = arr[istart];
else
{
int mid=istart+(iend-istart)/2 ;
build(2*node, istart, mid);
build(2*node+1, mid+1, iend);
// 回溯计算当前节点的val
segTree[node].val = min(segTree[2*node].val, segTree[2*node+1].val);
}
}
2、查询指定区间的最值
//qstart qend 待查询区间起始位置
int query(int node, int nstart, int nend, int qstart, int qend)
{
if(qend < nstart || qstart > nend)
return INF; // 无效,置无穷大
if(qstart <= nstart && qend >= nend)
return segTree[node].val;
//即将用到子节点,更新子节点的add
return min( query(2*node, nstart, mid, qstart, qend),
query(2*node+1, mid+1, nend, qstart, qend));
}
3、单节点更新,这是线段树相比RMQ等算法的优势地方,他可以在O(logn)的时间复杂度下修改元素值并更新所有相关的最值。
增加某个元素值,并更新最值
//index 指定增加值的数组下标
void UpdateOne(int node, int nstart, int nend, int index, int addval)
{
if(nstart == nend)
{
if(nstart == index)
segTree[node].val += addval;
return ;
}
int mid=nstart+(nend-nstart)/2 ;
UpdateOne(2*node, nstart, mid, index, addval);
UpdateOne(2*node+1, mid+1, nend, index, addval);
// 自下向上回溯节点
segTree[node].val = min( segTree[2*node].val , segTree[2*node+1].val);
}
4、区间更新 这也是线段树的优势,并且,算法通过增加一个延迟更新标记的变量AddMark,大大减少了更新最值的时间复杂度。
考虑当我们需要修改了一个区间(a,b)所有元素的val时,理论上来说,需要更新所有子节点的val (segTree[node].val) 【父节点可以通过回溯更新】,但是如果这个区间的子节点比较多的时候,如果一次性全部更新完所有子节点,复杂度肯定是O( (b-a)lgn ),这复杂度是比较高的,往往也是不必要的,(后续程序不一定会用到这些子节点,可能查询到他们的父节点时已经完成查询)。我们可以 需要的用到这些子节点时候再更新这些子节点。 当我们找到一个节点p,并且决定考虑其子节点时,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。
因此需要在线段树结构中加入延迟更新标记,我的程序中是加入addmark,表示节点的子孙节点在原来的值的基础上加上addMark的值,同时还需要修改创建函数build 和 查询函数 query,修改的代码用红色字体表示,其中区间更新的函数为update,代码如下:
struct seg //segtree下标从1开始
{
int AddMark;
int val;
}segTree[4*maxn]; //4倍空间
void build(int node, int istart, int iend)
{
// 初始化addmark = 0
segTree[node].AddMark = 0;
if(istart == iend)
segTree[node].val = arr[istart];
else
{
int mid=istart+(iend-istart)/2 ;
build(2*node, istart, mid);
build(2*node+1, mid+1, iend);
// 回溯计算当前节点的val
segTree[node].val = min(segTree[2*node].val, segTree[2*node+1].val);
}
}
void pushDown(int node)
{
if(segTree[node].AddMark != 0)
{
// 子节点增加addmark
segTree[2*node].AddMark += segTree[node].AddMark;
segTree[2*node+1].AddMark += segTree[node].AddMark;
// 子节点增加value
segTree[2*node].val += segTree[node].AddMark;
segTree[2*node+1].val += segTree[node].AddMark;
// 当前节点addmark取消
segTree[node].AddMark = 0;
}
}
//qstart qend 待查询区间起始位置
int query(int node, int nstart, int nend, int qstart, int qend)
{
if(qend < nstart || qstart > nend)
return INF; // 无效,置无穷大
if(qstart <= nstart && qend >= nend)
return segTree[node].val;
//即将用到子节点,更新子节点的add
pushDown(node);
int mid=nstart+(nend-nstart)/2 ;
return min( query(2*node, nstart, mid, qstart, qend),
query(2*node+1, mid+1, nend, qstart, qend));
}
//index 指定增加值的数组下标
void UpdateOne(int node, int nstart, int nend, int index, int addval)
{
if(nstart == nend)
{
if(nstart == index)
segTree[node].val += addval;
return ;
}
int mid=nstart+(nend-nstart)/2 ;
UpdateOne(2*node, nstart, mid, index, addval);
UpdateOne(2*node+1, mid+1, nend, index, addval);
// 自下向上回溯节点
segTree[node].val = min( segTree[2*node].val , segTree[2*node+1].val);
}
//nstart nend 当前区间起始位置
//astart aend 指定的更新区间起始位置
void updateArea(int node, int nstart, int nend, int astart, int aend, int addval)
{
if(nstart > aend || nend < astart)
return ;
if(nstart >= astart && nend <= aend)
{
//先标记 不急着更新子节点的值,等需要用到子节点时再更新
segTree[node].AddMark += addval;
segTree[node].val += addval;
return ;
}
else
{
// 需要用到子节点了,调用pushdown更新
pushDown(node);
int mid=nstart+(nend-nstart)/2 ;
updateArea(2*node, nstart, mid, astart, aend, addval);
updateArea(2*node+1, mid+1, nend, astart, aend, addval);
segTree[node].val=min(segTree[2*node].val, segTree[2*node+1].val);
}
}
举个例子:
当我们要对区间[0,2]的叶子节点增加2,利用区间查询的方法从根节点开始找到了非叶子节点[0-2],把它的值设置为1+2 = 3,并且把它的延迟标记设置为2,更新完毕;当我们要查询区间[0,1]内的最小值时,查找到区间[0,2]时,发现它的标记不为0,并且还要向下搜索,因此要把标记向下传递,把节点[0-1]的值设置为2+2 = 4,标记设置为2,节点[2-2]的值设置为1+2 = 3,标记设置为2(其实叶子节点的标志是不起作用的,这里是为了操作的一致性),然后返回查询结果:[0-1]节点的值4;当我们再次更新区间[0,1](增加3)时,查询到节点[0-1],发现它的标记值为2,因此把它的标记值设置为2+3 = 5,节点的值设置为4+3 = 7;
其实当区间更新的区间左右值相等时([i,i]),就相当于单节点更新,单节点更新只是区间更新的特例
------------------------------以上----------------------------------------------------