线段树


线段树+

目录

  • 朴素线段树
    • 定义
    • 用处
    • 树的储存
    • 建树
    • 区间求和
    • 区间加
  • 懒标优先级
  • 权值线段树
  • 标记永久化
  • 动态开点
  • 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 的右儿子” 了,动态开点会导致可持久化线段树不满足线段树的性质,所以我们要将每个节点的左右儿子记录下来。

  1. 建树

    建树过程与普通线段树相比,只是加点方式不同,而且有了返回值,以便于回溯时将自己的编号传给父节点。

    代码实现:

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;
}
  1. 修改

    在修改的过程中,我们先新建一个节点,赋给它对应的历史节点的信息(包括左右子节点和权值),同样在回溯的过程中将编号传给父亲。

    还有,每新建一棵线段树,都要将根节点记录下来,就是历史版本。

    代码实现:

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;
}
  1. 查询

    查询就和普通线段树基本一样了,由于新版本和历史版本完全相同,所以直接赋过去就可。

    代码实现:

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 见祖宗。

可持久化线段树码量不大,和普通线段树差不多。再加上本人有 不严重的 压行习惯。

坑点

  1. 空间

    线段树对的空间的需求极大,典型的空间换时间,所以数组千万不要开太大,可持续化线段树开到原数组大小的 23 被就好了,否则就会无情的 Runtime Error. 和 Memory Limit Exceeded. 。

  2. 还是空间

    可能有和我一样将每个节点代表的区间左右端点存入结构体中。但是,可持续化线段树的结构体中不要存,千万不要存。不做题永远不知道第二个点多毒瘤。

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 陌上花开