5.4训练总结线段树

5.4训练记录

内容简介

主要讲了线段树,线段树究竟是什么呢?其实就是一棵不那么满的二叉树,他有一下几个特点。

  • 维护序列的区间信息,方便查询
  • 每一个结点都是一个区间
  • 二叉树的基本性质,每一个非叶子结点都有 2 2 2 个分叉。

接下来总结他们的表示方式

根节点表示为 1 1 1 ,然后左儿子表示为 2 ∗ i 2 \ast i 2i ,右儿子表示为 2 ∗ i + 1 2 \ast i + 1 2i+1 。我们可以看这张图

请添加图片描述

还有一点需要注意:叶子节点长度为 1 1 1 ,没有左右儿子

那么每一个结点需要存储的有哪些信息呢?分别如下: l , r l,r l,r 表示的是他们所表示的区间从哪里开始,哪里结束。剩下的我后面再说。

空间与时间复杂度又是多少呢?空间的话开到 1 + 2 1+2 1+2 + ⋯ + \cdots + + + + 2 ⌊ log ⁡ n ⌋ 2^\lfloor \log n \rfloor 2logn ≤ \leq 2 ( ⌊ log ⁡ n ⌋ + 1 ) 2^(\lfloor \log n \rfloor+1) 2(logn+1) 差不多开 4 ∗ n 4 \ast n 4n 就可以了。时间复杂度建树 O ( n ) O(n) O(n)

然后我们写一下建树的代码

struct node
{
    long long l,r,sum;//区间长度以及和
}t[400007];
void build(int l, int r,int num)
{
    t[root].l=l;
    t[root].r=r;
    if(l==r)
	{
        t[num].sum=a[l];//初始化元素
    }
    int m=(l+r)/2;
    buld(root*2, l, m);//左儿子
    build(root*2+1, m+1, r);//右儿子
    t[num].v=t[num*2].v+t[num*2+1].v;//上传和
    return;
}

接下来我们看区间操作,比如区间加一个数和整体修改一起讲了,这里我们就要用一个操作,懒标记,我们可以把他要加的东西存在一个结点上,要用的时候下放,因为线段树是从根节点到叶子结点的,但是现在的子结点是错误的,所以要 push_down 。最后就可以 push_up 修改祖先,即可。但是又有一个问题出现了,怎么定位存在哪里呢?我们就可以用一个类似分治的思想来判断了。比如从中间阶段,如果与要查询的区间无交集,返回。正好包含,就是这个。否则两边都要判断。

请添加图片描述

总结一下两个操作的目的

push_down ⇒ \Rightarrow 将当前节点的懒标记传递给子结点。

push_up ⇒ \Rightarrow 用子树的信息更新父节点甚至更高级的。

上代码。

void push_down(int root)
{
    t[2*root].sum+=(t[2*root].r-t[2*root].l+1)*t[root].tag;//更新区间和
    t[2*root].tag+=t[root].tag;//tag累加
    t[2*root+1].sum+=(t[2*root+1].r-t[2*root+1].l+1)*t[root].tag;//更新区间和
    t[2*root+1].tag+=t[root].tag;//tag累加
    t[root].tag=0;//tag清零,不要忘记了
    return;
}
void push_up(int root)
{
	t[root].sum=t[2*root].sum+t[2*root+1].sum;//通过子结点合并父节点
	return;
}

接下来上区间修改和查询,不做过多赘述

void modify2(int root, int tdl, int tdr, int l, int r, long long k)
{
    if(r<tdl || tdr<l)
	{
        return;
    }
    if(l<=tdl && tdr<=r)
	{
        t[root].add=(t[root].add+k)%p;
        t[root].v=(t[root].v+k*(tdr-tdl+1))%p;
        return;
    }
    push_down(root, tdl, tdr);
    int m=(tdl+tdr)/2;
    modify2(root*2, tdl, m, l, r, k);
    modify2(root*2+1, m+1, tdr, l, r, k);
    t[root].v=(t[root*2].v+t[root*2+1].v)%p;
    return;
}
long long query(int root, int tdl, int tdr, int l, int r)
{
    if(r<tdl||tdr<l)
	{
        return 0;
    }
    if(l<=tdl&&tdr<=r)
	{
        return t[root].v;
    }
    push_down(root, tdl, tdr);
    int m=(tdl+tdr)/2;
    return (query(root*2, tdl, m, l, r)+query(root*2+1, m+1, tdr, l, r))%p;
}
例题讲解
火车线路

原题入口

