线段树学习笔记

线段树

线段树的作用&原理

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。

以上内容抄自百度百科.

在这里插入图片描述

一般来说线段树长这样(画的有点丑),每一个节点(非叶节点)都是从它的两个子树的根节点合并而成,所以线段树维护的值必须支持合并(具体下面会讲到),对于每一次的修改可以直接修改若干颗子树,可以证明最多修改的子树不会超过 log ⁡ 2 N \log_2N log2N棵,所以每次的修改的时间复杂度为 log ⁡ 2 N \log_2N log2N,查询同理.

可以在logN的复杂度内修改和查询一定的信息(如区间加一个数,查询区间和,查询区间最大值等).
线段树常用来维护DP,甚至可以使暴力获得更多分.(骗分必备)

线段树的实现

  • define
#define Lson now*2;
#define Rson now*2+1
#define Middle (left+right)/2
#define Left Lson,left,Middle
#define Right Rson,Middle+1,right
#define Now nowleft,nowright

先从最基础的开始例题1

  • PushUp

线段树最先要写的部分是合并(应该,个人习惯)
可以发现这里要维护的是一个区间和,那么合并就很简单了

void PushUp(int now)
{
	tree[now].sum=tree[Lson].sum+tree[Rson].sum;
}
  • Build

写完合并,接下来要建树(有些题目可能用不上)
建树时最多也就只会有 N ∗ 4 N*4 N4个节点,所以建树的复杂度是 O ( N ) O(N) O(N)的.

void Build(int now=1,int left=1,int right=N)//建树的初始值固定
{
	if(left==right)//叶节点时直接赋值
	{
		tree[now].sum=arr[left];
		return;
	}
	Build(Left);//建左子树
	Build(Right);//建右子树
	PushUp(now);//建完后需要合并
}
  • Lazy标记&PushDown

下面,就要引出线段树的精髓了:Lazy标记,又称懒标记,如果没有这个标记在每次修改时的时间复杂度会变得很高,没法达到只修改 log ⁡ 2 N \log_2N log2N个值.

在这里插入图片描述

如图,需要修改蓝色区域的值,那么它覆盖的部分为红色的两颗子树,但是绿色的位置的值也发生了改变,这时就需要在红色位置打上lazy标记,lazy标记也要支持合并,在以后的修改时需要查询到绿色部分时才会将标记下推.

在这里插入图片描述

如图,需要查询紫色部分的值,那么如果需要查询蓝色部分的那两颗子树的值,这时就需要将红色位置的标记下推,为了得到蓝色部分的真实的值.

void Down(int now,int left,int right,int lazy_)//修改子树
{
	tree[now].sum+=(right-left+1)*lazy_;//子树表示的值需要加上子树长度*每个数增加的值
	lazy[now]+=lazy_;//子树懒标记增加
}
void PushDown(int now,int left,int right)
{
	Down(Left,lazy[now]);//修改左子树
	Down(Right,lazy[now]);//修改右子树
	lazy[now]=0;//lazy标记必须清空
}
  • Updata

写下来就是修改部分了,这点并没有什么特别值得再说的地方.

void Updata(int nowleft,int nowright,int num,int now=1,int left=1,int right=N)
//其中的now,left,right一般不会变,所以就缺省了
{
	if(left>nowright||nowleft>right)return;//如果需要查询的区间与当前区间没有公共位置则推出
	if(nowleft<=left&&right<=nowright)//如果需要查询的区间包含了当前区间可以直接修改
	{
		tree[now].sum+=num*(right-left+1);
		lazy[now]+=num;//注意修改懒标记
		return;
	}
	PushDown(now,left,right);//需要推一下懒标记为了下方的修改
	Updata(Now,num,Left);//修改左子树
	Updata(Now,num,Right);//修改右子树
	PushUp(now);//合并
}
  • Query

查询与修改类似.

int Query(int nowleft,int nowright,int now=1,int left=1,int right=N)
{
	if(left>nowright||nowleft>right)return 0;//不在范围内
	if(nowleft<=left&&right<=nowright)//包含了
	{
		return tree[now].sum;
	}
	PushDown(now,left,right);//推懒标记
	int result=0;
	result+=Query(Now,Left);//需要将左右子树的值相加
	result+=Query(Now,Right);
	PushUp(now);//合并后再退出
	return result;
}

