把以前学的知识都总结一下 。
线段树是一种很有用的数据结构,很多时候都会用到他。网上讲解线段树的博客很多,但质量不一,我放一篇我认为讲得很好的:https://www.cnblogs.com/TheRoadToTheGold/p/6254255.html
基础概念上面的博客已经讲得很好了,我主要讲一下具体代码实现。
首先,线段树是基于二分设计的数据结构,所以一定要先学好二分再学线段树。(说到二分,二分答案一定要学,很关键)
我们以线段树求和为例
先看建树过程:
build(1,1,n) ;
void build(int now,int ll,int rr) { tree[now].l = ll ; tree[now].r = rr ; if(ll == rr){ tree[now].w = read() ; return ; } // 如果是叶子节点就读入数据,并返回 int mid = (ll+rr)>>1 ; build(ls,ll,mid) ; build(rs,mid+1,rr) ; // 建左子树与右子树 tree[now].w = tree[ls].w + tree[rs].w ; // 该节点的值为其左右孩子的值之和 }
我们把每个节点的下标设为“ll,rr”,以每个节点的左右下标的平均值“mid”作为分界线,左子树下标为“ll - mid”,右子树下标为“mid+1 - rr” 。 ( 其中ls == now*2 , rs == now*2+1 )。
然后是单点修改:
change_point(1) ;
int x,y ; void change_point(int now) { if(tree[now].ll == tree[now].rr) // 如果是目标叶子节点就修改并返回 { tree[now].w = y ; return ; } down(now) ; // 懒标记下放 ,下面会讲到 int mid = (tree[now].ll+tree[now].rr)>>1 ; if(x <= mid) change_point(ls) ; // 如果目标位置在左子树就查询左子树 else change_point(rs) ; // 否则查询右子树 tree[now].w = tree[ls].w + tree[rs].w ; // 因为子树的值修改了,所以父亲节点的值也要修改 }
可能会有的疑问:为什么查询到叶子节点就一定是目标节点?
因为我们是从根节点开始查询,而后每一步也都是包含目标节点的子树才会继续被查找,所以查到一个叶子节点时他就一定目标节点。
再看单点查询:
void ask_point(int now) { if(tree[now].ll == tree[now].rr) { ans = tree[now].w ; return ; } down(now) ; int mid = (tree[now].l+tree[now].r)>>1 ; if(x <= mid) ask_interval(ls) ; else ask_interval(rs) ; }
(没啥好解释的)
接下来我们看区间查询
void ask_interval(int now) { if(tree[now].l >= a && tree[now].r <= b) // 该节点完全被包括在目标区间内,则累加并返回 { ans += tree[now].w ; return ; } down(now) ; int mid = (tree[now].l+tree[now].r)>>1 ; if(a <= mid) ask_interval(ls) ; // 左子树内有目标区间的部分,就查询 if(b > mid) ask_interval(rs) ; // 右子树同理 }
(这个应该也没啥好解释的)
接下来我们看重头戏——区间修改
首先我们想,怎么区间修改呢?
如果每个点都去单点修改,那么复杂度就是O(n^2 * log)的,这样就体现不出线段树的优势了。
所以我们就要引入 懒标记 。
那么什么是懒标记呢?
就是我们遇到一个完全位于目标区间内的节点时,因为他的左右子树也都要整个修改,但日后查询的时候又不一定用得上,所以我们就先不更改其左右子树的数据,而给该节点加上一个懒标记。等到日后用到他的子树的时候,再把懒标记内储存的信息释放下去。
看一下实现的具体过程:
inline void down(int now) { if(!tree[now].z) return ; // 该节点没有被标记就不修改 tree[ls].w += tree[now].z*(tree[ls].r-tree[ls].l+1) ; // 修改左子树的值 tree[rs].w += tree[now].z*(tree[rs].r-tree[rs].l+1) ; // 修改右子树的值 tree[ls].z += tree[now].z ; tree[rs].z += tree[now].z ; // 因为子树的子树还是没有被修改,所以要在子树上加上懒标记 tree[now].z = 0 ; // 懒标记下放完毕就清零 }
注意:乘的是父亲节点的懒标记,而不是子节点自己的懒标记。(想想为什么?(提示:从懒标记本身的含义出发))
最后是区间修改的过程:
void change_interval(int now) { if(tree[now].l >= a && tree[now].r <= b) { tree[now].w += y*(tree[now].r-tree[now].l+1) ; tree[now].z += y ; return ; } down(now) ; int mid = (tree[now].l+tree[now].r)>>1 ; if(a <= mid) change_interval(ls) ; if(b > mid) change_interval(rs) ; tree[now].w = tree[ls].w + tree[rs].w ; }
[a,b]为目标区间,y为要加上的数。
//
一般来说用数组实现线段树就够用了。但如果用指针的话有几个好处:1,常数小;2,支持可持久化(主席树之类的会用到)
所以学习一下指针写法也是很有必要的。思路和数组版的一样,只要理解线段树的基础概念代码应该就能看懂。
struct Node { int w,z ; Node *ls , * rs ; // * 是指针的意思 }pool[N<<1] ; // 因为节点数最多不超过叶节点个数的两倍,所以开两倍大小即可 Node *newNode() { // 新建立一个节点 static int cnt = 0 ; // static的意思是这是个全局变量,但只有这个函数能调用,防止发生冲突 return &pool[++cnt] ; } inline void pushup(Node *cur) { // 用子节点更新过后的信息更新父亲节点 cur->w = cur->ls->w + cur->rs->w ; } Node *build(int l,int r) { Node *cur = newNode() ; // 建立新节点 if(l < r) { int mid = (l+r)>>1 ; cur->ls = build(l,mid) ; cur->rs = build(mid+1,r) ; pushup(cur) ; } else cur->w = read() ; return cur ; } inline void pushdown(Node *cur,int l,int r) { if(!cur->z) return ; int mid = (l+r)>>1 ; cur->ls->w += cur->z * (mid-l+1) ; cur->rs->w += cur->z * (r-mid) ; cur->ls->z += cur->z ; cur->rs->z += cur->z ; cur->z = 0 ; } void change_point(Node *cur,int l,int r) { if(l == r) { cur->w += y ; return ; } pushdown(cur,l,r) ; int mid = (l+r)>>1 ; if(x <= mid) change_point(cur->ls,l,mid) ; else change_point(cur->rs,mid+1,r) ; pushup(cur) ; } void ask_point(Node *cur,int l,int r) { if(l == r) { printf("%d ",cur->w) ; return ; } pushdown(cur,l,r) ; int mid = (l+r)>>1 ; if(x <= mid) ask_point(cur->ls,l,mid) ; else ask_point(cur->rs,mid+1,r) ; } void change_interval(Node *cur,int l,int r) { if(l >= a && r <= b) { cur->w += y*(r-l+1) ; cur->z += y ; return ; } pushdown(cur,l,r) ; int mid = (l+r)>>1 ; if(a <= mid) change_interval(cur->ls,l,mid) ; if(b > mid) change_interval(cur->rs,mid+1,r) ; pushup(cur) ; } void ask_interval(Node *cur,int l,int r) { if(l >= a && r <= b) { ans += cur->w ; return ; } pushdown(cur,l,r) ; int mid = (l+r)>>1 ; if(a <= mid) ask_interval(cur->ls,l,mid) ; if(b > mid) ask_interval(cur->rs,mid+1,r) ; } int main() { n = read() ; Node *root = build(1,n) ; }
当然你也可以像数组版一样把每个节点的左右端点的信息储存在结构体里,我这里用了传参的方式记录端点信息。
//
// 因为指针版是后加的,所以代码风格稍有改变,但也差不多,不影响阅读。
那如果修改操作不仅有加,还有乘怎么办?
设置两个变量 ? 然后呢 ?
( 请自行思考,具体题目参见luogu P3373 )
提示:把修改看成(add,mul)的二元组问题,然后考虑先加后乘还是先乘后加(其中有一种会出现除法从而导致精度误差)
仅仅会敲模板可不够,还要会运用。下面看几道例题:
https://www.luogu.org/problemnew/show/P1311
正解好像不是线段树,不过线段树能过,而且跑得不慢。
下面粘一下我当时写的代码:
#include<iostream> #include<cstdio> #include<cstring> #include<vector> #define LL long long #define ls (now<<1) #define rs (now<<1)+1 using namespace std; const int N = 200000 + 10 ; int n,k,p; int aa,bb; vector<int>hh[52]; bool b[N]; bool flag; LL ans ; struct node{ int l,r; bool w; }tree[N<<2]; inline int read() { int k = 0 , f = 1 ; char c = getchar() ; for( ; !isdigit(c) ; c = getchar()) if(c == '-') f = -1 ; for( ; isdigit(c) ; c = getchar()) k = k*10 + c-'0' ; return k*f ; } void build(int now,int ll,int rr) { tree[now].l = ll ; tree[now].r = rr ; if(ll == rr) { tree[now].w = b[ll] ; return ; } int mid = (ll+rr)>>1 ; build(ls,ll,mid); build(rs,mid+1,rr); if(tree[ls].w || tree[rs].w) tree[now].w = 1 ; } void ask_interval(int now) { if(tree[now].l >= aa && tree[now].r <= bb) { if(tree[now].w) flag = 1 ; return ; } if(flag) return ; int mid = (tree[now].l+tree[now].r)>>1 ; if(aa <= mid) ask_interval(ls) ; if(bb > mid) ask_interval(rs) ; } int main() { n = read() ; k = read() ; p = read() ; int x,y; for(register int i=1;i<=n;i++) { x = read() ; y = read() ; hh[x].push_back(i) ; if(y <= p) b[i] = 1 ; } build(1,1,n); for(register int i=0;i<k;i++) { for(register int j=0;j<hh[i].size()-1;j++) { for(register int k=j+1;k<hh[i].size();k++) { flag = 0 ; aa = hh[i][j] ; bb = hh[i][k] ; ask_interval(1) ; if(flag) { ans += hh[i].size()-k ; break ; // cout<<aa<<" "<<bb<<endl; } } /* flag = 0 ; aa = hh[i][j-1] ; bb = hh[i][j] ; ask_interval(1) ; if(flag) { ans++ ; cout<<aa<<" "<<bb<<endl; } */ } } cout<<ans; return 0; }
(未完待续... ...)