这道题我们可以把题目所要求的操作简化一下,把他的订购条件想象成区间和,也就不难想到线段树。还有一个注意点在终点站时,这些人都下车了,也就不用占据座位了,好,那么这道题也就没什么可以讲的了。只要认真看了前面应该就会写。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN=60005;
int n,s,m;
int tl,tr,x;
int tree[4*MAXN],col[4*MAXN];
void pushdown(int p)//基础的懒标记下移
{
	if(col[p])
	{
		tree[p*2]+=col[p];//左儿子更改
		tree[p*2+1]+=col[p];
		col[p*2]+=col[p];//右儿子更改
		col[p*2+1]+=col[p];
		col[p]=0;
	}
	return;
}
void modify(int l,int r,int p)//区间更改
{
	if(tl<=l&&tr>=r)//如果不包含,跳过
	{
		tree[p]+=x;
		col[p]+=x;
		return;
	}
	pushdown(p);
	int mid=(l+r)>>1;
	if(tl<=mid)//分治查找
		modify(l,mid,p*2);
	if(tr>mid)
		modify(mid+1,r,p*2+1);
    tree[p]=max(tree[p*2],tree[p*2+1]);
}
int query(int l,int r,int p)//区间查询
{
	if( tl<=l && tr>=r )
		return tree[p];
	pushdown(p);
	int ans=0,mid=(l+r)>>1;
	if(tl<=mid)
		ans=max(ans,query(l,mid,p*2));
	if(tr>mid)
		ans=max(ans,query(mid+1,r,p*2+1));
	return ans;
}
int main()
{
	scanf("%d%d%d",&n,&s,&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&tl,&tr,&x);
		tr--;
		if(x+query(1,n,1)<=s)
		{
			modify(1,n,1);
			printf("T\n");
			continue;
		}
		printf("N\n");
	}
	return 0;
}
线段树2

原题入口

这道题是一道对于线段树来说一道好题,那么我们来分析一下。因为区间加法与乘法对于模运算都是毫无影响的,那就很明显了。不就是多了一个区间乘法嘛,我们举一反三一下,还记得我们普通的区间加法怎么做呢?对,lazytag ,一个大杀器,那我们也不妨思考一下,我们可以开两个懒标记,一个是 a d d add add 另一个是 m u l mul mul 分别代表了加法和乘法,然后我们就可以以 O ( n log ⁡ n ) O( n \log n ) O(nlogn) 的算法解决,然后在 p u s h d o w n pushdown pushdown 的时候规定一下顺序。好,我们思考一下,如果加法优先,那么精度肯定会受到影响,但是乘法呢?貌似并不会。信友队的题目比较毒瘤,记得多取模。OK,上代码。