完整的代码

#include<bits/stdc++.h>
#define rap(i,first,last) for(int i=first;i<=last;++i)//就是那个rap蛤
#define Lson (now<<1)
#define Rson (now<<1|1)
#define Middle ((left+right)>>1)
#define Left Lson,left,Middle
#define Right Rson,Middle+1,right
#define Now nowleft,nowright 
using namespace std;
const int maxN=1e5+7;
int N,M;
struct Tree
{
	long long sum;
}tree[maxN*4];
long long arr[maxN];
int lazy[maxN*4];
void PushUp(int now)
{
	tree[now].sum=tree[Lson].sum+tree[Rson].sum;
}
void Build(int now=1,int left=1,int right=N)
{
	lazy[now]=0;
	if(left==right)
	{
		tree[now].sum=arr[left];
		return;
	}
	Build(Left);
	Build(Right);
	PushUp(now);
}
void Down(int now,int left,int right,int lazy_)
{
	tree[now].sum+=(right-left+1)*lazy_;
	lazy[now]+=lazy_;
}
void PushDown(int now,int left,int right)
{
	if(!lazy[now])return;
	Down(Left,lazy[now]);
	Down(Right,lazy[now]);
	lazy[now]=0;
}
void Updata(int nowleft,int nowright,int num,int now=1,int left=1,int right=N)
{
	if(left>nowright||nowleft>right)return;
	if(nowleft<=left&&right<=nowright)
	{
		tree[now].sum+=num*(right-left+1);
		lazy[now]+=num;
		return;
	}
	PushDown(now,left,right);
	Updata(Now,num,Left);
	Updata(Now,num,Right);
	PushUp(now);
}
long long Query(int nowleft,int nowright,int now=1,int left=1,int right=N)
{
	if(left>nowright||nowleft>right)return 0;
	if(nowleft<=left&&right<=nowright)
	{
		return tree[now].sum;
	}
	PushDown(now,left,right);
	long long result=Query(Now,Left)+Query(Now,Right);
	PushUp(now);
	return result;
}
int main()
{
	scanf("%d%d",&N,&M);
	rap(i,1,N)scanf("%lld",&arr[i]);
	Build();
	int check,left,right,num;
	rap(i,1,M)
	{
		scanf("%d%d%d",&check,&left,&right);
		if(check==1)
		{
			scanf("%d",&num);
			Updata(left,right,num);
		}
		if(check==2)
		{
			printf("%lld\n",Query(left,right));
		}
	}
	return 0;
}

推荐题目

线段树2
最大数
无聊的数列
方差
贪婪大陆
好一个一中腰鼓!(注意要用线段树,虽然暴力可以过)
序列维护
CPU监控

权值线段树

权值线段树的的原理&作用

权值线段树其实很简单,类似一个桶,每次修改都是单点修改,所以连lazy标记都不用

在这里插入图片描述

可以很容易得出整个数列第k大值,只需要在树上二分就行了.
也可以处理一些区间中出现次数最多的数的个数之类的问题.
例题

权值线段树的代码实现

  • Updata
void Updata(int num,int now=1,int left=1,int right=N)
{
	if(num>right||num<left)return;//按这个数的大小左右二分,最多LogN次就到叶节点,所以每次修改的时间复杂度为O(LogN)
	tree[now]++;//可以直接修改
	if(left==right)
	{
		return;
	}
	Updata(num,Left);
	Updata(num,Right);
}
  • Query
int Query(int num,int now=1,int left=1,int right=N)
{
	if(num==0)return 0;//可有可无
	if(left==right)//到叶节点说明找到了
	return left;
	if(tree[Lson]>=num)return Query(num,Left);//如果在左子树就往左子树找
	else return Query(num-tree[Lson],Right);//不在就往右子树找
}

有一点需要注意的是这类题中的数往往较大,但个数一般不能超过5e5,所以需要用到离散化.

推荐题目

三元上升子序列
普通平衡树(你没有看错)

动态开点

