可持久化线段树
前言:可持久化线段树,也叫作函数式线段树,也就是主席树,(。。。因为先驱就是fotile主席。。Orz。。。),主席树是在初二暑假接触到的,当时李昊大佬
讲解了一番,但是连第k大都不会写,所有十分局限,所以十分想改进当前这种尴尬的现状。
主席树就是利用函数式编程的思想来使线段树支持询问历史版本、同时充分利用它们之间的共同数据来减少时间和空间消耗的增强版的线段树。
相对于其它类似的数据结构:
相对于普通的线段树--->很多时候离线线段树求解可以转化为在线主席树求解。
主席数利用函数式线段树来维护数列,一般用来解决区间第k大问题
空间时间的复杂度小于树套树(常数小)
划分树也可以解决区间第k大问题,但划分树不支持修改,主席树可以(用树状数组维护){从而来支持修改操作}
主席树的结构:主席树的每个节点对应一颗线段树,此处有点抽象。在我们的印象中,每个线段树的节点维护的树左右子树下标以及当前节点对应区间的信息(信息视具体问题定)。对于一个待处理的序列a[1]、a[2]…a[n],有n个前缀。每个前缀可以看做一棵线段树,共有n棵线段树;若不采用可持久化结构,带来的严重后果就是会MLE(空间爆炸),即对内存来说很难承受。根据可持久化数据结构的定义,由于相邻线段树即前缀的公共部分很多,可以充分利用,达到优化目的,同时每棵线段树还是保留所有的叶节点只是较之前共用了很多共用节点。主席树很重要的操作就是如何寻找公用的节点信息,这些可能可能出现在根节点也可能出现在叶节点。
对于主席树的理解
首先转发一下大佬的理解:所谓主席树呢,就是对原来的数列[1..n]的每一个前缀[1..i](1≤i≤n)建立一棵线段树,线段树的每一个节点存某个前缀[1..i]中属于区间[L..R]的数一共有多少个(比如根节点是[1..n],一共i个数,sum[root] = i;根节点的左儿子是[1..(L+R)/2],若不大于(L+R)/2的数有x个,那么sum[root.left] = x)。若要查找[i..j]中第k大数时,设某结点x,那么x.sum[j] - x.sum[i - 1]就是[i..j]中在结点x内的数字总数。而对每一个前缀都建一棵树,会MLE,观察到每个[1..i]和[1..i-1]只有一条路是不一样的,那么其他的结点只要用回前一棵树的结点即可,时空复杂度为O(nlogn)。
个人陋见:本博文是写于个人A了第k大数问题之后的,感觉就是每一个前缀求建一棵线段树,因为相较与前一棵线段树,相差只有一条链,因此增加的空间,不过就是一条链的空间复杂度而已,所以
每次增加logn的空间复杂度,所以空间复杂度为O(N log N+N log N),因此线段树开4倍空间,主席树开7-8倍左右空间比较靠谱,如果开得下,开10倍,然后就是处理一个区间的问题,如查询l,r这
个区间,然后就是查询主席树中l-1,r这个区间就可以了,前缀的思想。
然后转一转大佬的作品
1.建树
首先需要建立一棵空的线段树,也是最原始的主席树,此时主席树只含一个空节点,此时设根节点为rt[0],表示刚开始的初值状态,然后依次对原序列按某种顺序更新,即将原序列加入到对应位置。此过程与线段树一样,时间复杂度为O(nlogn),空间复杂度O(nlog(n))(笔者目前没有完全搞清究竟是多少, 不过保守情况下,线段树不会超过4*n)
2.更新
我们知道,更新一个叶节点只会影响根节点到该叶节点的一条路径,故只需修改该路径上的信息即可。每个主席树的节点即每棵线段树的结构完全相同,只是对应信息(可以理解为线段树的结构完全一样,只是对应叶子节点取值不同,从而有些节点的信息不同,本质是节点不同),此时可以利用历史状态,即利用相邻的上一棵线段树的信息。相邻两颗线段树只有当前待处理的元素不同,其余位置完全一样。因此,如果待处理的元素进入线段树的左子树的话,右子树是完全一样的,可以共用,即直接让当前线段树节点的右子树指向相邻的上一棵线段树的右子树;若进入右子树,情况可以类比。此过程容易推出时间复杂度为O(logn),空间复杂度为 O(logn)。如图:
3.查询
先附上处理好之后的主席树, 如图:
是不是看着很晕。。。。。。笔者其实也晕了,我们把共用的节点拆开来,看下图:
啊, 这下清爽多了,一眼看下去就知道每个节点维护的是哪棵线段树了,TAT,如果早就这样写估计很快就明白了,rt[i]表示处理完前i个数之后所形成的线段树,即具有了前缀和的性质,那么rt[r] - rt[l-1]即表示处理的[l, r]区间喽。当要查询区间[1,3]的时候,我们只要将rt[3] 和 rt[0]节点相减即可得到。如图:
这样我们得到区间[l, r]的数要查询第k大便很容易了,设左节点中存的个数为cnt,当k<=cnt时,我们直接查询左儿子中第k小的数即可,如果k>cnt,我们只要去查右儿子中第k-cnt小的数即可,这边是一道很简单的线段树了。就如查找[1, 3]的第2小数(图上为了方便,重新给节点标号),从根节点1向下搜,发现左儿子2的个数为1,1<2,所有去右儿子3中搜第2-1级第1小的数,然后再往下搜,发现左儿子6便可以了,此时已经搜到底端,所以直接返回节点6维护的值3即可就可以了。
总结主席树
由以上可知,主席树是一种特殊的线段树集合,属于可持久化线段树,具备线段树的几乎所有优势。此处可持久化我的理解就是可以充分利用历史版本,从而达到优化目的,故主席树更新、查找的时间复杂度为O(log(M)),且总的空间复杂度为O(M*log(M)),这些对于复杂问题都是可以接受的。关于主席树的代码,结合具体题目会在后面的有所展示,欢迎关注!