掌握只为出现过的值域路径建节点的动态开点线段树,并理解两个根如何合并、一个多重集如何按第 $k$ 小边界分裂;能证明这些操作为什么只访问非空结构。
前置:值域线段树、逆序对、std::map 区间维护。
动态树的根代表一个值域多重集。三种基础操作:
insert(root,value):沿值域路径开点并加计数;merge(a,b):空树直接返回另一根,否则递归合并对应儿子、计数相加;split(root,k):按计数把最小的前 $k$ 个元素与其余元素拆成两根,只在分界路径复制/拆开节点。合并的复杂度不是固定 $O(\log V)$,而与两树重叠的非空节点数有关;常见“线段树合并总复杂度”证明依赖节点合并后不再分开、每个节点被消耗常数次。若之后还反复分裂,需要把分裂新建节点数单独计入。
split(k=0)、split(k=size) 应直接返回空根/原根,避免无意义开点。每个叶子建立只含一个权值的动态计数树。从前序描述末尾向前,用栈恢复子树:遇叶压根;遇内部点弹出左右两根,合并时累计正常顺序跨逆序数 $x$ 与交换顺序跨逆序数 $y$,答案加 $\min(x,y)$,再把合并根压回。
归纳假设两子树已取得各自最小内部逆序数。当前点只剩两种顺序,额外逆序分别为 $x,y$;选择较小者不影响子树内部最优,也不影响祖先只关心的合并多重集。因此局部最优可独立组合成全局最优。每个动态节点在合并链上被处理常数次,总复杂度 $O(n\log n)$。
scan prefix tree description in reverse:
leaf(value): push singleton_value_tree(value)
internal:
left, right = pop(), pop()
merged, normal, swapped = merge_and_count(left, right)
answer += min(normal, swapped)
push merged
用有序映射按块左端点存 [l,r],root,descending。split_position(pos) 找到包含位置的块;升序块取最小的前缀计数,降序块取最大的前缀计数,调用值域树按秩分裂。每次操作切出 $l,r+1$ 后,破坏式合并中间所有根,擦除旧块,插入一个带新方向的大块。最终在 $q$ 所在块按偏移查询第 k 小/大。
块不变量是:根的多重集等于该位置区间全部值,按方向读取根的有序值恰等于当前序列片段。边界分裂按实际读取顺序分出前后片段,保持不变量;排序操作合并的多重集与原区间相同,重新按指定方向读取正是排序结果。归纳覆盖全部操作。
初始开点与每次两次分裂共新增 $O((n+m)\log n)$ 节点;每个被擦除块只在一次合并中消失,映射操作另有对数因子。
blocks = singleton monotone blocks
for sort(op, l, r):
right_it = split_position(r + 1)
left_it = split_position(l)
root = destructive_merge(all roots in [left_it, right_it))
erase old blocks; insert block(l, r, root, direction=op)
locate block containing q
answer = kth(root, offset+1 if ascending else block_size-offset)
若只询问一个最终位置,本题也可“二分答案 + 01 Lazy Segment Tree”做得更短;本节选择合并/分裂路线是为了建立通用数据结构。可持久化要求不破坏旧根,与本节破坏式合并的所有权模型相反。
先实现只有 insert+merge 的值域树并核对多重集计数;再实现 split(k),用排序 vector 对拍。排序题需每次操作后把所有块展开,与朴素数组比较,特别覆盖边界恰在块首/块尾。