动态开点的作用&原理

动态开点是线段树中的一个基础知识,在线段树合并,分裂,主席树等地方都是必须用到的.
在一些用到权值线段树的题目中数据如果数据大于1e6基本就会跑不过了,但是,有了动态开点,可以节省很多用不到的点.

在这里插入图片描述

如图,在这样一颗线段树中,灰色部分的节点时没有用的如果在权值线段树中像这样灰色的节点的数量可能会很多,这样极大的浪费了空间,所以需要用一种新的方式来存这棵树,动态开点就是这样出现的.
在原本的线段树数中一个节点x的左儿子为x*2右儿子为x*2+1.
而现在,不能通过计算的方式获得儿子的位置,需要再开两个数组来记录左右儿子的位置,每次修改时如果需要修改到一个没有加入的节点时才会将这个节点放入这颗树中,这样可以节省很多的空间.

动态开点的代码实现

大体与普通线段树类似,但是在带修改的部分时传入的now为地址需要修改.
一下内容为线段树1动态开点的部分代码

//以下部分需要更改
#define Lson tree[now].lson
#define Rson tree[now].rson
struct Tree
{
	int lson,rson;
	long long sum;
}
  • PushDown
void Down(int &now/*在Down时会修改部分的值,所以需要传入地址*/,int left,int right,int add)
{
	if(!now)now=++cnt;//如果没有这个点就加入这个点
	tree[now].sum+=(right-left+1)*add;
	lazy[now]+=add;
}
void PushDown(int now,int left,int right)
{
	if(lazy[now])//如果不加在根本没有需要下传的标记时可能会多开很多的点
	{
		Down(Left,lazy[now]);
		Down(Right,lazy[now]);
		lazy[now]=0;
	}
}
  • Updata
void Updata(int nowleft,int nowright,int add,int &now/*这里也需要传入一个地址*/,int left=1,int right=N)
{
	if(nowleft>right||left>nowright)return;
	if(!now)now=++cnt;//如果没有这个点就加入这个点
	if(nowleft<=left&&right<=nowright)
	{
		tree[now].sum+=(right-left+1)*add;
		lazy[now]+=add;
		return;
	}
	PushDown(now,left,right);
	Updata(Now,add,Left);
	Updata(Now,add,Right);
	PushUp(now);
}
  • mian()
int root=1;//其实root并不会被修改,但是因为需要传入一个变量所以必须要有这样一个量
int mian()
{
	scanf("%d%d",&N,&M);
	cnt=1;
	rap(i,1,N)
	{
		scanf("%d",&arr[i]);
		Updata(i,i,arr[i],root);//直接加入这棵树
	}
	int check,L,R,X;
	rap(i,1,M)
	{
		scanf("%d%d%d",&check,&L,&R);
		if(check==1)
		{
			scanf("%d",&X);
			Updata(L,R,X,root);//这里也需要传入root
		}
		else
		printf("%lld\n",Query(L,R));//Query没有什么区别.
	}
}

线段树合并

线段树合并的原理&作用

线段树合并基于动态开点,所以需要先学习有关动态开点的内容后再看一下这部分内容
在一些题目中需要将两颗线段树和在一起,组成一棵更大的线段树,这时就需要用到线段树合并.

在这里插入图片描述

在如图所示的两颗线段树中(线段树为动态开点,灰色部分为没有值),如果把这两颗线段树合并后为:

在这里插入图片描述

在有值的位置将两颗线段树的值相加,如果有一棵线段树这一个位置没有值则为另一棵线段树的值.

可以将两颗线段树和为一棵线段树,看起来可能没什么用,先拿一道例题康康,查询第k大值,这就很容易想到权值线段树了,但是,将不同的岛相连后这两个岛可以说变为一个岛,这时,线段树合并就派上用场了,可以对于每个岛开一棵权值线段树,在不同的岛相连时将线段树合并,用并查集判断连通性和每一片岛中的root,这样就可以很轻松的写出这道题了.

线段树合并的代码实现

  • Merge

以下代码为将tree2合并到tree1上.

