可持久化线段树 & 主席树 || 超详细解释 + 模板

 

心血来潮 把这个基础算法结构补了

呐 先了解一下 可持久化线段树 是什么

自然是 可持久化 + 线段树 啦 多用于询问第m次修改后 某 节点 || 区间 的 值

线段树自然是很好理解的(这个不知道就去补一下吧)

然而可持久化怎么弄呢 总不能每次都copy整棵树吧 不然时空复杂度都打得要死

因此 聪明的灵长类动物——裸猿人类们啊 发现

在修改一个 节点 || 区间 时啊 改变的只有他的祖先们

因此 我们只需要将该 节点 || 区间涉及的点 和 他们的祖先 复制一遍 赋上修改后该 节点 || 区间 的值 即可

首先是 单点修改(区间修改全靠自觉啦) 具体细节见代码 题目仍旧是洛谷的模板

Tip1:单点 修改 && 查询 建树只需将序列二分下去 区间记录 左儿子 右儿子 节点上记录 点权值

Tip2:储存 节点 和 区间 的 数组 大概开 树上节点数的20倍(虽然本题我看有人开10倍 但我爆了 超凶的 =-=)

下面是代码加注释 真的超详细的=w=

#include <iostream>
#include <cstdio>
using namespace std;
const int MAX = 20000010;
struct Persistable_Segment_tree
{
	int ls,rs,v;//分别是该位置代表的节点或区间的左儿子右儿子和权值
} tr[MAX];
int edit[1 << 20] = {1},w[1 << 20],tot;//edit储存节点版本 注意第一个(下标0)赋值为1表第一个版本 w储存点初始权值
int build(int l,int r)
{//此处不能直接用++tot代替pos 因为跳转到子程序中继续搜索下去 tot值会增加 而此处值代表当前节点编号 应不变
	int pos = ++tot;//tot是当前数组末尾的位置 ++tot则是在末尾处新建储存节点或区间的相关信息 充分利用空间OwO真是太厉害了
	if (l == r)//区间左右相等 即只包括一个点 则只存点权
	{
		tr[pos].v = w[l];//记录初始点权
		return pos;//pos是当前节点的编号 需返回 用以让其父亲节点记录他
	}
	int mid = (l + r) >> 1;//二分存中点
	tr[pos].ls = build(l,mid);//记录当前节点的左儿子编号
	tr[pos].rs = build(++mid,r);//记录当前节点的右儿子编号
	return pos;//返回当前节点编号 需返回 用以让其父亲节点记录他
}
int update(int ed,int l,int r,int p,int k)//在ed版本的基础上 修改p点权值为k 记录当前区间最左&&最右端的点l&&r 
{//此处不能直接++tot代替pos 因为跳转到子程序中继续搜索下去 tot值会增加 而此处值代表当前节点编号 应不变
	int pos = ++tot;//记录当前节点编号 充分利用空间OwO真是太奇妙了
	if (l == r)//当搜索到单个节点了
	{
		tr[pos].v = k;//记录修改后节点权值
		return pos;//返回当前节点编号 让当前版本的父亲记录他
	}
	tr[pos].ls = tr[ed].ls;//将之前的该节点左儿子复制 (引用-->「把子节点指向前驱节点以备复用」)
	tr[pos].rs = tr[ed].rs;//将之前的该节点右儿子复制 因为之后只会改变两儿子之一的值 这样子可以确定该节点位置
	int mid = (l + r) >> 1;//二分存中点
	if (p <= mid) tr[pos].ls = update(tr[ed].ls,l,mid,p,k);//向下寻找 逼近p点 更改pos点的左儿子
	else tr[pos].rs = update(tr[ed].rs,++mid,r,p,k);//向下寻找 逼近p点 更改pos点的右儿子 用tr[ed]的原因是此时tr[pos]只有1深度的孩子的值
	return pos;//返回pos pos作为该点父亲的某个儿子的位置 用以记录
}
int found(int ed,int l,int r,int p)
{//ed是 某版本 储存区间1~n的值 的位置
	if (l == r) return tr[ed].v;//找到该点 此时ed已经变为 记录当前版本的p点的位置了 其v则是当前版本的p点的权值 返回
	int mid = (l + r) >> 1;
	if (p <= mid) return found(tr[ed].ls,l,mid,p);//向下寻找 逼近p点 ed变为ed的左儿子
	else return found(tr[ed].rs,++mid,r,p);//向下寻找 逼近p点 ed变为ed的右儿子
}
int main()
{
	int n,m,edition,mode,node,weight;//恪尽职守的变量定义
	scanf("%d%d",&n,&m);//发人深省的范围输入
	for (int a = 1 ; a <= n ; a ++) scanf("%d",&w[a]);//循规蹈矩的节点输入
	build(1,n);//建树 从区间 1 ~ n 开始递归 找左右儿子
	for (int a = 1 ; a <= m ; a ++)//循序渐进的命令处理
	{
		scanf("%d%d%d",&edition,&mode,&node);//五花八门的命令输入
		if (mode % 2)//巧妙绝伦的判断
		{
			scanf("%d",&weight);//扑朔迷离的补充输入
			edit[a] = update(edit[edition],1,n,node,weight);//update解释见子程序
		}//以update此时求出tr数组的末尾 edit[a]意为在第a个版本时修改的点为edit[a-1]到edit[a]的点(上面那行程序让本人想了很久很久)
		else//机智无比的转折
		{
			edit[a] = edit[edition];//因为复制没有创建新节点 因此当前版本的所有点等于当前版本(不是第a-1的版本)之前的所有点
			printf("%d\n",found(edit[edition],1,n,node));//输出查询某edition的某node的值
		}
	}
	return 0;//逢考必备的结尾
}

