主席树明明是个很棒的东西,可惜网上却很难找到很好的模板/讲解
包括卿学姐的讲解的模板,看得云里雾里,就感觉模板不太对劲,怎么边界用的是离散前的?
直至找到https://blog.finaltheory.me/algorithm/Chairman-Tree.html#7865d
发现边界确实直接用离散后的就行,毕竟是实质是权值线段树。
这是第一篇,讲模板,POJ2104,无修改的主席树模板,求区间第K大,主席树的典例
首先是空间,一般来说开<<5(32)倍或者直接40倍会比较好
因为初始为4倍(线段树空间)n,每次更新最坏要logn,加起来就是n(logn+4)的空间,而2的28次方大概是2e8,一般的n都没有这么大(也没有那么大内存),所以32倍空间已经足够了。
首先是前置技能:权值线段树。
权值线段树,顾名思义,就是以权值建树。以往我们的线段树存的是具体的值,而权值线段树,存的是数量,每个结点的sum存储该代表权值在该区间内的点的数量,安利一篇BLOGhttps://blog.csdn.net/Stupid_Turtle/article/details/80445998,图做得很棒,一看就懂。
然后就来到了我们的重点,主席树。
主席树的核心思想是:利用历史版本的线段树,只更新每次修改的内容。首先上一个视频https://www.bilibili.com/video/av4619406/?p=2,卿学姐的讲解,5分钟而已。
再上图
update表示我要更新的结点,更新此结点,按照普通的线段树,自然是更新从根结点到此节点整条链上的点的sum值。但是主席树就不一样了,主席树选择新建一条链。那没更新的点呢?继续用以前的就行啦。所以图中虚线链接的两点其实是同一个点,只要将父节点指向原先已经建好且没更新的点即可。下面上的模板是我该篇最上边引用的文章里的,加了些注释,且会进行说明。
#include <cstring>
#include <algorithm>
#include<cstdio>
#define MAX 100010
#define CLR(arr,val) memset(arr,val,sizeof(arr))
using namespace std;
const int INF = 0x3f3f3f3f;
//记录原数组、排序后的数组、每个元素对应的根节点
int nums[MAX], sorted[MAX], root[MAX];
int cnt;
struct TMD
{
int sum, L_son, R_son;
} Tree[MAX<<5];
inline int CreateNode( int _sum, int _L_son, int _R_son )
{
int idx = ++cnt;
Tree[idx].sum = _sum;
Tree[idx].L_son = _L_son;
Tree[idx].R_son = _R_son;
return idx;
}
void Insert( int & root, int pre_rt, int pos, int L, int R ) //相当于更新,每次更新一条链 权值线段树
{
//从根节点往下更新到叶子,新建立出一路更新的节点,这样就是一颗新树了。
root = CreateNode( Tree[pre_rt].sum + 1, Tree[pre_rt].L_son, Tree[pre_rt].R_son ); //指向旧版本的左右孩子
if ( L == R ) return;
int M = ( L + R ) >> 1;
if ( pos <= M )
Insert( Tree[root].L_son, Tree[pre_rt].L_son, pos, L, M );
else
Insert( Tree[root].R_son, Tree[pre_rt].R_son, pos, M + 1, R );
}
int Query( int S, int E, int L, int R, int K )
{
if ( L == R ) return L;
int M = ( L + R ) >> 1;
//下面计算的sum就是当前询问的区间中,左儿子中的元素个数。
int sum = Tree[Tree[E].L_son].sum - Tree[Tree[S].L_son].sum;
if ( K <= sum )
return Query( Tree[S].L_son, Tree[E].L_son, L, M, K );
else
return Query( Tree[S].R_son, Tree[E].R_son, M + 1, R, K - sum );
}
int main()
{
int n, m, num, pos, T;
while ( scanf("%d %d", &n, &m) != EOF )
{
cnt = 0; root[0] = 0;
for ( int i = 1; i <= n; ++i )
{
scanf("%d", &nums[i]);
sorted[i] = nums[i];
}
sort( sorted + 1, sorted + 1 + n );
num = unique( sorted + 1, sorted + n + 1 ) - ( sorted + 1 );
for ( int i = 1; i <= n; ++i )
{
//实际上是对每个元素建立了一颗线段树,保存其根节点
pos = lower_bound( sorted + 1, sorted + num + 1, nums[i] ) - sorted;
Insert( root[i], root[i - 1], pos, 1, num );
}
int l, r, k;
while ( m-- )
{
scanf("%d %d %d", &l, &r, &k);
pos = Query( root[l - 1], root[r], 1, num, k );
printf("%d\n", sorted[pos]);
}
}
}
这篇模板的重点,自然是insert过程。insert其实就是update,传入的参数pos是对应权值线段树的值。你可以把他当做离散以后,每个叶子结点的sum值就是下标为pos(权值为pos)的点的数量,而区间1~num则代表下标1~num(权值1~num)。
在insert过程里,我们每次要新建一个结点,因为该结点是需要被修改的,就是我们上面所说的在传统线段树需要被修改的链上的一点。但是因为这个是主席树,所以我们要新建而不是修改历史版本的该结点。同时,我们新建的该结点的子结点都指向历史版本(上一个版本)对应的该结点的子结点(这个应该比较好理解),因为我们不确定子结点需不需要更新,或者说,不确定哪个子结点需要更新,哪个不需要。然后继续向下遍历,如果该子结点要更新,那么就重复上述过程,新建一点,把历史版本子结点的子结点(孙子结点)连到我们新建的这个子结点上。
在查询过程,每个结点作差就代表在此查询区间,我每个结点对应的权值区间内的点有几个。比如我要求查询区间第5大的,那假设我左子结点代表权值在【1,2】内的点数量,右子结点代表权值在区间【3,5】的数量。如果我左子结点的sum>=5,就代表权值在【1,2】内的点至少有5个,那么查询区间的第5大一定在左子结点代表的权值区间内产生;反之,自然就在右子结点代表的权值区间内产生。
感觉写得不太好(wtcl),希望大家能看懂QAQ。
最近再把动态的更了,再更几道比较典型的题QWQ(我相信我不会咕咕咕的)。
(我才不会告诉你们其实这篇BLOG在两个月前就写了一半存了草稿,一直咕到现在)