活动 - AcWing
参考《算法竞赛进阶指南》-lyd
一、概述
1.简述、所需空间
线段树是一种基于分治思想的二叉树结构,用于区间上的信息统计。与树状数组相比,线段树是一种更通用的数据结构。
线段树每个节点代表一个区间。线段树具有唯一根节点,代表整个统计区间。
叶节点代表一个长度为1的元区间。
对于每个内部节点,它的左子节点是[l,mid],右子节点是[mid+1,r],其中mid=(l+r)/2向下取整。
上图展示了一棵线段树,可以发现除去树的最后一层,整棵线段树一定是一棵完全二叉树。
深度为logn。因此我们可以按照静态链表的“父子2倍”的结点编号方法。
这样就可以用一个结构体存储线段树结点。
理想情况下:N个结点的满二叉树有N+N/2+N/4+....+2+1=2N-1。但是要空出最后一层的空余,因此保存线段树的数组长度不能小于4*N。才能保证数组不会越界。
2.相关操作
线段树基本用途是对序列进行维护。线段树的二叉树结构可以很方便的从下到上传递信息。以区间最大值为例
(1)线段树的建树
首先定义节点结构体
struct Node{
int l,r;
int d;
}tr[4*N];
建树操作:即根据原始序列建树,使得tr数组内节点l,r能够被初始化,d根据原始序列被初始化。
思想:信息从上到下传递,因此采用递归思想。传入当前节点的编号,设定l,r。然后折半递归左右子树,最后根据左右子树信息修改当前节点信息。递归边界条件是递归到叶子节点。
void pushup(Node &u,Node &l,Node &r)
{
u.d=max(l.d,r.d);
}
void pushup(int u)
{
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r]};
else
{
tr[u]={l,r};
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
}
(2)单点修改
和树状数组类似,单点修改之后一步步递归回去。
首先递归边界是递归到目标叶子节点,修改之后return
如果不是叶子节点,则判断目标节点在左半边还是右半边,递归目标区间。
递归之后根据子节点信息修改当前节点信息。
void modify(int u,int x,int v)
{
if(tr[u].l==tr[u].r) tr[u].d=v;
else
{
int mid=tr[u].l+tr[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
(3)区间查询
过程和复杂度分析:
对于一个Node节点,范围是[p1,p2],可能有以下情况:
- l<=p1<=p2<=r,即查询区间完全覆盖当前节点,则直接返回上该节点信息
- 如果只有l在节点之内:
- l>mid,则说明只会继续递归右子树
- l<=mid,则只会递归左子树。
- 如果只有r在节点之内,则与2情况类似
- 如果l和r都在节点之内,
- 如果l,r都在mid左侧或右侧,则只会递归一棵子树
- 如果分别在两侧,则递归左右子树。
那么只有4(2)情况会分别递归左右两颗子树,而这种情况最多发生一次,因为这之后在节点上就会变成2或3情况。因此查询复杂度为O(2logn)。
二、例题
1.最大数
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 200010;
int m, p;
struct Node
{
int l, r;
int v; // 区间[l, r]中的最大值
}tr[N * 4];
void pushup(int u) // 由子节点的信息,来计算父节点的信息
{
tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}
void build(int u, int l, int r)
{
tr[u] = {l, r};
if (l == r) return;
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
int query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r) return tr[u].v; // 树中节点,已经被完全包含在[l, r]中了
int mid = tr[u].l + tr[u].r >> 1;
int v = 0;
if (l <= mid) v = query(u << 1, l, r);
if (r > mid) v = max(v, query(u << 1 | 1, l, r));
return v;
}
void modify(int u,int x,int v)
{
if(tr[u].l==tr[u].r) tr[u].v=v;
else
{
int mid=tr[u].l+tr[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
int main()
{
cin>>m>>p;
int last=0;
build(1,1,m);
int n=0;
while(m--)
{
char op[2];
int x;
cin>>op>>x;
if(*op=='Q')
{
last=query(1,n-x+1,n);
cout<<last<<endl;
}
else
{
modify(1,n+1,((LL)last+x)%p);
n++;
}
}
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/5199364/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.你能回答这些问题吗
查询最大连续子段和。是区间信息,所以考虑线段树。
那么我们就要考虑如何状态转移,即如何用子节点的值更新父节点。
在线段树的每个节点上,除了区间端点外,我们需要再维护4个信息:区间sum,区间最大连续子段和tmax,紧靠左端的最大连续子段和lmax,紧靠右端的最大连续子段和rmax。
线段树的整体框架不变,只需要完善build和modify函数中从下往上传递的信息。
void pushup(Node &u,Node &l,Node &r)
{
u.sum=l.sum+r.sum;
u.lmax=max(l.lmax,l.sum+r.lmax);
u.rmax=max(r.rmax,r.sum+l.rmax);
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
完整代码:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =500010;
struct Node{
int l,r;
int sum,lmax,rmax,tmax;
}tr[4*N];
int n,m;
int w[N];
void pushup(Node &u,Node &l,Node &r)
{
u.sum=l.sum+r.sum;
u.lmax=max(l.lmax,l.sum+r.lmax);
u.rmax=max(r.rmax,r.sum+l.rmax);
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],w[r],w[r],w[r]};
else
{
tr[u]={l,r};//注意要更新l,r
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int x,int v)
{
if(tr[u].l==tr[u].r) tr[u]={x,x,v,v,v,v};
else
{
int mid=tr[u].l+tr[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
Node query(int u,int l,int r)
{
//if(tr[u].l==tr[u].r) return tr[u]; 第一次写错的地方
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
else
{
int mid=tr[u].l+tr[u].r>>1;
if(l>mid) return query(u<<1|1,l,r);
else if(r<=mid) return query(u<<1,l,r);
else
{
auto left=query(u<<1,l,r);
auto right=query(u<<1|1,l,r);
Node res;
pushup(res,left,right);
return res;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
build(1,1,n);
int k,x,y;
while(m--)
{
cin>>k>>x>>y;
if(k==1)
{
if(x>y) swap(x,y);
cout<<query(1,x,y).tmax<<endl;
}
else
modify(1,x,y);
}
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/5199473/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3.区间最大公约数
区间修改和区间查询。
首先思考一下能不能把区间修改转变为单点修改。根据“更相减损术”:gcd(x,y)=gcd(x,y-x)
可以证明对多个整数仍然成立:
d能整除a1 a2-a1 则d能整除a1+(a2-a1)
所以
因此(差分序列的最大公约数和原数组区间左端)最大公约数 等于原数组的区间最大公约数(具体如上图)。
所以我们构建差分序列b。b[1]可为任意值,用线段树维护序列b的区间最大公约数。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =500010;
typedef long long LL;
int n,m;
struct Node{
int l,r;
LL sum,d;
}tr[4*N];
LL w[N];
LL gcd(LL a,LL b)
{
return b?gcd(b,a%b):a;
}
void pushup(Node &u,Node &l,Node &r)
{
u.sum=l.sum+r.sum;
u.d=gcd(l.d,r.d);
}
void pushup(int u)
{
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r)
{
if(l==r)
{
LL b=w[l]-w[l-1];
tr[u]={l,r,b,b};
}
else
{
tr[u].l=l,tr[u].r=r;
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int x,LL v)
{
if(tr[u].l==tr[u].r)
{
LL b=tr[u].sum+v;
tr[u]={x,x,b,b};
}
else
{
int mid=tr[u].l+tr[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
Node query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
else
{
int mid=tr[u].l+tr[u].r>>1;
if(r<=mid) return query(u<<1,l,r);
else if(l>mid) return query(u<<1|1,l,r);
else
{
auto left=query(u<<1,l,r);
auto right=query(u<<1|1,l,r);
Node res;
pushup(res,left,right);
return res;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
int l,r;
LL d;
char op[2];
while(m--)
{
cin>>op>>l>>r;
if(*op=='Q')
{
auto left=query(1,1,l);
Node right={0,0,0,0};
if(l+1<=r) right=query(1,l+1,r);
cout<<abs(gcd(left.sum,right.d))<<endl;
}
else
{
cin>>d;
modify(1,l,d);
if(r+1<=n) modify(1,r+1,-d);
}
}
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/5199926/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。