线段树
数据结构系列一下子跳到线段树了,私密马赛,最近太急于蓝桥杯比赛了,落后的教程一定会补上QAQ!
参考: 线段树 - OI Wiki
线段树是一种二叉搜索树、平衡二叉树,对于区间的修改、维护和查询时间复杂度优化为log级别。对区间不断做平分区间,直到划分到只有一个或多个数据的区间,可表示区间的和或者最小最大值。在进行更新区间操作时,通过小区间更新大区间。
对于下面的内容,我们主要针对于区间加法的线段树(即其节点表示区间之和)。
局限性:
问题需满足区间加法:对于[L,R]的区间,它的答案可以由[L,M]和[M+1,R]的答案合并求出。
不满足区间的众数、区间最长连续问题、最长不下降问题等。
建树
当我们拥有数组 并且用来区间求和之类的问题时,我们可以这样做:
以堆的方式实现存储(树形数组),将元素放至树的最下面一层。
由于我们使用树形数组,为了存储下数组 , 数组 的长度应当为 的四倍,并且初始节点位置为1。同时,当前区间[s,t]只有一个元素时(即s==t),意味着当前节点为叶子节点,就要让当前节点 。对于其它的情况,我们求得当前区间的中间数来区分左子树和右子树,并使用递归创建左子树和右子树,最后将左孩子和右孩子节点的值之和赋值给当前节点。
显然,初始建树时使用build(1,n,1)
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13
| const int N = 100000; int a[N]; int tree[4*N]; void build(int s,int t,int p){ if(s == t){ tree[p] = a[s]; }else{ int m = s+((t-s)>>1); build(s,m,p*2); build(s,m,p*2+1); tree[p] = tree[p*2]+tree[p*2+1]; } }
|
区间修改
我们假设目标区间为[l,r],当前区间为[s,t], 对目标区间的所有元素均加上值
由于是对目标区间的修改,很可能我们不用遍历完叶子节点,在当前区间刚好覆盖目标区间,或者目标区间由多个节点组成时,由于所有区间都是加上一个相同的值 ,我们可以修改结果为
修改完节点后记住要更新之前节点。
出于效率的需要,我们会用到懒惰标记 将此区间标记,表示这个区间的值已经更新,但它的子区间却没有更新,更新的信息就是标记里存的值。
区间修改步骤:
- 如果要修改的区间完全覆盖当前区间,直接更新这个区间,加上 标记(表示区间内元素加k)
- 如果没有完全覆盖,且当前区间有 标记,先下传 标记到子区间,再清除当前区间的标记
- 如果修改区间和左儿子有交集,搜索左儿子(避免死递归)
- 如果修改区间和右儿子有交集,搜索右儿子
- 最后将当前区间的值更新, 即节点的值为子节点值之和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int lazy[4*N]; void update(int l,int r,int k,int s,int t,int p){ if(l<=s && t<=r){ tree[p] += (t-s+1)*k; lazy[p] += k; }else{ int m = s+((t-s)>>1); if(lazy[p]){ tree[p*2] += (m-s+1)*lazy[p]; tree[p*2+1] += (t-m)*lazy[p]; lazy[p*2] += lazy[p]; lazy[p*2+1] += lazy[p]; lazy[p] = 0; } if(l<=m) update(l,r,s,m,1,p*2); if(r>m) update(l,r,m+1,t,p*2+1); tree[p] = tree[p*2] + tree[p*2+1]; } }
|
区间查询
我们想要得到 的区间和,和区间修改同理,在当前区间 被覆盖时,就返回对应值,否则就搜索当前节点的左孩子和有孩子(注意到,只有对应区间与目标区间有交集时才搜索),注意存在懒惰标记时需要下传。
步骤如下:
- 如果要查询的区间完全覆盖当前区间,直接返回当前节点的值
- 如果没有被完全包含当前区间,下传 标记
- 如果查询区间和左儿子有交集,搜索左儿子
- 如果查询区间和右儿子有交集,搜索右儿子
- 最后合并相关数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int getsum(int l,int r,int s,int t,int p){ if(l<=s && r<=t){ return tree[p]; }else{ int m = s + ((t-s) >> 1); if(lazy[p]){ tree[p*2] += (m-s+1)*lazy[p]; tree[p*2+1] += (t-m)*lazy[p]; lazy[p*2] += lazy[p]; lazy[p*2+1] += lazy[p]; lazy[p] = 0; } int sum = 0; if(l<=m) sum=getsum(l,r,s,m,p*2); if(r>m) sum+=getsum(l,r,s,m+1,p*2+1); return sum; } }
|
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| #include<bits/stdc++.h>
using namespace std; using ll = long long; const ll N = 1000000; ll a[N]; ll tr[4*N]; vector<ll> lazy(4*N,0);
void build(ll s,ll t,ll p){ if(s == t){ tr[p] = a[s]; return; } else{ ll m = s + ((t-s)>>1); build(s,m,p*2); build(m+1,t,p*2+1); tr[p] = tr[p*2] + tr[p*2+1]; } }
void update(ll l, ll r, ll c, ll s,ll t,ll p){ if(l <= s && t <= r){ tr[p] += c*(t-s+1); lazy[p] += c; return; }else{ ll m = s+((t-s)>>1); if(lazy[p]){ tr[p*2] += lazy[p]*((m-s+1)); tr[p*2+1] += lazy[p]*((t-m)); lazy[p*2] += lazy[p]; lazy[p*2+1] += lazy[p]; lazy[p] = 0; } if(l <= m) update(l,r,c,s,m,p*2); if(r > m) update(l,r,c,m+1,t,p*2+1); tr[p] = tr[p*2] + tr[p*2+1]; } }
ll getsum(ll l,ll r,ll s,ll t,ll p){ if(l <= s && t <= r){ return tr[p]; } ll m = s + ((t-s)>>1); ll sum = 0; if(lazy[p]){ tr[p*2] += lazy[p]*((m-s+1)); tr[p*2+1] += lazy[p]*((t-m)); lazy[p*2] += lazy[p]; lazy[p*2+1] += lazy[p]; lazy[p] = 0; } if(l <= m) sum = getsum(l,r,s,m,p*2); if(r > m) sum += getsum(l,r,m+1,t,p*2+1); return sum; }
int main(){ ll n,m; cin >> n >> m; for(ll i = 1; i <= n; i++){ cin >> a[i]; } build(1,n,1); while(m--){ ll op; cin >> op; if(op == 1){ ll x,y,k; cin >> x >> y >> k; update(x,y,k,1,n,1); }else if(op == 2){ ll x,y; cin >> x >> y; cout << getsum(x,y,1,n,1) << endl; } } }
|
实例
P3870 [TJOI2009] 开关 - 洛谷
对于这道题,我们知道:
- 区间修改不需要参数k,我们需要做的仅仅是将状态逆转,因此除了修改区间、当前区间、当前位置外,就不再需要别的参数
- 当灯开为1,灯关为0时,区间查询(区间求和)的结果就是答案。由于初始时,灯全部是关闭的,那么执行build函数时,只需要让 即可
同时,我们很容易发现对于区间 而言,区间灯开着的个数和灯关着的个数相加为 ,于是乎,对于第一种操作”逆转灯泡”而言,我们只需要执行 即可。同理可得下传懒惰标记时的处理方法。
另一种不需要数学的解法是,我们同时存储逆转后的值,记为, 在build时令其为1,在更新区间和下传标记时使用swap函数交换两种。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void modify(l,r,s,t,p){ if(l<=s&&t<=r){ tree[p] = (t-s+1)-tree[p]; } else{ int m = s+((t-s)>>1); if(lazy[p]){ tree[p*2] = (m-s+1) - tree[p*2]; tree[p*2+1] = (t-m) - tree[p*2+1]; lazy[p] = false; } } }
|
实际上,这道题的最关键点在于对懒惰标记的处理。
我们很容易想到,由于灯泡只有两种状态,那么懒惰标记应该可以用 和 表示, 表示该区间已逆转,当搜索当该区间时下传区间。当然,表示该区间未被逆转。
这是对的,但是在实际操作中我们也许会发现这样的错误:在更新区间的最后标记时,令其孩子节点的值为或者说。
实际上,当我们重复更新相同区间时, 之前已经为 的 会被再次被赋值为 , 但可能的值已经恢复原样了。同理,当我们下传懒惰标记时, 很可能其孩子节点已经为 , 从而造成了下传的混乱。我们很容易使用if条件解决,但为了方便起见,我们用0和1表示懒惰标记,并用异或操作^进行处理。
解决代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| #include<bits/stdc++.h>
using namespace std;
const int N = 1000000; int constract[4*N]; int tree[4*N]; vector<int> lazy(4*N,0);
void build(int s, int t, int p){ if(s == t){ tree[p] = 0; }else{ int m = s+((t-s)>> 1); build(s, m, p*2); build(m+1, t, p*2+1); tree[p] = tree[p*2] + tree[p*2+1]; } }
void modify(int l,int r,int s, int t,int p){ if(l<=s && t<=r){ tree[p] = (t-s+1)-tree[p]; lazy[p]^=1; } else{ int m = s+((t-s)>>1); if(lazy[p]){ tree[2*p] = (m-s+1) - tree[p*2]; tree[2*p+1] = (t-m) - tree[p*2+1]; lazy[p*2] ^= lazy[p]; lazy[p*2+1] ^= lazy[p]; lazy[p] = 0; } if(l<=m) modify(l,r,s,m,p*2); if(r>m)modify(l,r,m+1,t,p*2+1); tree[p] = tree[p*2] + tree[p*2+1]; } }
int getsum(int l, int r,int s, int t,int p){ int res = 0; if(l<=s && t<=r){ res = tree[p]; }else{ int m = s+((t-s)>>1);
if(lazy[p]){ tree[2*p] = (m-s+1) - tree[p*2]; tree[2*p+1] = (t-m) - tree[p*2+1]; lazy[p*2] ^= lazy[p]; lazy[p*2+1] ^= lazy[p]; lazy[p] = 0; } if(l<=m) res += getsum(l,r,s,m,p*2); if(r>m)res += getsum(l,r,m+1,t,p*2+1); } return res; }
int main(){ int n,m; cin >> n >> m; build(1,n,1); for(int c=0;c<m;c++){ int op,a,b; cin >> op >> a >> b; if(op == 0){ modify(a,b,1,n,1); }else if(op == 1){ cout << getsum(a,b,1,n,1) << endl; } } }
|