浅谈线段树
线段树
引入
线段树是较为常用的数据结构,一般用于维护区间信息。
线段树可以在 \(O(\log n)\) 的时间复杂度内实现单点修改,区间修改,区间查询等操作。
一般的在区间上进行操作的题目都可以考虑线段树。
普通线段树
基本思想
线段树,顾名思义,就是由线段组成的树。
我们结合图来理解一下:
最下方的是叶子结点,存放着原来数列的值,从上往下,可以看作是把整个序列的总和看作一条长线段,然后掰开放下去,一直掰倒不能再掰为止,也就是到了叶子节点。
所有点维护的都是当前端点的区间内的信息,比如上图是总和,而叶子结点比较特殊,是左右端点相等的区间。
这么好用的东西该怎么用呢?
修改操作的灵魂——lazy tag
我们从名字可以看出他是让我们偷懒用的。
我们在每一个结点都有一个对应的 lazytag
,他的具体作用就是存放当前的修改操作的值,比如说我区间加了一个值,如果要是当前结点代表的区间属于要修改的区间的话,我先存到这个里面,我先不下传,我只改这一个,这个时候我们的修改操作就大幅度加快了,在我们进行查询类的操作的时候,用到这个点的信息了,我们再下传给两个儿子结点,但也只是仅下传给两个儿子结点,因为可能儿子节点的信息是用不到的,所以我们还是秉持“能省就省”的原则,不去继续下传。
当然一个 lazytag
只能用于一个操作,比如让你同时进行区间加区间乘的话,还是老老实实开两个 lazytag
比较好。
变量与宏定义
#define int long long
#define N 1000100
#define rs x<<1|1
#define ls x<<1
using namespace std;
int n, m, a[N];
struct sb {int l, r, len, tag, sum;} e[N];
这个其实不是很重要,主要看个人喜好,这里为了防止对后面的代码理解出现问题才放上来,里面的 ls
是左儿子结点编号,rs
是右儿子结点编号。
结构体里面的 tag
就是 lazytag
后面会讲到,len
是当前区间的长度,l,r
是当前结点代表区间的左右端点,sum
代表的就是当前区间内的总和。
更新函数
这个按理来说是没有必要写个函数的,但是我喜欢写成函数。
inline void push_up(int x) { e[x].sum = e[ls].sum + e[rs].sum; }
inline void push_down(int x)
{
if(! e[x].tag) return ;
e[ls].tag += e[x].tag;
e[rs].tag += e[x].tag;
e[ls].sum += e[ls].len * e[x].tag;
e[rs].sum += e[rs].len * e[x].tag;
e[x].tag = 0;
return ;
}
这里的 push_up
函数就是用来更新当前点的 sum
值的,我们几乎在进行完所有操作都要调用一次,而在调用的时候,基本上都是进行完有关修改的操作的时候才进行。
push_down
函数是用来下传 lazytag
的,正如前文所说的,我们只下传到左右两个儿子即可,首先就是把当前点的 lazytag
的值给加到左右儿子的 lazytag
上,因为之前的左右儿子可能有没下传的 lazytag
;然后就是修改左右儿子的 sum
,具体就用到我们之前的 len
了,我们都知道累加的时候是整个区间每一个数都加,所以 sum
加的就是区间长度 \(\times\) 加上的值,最重要的一点就是我们要把当前下传完的点的 lazytag
给清空。
建树
我们上面打过一个比方,就是把一个序列给掰开,然后放下,再掰,一直到不能掰了为止。
这里我们是把大区间给不断从中间分开,一直分到不能再分,也就是到了叶子节点,也就是 \(l = r\) 的时候。
具体我们通过以下的代码来进行实现。
inline void build(int x, int l, int r)
{
e[x].l = l;
e[x].r = r;
e[x].len = r - l + 1;
if(l == r)return e[x].sum = a[l], void();
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
push_up(x);
return ;
}
其中的 push_up
函数就是利用更新完的子节点的总和更新当前节点的区间内的数的总和。
然后通过 build
函数我们得到了一棵树。
注意在这里我们可以直接把 l,r,sum
给处理出来,然后如果当前点代表的区间左右端点相等,说明当前点就是叶子节点,我们把对应的原序列的值赋给他。
区间求和
我们在求和的时候如果要是暴力的话肯定不行,考虑我们维护的 sum
有什么作用。
每一个结点的 sum
表示当前点代表的区间的所有数的总和,所以我们在查询的时候,也跟建树一样一半一半的往下传,如果传到一个点,他代表的区间属于当前我们要查的区间,我们就不必下传到叶子结点,直接返回当前点的 sum
即可。
inline int ask(int x, int nl, int nr)
{
if(nl <= e[x].l && nr >= e[x].r) return e[x].sum;
push_down(x);
int res = 0, mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) res += ask(ls, nl, nr);
if(nr > mid) res += ask(rs, nl, nr);
return res;
}
nl,nr
为要查询的区间的左右端点。
需要注意一点是,我们在每一次进行往下传的时候,我们需要先下传一下当前点的 lazytag
来保证左右儿子的值是修改完的。
对于当前要查询的区间的左端点小于等于 mid
就继续往下查询左儿子;当前要查询的区间的右端点大于 mid
的时候就继续往下查询右儿子。
区间修改
我们在修改的时候需要用到和区间求和一样的思想。
inline void add(int x, int nl, int nr, int v)
{
if(nl <= e[x].l && nr >= e[x].r)
{
e[x].tag += v;
e[x].sum += e[x].len * v;
return ;
}
push_down(x);
int mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) add(ls, nl, nr, v);
if(nr > mid) add(rs, nl, nr, v);
push_up(x);
return ;
}
先 push_down
一下是因为后面修改完是要 push_up
的,这样我们必须下传一下 lazytag
才能保证更新完以后的值是正确的。
我们在这里更新主要还是为了上面的查询操作。
到这里你就可以通过这道题目了:【模板】线段树 1 - 洛谷
完整 code:
#include <bits/stdc++.h>
#define int long long
#define N 1000100
#define rs x<<1|1
#define ls x<<1
using namespace std;
int n, m, a[N];
struct sb {int l, r, len, tag, sum;} e[N];
inline void push_up(int x) { e[x].sum = e[ls].sum + e[rs].sum; }
inline void push_down(int x)
{
if(! e[x].tag) return ;
e[ls].tag += e[x].tag;
e[rs].tag += e[x].tag;
e[ls].sum += e[ls].len * e[x].tag;
e[rs].sum += e[rs].len * e[x].tag;
e[x].tag = 0;
return ;
}
inline void build(int x, int l, int r)
{
e[x].l = l;
e[x].r = r;
e[x].len = r - l + 1;
if(l == r)return e[x].sum = a[l], void();
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
push_up(x);
return ;
}
inline void add(int x, int nl, int nr, int v)
{
if(nl <= e[x].l && nr >= e[x].r)
{
e[x].tag += v;
e[x].sum += e[x].len * v;
return ;
}
push_down(x);
int mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) add(ls, nl, nr, v);
if(nr > mid) add(rs, nl, nr, v);
push_up(x);
return ;
}
inline int ask(int x, int nl, int nr)
{
if(nl <= e[x].l && nr >= e[x].r) return e[x].sum;
push_down(x);
int res = 0, mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) res += ask(ls, nl, nr);
if(nr > mid) res += ask(rs, nl, nr);
return res;
}
signed main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
build(1, 1, n);
for(int i = 1; i <= m; i ++)
{
int op, x, y, z;
cin >> op;
if(op == 1)
{
cin >> x >> y >> z;
add(1, x, y, z);
}
if(op == 2)
{
cin >> x >> y;
cout << ask(1, x, y) << endl;
}
}
return 0;
}
区间乘 && 区间加
不能说是难,只能说很麻烦。
我们在之前提到过如果有多个操作的话一个 lazytag
是不够用的,所以我们需要多开一个来分别存放加和乘的 lazytag
。
我这里是用 tag1
来表示区间加的 lazytag
,用 tag2
来表示区间乘的 lazytag
。
我们需要更改的就是之前的 push_down
函数,因为我们相较上一个题目,只是多开了一个 lazytag
,所以我们这样写:
inline void push_down(int x)
{
if(e[x].tag1 == 0 && e[x].tag2 == 1) return ;
e[ls].tag2 = (e[ls].tag2 * e[x].tag2) % P;
e[rs].tag2 = (e[rs].tag2 * e[x].tag2) % P;
e[ls].tag1 = (e[ls].tag1 * e[x].tag2 + e[x].tag1) % P;
e[rs].tag1 = (e[rs].tag1 * e[x].tag2 + e[x].tag1) % P;
e[ls].sum = (e[ls].sum * e[x].tag2 % P + e[ls].len * e[x].tag1) % P;
e[rs].sum = (e[rs].sum * e[x].tag2 % P + e[rs].len * e[x].tag1) % P;
e[x].tag1 = 0;
e[x].tag2 = 1;
return ;
}
我们和之前的 push_down
函数的区别就是当前的多考虑了一个乘的 lazytag
,其实也就是多修改了一个 lazytag
,乘的就是直接乘上,但是对于加的 lazytag
需要先乘一下当前下传的点的 tag2
然后再加上 tag1
,这是因为如果要是当前点的 tag2
肯定是在儿子结点的 tag1
的后面的,因为我们在处理的时候遇到乘会直接先把 tag1
给乘上,这样我们就必须先让儿子结点的 tag1
给乘上当前结点的 tag2
,然后再加上当前结点的 tag1
就可以保证值是不变的。
再来看区间乘的操作函数:
inline void add2(int x, int nl, int nr, int v)
{
if(nl <= e[x].l && nr >= e[x].r)
{
e[x].tag1 = (e[x].tag1 * v) % P;
e[x].tag2 = (e[x].tag2 * v) % P;
e[x].sum = (e[x].sum * v) % P;
return ;
}
push_down(x);
int mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) add2(ls, nl, nr, v);
if(nr > mid) add2(rs, nl, nr, v);
push_up(x);
return ;
}
和区间加的操作函数的区别就是遇到当当前点代表的区间是属于要修改的区间的时候,我们就需要先把当前点的 tag1
给乘上要乘的数,因为很明显当前的乘的操作是在 tag1
的后面,所以里面的值也要乘上要乘的数,然后再对 tag2
进行修改,也就是直接乘上即可。
完整 code:
#include <bits/stdc++.h>
#define int long long
#define N 100100
#define rs x << 1 | 1
#define ls x << 1
using namespace std;
int n, m, a[N], P;
struct sb { int l, r, len, tag1, tag2 = 1, sum; } e[N << 2];
inline void push_up(int x) { e[x].sum = (e[ls].sum + e[rs].sum) % P; }
inline void push_down(int x)
{
if(e[x].tag1 == 0 && e[x].tag2 == 1) return ;
e[ls].tag2 = (e[ls].tag2 * e[x].tag2) % P;
e[rs].tag2 = (e[rs].tag2 * e[x].tag2) % P;
e[ls].tag1 = (e[ls].tag1 * e[x].tag2 + e[x].tag1) % P;
e[rs].tag1 = (e[rs].tag1 * e[x].tag2 + e[x].tag1) % P;
e[ls].sum = (e[ls].sum * e[x].tag2 % P + e[ls].len * e[x].tag1) % P;
e[rs].sum = (e[rs].sum * e[x].tag2 % P + e[rs].len * e[x].tag1) % P;
e[x].tag1 = 0;
e[x].tag2 = 1;
return ;
}
inline void build(int x, int l, int r)
{
e[x].l = l;
e[x].r = r;
e[x].len = r - l + 1;
if(l == r) return e[x].sum = a[l], void();
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
push_up(x);
return ;
}
inline void add1(int x, int nl, int nr, int v)
{
if(nl <= e[x].l && nr >= e[x].r)
{
e[x].tag1 = (e[x].tag1 + v) % P;
e[x].sum = (e[x].sum + e[x].len * v) % P;
return ;
}
push_down(x);
int mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) add1(ls, nl, nr, v);
if(nr > mid) add1(rs, nl, nr, v);
push_up(x);
return ;
}
inline void add2(int x, int nl, int nr, int v)
{
if(nl <= e[x].l && nr >= e[x].r)
{
e[x].tag1 = (e[x].tag1 * v) % P;
e[x].tag2 = (e[x].tag2 * v) % P;
e[x].sum = (e[x].sum * v) % P;
return ;
}
push_down(x);
int mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) add2(ls, nl, nr, v);
if(nr > mid) add2(rs, nl, nr, v);
push_up(x);
return ;
}
inline int ask(int x, int nl, int nr)
{
if(nl <= e[x].l && nr >= e[x].r) return e[x].sum;
push_down(x);
int res = 0, mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) res = (res + ask(ls, nl, nr) ) % P;
if(nr > mid) res = (res + ask(rs, nl, nr) ) % P;
return res % P;
}
signed main()
{
cin >> n >> m >> P;
for(int i = 1; i <= n; i ++) cin >> a[i];
build(1, 1, n);
for(int i = 1; i <= m; i ++)
{
int op, x, y, z;
cin >> op;
if(op == 1)
{
cin >> x >> y >> z;
add2(1, x, y, z);
}
if(op == 2)
{
cin >> x >> y >> z;
add1(1, x, y, z);
}
if(op == 3)
{
cin >> x >> y;
cout << ask(1, x, y) << endl;
}
}
return 0;
}
区间 max/min
没找到带修改操作的题目,或许是因为太毒瘤而且我也不会写。
我们建一下树,在建树的过程中只要把之前求和的 push_up
改成求 min
即可。
然后对于询问操作,我们就直接和之前的区间加一样,但是区别就是我们把累加改成求 min
。
code:
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
#define int long long
#define N 1001000
#define rs x << 1 | 1
#define ls x << 1
using namespace std;
int n, m, a[N];
struct sb { int l, r, maxn; } e[N];
inline void push_up(int x) { e[x].maxn = min(e[ls].maxn, e[rs].maxn); }
inline void build(int x, int l, int r)
{
e[x].l = l;
e[x].r = r;
if(l == r) return e[x].maxn = a[l], void();
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
push_up(x);
return ;
}
inline int ask(int x, int nl, int nr)
{
if(nl <= e[x].l && nr >= e[x].r) return e[x].maxn;
int res = INF, mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) res = min(res, ask(ls, nl, nr) );
if(nr > mid) res = min(res, ask(rs, nl, nr) );
return res;
}
signed main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
build(1, 1, n);
for(int i = 1; i <= m; i ++)
{
int l, r;
cin >> l >> r;
cout << ask(1, l, r) << " ";
}
return 0;
}
区间覆盖 && 区间加
单纯的区间覆盖没找到,所以用这道。
其实和区间乘 && 区间加的那道题目差不多。
用 tag1
表示区间加的 lazytag
,用 tag2
表示区间覆盖的 lazytag
。
我们在进行覆盖的修改函数的时候,我们就直接把当前点的 tag1
给清空成 \(0\) 即可,因为都被覆盖了,在那之前的操作也无意义了。
然后我们在处理的时候也是和区间乘一样先处理区间覆盖的 lazytag
然后再进行区间加的 lazytag
的修改。
code:
#include <bits/stdc++.h>
#define INF LONG_LONG_MAX
#define int long long
#define N 1000100
#define rs x << 1 | 1
#define ls x << 1
using namespace std;
int n, m, a[N];
struct sb {int l, r, len, sum, tag1, tag2 = -INF;} e[N << 2];
inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}
inline void push_up(int x) { e[x].sum = max(e[ls].sum, e[rs].sum); }
inline void push_down(int x)
{
if(e[x].tag2 != -INF)
{
e[ls].tag1 = 0;
e[rs].tag1 = 0;
e[ls].sum = e[ls].tag2 = e[x].tag2;
e[rs].sum = e[rs].tag2 = e[x].tag2;
e[x].tag2 = -INF;
}
if(e[x].tag1)
{
e[ls].tag1 += e[x].tag1;
e[rs].tag1 += e[x].tag1;
e[ls].sum += e[x].tag1;
e[rs].sum += e[x].tag1;
e[x].tag1 = 0;
}
return ;
}
inline void build(int x, int l, int r)
{
e[x].l = l;
e[x].r = r;
e[x].len = r - l + 1;
if(l == r) return e[x].sum = a[l], void();
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
push_up(x);
return ;
}
inline void add(int x, int nl, int nr, int v)
{
if(nl <= e[x].l && nr >= e[x].r)
{
e[x].tag1 += v;
e[x].sum += v;
return ;
}
push_down(x);
int mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) add(ls, nl, nr, v);
if(nr > mid) add(rs, nl, nr, v);
push_up(x);
return ;
}
inline void cover(int x, int nl, int nr, int v)
{
if(nl <= e[x].l && nr >= e[x].r)
{
e[x].tag1 = 0;
e[x].sum = e[x].tag2 = v;
return ;
}
push_down(x);
int mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) cover(ls, nl, nr, v);
if(nr > mid) cover(rs, nl, nr, v);
push_up(x);
return ;
}
inline int ask(int x, int nl, int nr)
{
if(nl <= e[x].l && nr >= e[x].r) return e[x].sum;
push_down(x);
int res = -INF, mid = (e[x].l + e[x].r) >> 1;
if(nl <= mid) res = max(res, ask(ls, nl, nr) );
if(nr > mid) res = max(res, ask(rs, nl, nr) );
return res;
}
signed main()
{
n = read();
m = read();
for(int i = 1; i <= n; i ++) a[i] = read();
build(1, 1, n);
for(int i = 1; i <= m; i ++)
{
int op = read(), l, r, x;
if(op == 1)
{
l = read(), r = read(), x = read();
cover(1, l, r, x);
}
if(op == 2)
{
l = read(), r = read(), x = read();
add(1, l, r, x);
}
if(op == 3)
{
l = read(), r = read();
cout << ask(1, l, r) << endl;
}
}
return 0;
}
区间开平方
区间开平方好像很难搞,因为这个东西一个区间的话没法用 lazytag
来搞。
我们知道开方这个东西是变化很快的,比如要是 \(10^{18}\) 的话,仅需要六次即可变成 \(1\)。
我们也知道,\(1\) 开完方还是 \(1\),所以我们可以利用这个特点来记录区间内的最大值,如果最大值是 \(1\) 了,那不就说明区间里的都是 \(1\) 了吗,那我们久完全可以跳过这里面的点了,而对于不是的直接暴力查找然后开方即可。
code:
#include <bits/stdc++.h>
#define int long long
#define N 5000100
#define rs (x << 1 | 1)
#define ls (x << 1)
using namespace std;
int n, m, a[N];
struct sb{int sum, maxn;}e[N << 2];
inline void push_up(int x) {e[x].maxn = max(e[ls].maxn, e[rs].maxn); e[x].sum = e[ls].sum + e[rs].sum;}
inline void build(int x, int l, int r)
{
if(l == r) return e[x].sum = e[x].maxn = a[l], void();
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
push_up(x);
return ;
}
inline void Sqrt(int x, int l, int r, int nl, int nr)
{
if(nl <= l && nr >= r && e[x].maxn == 1) return ;
if(l == r) return e[x].maxn = e[x].sum = sqrt(e[x].sum), void();
int mid = (l + r) >> 1;
if(nl <= mid) Sqrt(ls, l, mid, nl, nr);
if(nr > mid) Sqrt(rs, mid + 1, r, nl, nr);
push_up(x);
return ;
}
inline int ask(int x, int l, int r, int nl, int nr)
{
if(nl <= l && nr >= r) return e[x].sum;
int mid = (l + r) >> 1, res = 0;
if(nl <= mid) res += ask(ls, l, mid, nl, nr);
if(nr > mid) res += ask(rs, mid + 1, r, nl, nr);
return res;
}
signed main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
build(1, 1, n);
cin >> m;
for(int i = 1; i <= m; i ++)
{
int op, l, r;
cin >> op >> l >> r;
if(l > r) swap(l, r);
if(op == 0) Sqrt(1, 1, n, l, r);
if(op == 1) cout << ask(1, 1, n, l, r) << endl;
}
return 0;
}
其他操作
其他的操作比如区间次幂啥的都是恶心人的东西其实是我不会,当然线段树的题目都是将题目所需要的信息给维护一下,让时间复杂度更优。
到这里普通的线段树就告一段落了。
拓展——权值线段树
这个东西算是主席树的弱化版,不支持询问历史版本。
我们都知道桶排序,开一个数组记录和其下标相等的值的出现次数,权值线段树的思想也是这样。
比较经典的应用就是求逆序对。
我们考虑将数据排序并离散化后维护每一个数出现的次数。
我们在遍历到一个数的时候,先用 lower_bound
来查找位置,然后用 ask
函数来查找当前比他小的数出现了多少个,然后我们用 i-1-cnt
(cnt
为当前比他小的数的个数)来计算当前在序列里在当前数的前面但是比他大的数的个数,然后加到答案里,然后再把当前数插入即可。
code:
#include <bits/stdc++.h>
#define int long long
#define N 1000100
#define rs (x << 1 | 1)
#define ls (x << 1)
using namespace std;
int n, m, a[N], v[N], ans;
struct sb {int l, r, sum;} e[N << 2];
inline void push_up(int x) { e[x].sum = e[ls].sum + e[rs].sum; }
inline void build(int x, int l, int r)
{
e[x].l = l;
e[x].r = r;
if(l == r) return ;
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
return ;
}
inline void add(int x, int k)
{
if(e[x].l == e[x].r) { e[x].sum ++; return ; }
int mid = (e[x].l + e[x].r) >> 1;
if(k <= mid) add(ls, k);
else add(rs, k);
push_up(x);
return ;
}
inline int ask(int x, int nl, int nr)
{
// cout<<x<<endl;
if(nl <= e[x].l && nr >= e[x].r) return e[x].sum;
int mid = (e[x].l + e[x].r) >> 1, res = 0;
if(nl <= mid) res += ask(ls, nl, nr);
if(nr > mid) res += ask(rs, nl, nr);
return res;
}
signed main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i], v[i] = a[i];
sort(v + 1, v + n + 1);
m = unique(v + 1, v + n + 1) - v - 1;
build(1, 1, n);
for(int i = 1; i <= n; i ++)
{
int k = lower_bound(v + 1, v + m + 1, a[i]) - v;
int cnt = ask(1, 1, k);
// cout << "k:" << k <<endl;
// cout << "cnt:" << cnt << endl;
ans += (i - 1 - cnt);
add(1, k);
}
cout << ans << endl;
return 0;
}
优化空间——动态开点
我们通常的线段树开的空间是数据要求的四倍,这样才能保证不会出现不够用的情况,但是有的时候会爆空间,但是我们如果开小一两倍是不会爆的。
这个时候我们有可能开四倍有一堆是没用的,那么我们开这么多会显得很傻,这个时候就需要我们接下来要讲的动态开点线段树。
动态开点的意思就是我们不像前面说的一样嘎嘎开上四倍,然后一个 build
函数递归到底,而是当我们不需要这个点的时候,我们就先不开,等我们需要用了,我们直接新建一个点,这样就可以解决空间问题了。
但是有一个缺点就是左右儿子的编号不像之前那样有规律可以用 x<<1,x<<1|1
来表示,这个时候我们就可以用两个数组标记一下。
我们考虑一下如何用线段树来求这个方案数。
首先看到 \(10^{9}\) 的数据范围,就知道普通的线段树是会 MLE 的,所以我们用上刚刚新学的东西——动态开点线段树。
我们读到题目就知道是求符合要求的一段连续的子区间,所以我们可以直接先预处理出来前缀和,然后我们考虑从 \(x\sim i\) 的合法方案应该去哪里找
分开解一下就是:
那么我们想到之前的权值线段树,我们对于每一个前缀和都开一个点来维护个数,然后我们遍历到一个点先找大于等于 \(sum_{i} - R\) 且小于等于 \(sum_{i}-L\) 的点的个数,注意不能全插完再找,因为必须是连续的子序列,所以我们当前点只能和前面的前缀和组合。我们查询完以后就直接插入即可。
code:
#include <bits/stdc++.h>
#define int long long
#define N 1010000
using namespace std;
const int M = 1e10;
struct sb {int ls, rs, sum;} e[N * 6];
int n, rt, tot = 0, L, R, sum[N], ans;
inline int node()//动态开点
{
tot++;
e[tot].ls = e[tot].rs = e[tot].sum = 0;
return tot;
}
inline void push_up(int x) { e[x].sum = e[e[x].ls].sum + e[e[x].rs].sum; }
inline void add(int x, int k, int v, int l, int r)
{
if(l == r) return e[x].sum += v, void();
int mid = (l + r) >> 1;
if(k <= mid)
{
if(! e[x].ls) e[x].ls = node();
add(e[x].ls, k, v, l, mid);
}
else
{
if(! e[x].rs) e[x].rs = node();
add(e[x].rs, k ,v, mid + 1, r);
}
push_up(x);
return ;
}
inline int ask(int x, int nl, int nr, int l, int r)
{
if(nl <= l && nr >= r) return e[x].sum;
int mid = (l + r) >> 1, res = 0;
if(nl <= mid)
{
if(! e[x].ls) e[x].ls = node();
res += ask(e[x].ls, nl, nr, l, mid);
}
if(nr > mid)
{
if(! e[x].rs) e[x].rs = node();
res += ask(e[x].rs, nl, nr, mid + 1, r);
}
return res;
}
signed main()
{
cin >> n >> L >> R;//不能超出的左右区间
for(int i = 1; i <= n; i ++)
{
int x;
cin >> x;
sum[i] = sum[i - 1] + x;//处理前缀和
}
rt = node();
add(rt, sum[0], 1, -M, M);//先把树根给插入进去,在0的位置加一
for(int i = 1; i <= n; i ++)//遍历后面n个数
{
ans += ask(rt, sum[i] - R, sum[i] - L, -M, M);//累加答案
add(rt, sum[i], 1, -M, M);//算完就给插入进去
}
cout << ans << endl;
return 0;
}
拓展——标记永久化
此部分由 yi_fan0305 友情赞助!
我们在进行操作的时候需要下传 lazytag
,没有效率,所以有个聪明人又发明了可以小小优化一下常数的标记永久化。
好处:
- 码量小,不用写
push_down
和push_up
(其实我觉得比原来大了(?)。 - 在可持久化线段树上应用该技巧能做到区间修改的效果。
坏处:
- 适用范围有限,只有当求的东西满足区间贡献独立。比如区间加法。
区间最大值就无法标记永久化。 - 多标记好像也不适用。
当然我们一般是不用的因为局限性有些大。
我们在讲 lazytag
向下递归的过程中,如果当前区间正好等于查询区间,那就直接改 lazytag
和数值,倘若当前区间包含查询区间但不与查询区间相等,那我们只修改值,这些操作与线段树修改操作很像。
需要注意的是,如果查询的区间横跨左右两个孩子区间,那我们需要将查询区间也从 mid
处分开。
按照一般的写法,在向下递归时,我们还要用递归把 lazytag
也一起向下传递,而标记永久化则是舍弃了向下传递 lazytag
这个操作,我们在查询时设置一个值,用它来记录沿路的 lazytag
,最后一起统计即可。
为什么要记录沿路的 lazytag
呢?
如果包含该区间的大区间被打上了 lazytag
,则说明这一整个大区间都受到这个 lazytag
的影响,所以把它记录下来。
最后处理答案时,就是将 lazytag
的和乘上这个区间的长度,add
记录的是 lazytag
和,可以将这个 add
看作是对于这个区间的每个元素一共要增加的值。
code:
#include <bits/stdc++.h>
#define int long long
#define N 1000100
#define rs (x << 1 | 1)
#define ls (x << 1)
using namespace std;
int n, m, a[N];
struct sb {int l, r, len, sum, tag;} e[N << 2];
inline void build(int x, int l, int r)
{
e[x].l = l;
e[x].r = r;
e[x].len = r - l + 1;
if(l == r) return e[x].sum = a[l], void();
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
e[x].sum = e[ls].sum + e[rs].sum;
return ;
}
inline void add(int x, int nl, int nr, int v)
{
e[x].sum += (nr - nl + 1) * v;
if(e[x].l == nl && e[x].r == nr) return e[x].tag += v, void();
int mid = (e[x].l + e[x].r) >> 1;
if(nr <= mid) add(ls, nl, nr, v);
else if(nl > mid) add(rs, nl, nr, v);
else add(ls, nl, mid, v), add(rs, mid + 1, nr, v);
return ;
}
inline int ask(int x, int nl, int nr, int cnt)
{
if(nl == e[x].l && nr == e[x].r) return e[x].sum + e[x].len * cnt;
int res = 0, mid = (e[x].l + e[x].r) >> 1;
if(nr <= mid) res = ask(ls, nl, nr, cnt + e[x].tag);
else if(nl > mid) res = ask(rs, nl, nr, cnt + e[x].tag);
else res = ask(ls, nl, mid, cnt + e[x].tag) + ask(rs, mid + 1, nr, cnt + e[x].tag);
return res;
}
signed main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
build(1, 1, n);
for(int i = 1; i <= m; i ++)
{
int op, l, r, v;
cin >> op;
if(op == 1)
{
cin >> l >> r >> v;
add(1, l, r, v);
}
if(op == 2)
{
cin >> l >> r;
cout << ask(1, l, r, 0) << endl;
}
}
return 0;
}
值得一提的是,在我两者都使用 cin cout
的时候效率并没有差多少。
写在最后
其实还有线段树合并啥的没整理但是好像在线段树的基础上改的越来越多了,所以这里没有写,以后可能会单独开博客写。
本来还打算写一下 zkw 线段树,看了一会儿没看懂先鸽了。
有没看懂的欢迎留言。
本文来自博客园,作者:北烛青澜,转载请注明原文链接:https://www.cnblogs.com/Multitree/p/17491842.html
The heart is higher than the sky, and life is thinner than paper.