我们在这篇博客里将具体介绍一种超级毒瘤超级高效的算法
线段树
首先来认识一下线段树
什么是线段树呢:
线段树是一种二叉树,也就是对于一个线段,我们会用一个二叉树来表示。比如说一个长度为6的线段,我们可以表示成这样

这个图是什么意思呢?
tree[rt].sum = tree[l].sum + tree[r].sumstruct node{
int l,r,sum;//l表示左儿子 r表示右儿子 sum表示当前节点存储的权值
}tree[maxn*4];
void build(int i,int l,int r){
tree[i].l = l;tree[i].r = r;
if(l == r){
tree[i].sum = a[l];//a数组存储给出的数组初始值
return;
}
int mid = (l+r)/2;
build(i*2,l,mid);
build(i*2+1,mid+1,r);
tree[i].sum = tree[i*2].sum+tree[i*2+1].sum;
return;
}
这就是线段树的建树方法 如果你要问为什么我们要花好几倍的内存去建树来完成一个数组就能完成的事情 那就是因为我们需要让这个超级大的数组去干一些比较困难的事情
那什么是比较困难的事情呢 让我们进入下个专题
for(int i = 1;i<=5;++i){ans+=a[i]};因此用代码怎么实现呢 也很简单
先让我们总结一下线段树的查询方式:
int search(int rt,int l,int r){
if(tree[rt].r < l ||tree[rt].l > r)return 0;
if(tree[rt].l >= l && tree[rt].r <= r)return tree[rt].sum;
int ans = 0;
if(tree[rt*2].r >= l)ans += search(2*rt,l,r);
if(tree[rt*2+1].l <= r)ans += search(2*rt+1,l,r);
return ans;
}
?那单点修改呢
?这个相对就简单许多了
void add(int rt,int x,int k){
if(tree[rt].l == tree[rt].r){//到达叶子节点 说明找到该位置
tree[rt].sum += k;
return;
}
if(x <= tree[rt*2].r)add(rt*2,x,k); // 递归搜索左儿子
else add(rt*2+1,x,k);//递归搜索右儿子
tree[rt].sum = tree[rt*2].sum + tree[rt*2+1].sum;//重新将权值加和
return;
}
区间修改和单点查询的方法有很多
为了一会对pushdown的讲解 我们这里说一种比较便于下面理解的方法
区间修改和区间查询很像
void build(int l,int r,int rt){
tree[rt].num=0;
tree[rt].l=l;
tree[rt].r=r;
if(l==r)
return ;
int mid=(r+l)/2;
build(l,mid,rt*2);
build(mid+1,r,rt*2+1);
}
void add(int rt,int l,int r,int k){
if(tree[rt].l>=l && tree[rt].r<=r){
tree[rt].num+=k;
return ;
}
if(tree[rt*2].r>=l)
add(rt*2,l,r,k);
if(tree[rt*2+1].l<=r)
add(rt*2+1,l,r,k);
}
单点查询可以去寻找这个节点 路上遇到的所有标记都要累加起来 最后再加上这个节点的初始值
用代码实现大概是这个样子
void search(int rt,int dis){
ans+=tree[rt].num;
if(tree[rt].l==tree[rt].r)
return ;
if(dis<=tree[rt*2].r)
search(rt*2,dis);
if(dis>=tree[rt*2+1].l)
search(rt*2+1,dis);
}
//主函数中
printf("%d\n",ans+a[x]);//a[x]为目标位置的初始值
建议将上面的板子打熟再向下继续观看
前方高难
看到这样的题你或许会想 不就是上边那两种放在一起吗
但是如果你真的这样写完了代码你会发现 WA
为什么呢
先来回想一下刚才的操作:将区间加上标记 最终查询的时候去从上往下找 将标记累加最后再加上初始值
但是这样真的可以吗?
答案是否定的 原因很简单:如果你要求1~3区间的和 而你刚刚将3~5的区间加上标记 因为1~3并不包含3~5的标记 所以我们计算后的结果并不是加k之后的和 而是初始值的和
那如何解决这个问题呢? 也很简单:只要将我们的标记k下放到i的儿子不就好了吗
所以我们的算法雏形就出来了(这也是线段树最毒瘤而且难调最具有魅力的地方)
tree[rt].sum += k*(tree[rt].r - tree[rt].l + 1)void pushdown(long long rt){
if(tree[rt].lazy != 0){//如果当前区间已经被标记
tree[rt*2].lazy += tree[rt].lazy;//下放到左儿子
tree[rt*2+1].lazy += tree[rt].lazy;//下放到右儿子
long long mid = (tree[rt].l + tree[rt].r)/2;
tree[rt*2].sum += tree[rt].lazy*(mid - tree[rt*2].l + 1);//更新左儿子的值
tree[rt*2+1].sum += tree[rt].lazy*(tree[rt*2+1].r - mid);//更新右儿子的值
tree[rt].lazy = 0;//清空当前节点的懒惰标记
}
return;
}
void add(long long rt,long long l,long long r,long long k){
if(tree[rt].l >= l && tree[rt].r <= r){//如果当前区间完全包含在目标区间直接更新并且标记懒惰标记
tree[rt].sum += k*(tree[rt].r-tree[rt].l+1);//更新当前区间的权值
tree[rt].lazy += k;//增加懒惰标记
return;
}
pushdown(rt);//下放懒惰标记
if(tree[rt*2].r >= l)add(rt*2,l,r,k);//递归更新左儿子
if(tree[rt*2+1].l <= r)add(rt*2+1,l,r,k);//递归更新右儿子
tree[rt].sum = tree[rt*2].sum+tree[rt*2+1].sum;//更新当前节点的权值
return;
}
区间查询的时候和之前几乎一样 不同的是要进行懒惰标记的下放之后在累加
long long search(long long rt,long long l,long long r){
if(tree[rt].l >= l && tree[rt].r <= r)return tree[rt].sum;//如果当前区间完全包含在目标区间内 直接返回当前区间的权值
if(tree[rt].r < l || tree[rt].l > r)return 0;//如果当前区间和目标区间完全没有关系 直接返回0
pushdown(rt);//下放懒惰标记
long long s = 0;
if(tree[rt*2].r >= l)s += search(rt*2,l,r);
if(tree[rt*2+1].l <= r)s += search(rt*2+1,l,r);
return s;//最后返回这个区间的和
}
线段树模型大概就是这个样子(线段树还是比较受出题人青睐的难道是因为难调??)
附上练习攻略:
简单线段树建议用洛谷P3374【模板】树状数组1
????????洛谷P3368【模板】树状数组2练习板子
如果简单线段树没有问题了
可以去尝试一下:洛谷P3372【模板】线段树1
????????洛谷P3373【模板】线段树2
????????洛谷P6242【模板】线段树3
谢谢观看
点个关注>_<
原文:https://www.cnblogs.com/2004-08-20/p/13197875.html