本文已参与「新人创作礼」活动,一起开启掘金创作之路。 超级基础的讲解!!!! 我先将如何通过线段树解决区间最大值问题来表示一个线段树
线段树的核心思想时通过一个点来代表一个区间的信息,如下图(圆圈里面代表的时序号,下面是区间)
我们将序号存入一维数组中,然后用序号代表下面区间的信息。
将区间进行二分分为左孩子和右孩子。
这些序号有一点规律,一个点的序号是rt,那么左孩子的序号是2rt,右孩子的序号是2rt + 1。
首先来建树,建树是利用递归的思想,建左数,再建右树,再用左树和右树的信息建立当前节点
//序号为rt并且代表区间l到r
void bulid(int l, int r, int rt)
{
if(l == r)//递归到叶节点
{
scanf("%d",&tree[rt]);
return;
}
int mid = (l + r)>>1;
bulid(l, mid, 2*rt);//左孩子
bulid(mid + 1, r, 2*rt + 1);//右孩子
push_up(rt);//建立当前节点,可以根据需求更改
}
求区间最大值的push_up()的写法如下
void push_up(int rt)
{
tree[rt] = max(tree[2*rt], tree[2*rt + 1]);
}
接下来是更新操作(将某个点x修改为c),注意:这个点的位置是在区间中的,而非rt。 思路是这样的,先对当前区间进行判断,如果x小于当前区间中点,则在左孩子区间,否则在右孩子区间,一直递归,直到区间的长度为1
void update(int l, int r,int rt, int x, int c)
{
if(l == r)//区间长度为1
{
tree[rt] = c;
return;
}
int mid = (l + r)>>1;
if(x <= mid)//在左孩子
{
update(l, mid, 2*rt, x, c);
}
else//在右孩子
{
update(mid+1, r, 2*rt+1 , x, c);
}
push_up(rt);//更新当前节点
}
然后是查询操作 思路:将查询区间不断二分切割,分成若干个包含于查询区间得区间,并将区间值返回。
int query(int l,int r, int rt, int L, int R)
{
if(L <= l && r <= R)
{
return tree[rt];
}
int ans = 0;
int mid = (r + l)>>1;
if(L <= mid)//如果查询区间在左孩子中由分布
{
ans = max(query(l, mid, 2*rt, L ,R), ans);//更新答案
}
if(mid + 1 <= R)//如果查询区间在右孩子中由分布
{
ans = max(query(mid + 1, r, 2*rt + 1, L ,R), ans);//更新答案
}
return ans;
}
然后如果数据区间为N,那么我们的线段树是四倍的N。
下面写道题巩固一下: I hate it 题解:
#include<bits/stdc++.h>
using namespace std;
#define N 200000+10
int tree[4*N];
void bulid(int l, int r, int rt);
void push_up(int rt);
void update(int l, int r,int rt, int x, int c);
int query(int l,int r, int rt, int L, int R);
//序号为rt并且代表区间l到r
void bulid(int l, int r, int rt)
{
if(l == r)//递归到叶节点
{
scanf("%d",&tree[rt]);
return;
}
int mid = (l + r)>>1;
bulid(l, mid, 2*rt);//左孩子
bulid(mid + 1, r, 2*rt + 1);//右孩子
push_up(rt);//建立当前节点
}
void push_up(int rt)
{
tree[rt] = max(tree[2*rt], tree[2*rt + 1]);
}
//将点x修改为c
void update(int l, int r,int rt, int x, int c)
{
if(l == r)//区间长度为1
{
tree[rt] = c;
return;
}
int mid = (l + r)>>1;
if(x <= mid)//在左孩子
{
update(l, mid, 2*rt, x, c);
}
else//在右孩子
{
update(mid+1, r, 2*rt+1 , x, c);
}
push_up(rt);//更新当前节点
}
//查询区间L到R的最大值
int query(int l,int r, int rt, int L, int R)
{
if(L <= l && r <= R)
{
return tree[rt];
}
int ans = 0;
int mid = (r + l)>>1;
if(L <= mid)//如果查询区间在左孩子中由分布
{
ans = max(query(l, mid, 2*rt, L ,R), ans);//更新答案
}
if(mid + 1 <= R)//如果查询区间在右孩子中由分布
{
ans = max(query(mid + 1, r, 2*rt + 1, L ,R), ans);//更新答案
}
return ans;
}
int main()
{
int n, m;
while(cin>>n>>m)
{
bulid(1, n, 1);
char op[2];//注意不要用char op
int x, c;
for(int i = 1; i <= m; ++i)
{
scanf("%s %d %d", op, &x, &c);
if(op[0] == 'Q')
{
cout<<query(1, n, 1,x, c)<<endl;
}
else
{
update(1, n, 1, x, c);
}
}
}
}
lazy标记是什么,为什么要加一个lazy标记,还是这张图
如果我们要求在区间[5,7]内所有的点都加上k呢,我们难道需要调用三次update对于点更新吗,这个区间还是很小,但如果我们增加的区间非常大呢,每次我们都需要点更新的话,其时间复杂度无疑是巨大的,这有违我们开设线段树的初衷。
我们可以看到[5, 7]完全覆盖了序号3,那我们在序号3增加的值就是覆盖的长度乘以k,就是(7-5+1)*3,于是我们可以想到,如果我们引入一个lazy标记(等于k)在序号3上,我们并不更新序号3以下的点,而是在需要访问下面的点的时候将lazy标记往下推,这样我们就很“懒”的省了很多事,因为并不是每个被增加的点都会被访问。
值得注意的是:打了lazy标记的序号是已经更新完了的,在序号之下的是没有更新的
我们只需要在以上的操作稍作修改(ps:这次是求区间和)
首先是push_up和push_down
void push_up(ll rt)
{
tree[rt] = tree[2*rt] + tree[2*rt + 1];
}
//将lazy标记往下推给两个孩子
void push_down(ll rt, ll llen, ll rlen)
{
if(lazy[rt])
{
lazy[rt*2] += lazy[rt];
lazy[rt*2+1] += lazy[rt];
tree[rt*2] += lazy[rt] * llen;
tree[rt*2+1] += lazy[rt] * rlen;
lazy[rt] = 0;//lazy清零
}
}
建树
void bulid(ll l, ll r, ll rt)
{
if(l == r)//递归到叶节点
{
cin>>tree[rt];
return;
}
ll mid = (l + r)>>1;
bulid(l, mid, 2*rt);//左孩子
bulid(mid + 1, r, 2*rt + 1);//右孩子
push_up(rt);//建立当前节点
}
更新
void update(ll l, ll r,ll rt, ll L, ll R, ll c)
{
if(L <= l && r <= R)//当所加区间覆盖了某个孩子
{
tree[rt] += (r - l + 1) * c//更新这个孩子
lazy[rt] += c;//打上lazy标记
return;
}
ll mid = (l + r)>>1;
push_down(rt, mid - l + 1, r - mid);
if(L <= mid)//在左孩子
{
update(l, mid, 2*rt, L, R, c);
}
if(mid + 1 <= R)
{
update(mid + 1 , r, 2*rt + 1, L, R, c);
}
push_up(rt);//更新当前节点
}
查询
ll query(ll l,ll r, ll rt, ll L, ll R)
{
if(L <= l && r <= R)
{
return tree[rt];
}
ll ans = 0;
ll mid = (r + l)>>1;
push_down(rt, mid - l + 1, r - mid);//不管该节点是否有lazy都可以试着往下推
if(L <= mid)//如果查询区间在左孩子中由分布
{
ans += query(l, mid, 2*rt, L , R);
}
if(mid + 1 <= R)//如果查询区间在右孩子中由分布
{
ans += query(mid + 1, r, 2*rt + 1, L ,R);//更新答案
}
return ans;
}
例题:线段树模板
题解:
#include<bits/stdc++.h>
using namespace std;
#define N 200000+10
typedef long long ll;
ll tree[4*N];
ll lazy[4*N];
void bulid(ll l, ll r, ll rt);
void push_up(ll rt);
void push_down(ll rt, ll llen, ll rlen);
void update(ll l, ll r,ll rt, ll L, ll R, ll c);
ll query(ll l,ll r, ll rt, ll L, ll R);
//序号为rt并且代表区间l到r
void bulid(ll l, ll r, ll rt)
{
if(l == r)//递归到叶节点
{
cin>>tree[rt];
return;
}
ll mid = (l + r)>>1;
bulid(l, mid, 2*rt);//左孩子
bulid(mid + 1, r, 2*rt + 1);//右孩子
push_up(rt);//建立当前节点
}
void push_up(ll rt)
{
tree[rt] = tree[2*rt] + tree[2*rt + 1];
}
//将lazy标记往下推给两个孩子
void push_down(ll rt, ll llen, ll rlen)
{
if(lazy[rt])
{
lazy[rt*2] += lazy[rt];
lazy[rt*2+1] += lazy[rt];
tree[rt*2] += lazy[rt] * llen;
tree[rt*2+1] += lazy[rt] * rlen;
lazy[rt] = 0;//lazy清零
}
}
//将区间[L, R]修改为c
void update(ll l, ll r,ll rt, ll L, ll R, ll c)
{
if(L <= l && r <= R)//当所加区间覆盖了某个孩子
{
tree[rt] += (r - l + 1) * c//更新这个孩子
lazy[rt] += c;//打上lazy标记
return;
}
ll mid = (l + r)>>1;
push_down(rt, mid - l + 1, r - mid);
if(L <= mid)//在左孩子
{
update(l, mid, 2*rt, L, R, c);
}
if(mid + 1 <= R)
{
update(mid + 1 , r, 2*rt + 1, L, R, c);
}
push_up(rt);//更新当前节点
}
//查询区间L到R的最大值
ll query(ll l,ll r, ll rt, ll L, ll R)
{
if(L <= l && r <= R)
{
return tree[rt];
}
ll ans = 0;
ll mid = (r + l)>>1;
push_down(rt, mid - l + 1, r - mid);//不管该节点是否有lazy都可以试着往下推
if(L <= mid)//如果查询区间在左孩子中由分布
{
ans += query(l, mid, 2*rt, L , R);
}
if(mid + 1 <= R)//如果查询区间在右孩子中由分布
{
ans += query(mid + 1, r, 2*rt + 1, L ,R);//更新答案
}
return ans;
}
int main()
{
ll n, m;
cin>>n>>m;
bulid(1, n, 1);
for(ll i = 1; i <= m; ++i)
{
ll a,L,R,k;
cin>>a;
if(a == 1)
{
cin>>L>>R>>k;
update(1, n, 1, L, R, k);
}
else
{
cin>>L>>R;
cout<<query(1, n, 1, L, R)<<endl;
}
}
}
总结:谢谢各位的观看,如果有什么不对的地方请斧正^_^。