本博客同步更新至 https://startcraft.cn
介绍
线段树的应用场景在区间修改和区间查询上,修改和查询的时间复杂度都为O(log n)
线段树如它的名字一样将区间分成一段一段的,如123456789这一串数字,以区间求和为例
上图就是构建的线段树,其中结点里的值是该结点表示的区间的和,中括号里的数字代表表示的区间
实现(以区间求和为例)
定义
线段树的根结点定义为1,那么左子节点就是n*2,右子节点是n*+1
通常2*n在代码中写成 n<<1 。 2*n+1写成 n<<1|1 。
线段树所需的空间
足够的空间 = 数组大小n的四倍。
实际上足够的空间 = (n向上扩充到最近的2的某个次方)的两倍。
const int maxn = 100005;//元素的个数
ll tree[maxn << 2];//线段树的结点,大小开元素的4倍
ll add[maxn << 2];//标记数组(懒惰标记)区间修改会用到
建树
void push_up(int id)//根据子节点来更新父节点
{
tree[id] = tree[id << 1] + tree[id << 1 | 1];
}
void build(int l, int r, int id)//l,r表示当前区间,id表示当前结点的下标
{
if (l == r)//到叶子结点就赋值
{
tree[id] = num[l];
return ;
}
int mid = (l + r) >> 1;
build(l, mid, id << 1);//递归构建左子树
build(mid + 1, r, id << 1 | 1);//递归构建右子树
push_up(id);//当前结点的子节点已经构建完成,然后更新当前结点
}
单点修改
void update(int l, int r, int now, int number, int pos)
//l,r是当前区间,now是当前结点下标,number是要修改的数,pos是要修改的下标
{
if (l == r)//到叶子结点直接修改
{
tree[now] += number;
return ;
}
int mid = (l + r) >> 1;
if (pos <= mid)//判断要修改的点在左子树还是右子树,然后递归修改
update(l, mid, now << 1, number, pos);
else
update(mid + 1, r, now << 1 | 1, number, pos);
push_up(now);//当前结点的子节点已经更新,所以更新当前结点
}
区间修改
关于区间的修改需要用到标记数组,改数组的含义是"当前结点的值已经更新,但是子节点的值未更新"
如add[7]=3,代表结点7的值已经更新,但是结点7的子节点都还没有加上这个值,这就需要一个下推标记的函数
void push_down(int id, int ln, int rn)
//id代表当前结点,ln代表左区间的元素个数,rn代表右区间的元素个数
{
if (add[id] != 0)
{
tree[id << 1] += add[id] * ln;//更新左区间的值
tree[id << 1 | 1] += add[id] * rn;//更新右区间的值
add[id << 1] += add[id];//将标记下推,更新左区间标记
add[id << 1 | 1] += add[id];//更新右区间标记
add[id] = 0;//清空当前结点标记
}
}
修改
void update(int l, int r, int L