其实这行数似乎比线段树的代码还少=-=这世道究竟......

 

好了接下来是 主席树 不会讲的太详细哈 =-=

2018.9.8 经过一个多月偶尔的攻关 Frocean 还是决定向 STL 低下了头 QAQ

2018.10.20 经过 Frocean 暗度陈仓的努力 最终还是赶跑了 大恶魔 STL

主席树是用到了前缀和的思想 =-=

然而一开始看网上各位 dalao 的解释完全不懂 然后通过各种神奇的方式弄清了——

首先套用一下 “ 对于原序列的每一个前缀 [ 1 ... i ] 建立出一棵线段树维护值域上每个数出现的次数,则其树是可减的 ”

什么鬼东西 原序列哪里有前缀 不是输入一个个权值嘛 而且每个前缀建一棵树 这是要爆炸

(轻蔑) 事实上这个的意思是......我还是举个例子吧

首先我们要把所有数的边界弄出来做线段树的边界 (什么线段树啊 都没说清楚) 别急 说好了举例子的

例如我们一个数列 5 个数 互不相同 比如 233 19260817 6666 19491001 和 1000000007 五个数

最小最大是什么? 好找到了我们拿他们做线段树的边界......等等这会死人的啊

于是离散一下 =-= 变成 1 3 2 4 和 5 到时候求答案转换回去即可

好了线段树怎么造呢 看我强迫症画了十五分钟的图 (画图太慢所以基本不画OvO)

差不多了吧 =-= 意思很清楚

每次读入一个数 然后包含他离散后数值的树的区间 size 都加一 相当于单点修改

如果求区间 l 到 r 的 最小值 就用 第 r 次更新后树的形态 减去 第 l - 1 (记住是 l - 1) 次更新的树的形态

因此我们不能在空间只有 n 的 线段树上做这事......之前不是有可持久化线段树嘛

那我们对于每次更新到的点 像可持久化线段树一样 把加入第 i 个点当成第 i 个版本 然后同样多加一堆节点就好了 然后就没有然后了哈

话说网上那些图都不画单数个节点的真的是烦躁啊......

Tip1 : 初始离散化 + 去重 这里用手打的但是没去重 (手打去重我过不了QAQ)

Tip2 : 主席树建树先确定边界 然后只用记每个点的左儿子和右儿子哈 权值到时候搜到第 k 大的时候返回点位置即可 (多个相同也不怕 看判断方法)

Tip3 : 主席树插入大概和上面的那树差不多 于是关于 edit 和 ed 的 自觉上翻

Tip4 : 主席树查询很重要 细看哦 没有细讲主要是懒得画图......

洛谷的模板戳这里 还有注释'可能'在代码里面哦 我对我的码风莫名自信~

#include <algorithm>
#include <cstdio>
using namespace std;
const int MAXN = 200010;
int v[MAXN],bot[MAXN],newv[MAXN],edit[MAXN],tot;
//v存原点权 bot存点id (此处接sort处注释) 
struct node {
    int l,r,siz; //l r 找位置 然后 siz 就是当前区间内的数的数量
} tr[MAXN << 5];
inline short cmp(int x,int y) {return v[x] < v[y];}
//通过比较v数组大小让bot当作排好序 
inline int r()
{
	char q = getchar(); int x = 0;
	while (q < '0' || q > '9') q = getchar();
	while ('0' <= q && q <= '9') x = (x << 3) + (x << 1) + q - (3 << 4),q = getchar();
	return x;
}
void insert(int ed,int l,int r,int i)
{ //同 可持久化线段树 但是有点修改 不过原理一样的
	++tr[ed].siz;
	if (l == r) return;
	int mid = (l + r) >> 1;
	if (i <= mid)
	{
		tr[++tot] = tr[tr[ed].l]; // 这种是l r siz一起更新的 省位置
		tr[ed].l = tot;
		insert(tr[ed].l,l,mid,i);
		return;
	}
	tr[++tot] = tr[tr[ed].r];
	tr[ed].r = tot;
	insert(tr[ed].r,++mid,r,i);
}
int out(int l,int r,int i,int j,int k)
{ // 查到点了返回点权 其实可以放在最外层..但我太懒了
	if (l == r) return v[bot[l]];
	int mid = (l + r) >> 1;
	int ex = tr[tr[j].l].siz - tr[tr[i].l].siz;
	//important ex是当前区间左儿子区间的size值 这里用到了二叉查找树的思想
	if (ex >= k) return out(l,mid,tr[i].l,tr[j].l,k);
	return out(++mid,r,tr[i].r,tr[j].r,k - ex);
} //减去后是剩下要查的区间里第k大的点数 之前是之前(相对的)全部区间里第k大的点数
int main()
{
	int n = r(),m = r();
	for (int a = 1 ; a <= n ; ++ a) v[a] = r(),bot[a] = a;
	sort(bot + 1,bot + n + 1,cmp);
	for (int a = 1 ; a <= n ; ++ a) newv[bot[a]] = a;
	for (int a = 1 ; a <= n ; ++ a)
	{
		edit[a] = ++tot; //当前版本树开头接上上个节点的编号(+1) 
		tr[tot] = tr[edit[a - 1]]; //这个如果接版本 0 完全没问题 
		insert(edit[a],1,n,newv[a]); //更新 确定子树有范围不怕 
	}
	while (m--)
	{
		int i = r(),j = r(),k = r();
		printf("%d\n",out(1,n,edit[i - 1],edit[j],k)); //处理询问 
	}
	return 0;
}

 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值