5.4训练记录
内容简介
主要讲了线段树,线段树究竟是什么呢?其实就是一棵不那么满的二叉树,他有一下几个特点。
- 维护序列的区间信息,方便查询
- 每一个结点都是一个区间
- 二叉树的基本性质,每一个非叶子结点都有 2 2 2 个分叉。
接下来总结他们的表示方式
根节点表示为 1 1 1 ,然后左儿子表示为 2 ∗ i 2 \ast i 2∗i ,右儿子表示为 2 ∗ i + 1 2 \ast i + 1 2∗i+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 2⌊logn⌋ ≤ \leq ≤ 2 ( ⌊ log n ⌋ + 1 ) 2^(\lfloor \log n \rfloor+1) 2(⌊logn⌋+1) 差不多开 4 ∗ n 4 \ast n 4∗n 就可以了。时间复杂度建树 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;
}