线段树
参考大神的博客写的:
大神详解(写的真的很好)
tree[i].l和tree[i].r
分别表示这个点代表的线段的左右下标,tree[i].sum
表示这个节点表示的线段和。
一颗二叉树,她的左儿子和右儿子编号分别是她*2
和她*2+1
,
建树
inline void build(int i,int l,int r){//递归建树
tree[i].l=l;tree[i].r=r;
if(l==r){//如果这个节点是叶子节点
tree[i].sum=input[l];
return ;
}
int mid=(l+r)>>1;
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 ;
}
一、简单(无pushdown)的线段树
1、单点修改,区间查询
我们总结一下,线段树的查询方法:
- 如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
- 如果这个区间的左儿子和目标区间有交集,那么搜索左儿子
- 如果这个区间的右儿子和目标区间有交集,那么搜索右儿子
区间查询(求区间的和)
inline int search(int i,int l,int r){
if(tree[i].l>=l && tree[i].r<=r)//如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
return tree[i].sum;
if(tree[i].r<l || tree[i].l>r) return 0;//如果这个区间和目标区间毫不相干,返回0
int s=0;
if(tree[i*2].r>=l) s+=search(i*2,l,r);//如果这个区间的左儿子和目标区间又交集,那么搜索左儿子
if(tree[i*2+1].l<=r) s+=search(i*2+1,l,r);//如果这个区间的右儿子和目标区间又交集,那么搜索右儿子
return s;
}
单点修改
然我们怎么修改这个区间的单点,其实这个相对简单很多,你要把区间的第dis位加上k。
那么你从根节点开始,看这个dis是在左儿子还是在右儿子,在哪往哪跑,
然后返回的时候,还是按照tree[i].sum=tree[i*2].sum+tree[i*2+1].sum
的原则,更新所有路过的点
inline void add(int i,int dis,int k){
if(tree[i].l==tree[i].r){//如果是叶子节点,那么说明找到了
tree[i].sum+=k;
return ;
}
if(dis<=tree[i*2].r) add(i*2,dis,k);//在哪往哪跑
else add(i*2+1,dis,k);
tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//返回更新
return ;
}
书写规范
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;
int n, a[N], opnum;
int op, pos, L, R, k;
struct segment_tree {
struct tree {
int l, r;
int lazy;
int sum;
}tr[N * 4];
void build(int i, int l, int r) { //创建一棵树,i是节点,l时左边界,r是右边界
tr[i].l = l, tr[i].r = r; // 该结点的区间[l , r];
if (l == r) { //当前为叶节点
tr[i].sum = a[l]; //叶节点存储的是单个数字且是a数组中的数字
return;
}
int mid = (l + r) >> 1;
build(i * 2, l, mid); // 递归构造左子树
build(i * 2 + 1, mid + 1, r); //递归构造右子树
tr[i].sum = tr[i * 2].sum + tr[i * 2 + 1].sum; //更新结点sum的值,是从下往上确定每个节点的sum的
}
inline void add(int i, int dis, int k) {//单点修改,i是节点,dis是修改的位置,k是增加或减少的值
if (tr[i].l == tr[i].r) { //判断到叶节点的时候说明找到了该数,该数字 + k;
tr[i].sum += k;
return;
}
if (dis <= tr[i * 2].r) add(i * 2, dis, k); //dis在哪里就往哪边靠
else add(i * 2 + 1, dis, k);
tr[i].sum = tr[i * 2].sum + tr[i * 2 + 1].sum; //从下往上重新确定路径上的节点的sum值
return;
}
inline int serch(int i, int l, int r) { //区间查询,i是节点,要求[l, r]内值的和
if (tr[i].l >= l && tr[i].r <= r) { //当节点的整个区间都被包含时就返回sum
return tr[i].sum;
}
else if (tr[i].l > r || tr[i].r < l) return 0; // 当节点区间和所求的区间没有任何关系时返回0
int s = 0;
if (tr[i * 2 + 1].l <= r) s += serch(i * 2 + 1, l, r); // 当节点的右区间有重合时,就递归往又找,同时累加s
if (tr[i * 2].r >= l) s += serch(i * 2, l, r); //当节点的左区间有重合时,同理操作
return s;
}
}ST;
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> opnum;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
ST.build(1, 1, n); // 建树
for (int i = 1; i <= opnum; i++) {
cin >> op;
if (op == 1) {
cin >> pos >> k;
ST.add(1, pos, k); // 单点修改
}
else {
cin >> L >> R;
int re = ST.serch(1, L, R); // 区间查询
cout << re << '\n';
}
}
}
2、区间修改,单点查询
区间修改和单点查询,我们的思路就变为:如果把这个区间加上k,相当于把这个区间涂上一个k的标记,然后单点查询的时候,就从上跑到下,把沿路的标记加起来就好。
这里面给区间贴标记的方式与上面的区间查找类似,原则还是那三条,只不过第一条:如果这个区间被完全包括在目标区间里面,直接返回这个区间的值变为了如果这个区间如果这个区间被完全包括在目标区间里面,将这个区间标记k
区间修改
void modify(int p, int l, int r, int k)
{
if(tr[p].l >= l && tr[p].r <= r) {
tr[p].num += k;
return ;
}
int mid = tr[p].l + tr[p].r >> 1;
if(l <= mid) modify(p << 1, l, r, k);
if(r > mid) modify(p << 1 | 1, l, r, k);
}
/*
inline void add(int i,int l,int r,int k){
if(tree[i].l>=l && tree[i].r<=r){//如果这个区间被完全包括在目标区间里面,讲这个区间标记k
tree[i].sum+=k;
return ;
}
if(tree[i*2].r>=l)
add(i*2,l,r,k);
if(tree[i*2+1].l<=r)
add(i*2+1,l,r,k);
}
*/
单点查询
就是dis在哪往哪跑,把路径上所有的标价加上就好了:
void query(int p, int x)
{
ans += tr[p].num;//一路加起来
if(tr[p].l == tr[p].r) return;
int mid = tr[p].l + tr[p].r >> 1;
if(x <= mid) query(p << 1, x);
else query(p << 1 | 1, x);
}
/*
void search(int i,int dis){
ans+=tree[i].sum;//一路加起来
if(tree[i].l==tree[i].r)
return ;
if(dis<=tree[i*2].r)
search(i*2,dis);
if(dis>=tree[i*2+1].l)
search(i*2+1,dis);
}
*/
书写规范
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 7;
int n, m, s, t;
int ans;
int a[maxn];
struct segment_tree
{
struct node
{
int l, r;
int num;
}tr[maxn * 4];
void build(int p, int l, int r)
{
tr[p] = {l, r, 0};
if(l == r) {
tr[p].num = a[l];
return ;
}
int mid = l + r >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
}
void modify(int p, int l, int r, int k)
{
if(tr[p].l >= l && tr[p].r <= r) {
tr[p].num += k;
return ;
}
int mid = tr[p].l + tr[p].r >> 1;
if(l <= mid) modify(p << 1, l, r, k);
if(r > mid) modify(p << 1 | 1, l, r, k);
}
void query(int p, int x)
{
ans += tr[p].num;
if(tr[p].l == tr[p].r) return;
int mid = tr[p].l + tr[p].r >> 1;
if(x <= mid) query(p << 1, x);
else query(p << 1 | 1, x);
}
}ST;
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
scanf("%d", &a[i]);
}
ST.build(1, 1, n);
for (int i = 1; i <= m; ++ i) {
int c;
scanf("%d", &c);
if(c == 1) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
ST.modify(1, x, y, z);
}
else {
ans = 0;
int x;
scanf("%d", &x);
ST.query(1, x);
printf("%d\n", ans);
}
}
return 0;
}
/*
int main()
{
n = 100;
for (int i = 1; i <= n; ++ i) {
a[i] = i;
}
ST.build(1, 1, n);
m = 10;
for (int i = 1; i <= m; ++ i) {
int l = 1, r = 100;
ST.modify(1, l, r, 10000);
ans = 0;
// query(p, x), p = 1, x 为想要查询的点的下标
ST.query(1, 50); // 单点查询 下标为 50 的点的值,ans = 50 + 10000 * i
cout << i << " " << ans << '\n';
ans = 0;
ST.query(1, 100); // 单点查询 ans = 100 + 10000 * i
cout << i << " " << ans << '\n';
}
return 0;
}
*/
二、进阶线段树(push_down)
1、区间修改,区间查询
因为如果对于1~4这个区间,你把1~3区间+1,相当于把节点1~2和3标记,但是如果你查询2~4时,你会发现你加的时没有标记的2节点和没有标记的3~4节点加上去,结果当然是错的。
你会想到,我们只需要在查询的时候,如果我们要查的2节点在1~2区间的里面,那我们就可以把1~2区间标记的那个+1给推下去这样就能顺利地加上了。
做法步骤:
1、如果当前区间被完全覆盖在目标区间里,讲这个区间的sum+k*(tree[i].r-tree[i].l+1)
2、如果没有完全覆盖,则先下传懒标记
3、如果这个区间的左儿子和目标区间有交集,那么搜索左儿子
4、如果这个区间的右儿子和目标区间有交集,那么搜索右儿子
push_down
inline void push_down(int p) {
if (tr[p].lazy != 0) { //如果父节点lazy标记不为0,既让lazy标记下传
tr[p * 2].lazy += tr[p].lazy;
tr[p * 2 + 1].lazy += tr[p].lazy;
int mid = (tr[p].l + tr[p].r) >> 1;
tr[p * 2].sum += tr[p].lazy * (tr[p * 2].r - tr[p * 2].l + 1);//改变两个子节点的sum
tr[p * 2 + 1].sum += tr[p].lazy * (tr[p * 2 + 1].r - tr[p * 2 + 1].l + 1);
tr[p].lazy = 0;//下传后自己的lazy标记变为0
}
}
区间修改
和无push_down版本思想差不多,就多了lazy的标记下传
inline void add(int p, int l, int r, int k) {
if (tr[p].l >= l && tr[p].r <= r) {
tr[p].sum += k * (tr[p].r - tr[p].l + 1);
tr[p].lazy += k; //完全包含就将lazy标记加上k用来之后传给子节点
return;
}
push_down(p); //如果没有完全找到就往下找,同时下传lazy和子节点的sum;
if (tr[p * 2].r >= l) add(p * 2, l, r, k); //哪边有交集就往哪边走;
if (tr[p * 2 + 1].l <= r) add(p * 2 + 1, l, r, k);
tr[p].sum = tr[p * 2].sum + tr[p * 2 + 1].sum;//自下向上更新sum
return;
}
区间查询
和无push_down版本思想差不多,就多了lazy的标记下传
inline int search(int p, int l, int r) {
if (tr[p].l >= l && tr[p].r <= r) { //如果完全包含就返回sum,并加在s上
return tr[p].sum;
}
if (tr[p].l > r || tr[p].r < l) return 0;
push_down(p);如果没有完全找到就往下找,同时下传lazy和子节点的sum;
int s = 0;
if (tr[p * 2].r >= l) s += search(p * 2, l, r);
if (tr[p * 2 + 1].l <= r) s += search(p * 2 + 1, l, r);
return s;
}
}ST;
区间修改和区间查询书写规范
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 7;
int n, m, x, y, k;
int a[N];
struct segment_tree {
struct tree {
int l, r, lazy, sum;
}tr[N * 4];
void build(int p, int l, int r) {
tr[p].l = l, tr[p].r = r;
if (l == r) {
tr[p].sum = a[l];
tr[p].lazy = 0;
return;
}
int mid = (l + r) >> 1;
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);
tr[p].sum = tr[p * 2].sum + tr[p * 2 + 1].sum;
return;
}
inline void push_down(int p) {
if (tr[p].lazy != 0) { //如果父节点lazy标记不为0,既让lazy标记下传
tr[p * 2].lazy += tr[p].lazy;
tr[p * 2 + 1].lazy += tr[p].lazy;
int mid = (tr[p].l + tr[p].r) >> 1;
tr[p * 2].sum += tr[p].lazy * (tr[p * 2].r - tr[p * 2].l + 1);//改变两个子节点的sum
tr[p * 2 + 1].sum += tr[p].lazy * (tr[p * 2 + 1].r - tr[p * 2 + 1].l + 1);
tr[p].lazy = 0;//下传后自己的lazy标记变为0
}
}
inline void add(int p, int l, int r, int k) {
if (tr[p].l >= l && tr[p].r <= r) {
tr[p].sum += k * (tr[p].r - tr[p].l + 1);
tr[p].lazy += k; //完全包含就将lazy标记加上k用来之后传给子节点
return;
}
push_down(p); //如果没有完全找到就往下找,同时下传lazy和子节点的sum;
if (tr[p * 2].r >= l) add(p * 2, l, r, k); //哪边有交集就往哪边走;
if (tr[p * 2 + 1].l <= r) add(p * 2 + 1, l, r, k);
tr[p].sum = tr[p * 2].sum + tr[p * 2 + 1].sum;//自下向上更新sum
return;
}
inline int search(int p, int l, int r) {
if (tr[p].l >= l && tr[p].r <= r) { //如果完全包含就返回sum,并加在s上
return tr[p].sum;
}
if (tr[p].l > r || tr[p].r < l) return 0;
push_down(p);如果没有完全找到就往下找,同时下传lazy和子节点的sum;
int s = 0;
if (tr[p * 2].r >= l) s += search(p * 2, l, r);
if (tr[p * 2 + 1].l <= r) s += search(p * 2 + 1, l, r);
return s;
}
}ST;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
ST.build(1, 1, n);
for (int i = 1, op; i <= m; i++) {
cin >> op;
if (op == 1) {
cin >> x >> y >> k;
ST.add(1, x, y, k);
}
else {
cin >> x >> y;
int re = ST.search(1, x, y);
cout << re << '\n';
}
}
}
三、乘法(根号/除法)线段树
1、乘法线段树
inline void push_down(long long i){ //注意这种级别的一定要开long long;
long long k = tree[i].lz;
tree[i<<1].sum = tree[i<<1] * k % p;
tree[i<<1|1].sum = tree[i<<1|1].sum * k % p;
tree[i<<1].lz = tree[i<<1].lz * k % p;
tree[i<<1|1].lz = tree[i<<1|1].lz * k % p;
tree[i].lz = 1;
return;
}
inline void push_down(long long i){//注意这种级别的数据一定要开long long
long long k1=tree[i].mlz,k2=tree[i].plz;
tree[i<<1].sum=(tree[i<<1].sum*k1+k2*(tree[i<<1].r-tree[i<<1].l+1))%p;//先乘后加
tree[i<<1|1].sum=(tree[i<<1|1].sum*k1+k2*(tree[i<<1|1].r-tree[i<<1|1].l+1))%p;
tree[i<<1].mlz=(tree[i<<1].mlz*k1)%p;
tree[i<<1|1].mlz=(tree[i<<1|1].mlz*k1)%p;
tree[i<<1].plz=(tree[i<<1].plz*k1+k2)%p; //加号标记也要先乘再加
tree[i<<1|1].plz=(tree[i<<1|1].plz*k1+k2)%p;
tree[i].plz=0;
tree[i].mlz=1;//注意乘法标记要变为1;
return ;
}
除了push_down ,其他和普通加法线段树做法几乎一样。
2、根号线段树/除法(整除)线段树
根号线段树和除法线段树一样,如果开根号或者除会出现精度问题,因为c++是向下取整的。
c++的除法是向下取整,很明显,(a+b)/k!=a/k+b/k
(在向下取整的情况下),而根号,很明显根号(a)+根号(b)!=根号(a+b)
我们对于每个区间,维护她的最大值和最小值,然后每次修改时,如果这个区间的最大值根号和最小值的根号一样,说明这个区间整体根号不会产生误差,就直接修改(除法同理)
其中,lazytage把除法当成减法,记录的是这个区间里每个元素减去的值。
1、根号线段树的区间修改
inline void push_down(int i){
if(!tree[i].lz) return ;
long long k = tree[i].lz;
tree[i*2].lz += k;
tree[i*2+1].lz += k;
tree[i*2].sum -= (tree[i*2].r-tree[i*2].l+1)*k;
tree[i*2+1].sum -= (tree[i*2+1].r-tree[i*2+1].l+1)*k;
tree[i*2].minn -= k;
tree[i*2+1].minn -= k;
tree[i*2].maxx -= k;
tree[i*2+1].maxx -= k;
tree[i].lz = 0;
return;
}
inline void Sqrt(int i,int l,int r){
if(tree[i].l>=l && tree[i].r<=r && (tree[i].minn-(long long)sqrt(tree[i].minn))==(tree[i].maxx-(long long)sqrt(tree[i].maxx))){//如果这个区间的最大值最小值一样
long long u=tree[i].minn-(long long)sqrt(tree[i].minn);//计算区间中每个元素需要减去的
tree[i].lz+=u;
tree[i].sum-=(tree[i].r-tree[i].l+1)*u;
tree[i].minn-=u;//就不用修改区间内其他的值缩短时间复杂度
tree[i].maxx-=u;
//cout<<"i"<<i<<" "<<tree[i].sum<<endl;
return ;
}
if(tree[i].r<l || tree[i].l>r) return ;
push_down(i);
if(tree[i*2].r>=l) Sqrt(i*2,l,r);
if(tree[i*2+1].l<=r) Sqrt(i*2+1,l,r);
tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//自下向上修改节点的sum
tree[i].minn=min(tree[i*2].minn,tree[i*2+1].minn);//维护最大值和最小值(也是自下而上的)
tree[i].maxx=max(tree[i*2].maxx,tree[i*2+1].maxx);
//cout<<"i"<<i<<" "<<tree[i].sum<<endl;
return ;
}
2、除法线段树的区间修改
和根号类似直接上代码
inline void push_down(int i){
if(!tree[i].lz) return ;
long long k = tree[i].lz;
tree[i*2].lz += k;
tree[i*2+1].lz += k;
tree[i*2].sum -= (tree[i*2].r-tree[i*2].l+1)*k;
tree[i*2+1].sum -= (tree[i*2+1].r-tree[i*2+1].l+1)*k;
tree[i*2].minn -= k;
tree[i*2+1].minn -= k;
tree[i*2].maxx -= k;
tree[i*2+1].maxx -= k;
tree[i].lz = 0;
return;
}
inline void Div(int i,int l,int r, int k){
if(tree[i].l>=l && tree[i].r<=r && (tree[i].minn-(tree[i].minn/k))==(tree[i].maxx-(tree[i].maxx / k))) {//如果这个区间的最大值最小值一样
long long u=tree[i].minn-(tree[i].minn / k);//计算区间中每个元素需要减去的
tree[i].lz+=u;
tree[i].sum-=(tree[i].r-tree[i].l+1)*u;
tree[i].minn-=u;//就不用修改区间内其他的值缩短时间复杂度
tree[i].maxx-=u;
//cout<<"i"<<i<<" "<<tree[i].sum<<endl;
return ;
}
if(tree[i].r<l || tree[i].l>r) return ;
push_down(i);
if(tree[i*2].r>=l) Div(i*2,l,r);
if(tree[i*2+1].l<=r) Div(i*2+1,l,r);
tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//自下向上修改节点的sum
tree[i].minn=min(tree[i*2].minn,tree[i*2+1].minn);//维护最大值和最小值(也是自下而上的)
tree[i].maxx=max(tree[i*2].maxx,tree[i*2+1].maxx);
//cout<<"i"<<i<<" "<<tree[i].sum<<endl;
return ;
}