void Merge(int &tree1/*因为tree1是要修改的,所以需要传入地址*/,int tree2,int left=1,int right=N)
{
	if(!tree1||!tree2)//当当前子树在tree1和tree2中有一个没有时直接就是有的那棵
	{
		tree1+=tree2;
		return;
	}
	if(left==right)//叶节点时tree1不变,将叶节点的值相加
	{
		tree[tree1].sum=tree[tree1].sum+tree[tree2].sum;
		return;
	}
	//向左右合并这颗线段树
	Merge(tree[tree1].lson,tree[tree2].lson,left,Middle);
	Merge(tree[tree1].rson,tree[tree2].rson,Middle+1,right);
	PushUp(tree1);//将合并后的线段树上的值合并
}

线段树合并的复杂度

可以发现线段树合并时如果两颗树都有的部分需要全部扫一遍,有一棵树没有的部分可以直接返回,所以它的复杂度为两树在同一位置都有节点的节点的个数,而一颗树中只有 N ∗ 4 N*4 N4个节点,所以线段树合并的时间复杂度为 O ( N ) O(N) O(N).

线段树空间回收

空间回收的作用&原理

可以发现在线段树合并中会浪费掉很多的空间(每次合并节点时就会有一个节点被浪费掉),在比赛中每一点的空间都是极为珍贵,于是就出现了线段树的空间回收.
可以将一些不再会有用的点先存放起来,在未来需要开出一个新的点时可以先将这些被扔掉的点用掉,在线段树合并中可以节省大量的空间.
空间回收的原理也非常简单,一个 r u b b i s h rubbish rubbish数组,对于每一次浪费掉的节点放入这个数组中,如果需要开一个新的节点时可以看一下这个数组中有没有节点,有就用掉,没有就再开一个新的节点,这样可以节省大量的空间.

空间回收的代码实现

  • Del

删除一个点

void Del(int now)
{
	tree[now].lson=tree[now].rson=0;
	tree[now]./*这个节点中的内容都要删除*/;
	rubbish[++tot]=now;//可以将这个rubbish数组理解为一个栈,tot为栈顶
}
  • New

建一个新的节点.

int New()
{
	if(tot)return rubbish[tot--];//弹出栈顶元素
	return ++cnt;
}//代码非常的简单,但是有着巨大的作用

空间回收只能用在一些特殊的线段树中(主要是线段树合并).

线段树分裂

线段树分裂的原理

这是一个没有什么用的东西
找了很久也没有什么特别好的题和讲解的博客,于是自己yy出了一道题.

在这里插入图片描述

如图这样的一棵线段树,需要分裂出其中橙色的部分,需要新建几个节点(绿色),需要把原来的数
中与这颗子树有关的边断开(红色线段树分开的边).

线段树分裂的代码实现

  • Split
void Split(int &tree1,int &tree2,int nowleft,int nowright,int left=1,int right=N)
//在tree1这棵权值线段树中把left~right的部分分裂到tree2中
{
	if(right<nowleft||nowright<left)return;//不在区间内
	if(!tree1)return;//如果tree1没有那自然没有用了
	if(nowleft<=left&&right<=nowright)//如果在范围内就直接赋值
	{
		tree2=tree1;//直接连到tree2中
		tree1=0;//如果当前区间已经被完全覆盖了就需要把这条边断开
		return;
	}
	if(left==right)return;//叶节点返回
	if(!tree2)tree2=New();//如果不在范围内需要开一个新的点(绿色部分)
	//左右区间分裂
	Split(tree[tree1].lson,tree[tree2].lson,Now,left,Middle);
	Split(tree[tree1].rson,tree[tree2].rson,Now,Middle+1,right);
	PushUp(tree1);//最后合并信息
	PushUp(tree2);
}

线段树分裂的时间复杂度

最多只会断开 log ⁡ 2 N \log_2N log2N条边,所以时间复杂度是 O ( log ⁡ 2 N ) O(\log_2N) O(log2N).

标记永久化

标记永久化的作用&原理

普通的线段树需要PushUpPushDown,那有没有存在一种线段树在修改时不用到他们呢,于是标记永久化就出现了.
可以缩短代码,再也不用写PushUp和PushDown了,主要用在主席树这种不适合修改的数据结构上.
对于每次修改就在修改的最上层的点上打上标记,在查询时就只需要一路向下将标记合并就行了.

