主席树
废话还是要说:主席树可以用来解决如下问题:“给出一列数,a1,a2…an,每次询问其中连续的一段区间ai到aj其中的第K大的数是多少?” 主席树的主体是线段树,准确的说,是很多棵线段树,存的是一段数字区间出现次数(所以要先离散化可能出现的数字)。举个例子,假设我每次都要求整个序列内的第 k 小,那么对整个序列构造一个线段树,然后在线段树上不断找第 k 小在当前数字区间的左半部分还是右半部分。这个操作和平衡树的 Rank 操作一样,只是这里将离散的数字搞成了连续的数字。
其实你们可以自己查
一些链接
1、算法过程图解:殇雪的blog
第三张图片很可以让我们很感性地了解主席树的优势和构造。
ta的博客是百度里第一个吧
2、博客最后一张图拯救了我对主席树的理解
就是这张图片,它很好地展示了主席树中应对”n颗树“的空间优化。
可以看作建树后(黑线组成的树)第一次插入新值(也就是红线部分)组成的第二棵树(红线部分以及红线下的黑线部分 共同组成的树)
![]()
图比较简单好懂。
算法理解
网上的代码要么就是数组型,要么就是传很多参数的struct型。
搞得我每学一个算法就需要重新规划代码思路,重新构建代码风格。
所以我尝试用我的理解和习惯,尽量把不必要的参数存在结构体里了。建树:就是线段树。但是值得注意的是:是否需要建立初始树,也就是root[0]领导的树。
如果不建,那么就需要很耐心地在各个子函数里传区间参数[l,r]。习惯用数组的朋友。
如果建,就可以不需要,那就得把l,r存在struct里。
(不对请指正,谢谢。)更新:主席树根据n个数据,会建立n个线段树,即每插入(和更新有点不一样,更新是同一位置赋值,插入是在初始化的位置上赋值,前者是动态主席树,后者是静态。)一个数据,就建一棵树(i),显然除了一条路径不同之外,树的其他部分可以完全不必要再开空间,直接继承于第i-1颗树,继承是直接把第i棵树的不变区间连到上一棵树对应区间。他们之间只有log(n)个节点是不同的,每次新增的空间只需要log(n),每次更新也是log(n)的时间。
意义:我们要修改一个叶子结点的值,并且不能影响旧版本的结构。
在从根结点递归向下寻找目标结点时,将路径上经过的结点都复制一份。
找到目标结点后,我们新建一个新的叶子结点,使它的值为修改后的版本,并将它的地址返回。
对于一个非叶子结点,它至多只有一个子结点会被修改,那么我们对将要被修改的子结点调用修改函数,那么就得到了它修改后的儿子。
在每一步都向上返回当前结点的地址,使父结点能够接收到修改后的子结点。
在这个过程中,只有对新建的结点的操作,没有对旧版本的数据进行修改。注意:
1、每一棵树都有各自独立的根,每个根与更新次数一一对应,存在一个数组里(比如叫它:root[i])
2、第i次更新,对应树存的数据管理范围是[1,i],有类似前缀的意味。
于是这样的结构具有了可加减性,所以我们不但可以查询[1,x],还可以查询[x,y],那么[x,y]=[1,y]-[1,x-1];查询:这里比普通线段树多传一个关键的数据,就是”哪一次询问“,同样位置可以维护不同的更新时间的数据。(“可持续化”就是这个意思。)
于是我们有了如下经典问题:
(摘自cyendra的博客)区间第K小值问题
有n个数,多次询问一个区间[L,R]中第k小的值是多少。查询[1,n]中的第K小值
我们先对数据进行离散化(离散后的数据放在了rank[ ]里),然后按值域建立线段树,线段树中维护某个值域中的元素个数。
在线段树的每个结点上用cnt记录这一个值域中的元素个数。
那么要寻找第K小值,从根结点开始处理,若左儿子中表示的元素个数大于等于K,那么我们递归的处理左儿子,寻找左儿子中第K小的数;
若左儿子中的元素个数小于K,那么第K小的数在右儿子中,我们寻找右儿子中第K-(左儿子中的元素数)小的数。查询区间[L,R]中的第K小值
我们按照从1到n的顺序依次将数据插入可持久化的线段树中,将会得到n+1个版本的线段树(包括初始化的版本),将其编号为0~n。(0就是初始线段树。)考虑第i个版本的线段树的结点P,P中储存的值表示[1,i]这个区间中,P结点的值域中所含的元素个数;
假设我们知道了[1,R]区间中P结点的值域中所含的元素个数,也知道[1,L-1]区间中P结点的值域中所包含的元素个数,显然用第一个个数减去第二个个数,就可以得到[L,R]区间中的元素个数。
因此我们对于一个查询[L,R],同步考虑两个根root[L-1]与root[R],用它们同一个位置的结点的差值就表示了区间[L,R]中的元素个数,利用这个性质,从两个根节点,向左右儿子中递归的查找第K小数即可。
代码理解
模板题。
离散
//全局部分
struct nodd
{
int v, id ;
friend bool operator < ( nodd a, nodd b )
{
return a.v == b.v ? a.id < b.id : a.v < b.v ;
}
} a[maxn] ;
//离散部分
for(i=1;i<=n;i++)
{
Read(a[i].v) ;
a[i].id =i;
}
sort(a+1,a+n+1) ;
for(i=1;i<=n;i++)
Rank[a[i].id]=i ;
//其实比较好理解,不理解手推就可以。
插入
阅码注意:
1、哪里有取地址符?
2、变量是全局变量,还是局部变量,还是结构体中的变量?
更新函数\插入函数
3、其实这里的代码很重要,其他类型的主席树问题这里是基本不变的。思想很重要
//主函数部分
for (i=1;i<=n;i++)
{
rt[i]=rt[i-1] ;//1-这一句是灵魂吧。要说的太多了,我写在下面。
insert(Rank[i],rt[i],1,n) ;//2-把离散后的第i个数,插入到root[i]领导的主席树中,区间长度l,r。
}
//子函数部分
void insert ( int K, int& x, int l, int r ) //3-额,这个k是数据key,不是第k大的k。x前面有一个取地址,哈。
{
tree[++cnt]=tree[x] ;//4-
x=cnt;//5-
tree[x].v++;//6-增加数量
int mid =(l+r)>>1 ;//7-没啥说的,占地方
if(l==r)return ;
if(K<=mid) insert(K,x[tree].l,l,mid) ;//8-为什么不是teer<<1?
else insert(K,x[tree].r,mid+1,r) ;//9-为什么不是teer<<1|1?
}
1-从一开始理解,我们有一颗空树。如果我们不这样写,我们直接传root[1],那当我们递归到我们不需要改的区间,我们根本无法找到初始树的位置,所以暂且把初始树的root完全复制过来.
这样,因为每一颗树的根节点储存了它儿子的标号,所以这一步我们相当于第1棵树完全等价于第0棵树,只多开了一个root,其他的已经!!完全检索!!到上一棵树里。
然后在新的树里把不一样的树枝修改成新的值,并且新开空间就可以了。所以之后递归就是if,else if,而不是if,if.只选择一条路。那么以此类推,第i棵树继承第i-1棵树就行了。
3-取地址,为啥呢?我们看主函数里是怎么调用的:
insert(Rank[i],rt[i],1,n) ;
从一开始理解:也就是说我们要在递归的时候修改root[i]里存的值,以前存的是上一棵树的编号,多没面子啊,我们要给每一个根一个实际的地位,(为啥我就不废话了)
所以一进去我们就根据调用次数cnt来创造一个新的节点“tree[++cnt]”(4-),然后 改变x(x=cnt)(5-),也就是把rt[i](为什么这里是rt明白了吧,他就是个索引,真的root被他索引,上面提到的root都是rt)指向了root[++cnt]这个真的根节点。
6-因为刚才提到了可加减性,数 第i次修改时 这个节点管辖范围内的有小数据有几个。
8-9-从这个函数一开始我们就可以知道:
节点编号的机制不是 *2 和*2+1
而是根据调用次数由cnt管控。所以要存在每一个节点里。
int query ( int rt1, int rt2, int l, int r, int K )
{
if (l==r) return l;
int mid=(l+r)>>1;
int num=tree[rt2].l[tree].v - tree[rt1].l[tree].v ;//可加减性,为啥这样做,因为几点存的是前缀和,我们要特定区间就把前面的减去咯。你也可以这样认为:假设查第999大,当前节点总共管3个数,你就需要把前面没用的减去咯。
if ( K <= num )
return query(rt1[tree].l,rt2[tree].l,l,mid,K) ;
else
return query(rt1[tree].r,rt2[tree].r,mid+1,r,K-num ) ;
}
附上完整代码:
以后改一改吧,用的WT_cnyali的代码。比较习惯这样代码风格的。
#include <bits/stdc++.h>
using namespace std ;
bool Read ( int &x )
{
char c = getchar() ; x = 0 ; bool f = 0 ;
while ( !isdigit(c) )
{
if ( c == '-' ) f = 1 ;
if ( c == EOF ) return false ; c = getchar() ;
}
while ( isdigit(c) )
{ x = 10 * x + c - '0' ; c = getchar() ; }
if (f) x = -x ;return true ;
}
void Print ( int x )
{
int len=0,a[50] ;
if ( x == 0 ) { putchar('0') ; return ; }
if ( x < 0 ) { putchar('-') ; x = -x ; }
while (x) { a[++len] = x%10 ; x /= 10 ; }
while (len) putchar(a[len--]+'0') ;
}
const int maxn=200010 ;
int n,m,rt[maxn],Rank[maxn],cnt ;
struct node
{
int l, r, v ;
} tree[maxn<<5] ;
struct nodd
{
int v, id ;
friend bool operator < ( nodd a, nodd b )
{
return a.v == b.v ? a.id < b.id : a.v < b.v ;
}
} a[maxn] ;
void insert ( int K, int& x, int l, int r )
{
tree[++cnt]=tree[x] ;
x=cnt;
tree[x].v++;
int mid =(l+r)>>1 ;
if(l==r)return ;
if(K<=mid) insert(K,x[tree].l,l,mid) ;
else insert(K,x[tree].r,mid+1,r) ;
}
int query ( int rt1, int rt2, int l, int r, int K )
{
if ( l == r ) return l ;
int mid = l+r >> 1 ;
int num = tree[rt2].l[tree].v - tree[rt1].l[tree].v ;
if ( K <= num ) return query ( rt1[tree].l, rt2[tree].l, l, mid, K ) ;
else return query ( rt1[tree].r, rt2[tree].r, mid+1, r, K-num ) ;
}
int main()
{
int i,j,k,x,y;
Read(n) ; Read(m) ;
for (i=1;i<=n;i++)
{
Read(a[i].v) ;
a[i].id=i ;
}
sort (a+1,a+n+1) ;
for (i=1;i<=n;i++)
Rank[a[i].id]=i ;
for (i=1;i<=n;i++)
{
rt[i]=rt[i-1] ;
insert(Rank[i],rt[i],1,n) ;
}
while(m--)
{
Read(x);Read(y);Read(k) ;
Print(a[query(rt[x-1],rt[y],1,n,k)].v ) ;
putchar('\n') ;
}
return 0 ;
}