线段树和树状数组是很常用的数据结构,用来处理区间问题,包括区间的修改和查询。
线段树
线段树,顾名思义,是将区间分成一段一段来进行区间操作。对于每个子节点,都表示整个序列的一段子区间;对于叶子节点而言,都表示序列中单个元素信息;子节点不断向自己的父亲节点传递信息,而父节点存储的信息是它每一个子节点信息的整合。
线段树可以维护支持结合律的数据,比如加和,乘法,最大/最小值。
如何进行分段?
考虑将区间$[l,r]$分成两半$[l,mid]、[mid+1,r]$,相当于将区间对半分,对于每个区间都这样分段,最终段数为$O(n\log n)$.
当我们对区间$[l,r]$进行操作时,从最大的区间开始,从此去找对应操作的区间。
- $[l,r]$包含在当前节点的左区间内,接着往左区间走
- $[l,r]$包含在当前节点的右区间内,接着往右区间走
- $[l,r]$跨过当前节点区间的中点,左边操作$[l,mid]$,右边操作$[mid+1,r]$
如此反复,直到找到的区间与操作区间一致,就进行操作。
线段树大致图像如下:
当涉及到区间的修改,如加减、乘除时,我们可以在找到的区间上打上懒标记($lazy tag$),这样不用每次往下遍历整棵线段树。在进行修改和查询时要注意懒标记的下传,还要向上更新节点维护的值。
线段树1
懒标记的优先级
当区间的修改包括了加和乘时,需要设置两个标记,$add$加标记、$mul$乘标记。而这时候需要注意优先级问题。
先进行乘法后进行加法。
如果我维护的值是$a$,我的懒标记有$+b$和$\times c$,可以发现$(a+b)\times c \neq a\times c+b$.
而在记录懒标记时,加法和乘法两种标记放到一起,需要确定一个优先级。
我们分析一下两种顺序:
- 先加后乘:$(a+b)\times c = a\times c+b\times c$
- 先乘后加:$a\times c+b$
上面先加后乘的式子相当于下面的式子,在加法上面多乘了一个$c$
所以,只要是先加后乘的式子,只要在$b$上$\times c$就可以转化为先乘后加的式子
具体操作是在添加乘法标记的时候,先将加法标记$\times c$即可
懒标记下传推导
在传递懒标记$pushdown$时
假设当前节点是$o$,$add[o]$是当前节点的加法标记,$mul[o]$是当前节点的乘法标记,$sum[o]$是当前节点维护的和,$ls$是左儿子的编号,$rs$是右儿子的编号。这里当前节点的值已经维护好,儿子还没维护好。
以左儿子为例,根据先乘后加的顺序,给左儿子乘上自己的乘法标记,再加上自己的加法标记。
$$sum[ls] = sum[ls]\times mul[o]+add[o]\times (r-l+1)$$
这样,左儿子的$sum$值就维护好了。那么如果儿子有懒标记呢?
如果儿子有懒标记,它的懒标记要维护一个值$s$,它维护后的值$s’$应该是
$$s’ = s\times mul[ls]+add[ls]$$
现在又要给它加上父节点$o$的懒标记,那么维护后的值应该是
$$
\begin{equation}\begin{split}
s’’&=s’\times mul[o] + add[o] \
&=(s\times mul[ls]+add[ls])\times mul[o]+add[o] \
&=s\times mul[ls]\times mul[o]+add[ls]\times mul[o]+add[o]
\end{split}\end{equation}
$$
如果$mul[ls]’,add[ls]’$是维护后的懒标记,我们就知道了懒标记应该怎么维护
$$mul[ls]’ = mul[ls]\times mul[o]$$
$$add[ls]’=add[ls]\times mul[o]+add[o]$$
在维护懒标记时,乘法标记乘以父节点的乘标记,加法标记先乘以父节点的乘标记,再加上父节点的加标记即可。
线段树2
模板
展开查看代码 >folded1 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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
| #include <iostream> #include <cstring> #include <cmath> #include <algorithm> #include <queue> #include <stack> #include <vector>
#define INF 0x3f3f3f3f using namespace std; typedef long long ll; const int maxn = 1e5 + 10;
inline ll read() { ll x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); } return x * f; } struct node { int l, r; ll sum, add, mul = 1; } tree[maxn << 2];
ll a[maxn]; ll n, m, p;
inline int ls(int pos) { return pos << 1; } inline int rs(int pos) { return pos << 1 | 1; }
void pushup(int pos) { tree[pos].sum = (tree[ls(pos)].sum + tree[rs(pos)].sum) % p; }
void build(int pos, int l, int r) { tree[pos].l = l, tree[pos].r = r; if (l == r) { tree[pos].sum = a[l] % p; return; } int mid = l + r >> 1; build(ls(pos), l, mid); build(rs(pos), mid + 1, r); pushup(pos); }
void pushdown(int pos) { if (tree[pos].mul != 1) { (tree[ls(pos)].mul *= tree[pos].mul) %= p; (tree[rs(pos)].mul *= tree[pos].mul) %= p; (tree[ls(pos)].add *= tree[pos].mul) %= p; (tree[rs(pos)].add *= tree[pos].mul) %= p; (tree[ls(pos)].sum *= tree[pos].mul) %= p; (tree[rs(pos)].sum *= tree[pos].mul) %= p; tree[pos].mul = 1; } if (tree[pos].add) { (tree[ls(pos)].add += tree[pos].add) % p; (tree[rs(pos)].add += tree[pos].add) % p; (tree[ls(pos)].sum += (tree[ls(pos)].r - tree[ls(pos)].l + 1) * tree[pos].add % p) %= p; (tree[rs(pos)].sum += (tree[rs(pos)].r - tree[rs(pos)].l + 1) * tree[pos].add % p) %= p; tree[pos].add = 0; } }
void updateAdd(int pos,int l,int r,int k) { if(tree[pos].l==l&&tree[pos].r==r){ (tree[pos].sum += (r-l+1) * k % p) %= p; (tree[pos].add += k) %= p; return; } pushdown(pos); int mid = tree[pos].l+tree[pos].r>>1; if(r<=mid) updateAdd(ls(pos),l,r,k); else if(l>mid) updateAdd(rs(pos),l,r,k); else updateAdd(ls(pos),l,mid,k),updateAdd(rs(pos),mid+1,r,k); pushup(pos); }
void updateMul(int pos,int l,int r,int k) { if(tree[pos].l==l&&tree[pos].r==r){ (tree[pos].sum *= k) %= p; (tree[pos].add *= k) %= p; (tree[pos].mul *= k) %= p; return; } pushdown(pos); int mid = tree[pos].l+tree[pos].r>>1; if(r<=mid) updateMul(ls(pos),l,r,k); else if(l>mid) updateMul(rs(pos),l,r,k); else updateMul(ls(pos),l,mid,k),updateMul(rs(pos),mid+1,r,k); pushup(pos); }
ll query(int pos,int l,int r) { if(tree[pos].l==l&&tree[pos].r==r){ return tree[pos].sum; } pushdown(pos); int mid = tree[pos].l+tree[pos].r>>1; if(r<=mid) return query(ls(pos),l,r); else if(l>mid) return query(rs(pos),l,r); else return (query(ls(pos),l,mid)+query(rs(pos),mid+1,r))%p; }
int main() { cin>>n>>m>>p; for(int i=1;i<=n;i++) a[i] = read(); build(1,1,n); while(m--) { int opt; ll x,y,k; opt = read(),x = read(),y = read(); if(opt==1){ k = read(); updateMul(1,x,y,k); } else if(opt==2){ k = read(); updateAdd(1,x,y,k); } else if(opt==3){ printf("%lld\n",query(1,x,y)); } } return 0; }
|
树状数组
单点修改,区间查询
树状数组用于维护前缀和,可以单点修改。
大致图像如下
每个节点维护值的长度是该节点二进制最低位1代表的值。
树状数组中$lowbit$函数是得到一个数的最低位1代表的值,比如$lowbit(5)=1,lowbit(4)=4$.
如果改变$x$位置的值,就加上该位置的$lowbit$,一直加到$n$,就维护了树状数组。
查询时反过来,从$x$位置开始,减去当前位置的$lowbit$,一直减到$1$,就得到$x$位置的前缀和。
树状数组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
| int a[maxn],n,m; ll tree[maxn];
int lowbit(int x) { return x & (-x); }
void add(int x,int k) { for(int i=x;i<=n;i+=lowbit(i)) tree[i] += k; }
ll query(int x) { ll ans = 0; for(int i=x;i;i-=lowbit(i)) ans += tree[i]; return ans; }
int main() { cin>>n>>m; for(int i=1;i<=n;i++) a[i] = read(); for(int i=1;i<=n;i++) add(i,a[i]); int opt,x,y,k; while(m--) { opt = read(); if(opt==1) { x = read(),k=read(); add(x,k); } else if(opt == 2) { x = read(),y = read(); printf("%d\n",query(y)-query(x-1)); } } return 0; }
|
区间修改,单点查询
区间修改用到差分数组的知识,若原数组为$a$,其差分数组为$b$,则$a[i] = b[1]+b[2]+\cdots +b[i]$.
若要修改原数组$[l,r]$区间上的值,比如都加$2$,只需要在差分数组中$b[l]$位置$+2$,$b[r+1]$位置$-2$即可。
由于树状数组维护前缀和,所以用树状数组维护原数组,用差分数组建树状数组,查询时直接查询即可。
树状数组2
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
| int a[maxn],n,m; ll tree[maxn];
int lowbit(int x) { return x & (-x); }
void add(int x,int k) { for(int i=x;i<=n;i+=lowbit(i)) tree[i] += k; }
ll query(int x) { ll ans = 0; for(int i=x;i;i-=lowbit(i)) ans += tree[i]; return ans; }
int main() { cin>>n>>m; for(int i=1;i<=n;i++) a[i] = read(); for(int i=1;i<=n;i++) add(i,a[i]-a[i-1]); int opt,x,y,k; while(m--) { opt = read(); if(opt==1) { x = read(),y = read(),k=read(); add(x,k); add(y+1,-k); } else if(opt == 2) { x = read(); printf("%d\n",query(x)); } } return 0; }
|
总结
虽说只是模板,但是线段树这两题还是写了很久,代码量巨大。不过也比之前理解地更清楚了,虽说线段树是很有用的工具,但对于我来讲估计用上它的概率不大,用上的也写不出(太菜了)。