标记永久化的代码实现

  • 标记
struct Tree
{
	long long sum;
	long long tag;//需要加上一个标记用的量,但是不用懒标记
}tree[maxN*4];
  • PushDown

它死了

  • Updata
void Updata(int nowleft,int nowright,int add,int now=1,int left=1,int right=N)
{
	if(nowright<left||right<nowleft)return;
	tree[now].sum+=1ll*(min(nowright,right)-max(nowleft,left)+1)*add;
	//修改的区间对于当前区间的贡献
	if(nowleft<=left&&right<=nowright)
	{
		tree[now].tag+=add;//包含了就在tag中加上
		return;
	}
	Updata(Now,add,Left);
	Updata(Now,add,Right);
	//PushUp好像也没有什么必要了
}
  • Query
long long Query(int nowleft,int nowright,int now=1,int left=1,int right=N)
{
	if(nowright<left||right<nowleft)return 0;
	if(nowleft<=left&&right<=nowright)return tree[now].sum;//包含就直接返回
	return 1ll*(min(nowright,right)-max(nowleft,left)+1)*tree[now].tag//当前的tag对于这次查询的贡献
	+Query(Now,Left)+Query(Now,Right);//左右区间的值
}

二维线段树

二维线段树的作用&原理

二维线段树有两种写法,四分树树套树,根据dalao的说法,四分树非常的菜很容易被卡,最坏时修改需要O(N),所以以下内容主要讲树套树的写法.
顾名思义,二维线段树是用来修改二维平面上的东西(用处其实不大),这里的树套树是在线段树的每个节点上再开一棵线段树,这样每次修改和查询的时间复杂度就是 O ( N log ⁡ 2 2 N ) O(N\log^2_2N) O(Nlog22N)(大概).

二维线段树的代码实现

拿出一道模板题.
区间最大值+区间覆盖(但是这里的区间是一个矩形),对于线段树其实没什么变化,就是要用上标记永久化,据说二维线段树不支持打标记.

  • 内层的线段树
struct SegmentTreeX//用两个结构体表示外传的线段树和内层的线段树,这样方便一点
{
	int tree[maxN<<2],tag[maxN<<2]/*用于记录标记*/;
	void Updata(int nowleft,int nowright,int cover,int now=1,int left=1,int right=M)//这里就是最普通的线段树了
	{
		if(nowright<left||right<nowleft)return;
		tree[now]=max(tree[now],cover);//修改当前子树
		if(nowleft<=left&&right<=nowright)
		{
			tag[now]=max(tag[now],cover);///修改标记
			return;
		}
		Updata(Now,cover,Left);
		Updata(Now,cover,Right);
	}
	int Query(int nowleft,int nowright,int now=1,int left=1,int right=M)//查询,实在没什么好说的
	{
		if(nowright<left||right<nowleft)return -INF;
		if(nowleft<=left&&right<=nowright)
		return max(tree[now],tag[now]);
		return max(tag[now],//要和标记取max
			   max(Query(Now,Left),Query(Now,Right)));
	}
};
  • 外层的线段树
struct SegmentTreeY//外层线段树
{
	SegmentTreeX tree[maxN<<2],tag[maxN<<2];//这里的每个点和标记也是一颗线段树
	void Updata(int nowlx,int nowly,int nowleft,int nowright,int cover,int now=1,int left=1,int right=N)//其余基本相同,就是在原先的标记修改和子树修改需要改成修改内层的线段树
	{
		if(nowright<left||right<nowleft)return;
		tree[now].Updata(nowlx,nowly,cover);
		if(nowleft<=left&&right<=nowright)
		{
			tag[now].Updata(nowlx,nowly,cover);
			return;
		}
		Updata(NowX,cover,Left);
		Updata(NowX,cover,Right);
	}
	int Query(int nowlx,int nowly,int nowleft,int nowright,int now=1,int left=1,int right=N)//查询也差不多
	{
		if(nowright<left||right<nowleft)return -INF;
		if(nowleft<=left&&right<=nowright)
		return tree[now].Query(nowlx,nowly);
		return max(tag[now].Query(nowlx,nowly),
			   max(Query(NowX,Left),Query(NowX,Right)));
	}
}SegmentTree;