#include<bits/stdc++.h>
using namespace std;
int p;
long long a[100007];
struct node
{
    long long v, mul, add;//v是答案,mul和add是两个懒标记
}t[400007];
void build(int root, int l, int r)//初始化lazytag,建树
{
    t[root].mul=1;//初始化,别忘了乘法是1
    t[root].add=0;
    if(l==r)
	{
        t[root].v=a[l];
    }
    else
	{
        int m=(l+r)/2;
        build(root*2, l, m);//左右建树
        build(root*2+1, m+1, r);
        t[root].v=t[root*2].v+t[root*2+1].v;
    }
    t[root].v%=p;
    return;
}
void push_down(int root, int l, int r)//下放懒标记
{
    int m=(l+r)/2;
    t[root*2].v=(t[root*2].v*t[root].mul+t[root].add*(m-l+1))%p;//老样子,不多说
    t[root*2+1].v=(t[root*2+1].v*t[root].mul+t[root].add*(r-m))%p;
    t[root*2].mul=(t[root*2].mul*t[root].mul)%p;//先乘法,后加法
    t[root*2+1].mul=(t[root*2+1].mul*t[root].mul)%p;
    t[root*2].add=(t[root*2].add*t[root].mul+t[root].add)%p;
    t[root*2+1].add=(t[root*2+1].add*t[root].mul+t[root].add)%p;
    t[root].mul=1;//父节点初始化,下放过后没了
    t[root].add=0;
    return;
}
void modify1(int root, int tdl, int tdr, int l, int r, long long k)//乘法修改
{
    if(r<tdl||tdr<l)//假如本区间和给出的区间没有交集
	{
        return;
    }
    if(l<=tdl&&tdr<=r)/假如给出的区间包含本区间
	{
        t[root].v=(t[root].v*k)%p;
        t[root].mul=(t[root].mul*k)%p;
        t[root].add=(t[root].add*k)%p;
        return;
    }
    push_down(root, tdl, tdr);//假如给出的区间和本区间有交集,但是也有不交叉的部分,也就是横跨左右两个部分
    int m=(tdl+tdr)/2;
    modify1(root*2, tdl, m, l, r, k);
    modify1(root*2+1, m+1, tdr, l, r, k);
    t[root].v=(t[root*2].v+t[root*2+1].v)%p;
    return;
}
void modify2(int root, int tdl, int tdr, int l, int r, long long k)//加法修改,类似
{
    if(r<tdl || tdr<l)
	{
        return;
    }
    if(l<=tdl && tdr<=r)
	{
        t[root].add=(t[root].add+k)%p;
        t[root].v=(t[root].v+k*(tdr-tdl+1))%p;
        return;
    }
    push_down(root, tdl, tdr);
    int m=(tdl+tdr)/2;
    modify2(root*2, tdl, m, l, r, k);
    modify2(root*2+1, m+1, tdr, l, r, k);
    t[root].v=(t[root*2].v+t[root*2+1].v)%p;
    return;
}
long long query(int root, int tdl, int tdr, int l, int r)//查询
{
    if(r<tdl||tdr<l)
	{
        return 0;
    }
    if(l<=tdl&&tdr<=r)
	{
        return t[root].v;
    }
    push_down(root, tdl, tdr);
    int m=(tdl+tdr)/2;
    return (query(root*2, tdl, m, l, r)+query(root*2+1, m+1, tdr, l, r))%p;
}
int main()//主函数,跟随题意
{
    int n, m;
    scanf("%d%d%d", &n, &m, &p);
    for(int i=1;i<=n;i++)
	{
        scanf("%lld",&a[i]);
    }
    build(1,1,n);
    while(m--)
	{
        int opt;
        scanf("%d", &opt);
        int x, y;
        long long k;
        if(opt==1)
		{
            scanf("%d%d%lld", &x, &y, &k);
            modify1(1, 1, n, x, y, k);
        }
        else if(opt==2)
		{
            scanf("%d%d%lld", &x, &y, &k);
            modify2(1, 1, n, x, y, k);
        }
        else
		{
            scanf("%d%d", &x, &y);
            printf("%lld\n", query(1, 1, n, x, y));
        }
    }
    return 0;
}
贪婪大陆

原题入口

这道题其实有多种做法,分别是树状数组和线段树,我们这里先讲线段树的做法。其实还好,就是区间查询,没有想象中的那么难。

#include<bits/stdc++.h>
using namespace std;
const int N=100000+10;
struct node
{
    int left,right,sumr,suml;
}tree[N*4];
int n,m;
void build(int id,int l,int r)//建树
{
    tree[id].left=l;
    tree[id].right=r;
    if(l==r)
    	return;
    int mid=(l+r)>>1;
    build(id*2,l,mid);
    build(id*2+1,mid+1,r);
}
void update1(int id,int x)//修改起点
{
    if(tree[id].left==tree[id].right)
    {
        tree[id].suml++;return;
    }
    if(x>tree[ls].right)
    	update1(id*2+1,x);
    else 
    	update1(id*2,x);
    tree[id].suml=tree[id*2].suml+tree[id*2+1].suml;
}
void update2(int id,int x)//修改终点
{
    if(tree[id].left==tree[id].right)
    {
        tree[id].sumr++;
        return;
    }
    if(x>tree[id*2].right)
    	update2(id*2+1,x);
    else 
    	update2(id*2,x);
    tree[id].sumr=tree[ls].sumr+tree[rs].sumr;
}
int query1(int id,int l,int r)//查询起点
{
    if(r<tree[id].left || tree[id].right<l)
    	return 0;
    if(l<=tree[id].left&&tree[id].right<=r)
    	return tree[id].suml;
    return query1(id*2,l,r)+query1(id*2+1,l,r);
}
int query2(int id,int l,int r)//查询终点
{
    if(tree[id].left>r||tree[id].right<l)
    	return 0;
    if(tree[id].left>=l&&tree[id].right<=r)
    	return tree[id].sumr;
    return query2(id*2,l,r)+query2(id*2+1,l,r);
}
int main()
{
    scanf("%d%d",&n,&m);
    build(1,1,n);//建树
    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        if(x==1)
        {
            update1(1,y);
            update2(1,z);
        }
        if(x==2)
        {
            int s=query1(1,1,z);
            int t=query2(1,1,y-1);
            printf("%d\n",s-t);//终点的sum-起点的sum
        }
    }
    return 0;
}
  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值