线段树算法讲解
问:给你一个长度为n的数组,然后询问m次,每次询问是给定两个数x,y。求在【x,y】区间的数之和。
也许你会说遍历,但若n为1e6,m为1e3呢?这是可以用一种数据结构——线段树。
线段树的每一个点维护一个[l,r)的区间。长这样:
图中,数组长度为6,第一个节点维护的是【1,6】的最大值,左节点维护的是【1,3】的最大值,以此类推,直至叶子节点。
下面,以求区间和为例来进一步说明。
定义:
const int maxn = 1e5 + 10;
struct node{
int l, r;//左右节点
int data;//存储的值
int mid()
{
return (l + r) >> 1;
}
}sum[maxn << 2];//一般开四倍空间
线段树的构建
void build(int root, int l, int r)
{
sum[root].l = l, sum[root].r = r;//给每个节点所维护的区间赋值
if(sum[root].l == sum[root].r)//到达叶子节点
{
sum[root].data = a[sum[root].l];
return;
}
int mid = sum[root].mid();
build(root << 1, l, mid);//左右递归建树
build(root << 1 | 1, mid + 1, r);
push_up(root);//精髓所在
}
观察代码,可以发现,线段树的构建是不断地递归直至叶子节点赋值。叶子节点赋值之后用push_up函数为其父亲节点赋值。
push_up函数
void push_up(int root)
{
sum[root].data = sum[root << 1].data + sum[root << 1 | 1].data;
}
单点修改
void update(int root, int p, int v)//p位置的值加v
{
if(sum[root].l == sum[root].r)//找到需要修改的节点
{
sum[root].data += v;
return;
}
int mid = sum[root].mid();
if(p <= mid)//若在左子节点内
update(root << 1, p, v);
else if(p > mid)//若在右子节点内
update(root << 1 | 1, p, v);
push_up(root);//更新父节点
}
单点修改情况下的查询
int query(int root, int l, int r)
{
int ans = 0;
if(sum[root].l >= l && sum[root].r <= r)
{
return sum[root].data;
}
int mid = sum[root].mid();
if(mid >= l)//若所查询区间和左子节点区间有交集
ans += query(root << 1, l, r);
if(mid + 1 <= r)//若和右子节点区间有交集
ans += query(root << 1 | 1, l, r);
return ans;
}
上面是单点修改的情况,下面讲解区间修改
在对某个区间修改时需要用到lazy标记。当使用lzay标记某个区间时,此区间之上的区间显示的内容是修改过的,但其子区间没有被修改,当需要用到子区间时会通过push_down函数进行修改,这样做主要是为了减少时间复杂度。
void update(int root, int l, int r,