扫描线

扫描线的作用&原理

放一道模板题.
给出N个矩形,求最终所覆盖的面积,看起来很像一道二维线段树,但是数据范围很大,所以就T掉了,所以,伟大的人类发明了一种新的解决方法–扫描线.
在这里插入图片描述

图中的蓝色线段为扫面线.
先扫到:

在这里插入图片描述
可以发现在到下一条平行于扫描线的边之间是一个长方形,可以直接计算出来.
![在这里插入图片描述](http
s://img-blog.csdnimg.cn/20200207112930605.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N4eV9fb3J6,size_16,color_FFFFFF,t_70)
同样的方法可以将这个图形分成若干不相交的长方形之和.

在这里插入图片描述

每一块长方形的宽就是相邻的两条平行与扫面线的边之间的距离,长就是扫描线的长了,所以,问题就变成了如何计算扫面线的长,一个长方形有两条与扫描线平行的边,遇到左边的一条时就加上1,遇到右边时就减去1,这样扫面线的长度就是不为0的位置的个数,区间加减1很容易就想到线段树了.

扫描线的实现

#include<bits/stdc++.h>
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int maxN=2e5+7;
int N,M;
map<long long,long long>Hash;
long long place[maxN*2];
long long sor[maxN*2];
struct Line
{
	int val;
	long long x,fy,ly;
	void into(int X,int F,int L,int V)
	{
		x=X;
		fy=F;
		ly=L;
		val=V;
	}
}line[maxN*2];
bool cmp(Line a,Line b)
{
	return a.x<b.x;
}
struct Tree
{
	int cover;
	int len;
}tree[maxN*4];
#define LSON (now<<1)
#define RSON (now<<1|1)
#define MIDDLE ((left+right)>>1)
#define LEFT LSON,left,MIDDLE
#define RIGHT RSON,MIDDLE+1,right
#define NOW now_left,now_right
void PushUp(int now,int left,int right)
{
	if(tree[now].cover)//如果被完全覆盖,那么长度就是可以直接计算
	{
		tree[now].len=place[right+1]-place[left];
	}
	else//没有被完全覆盖就合并儿子节点的信息
	{
		tree[now].len=tree[LSON].len+tree[RSON].len;
	}
}
void Build(int now=1,int left=1,int right=N)//建树,没什么用
{
	if(left==right)
	{
		tree[now].cover=tree[now].len=0;
		return;
	}
	Build(LEFT);
	Build(RIGHT);
	PushUp(now,left,right);
}
void Updata(int now_left,int now_right,int add,int now=1,int left=1,int right=N)
{
	if(now_right<=left||right+1/*注意需要加一,且用小于等于*/<=now_left)
	{
		return;
	}
	if(now_left<=left&&right+1/*注意加一*/<=now_right)
	{
		tree[now].cover+=add;
		PushUp(now,left,right);
		return;
	}
	Updata(NOW,add,LEFT);
	Updata(NOW,add,RIGHT);
	PushUp(now,left,right);
}
int main()
{
	scanf("%d",&M);
	long long x1,y1,x2,y2;
	REP(i,1,M)
	{
		scanf("%lld%lld%lld%lld",&x1,&y1,&x2,&y2);
		sor[i*2-1]=y1;//数据太大,需要对于纵坐标离散
		sor[i*2]=y2;
		line[i*2-1].into(x1,y1,y2,1);
		line[i*2].into(x2,y1,y2,-1);
	}
	sort(sor+1,sor+1+M*2);
	sort(line+1,line+1+M*2,cmp);
	sor[0]=114514233;
	REP(i,1,M*2)
	{
		if(sor[i]!=sor[i-1])
		{
			Hash[sor[i]]=++N;
			place[N]=sor[i];
		}
	}
	Build();
	long long answer=0;
	REP(i,1,M*2-1)
	{
		Updata(Hash[line[i].fy],Hash[line[i].ly],line[i].val);//修改操作
		answer+=tree[1].len/*扫描线长*/*(line[i+1].x-line[i].x)/*两边之间的距离*/;
	}
	printf("%lld",answer);
}

主席树

为什么叫主席树

主席树由一位名叫黄嘉泰的神仙发明,然后可以惊奇的发现黄嘉泰的拼音首字母是hjt,然后…所以就叫主席树了.

主席树的作用&原理

在了解主席树之前先了解一下什么是持久化,类似有关访问历史版本的题目往往和持久化有关,如果需要一个可持久化的线段树需要怎么办呢,一个非常暴力的方法就是将所有历史版本的线段树都保存下来,但这样想必会MLE,且每次复制一个版本也需要 O ( N ) O(N) O(N)的复杂度所以还会TLE.于是就出现了主席树这样一个神奇的数据结构.

在这里插入图片描述

如这样的一颗权值线段树,需要加如一个3.

在这里插入图片描述

可以发现只有红色部分的点的值改变了, 于是就发现每次的修改后有大量的点与之前相同,直接复制就浪费了大量空间,于是可以将新的产生的节点与没有改变的节点相连,这样就可以重复利用这些在历史版本中没有改变的值,节省大量空间,如这样的一个单点修改只会对一条链上的值产生影响,每一颗树就只要产生 log ⁡ 2 N \log_2N log2N个节点.

在这里插入图片描述

最终大概就是这个样子(完全不像树).

主席树的代码实现

单点修改

  • define
//码风略微改变
#define LSON tree[now].lson
#define RSON tree[now].rson
#define MIDDLE ((left+right)>>1)
#define LEFT LSON,left,MIDDLE
#define RIGHT RSON,MIDDLE+1,right
#define NOW now_left,now_right
//再增加以下两句
#define NEW_LSON tree[new_tree].lson
#define NEW_RSON tree[new_tree].rson
  • Updata(权值线段树+动态开点)
void Updata(int &new_tree/*产生的一个新版本*/,int num/*num这个数在线段树中+1*/,int now,int left=1,int right=N)
{
	if(num<left||right<num)//不会被修改到就直接连会原来的版本
	{
		new_tree=now;
		return;
	}
	new_tree=++cnt;//一个新的节点
	if(left==right)//叶节点就单点修改
	{
		tree[new_tree].sum=tree[now].sum+1;//在原版本中+1得到新版本
		return;
	}
	Updata(NEW_LSON,num,LEFT);//对于左右子树继续修改
	Updata(NEW_RSON,num,RIGHT);
	PushUp(new_tree);//合并信息
}

区间修改主席树

区间修改主席树的作用&原理

例如在线段树1加上一个可持久化.可以发现如果像原来的主席树一样的方法去写每次修改最大就会产生一颗完整的线段树,肯定是会MLE的,所以就要拿出之前说过的标记永久化.

在这里插入图片描述

对于一个修改的位置本来它的子孙都应该是被修改了,但是,加上了标记永久化以后就只需要修改自己,不用修改子孙了,所以对于完全被覆盖的点的左右儿子都还是原来的版本的值,注意在产生新版本的时候需要将原来的标记也赋值到新版本中.

区间修改主席树的实现

  • Updata
void Updata(int now_left,int now_right,int add,int &new_tree,int now,int left=1,int right=N)
{
	if(now_left>right||left>now_right)
	{
		new_tree=now;
		return;
	}
	new_tree=++cnt_point;
	tree[new_tree].sum=tree[now].sum+
	1ll*add*(min(now_right,right)-max(now_left,left)+1);//当前修改对于当前区间的贡献
	tree[new_tree].tag=tree[now].tag;//赋值原来的标记
	if(now_left<=left&&right<=now_right)//被完全覆盖
	{
		tree[new_tree].tag+=add;
		NEW_LSON=LSON;//左右儿子不变
		NEW_RSON=RSON;
		return;
	}
	Updata(NOW,add,NEW_LSON,LEFT);
	Updata(NOW,add,NEW_RSON,RIGHT);
}
  • Query
long long Query(int now_left,int now_right,int now,int left=1,int right=N)
{
	//大致与普通查询相同
	if(!now)
	{
		return 0;
	}
	if(now_left>right||left>now_right)
	{
		return 0;
	}
	if(now_left<=left&&right<=now_right)
	{
		return tree[now].sum;
	}
	return Query(NOW,LEFT)+Query(NOW,RIGHT)+
	1ll*tree[now].tag*(min(now_right,right)-max(now_left,left)+1);//当前范围中的标记对于需要查询范围的贡献
}

带修主席树

主席树修改的原理

先扔一道例题.

静态区间kth可以用主席树维护一个前缀每个数出现的次数,这样就可以用通过类似前缀和求区间和将这一个区间中每个数出现的次数计算出来,对于每个点开一颗线段树,因为有大量的相同点,所以可以用主席树.
再来看这道题,加上了一个单点修改的操作.可以再仔细想想,前缀和会想到什么呢…那就是树状数组啦,所以只要在树状数组上套一个线段树就好了,因为是单点修改,然后,带修主席树就变成了树状数组套线段树.

在代码中并没有出现主席树.

代码实现

先建一下这个森林(一堆树)

  • Updata
void Updata(int num,int val,int &new_tree,int now,int left=1,int right=len)
//虽然不用主席树,但还是写了主席树(好像和前面没什么变化)
{
	if(num<left||right<num)
	{
		new_tree=now;
		return;
	}
	if(!new_tree)
	{
		new_tree=++point_cnt;//一个新节点
	}
	if(left==right)
	{
		tree[new_tree].sum=tree[now].sum+val;
		return;
	}
	Updata(num,val,NEW_LEFT,LEFT);
	Updata(num,val,NEW_RIGHT,RIGHT);
	PushUp(new_tree);
}
void UpdataAdd(int top,int num,int val/*需要将原来的树减去,加上新的一个数*/)
{
	for(int now=top;now<=N;now+=lowbit(now))//在树状数组上修改
	{
		Updata(num,val,root[now],root[now]);//在当前的树上修改
	}
}
  • Build
void Build()
{
	REP(i,1,N)
	{
		for(int now=i;now<=N;now+=lowbit(now))//一个点一个点地放入树状数组,记录前缀
		{
			Updata(Hash[arr[i]],1,root[now],root[now]);
		}
	}
}
  • Query
int add_tree[maxN];//记录在树状数组中需要加上的位置的树的当前根节点
int cut_tree[maxN];//记录在树状数组中需要减去的位置的树的当前根节点
int Query(int k,int num_add,int num_cut,int left=1,int right=len)
{
	if(left==right)return left;
	int sum=0;
	REP(i,1,num_add){sum+=tree[tree[add_tree[i]].lson].sum;}//计算出所有左子树中的树的个数
	REP(i,1,num_cut){sum-=tree[tree[cut_tree[i]].lson].sum;}
	if(sum>=k)
	{
		REP(i,1,num_add){add_tree[i]=tree[add_tree[i]].lson;}//将根节点赋值为左儿子
		REP(i,1,num_cut){cut_tree[i]=tree[cut_tree[i]].lson;}
		return Query(k,num_add,num_cut,left,MIDDLE);
	}
	REP(i,1,num_add){add_tree[i]=tree[add_tree[i]].rson;}//同理赋值为右儿子
	REP(i,1,num_cut){cut_tree[i]=tree[cut_tree[i]].rson;}
	return Query(k-sum,num_add,num_cut,MIDDLE+1,right);
}
int QueryKth(int left,int right,int k)
{
	int num_add=0,num_cut=0;
	for(int now=right;now;now-=lowbit(now))//将树状数组中需要加上的部分放入一个数组
	{
		add_tree[++num_add]=root[now];
	}
	for(int now=left-1;now;now-=lowbit(now))//同理,将需要减去的部分放入一个数组
	{
		cut_tree[++num_cut]=root[now];
	}
	return Query(k,num_add,num_cut);//查询kth
}

后记

文章中还有若干写的不清楚的地方,也有一些懒得写的地方,以后有空时可能会把锅补上.(发现问题可以私信我)
有关平衡树,多项式等内容,在若干年内也有可能会出类似学习笔记.
to be continued

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值