线段树
线段树+
目录
- 朴素线段树
- 定义
- 用处
- 树的储存
- 建树
- 区间求和
- 区间加
- 懒标优先级
- 权值线段树
- 标记永久化
- 动态开点
- zkw 线段树
- 拆位线段树
- 可持久化
朴素线段树
定义
线段树是一种二叉搜索树,线段树的每个结点都存储了一个区间,也可以理解成一个线段。
用处
维护区间信息。线段树可以在 \(O(\log n)\) 的时间复杂度内实现单点修改,区间修改,区间查询等操作。
最典型的,也是最简单的就是 区间加 和 区间求和 。
以 此题 为例,就代表最简单的线段树操作了。
树的储存
线段树比较直观的理解方式是看图:
(比较懒,直接从网上找了一个,我也不知道来自哪个 dalao 的博客)
这个图中每个节点有三个数:上边是点的编号,左边是区间的左端点,右边是区间的右端点。
个人习惯用结构体:
struct Segment_tree{
int le;//区间左端点
int ri;//区间右端点
int val;//区间维护的值,图中未展示
int tag;//懒标记,之后会提到
}tre[inf*4];
虽然图中的线段树不是每个节点都画了出来,但那些节点确确实实存在,因此线段树大概需要所维护的数组大小的 4 倍左右。
建树
对于线段树来说,每个节点 \(i\) 的左儿子的编号都是 \(2\times i\),右儿子的编号都是 \(2\times i+1\),同时区间大小对半分(见上图)。
通俗的理解一下,1 号节点代表整个区间,2 号节点代表左半个区间,3 号节点代表右半个区间。
这个代表可以是区间最值,区间和,区间 GCD 等题目要求维护的东西。
那以此题来说,就是区间和了。
建树的时候,先递归到叶子,将原数组的数分别赋到对应的叶节点,然后回溯时再将左右两个子树的权值加回来。
代码实现:
void build(int i,int l,int r)
{
tre[i].le=l;tre[i].ri=r;
if(l==r)
{
tre[i].val=a[l];
return;
}
int mid=(l+r)>>1;
build(i*2,l,mid);
build(i*2+1,mid+1,r);
tre[i].val=tre[i*2].val+tre[i*2+1].val;
}
区间求和
基本思路就是若覆盖则返回,否则递归到下一层合适的区间再返回。
举个例子,还是由上图,手模一下查询区间 [3,5] 的和。
首先进入 1 号节点。
1 号节点太大,[3,5] 无法将其覆盖,那么就找一号节点的两个子节点:2 号节点和 3 号节点。
2 号节点与 [3,5] 有交集,3 号节点与 [3,5] 并无交集,就递归到 2 号节点。同理,再递归到 4 号节点和 5 号节点。
这时注意 5 号节点。5 号节点不仅与 [3,5] 有交集,而且能被其覆盖,那么这时就可以直接返回 5 号节点的权值,并在回溯时累加到答案中。
Code:
int ask(int i,int le,int ri)
{
if(le<=tre[i].l&&tre[i].r<=ri)
return tre[i].val;
int mid=(tre[i].l+tre[i].r)>>1,ans=0;
if(le<=mid)ans+=ask(i<<1,le,ri);
if(mid
区间加
基本的思路和区间求和相似,同样是找交集。
但是每次都将所覆盖的区间全部更新的话复杂度是 \(O(n\log n)\),而暴力的时间复杂度是 \(O(n)\) 的,多个 log。
如果想要我们的线段树比暴力快的话,我们需要引入一个 懒惰标记 ,顾名思义,就是偷懒。此时的单次修改时间复杂度就成了 \(O(\log n)\)。
因为每次修改不一定会马上被查询。比如上图中我更新[6,9],查询 [3,5],那么这次的更新对下次的询问没有任何影响。我们就用懒标将这次更新储存下来,来表示 这个区间的每个元素都有 tag 还没加上 。
然后查询或者再次更新的时候,就将这个懒标下放到两个儿子上。
代码:
void update(int i,int le,int ri,int key)
{
if(le<=tre[i].le&&tre[i].ri<=ri)
{
tre[i].val+=key*(tre[i].ri-tre[i].le+1);
tre[i].tag+=key;
return;
}
if(tre[i].tag)pushdown(i);
int mid=(tre[i].l+tre[i].r)>>1;
if(le<=mid)update(i<<1,le,ri,key);
if(mid
pushdown 即为:
void pushdown(int i)
{
tre[i<<1].val+=tre[i].tag*(tre[i<<1].r-tre[i<<1].l+1);
tre[i<<1|1].val+=tre[i].tag*(tre[i<<1|1].r-tre[i<<1|1].l+1);
tre[i<<1].tag+=tre[i].tag;
tre[i<<1|1].tag+=tre[i].tag;
tre[i].tag=0;
}
那么区间求和也需要加上这个 pushdown:
int ask(int i,int le,int ri)
{
if(le<=tre[i].l&&tre[i].r<=ri)
return tre[i].val;
if(tre[i].tag)pushdown(i);
int mid=(tre[i].l+tre[i].r)>>1,ans=0;
if(le<=mid)ans+=ask(i<<1,le,ri);
if(mid
线段树拓展
懒标优先级
当我们的线段树维护的不再是一种区间操作,而是两种或者更多,要如何选择懒标的优先级,以便于我们更简单的维护我们想要维护的?
先看两个例子:P3373 线段树 2 和 P1253 扶苏的问题。
P3373 是 加法懒标 和 乘法懒标 之间的优先级问题,而 P1253 是 加法懒标 和 推平懒标 之间的优先级问题。
这里的优先级,主要是指在放置懒标和 pushdown 时,两个懒标对彼此是否有影响。
显然啊显然,两个题中,加法懒标的优先级都是比较低的。即加法懒标不能影响乘法懒标和推平懒标。
以 P3373 为例,如果先乘后加就是:\((val+add)\times k=val\times k+add\times k\),而先加后乘就是:\(val\times mul+k=(val+\dfrac{k}{mul})\times mul\)。如果按后者进行计算,会引起精度误差,也不便于维护。
再看向 P1253,则是区间推平后消除加法懒标。
这样的 pushdown 就会很长:
P3373
void pushdown(int i)
{
tre[i<<1].val=(tre[i<<1].val*tre[i].mul)%p;
tre[i<<1|1].val=(tre[i<<1|1].val*tre[i].mul)%p;
tre[i<<1].add=(tre[i<<1].add*tre[i].mul)%p;
tre[i<<1|1].add=(tre[i<<1|1].add*tre[i].mul)%p;
tre[i<<1].mul=(tre[i<<1].mul*tre[i].mul)%p;
tre[i<<1|1].mul=(tre[i<<1|1].mul*tre[i].mul)%p;
tre[i<<1].val=(tre[i<<1].val+tre[i].add*(tre[i<<1].ri-tre[i<<1].le+1))%p;
tre[i<<1|1].val=(tre[i<<1|1].val+tre[i].add*(tre[i<<1|1].ri-tre[i<<1|1].le+1))%p;
tre[i<<1].add=(tre[i<<1].add+tre[i].add)%p;
tre[i<<1|1].add=(tre[i<<1|1].add+tre[i].add)%p;
tre[i].add=0;tre[i].mul=1;
}
P1253
void pushdown(int i)
{
if(tre[i].ass)
{
tre[i<<1].maxn=tre[i<<1|1].maxn=tre[i].tag1;
tre[i<<1].tag1=tre[i<<1|1].tag1=tre[i].tag1;
tre[i<<1].tag2=tre[i<<1|1].tag2=0;
tre[i<<1].ass=tre[i<<1|1].ass=1;
tre[i].ass=0;
}
tre[i<<1].maxn+=tre[i].tag2;
tre[i<<1|1].maxn+=tre[i].tag2;
tre[i<<1].tag2+=tre[i].tag2;
tre[i<<1|1].tag2+=tre[i].tag2;
tre[i].tag2=0;
}
权值线段树
如果说普通的线段树维护的是区间的数,那么权值线段树维护的就是区间的数的个数,即维护这个区间的桶。
一般用于求解区间第 k 大值的问题。
建树:
void build(int i,int l,int r)
{
tre[i].le=l;tre[i].ri=r;
if(l==r)
{
tre[i].val=tong[l];
return;
}
int mid=(l+r)>>1;
build(i<<1,l,mid);
build(i<<1|1,mid+1,r);
tre[i].val=tre[i<<1].val+tre[i<<1|1].val;
}
插入:
void ins(int i,int k,int cnt)
{
if(tre[i].le==tre[i].ri)
{
tre[i].val+=cnt;
return;
}
int mid=(tre[i].le+tre[i].ri)>>1;
if(k<=mid)ins(i<<1,k,cnt);
else ins(i<<1|1,k,cnt);
tre[i].val=tre[i<<1].val+tre[i<<1|1].val;
}
查询个数:
int ask_num(int i,int k)
{
if(tre[i].le==tre[i].ri)
return tre[i].val;
int mid=(tre[i].le+tre[i].ri)>>1;
if(k<=mid)return ask_num(i<<1,k);
return ask_num(i<<1|1,k);
}
查询第 k 大值:
int ask_rank(int i,int k)
{
if(tre[i].le==tre[i].ri)
return tre[i].le;
int mid=(tre[i].le+tre[i].ri)>>1;
if(k
标记永久化
线段树操作中最著名的就是懒标。
当懒标 pushup 和 pushdown 时,如果对应的两个儿子是两个节点,还可以接受。但有时,两个儿子是两棵树,比如线段树的树套树,一维线段树的 pushup,对应二维每个节点对应 pushup,时间复杂度 \(O(n^2log^2n)\)。
所以要标记永久化。
举最简单的例子,用标记永久化写 P3372 就是这样:
#include
#define int long long
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=1e5+7;
int n,m,op,l,r,v;
int a[inf];
struct a_tree{
int le,ri;
int val,tag;
}tre[inf<<2];
void build(int i,int l,int r)
{
tre[i].le=l,tre[i].ri=r;
if(l==r)
{
tre[i].val=a[l];
return;
}
int mid=(l+r)>>1;
build(i<<1,l,mid);
build(i<<1|1,mid+1,r);
tre[i].val=tre[i<<1].val+tre[i<<1|1].val;
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a>1;
if(l<=mid)update(i<<1,l,r,k);
if(mid>1,ans=0;
if(l<=mid)ans+=ask(i<<1,l,r,k+tre[i].tag);
if(mid
动态开点
众所周知,线段树需要 4 倍空间。
如果让你维护一个值域为 \(10^9\) 的权值线段树,需要的空间就是 \(4\times 10^9\),显然必炸。
那么我们摒弃传统的线段树,尝试动态开点。
顾名思义,动态开点就是只开用到的点,不用的点先不开。那么我们建的是一颗残疾的线段树。
还是 P3372,用动态开点就是:
#include
#define int long long
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=1e5+7;
int n,m,root,cnt;
int a[inf],s[inf];
struct a_tree{
int ls,rs;
int val,tag;
}tre[inf<<2];
void new_node(int &i,int l,int r)
{
i=++cnt;tre[i].val=s[r]-s[l-1];
}
void pushdown(int i,int l,int r)
{
if(l==r)return;int mid=(l+r)>>1;
if(tre[i].ls==0)new_node(tre[i].ls,l,mid);
if(tre[i].rs==0)new_node(tre[i].rs,mid+1,r);
tre[tre[i].ls].tag+=tre[i].tag;
tre[tre[i].rs].tag+=tre[i].tag;
tre[tre[i].ls].val+=(mid-l+1)*tre[i].tag;
tre[tre[i].rs].val+=(r-mid)*tre[i].tag;
tre[i].tag=0;
}
void pushup(int i,int l,int r)
{
if(l==r)return;int mid=(l+r)>>1;
if(tre[i].ls==0)new_node(tre[i].ls,l,mid);
if(tre[i].rs==0)new_node(tre[i].rs,mid+1,r);
tre[i].val=tre[tre[i].ls].val+tre[tre[i].rs].val;
}
void update(int &i,int l,int r,int x,int y,int k)
{
if(i==0)new_node(i,l,r);
if(x<=l&&r<=y)
{
tre[i].val+=(r-l+1)*k;
tre[i].tag+=k;
return;
}
if(tre[i].tag)pushdown(i,l,r);
int mid=(l+r)>>1;
if(x<=mid)update(tre[i].ls,l,mid,x,y,k);
if(mid>1,ans=0;
if(x<=mid)ans+=ask(tre[i].ls,l,mid,x,y);
if(mid
可以发现,与传统的线段树不同的地方就是左右儿子的编号。
传统线段树的 \(i\) 节点的左儿子是 \(i\times2\),右儿子是 \(i\times2+1\),而动态开点线段树的 \(i\) 节点的左右儿子的则是储存为 \(tre_i.ls,tre_i.rs\),先用先开先存。
zkw 线段树
与朴素线段树的区别
朴素的线段树都是递归形式的,占用栈空间大,常数大。zkw 线段树则是循环形式,常数小,代码短。
朴素的线段树是找到根节点,然后向下递归维护。zkw 线段树则是先找叶节点,然后上浮维护到根节点。
对于朴素的线段树(如最上边的图),叶节点是很难确定的。但如果我们要维护的是一棵满二叉树,找到叶节点就很容易了。
如下图:

(同样是直接从网上找的)
这样的线段树在节点上就会有一些奇妙的性质:
-
共有 n 个叶节点,代表区间 [x,x] 的叶节点对应的编号就是 n+x。
-
若某节点的编号为 i,则其父亲的编号为 \(\lfloor\dfrac{i}{2}\rfloor\),左儿子的编号为 \(2\times i\),右儿子的编号为 \(2\times i+1\)。
-
若两个节点分别为 i,j,且 i,j 是兄弟节点(即父节点相同),那么 \(i\oplus j=1\)。
-
除根节点外,编号为偶数的节点为其父节点的左儿子,编号为奇数的节点是其父节点的右儿子。
建树比较简单。
我们需要将原序列的值赋到叶节点上,首先要保证叶节点能够容纳下原序列。
将叶节点赋值之后,便和朴素的线段树一样,将点权 pushup。
然后是区间修改和区间查询。
如果要维护区间 [i,j] 这个闭区间,要转化为维护(i-1,j+1) 这个开区间。
如果当前节点是正好是区间左端点,而且是其父节点的左儿子,那么他的兄弟就是要维护的区间的一部分。同理,如果当前节点是正好是区间右端点,而且是其父节点的右儿子,那么他的兄弟也是要维护的区间的一部分。
如果我满足条件,就维护我的兄弟和我的父亲
当两个节点是兄弟节点时,也就是循环结束的时候。这时再将父节点到根节点这段维护了,就是 zkw 线段树的全过程了。
在上浮过程中,懒标上传比较困难,所以选择标记永久化。
比较难讲,看了网上好多 zkw 的讲解,都有些朦胧。
可以根据代码理解,依旧是 P3372:
#include
#define int long long
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=1e5+7;
int n,q,m=1,op,l,r,v;
struct zkw_sgt{
int le,ri;
int val,tag;
}tre[inf<<2];
void build()
{
while(m<=n)m<<=1;
for(int i=m+1;i<=m+n;i++)
tre[i].val=re();
for(int i=m-1;i>0;i--)
tre[i].val=tre[i<<1].val+tre[i<<1|1].val;
}
void update(int i,int j,int k)
{
int ltag=0,rtag=0,len=1;
while(i^j^1)
{
if(i&1^1)tre[i^1].tag+=k,ltag+=len;
if(j&1)tre[j^1].tag+=k,rtag+=len;
tre[i>>1].val+=k*ltag;
tre[j>>1].val+=k*rtag;
i>>=1,j>>=1,len<<=1;
}
int tag=ltag+rtag;i>>=1;
while(i)
{
tre[i].val+=k*tag;
i>>=1;
}
}
int ask(int i,int j)
{
int ltag=0,rtag=0,len=1,ans=0;
while(i^j^1)
{
if(i&1^1)ans+=tre[i^1].val+tre[i^1].tag*len,ltag+=len;
if(j&1)ans+=tre[j^1].val+tre[j^1].tag*len,rtag+=len;
if(tre[i>>1].tag)ans+=tre[i>>1].tag*ltag;
if(tre[j>>1].tag)ans+=tre[j>>1].tag*rtag;
i>>=1,j>>=1,len<<=1;
}
int tag=ltag+rtag;i>>=1;
while(i)
{
if(tre[i].tag)ans+=tre[i].tag*tag;
i>>=1;
}
return ans;
}
signed main()
{
n=re();q=re();
build();
for(int i=1;i<=q;i++)
{
op=re(),l=re(),r=re();
if(op==1)v=re(),update(l+m-1,r+m+1,v);
else wr(ask(l+m-1,r+m+1)),putchar('\n');
}
return 0;
}
拆位线段树
模板
首先明确,\((a+b)\oplus c\ne (a\oplus c)+(b\oplus c)\)。
那么这个题直接用线段树维护区间和显然是行不通的。将区间求和转化成多个单点求和甚至比暴力还慢。
对于牵扯到异或运算的,有两种解决措施:
-
异或前缀和
-
拆位
对于这个题来说,\(a_i\le10^6\),大概只需要拆 20 位左右。
然后用 20 棵线段树维护每一位。再结合左移和右移完成对输入数据和输出结果的处理。
代码如下:
#include
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(long long s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=1e5+7;
int n,m,a[inf];
struct a_tree{
int le,ri,siz;
int val,tag;
}tre[20][inf<<2];
void build(int k,int i,int l,int r)
{
tre[k][i].le=l,tre[k][i].ri=r;tre[k][i].siz=r-l+1;
if(l==r)
{
tre[k][i].val=(a[l]>>k)&1;
return;
}
int mid=(l+r)>>1;
build(k,i<<1,l,mid);
build(k,i<<1|1,mid+1,r);
tre[k][i].val=tre[k][i<<1].val+tre[k][i<<1|1].val;
}
void pushdown(int k,int i)
{
tre[k][i<<1].val=tre[k][i<<1].siz-tre[k][i<<1].val;
tre[k][i<<1|1].val=tre[k][i<<1|1].siz-tre[k][i<<1|1].val;
tre[k][i<<1].tag^=1;tre[k][i<<1|1].tag^=1;
tre[k][i].tag=0;
}
void update(int k,int i,int l,int r)
{
if(l<=tre[k][i].le&&tre[k][i].ri<=r)
{
tre[k][i].val=tre[k][i].siz-tre[k][i].val;
tre[k][i].tag^=1;
return;
}
if(tre[k][i].tag)pushdown(k,i);
int mid=(tre[k][i].le+tre[k][i].ri)>>1;
if(l<=mid)update(k,i<<1,l,r);
if(mid>1,ans=0;
if(l<=mid)ans+=ask(k,i<<1,l,r);
if(mid>i)&1;
if(v)update(i,1,l,r);
}
}
}
return 0;
}
可持久化线段树
例题
可持久化线段树模板
主席树模板
关于可持久化
能够保留每一个历史版本的数据结构。
那么可持久化线段树就是能保留历史版本的线段树。
原谅我之前一直叫它可持续化线段树 。
思路
拿 模板 1 来讲,
因为要保留历史版本。
那么如果对于每次的的修改和查询均新生成一棵线段树的话……
一棵线段树需要 \(4\times n\) 的空间, \(m\) 次操作代表了 \(m\) 棵线段树,空间复杂度 \(O(4mn)\) ……

所以不能直接暴力。
可持久化线段树的基本思想就是动态开点。
当对一个叶节点进行修改之后,你会发现整棵树上修改的点会形成一条链,那我们就将这条链维护出来,也就是下图的红色节点(图片来自 OI wiki )。

这时,我们就不能简单地 “ i<<1 是 i 的左儿子, i<<1|1 是 i 的右儿子” 了,动态开点会导致可持久化线段树不满足线段树的性质,所以我们要将每个节点的左右儿子记录下来。
-
建树
建树过程与普通线段树相比,只是加点方式不同,而且有了返回值,以便于回溯时将自己的编号传给父节点。
代码实现:
int build(int i,int le,int ri)
{
i=++cnt;
if(le==ri)
{
tre[i].val=a[le];
return i;
}
int mid=(le+ri)>>1;
tre[i].lsn=build(tre[i].lsn,le,mid);
tre[i].rsn=build(tre[i].rsn,mid+1,ri);
return i;
}
-
修改
在修改的过程中,我们先新建一个节点,赋给它对应的历史节点的信息(包括左右子节点和权值),同样在回溯的过程中将编号传给父亲。
还有,每新建一棵线段树,都要将根节点记录下来,就是历史版本。
代码实现:
int change(int i,int id,int key,int le,int ri)
{
tre[++cnt]=tre[i];
i=cnt;
if(le==ri)
{
tre[i].val=key;
return i;
}
int mid=(le+ri)>>1;
if(id<=mid)tre[i].lsn=change(tre[i].lsn,id,key,le,mid);
else tre[i].rsn=change(tre[i].rsn,id,key,mid+1,ri);
return i;
}
-
查询
查询就和普通线段树基本一样了,由于新版本和历史版本完全相同,所以直接赋过去就可。
代码实现:
long long ask(int i,int id,int le,int ri)
{
if(le==ri)
return tre[i].val;
int mid=(le+ri)>>1;
if(id<=mid)return ask(tre[i].lsn,id,le,mid);
else return ask(tre[i].rsn,id,mid+1,ri);
}
还记得那首诗嘛?
一年 OI 一场空,不开 long long 见祖宗。
可持久化线段树码量不大,和普通线段树差不多。再加上本人有 不严重的 压行习惯。
坑点
-
空间
线段树对的空间的需求极大,典型的空间换时间,所以数组千万不要开太大,可持续化线段树开到原数组大小的 23 被就好了,否则就会无情的
Runtime Error.和Memory Limit Exceeded.。 -
还是空间
可能有和我一样将每个节点代表的区间左右端点存入结构体中。但是,可持续化线段树的结构体中不要存,千万不要存。不做题永远不知道第二个点多毒瘤。
Code
#include
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(long long s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=1e6+7;
struct long_tree{
int lsn,rsn;
long long val;
}tre[inf*23];
int n,m,cnt;
int a[inf],root[inf];
int v,op,id,k;
int build(int i,int le,int ri)
{
i=++cnt;
if(le==ri)
{
tre[i].val=a[le];
return i;
}
int mid=(le+ri)>>1;
tre[i].lsn=build(tre[i].lsn,le,mid);
tre[i].rsn=build(tre[i].rsn,mid+1,ri);
return i;
}
int change(int i,int id,int key,int le,int ri)
{
tre[++cnt]=tre[i];
i=cnt;
if(le==ri)
{
tre[i].val=key;
return i;
}
int mid=(le+ri)>>1;
if(id<=mid)tre[i].lsn=change(tre[i].lsn,id,key,le,mid);
else tre[i].rsn=change(tre[i].rsn,id,key,mid+1,ri);
return i;
}
long long ask(int i,int id,int le,int ri)
{
if(le==ri)
return tre[i].val;
int mid=(le+ri)>>1;
if(id<=mid)return ask(tre[i].lsn,id,le,mid);
else return ask(tre[i].rsn,id,mid+1,ri);
}
int main()
{
n=re();m=re();
for(int i=1;i<=n;i++)
a[i]=re();
root[0]=build(0,1,n);
for(int i=1;i<=m;i++)
{
v=re();op=re();id=re();
if(op==1)k=re(),root[i]=change(root[v],id,k,1,n);
else wr(ask(root[v],id,1,n)),putchar('\n'),root[i]=root[v];
}
return 0;
}
主席树
线段树可以加可持久化,权值线段树也可以。
主席树,全名 可持久化权值线段树 。
引入
那么看一下 模板 2 。
给定 n 个整数构成的序列 a,将对于指定的闭区间 [l,r] 查询其区间内的第 k 小值。
好像就是把区间从 \(1\sim n\) 扩展到了 \(l\sim r\) 。
于是我们就从权值线段树扩展到了可持续化权值线段树。
思路
- STL 暴力。
可以看一下 这个题
我们就用这个题不让用的 nth_element 。
这个东西可以直接在 \(O(n)\) 的时间内求区间最小值。
我们找一个临时数组 ls 存 a[l]~a[r] ,然后就用 nth_element 苟到了 50 分。
就像酱紫:
#include
#include
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=2e5+7;
int n,m,l,r,k;
int a[inf],ls[inf];
int main()
{
n=re();m=re();
for(int i=1;i<=n;i++)
a[i]=re();
for(int ii=1;ii<=m;ii++)
{
l=re();r=re();k=re()-1;
int i=l,j=0;
while(i<=r)
ls[j]=a[i],i++,j++;
nth_element(ls,ls+k,ls+j);
wr(ls[k]);putchar('\n');
}
return 0;
}
- 正解
结合前缀和,用权值线段树维护 \(1\sim i\) 。
查询时再结合差分查询 \((1\sim r)-(1\sim (l-1))\) 。
将整个数组排序,去重,然后对于原来的每个点,建 1 到值域的主席树。
如果值域太大但实际上数很少时,可以考虑离散化,但显然这个题不需要。
最后对每个询问进行查询。
Code
此题比上述的可持久化线段树还短。
#include
#include
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=2e5+7;
struct long_tree{
int lsn,rsn;
int sum;
}tre[inf<<5];
int n,m,cnt;
int a[inf],b[inf];
int root[inf];
int l,r,k;
int add(int i,int le,int ri,int key)
{
tre[++cnt]=tre[i];
i=cnt;tre[i].sum++;
if(le==ri)return i;
int mid=(le+ri)>>1;
if(key<=b[mid])tre[i].lsn=add(tre[i].lsn,le,mid,key);
else tre[i].rsn=add(tre[i].rsn,mid+1,ri,key);
return i;
}
int ask(int i,int j,int le,int ri,int k)
{
if(le==ri)return le;
int mid=(le+ri)>>1,
ans=tre[tre[j].lsn].sum-tre[tre[i].lsn].sum;
if(k<=ans)return ask(tre[i].lsn,tre[j].lsn,le,mid,k);
else return ask(tre[i].rsn,tre[j].rsn,mid+1,ri,k-ans);
}
int main()
{
n=re();m=re();
for(int i=1;i<=n;i++)
a[i]=b[i]=re();
sort(b+1,b+n+1);
int num=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++)
root[i]=add(root[i-1],1,num,a[i]);
for(int i=1;i<=m;i++)
{
l=re(),r=re(),k=re();
wr(b[ask(root[l-1],root[r],1,num,k)]);
putchar('\n');
}
return 0;
}
扫描线
P5490 扫描线
线段树套线段树
P3810 陌上花开