可持久化线段树
参见《算法竞赛》。
以静态区间第 \(k\) 小值为例来讲解主席树。
首先,考虑 \([1,i]\) 中第 \(k\) 小值的解法。
显然,可以权值线段树二分,利用 BST 的思想求解。
那么考虑对于 \(i\in [1,n]\),求解 \([1,i]\) 中第 \(k\) 小值。
为了简化问题,令 \(n=2^k\),其中 \(k\) 为非负整数。
对于 \(i=1\) 的问题,我们习惯用一棵大小为 \(1\) 的线段树来解决(从线段树的角度看问题)。但是这时用一棵大小为 \(2^{k+1}-1\) 的线段树来解决,即一棵满二叉树,有 \(2^k\) 个叶子节点,参见《算法竞赛》第 204 页。
为什么要这么做?
因为这样,我们对于 \(i=n=2^k\) 的问题也可以在这同一棵线段树上解决。
注意,这仍然是一棵权值线段树。
参见《算法竞赛》第 204 页,来模拟 \(i\in [1,n]\) 的权值线段树情况。
发现当将 \(a_i\) 加入到权值线段树中时,修改的路径是树上的一条从根走到叶子的链,每次修改只会改变 \(k+1\) 个节点,即树的深度。
设当前加入的是 \(a_i\),我们可以很快捷的利用线段树二分求出 \([1,i]\) 的第 \(k\) 小值;现在再来思考 \([L,R]\) 的第 \(k\) 小值求法。
为了解决这个问题,我们先假设对于 \(i\in [1,n]\),每个 \([1,i]\) 都开一个权值线段树。(刚才只开了一棵线段树)
《算法竞赛》第 205 页有极为详细的描述。
考虑对于权值线段树上的一个节点,下标中的 \([l,r]\) 代表着它所维护的区间;其权值代表该区间中的元素个数。
令 \(f(x)\) 代表当加入 \(a_x\) 时的线段树,即 \(x\) 这一历史版本。
显然,\(f(R),[l,r]\) 代表 \(R\) 这一历史版本中 \([l,r]\) 这一节点的权值。更通俗的讲,就是 \([1,R]\) 这段区间中 \([l,r]\) 的元素个数,注意,这里 \(l,r\) 均指权值。
同理,我们可以用 \(f(L-1),[l,r]\) 得到 \([1,L-1]\) 中 \([l,r]\) 的元素个数。
那么,利用前缀和的思想,容易得到 \([L,R]\) 中 \([l,r]\) 的元素个数即为 \(f(R)-f(L-1),[l,r]\)。
所以我们直接对线段树上对应节点做相减操作,就得到了 \(f([L,R])\)。
我们把 \(f(R)-f(L-1)=f([L,R])\) 这一运算称为线段树减法。
目前来看,得到 \(f([L,R])\) 需要线段树大小的时间复杂度。但是,将其与区间第 \(k\) 小值联系在一起,我们发现只需要对查询路径及路径上节点的儿子做减法即可。于是复杂度变成了 \(O(\log n)\)。
单次查询 \(O(\log n)\) ,这已经是一个相当优秀的复杂度,但是由于我们需要建 \(n\) 棵线段树,空间复杂度会高达 \(n^2\),并且空间常数不小。同时,进行 \(n\) 次建树的时间复杂度会到达 \(O(n^2)\)。
考虑优化。;
还记得一开始所说的,在一棵树上操作时,每次只需要改变 \(k+1\) 个节点吗?
显然,\(f(i+1)\) 与 \(f(i)\) 的不同之处就在这 \(k+1\) 个节点,于是我们只需要将这 \(k+1\) 个节点与前一棵树做区分即可。
如何做区分呢?
参见《算法竞赛》第 205 页。
我们新建 \(\log\) 个点,将其与 \(f(i-1)\) 连边,记录一下当前根节点即可。
但正如第 206 页所说,我们并不需要建一棵初始空树,用一棵在结构和逻辑上均不完整的线段树也能得出答案。
关于可持久化数组,也可以用主席树维护。把叶子节点设为权值,其余节点设为空,然后按照主席树的方式维护即可。
但是这样维护稍显多余,无论是时间还是空间都给人可以优化的感觉。一种比较好的方法,是按照题解区 Elagia的做法,离线解决问题。