线段树 + lazy标记

453 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。 超级基础的讲解!!!! 我先将如何通过线段树解决区间最大值问题来表示一个线段树

线段树的核心思想时通过一个点来代表一个区间的信息,如下图(圆圈里面代表的时序号,下面是区间) 在这里插入图片描述 我们将序号存入一维数组中,然后用序号代表下面区间的信息。 将区间进行二分分为左孩子和右孩子。 这些序号有一点规律,一个点的序号是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;
        }
    }
}

总结:谢谢各位的观看,如果有什么不对的地方请斧正^_^。