线段树的三个高端算法,动态开点,储存权值,还可以合并。
零、背景
《leetcode 第 258 场算法比赛》的第四题是一道很有趣的题。
比赛的时候我使用贪心,把树转化为链,然后 DFS 通过了。
上上上篇文章《树上并查集》介绍了一个万能的树上并查集方法,甚至允许有重复权重值。
上上篇文章《RMQ LCA》介绍了一个 LCA 的方法,不过不支持重复权重值。
上篇文章《树递归编号》介绍了树递归编号的方法来解决问题,也不支持重复权重值。
今天我再分享一个高端线段树的算法来解决这个问题。
一、题目
给一个有根树,每个节点有一个正整数值(互不相同)。
对于每个子树,问子树的节点值里面,最小的未出现的正整数是多少。
最小的未出现的正整数有个专业名称,叫做 mex(Minimum excludant)。
二、朋友圈
比赛后,有人给我发了一个题解。
我一看,完全看不懂。
于是发了一个朋友圈,问是啥算法。
朋友圈的大佬有人说是线段树合并,有人说是权值线段树。
我一脸懵逼,这是什么呢?怎么没听说过呢?
三、权值线段树
普通的线段树是给一个坐标位置,可以设置值。
然后可以查询区间最值或者区间和等问题。
权值线段树只关心下标这一个数据,用于统计不同下标出现的次数。
储存了这个数据后,就可以查询第 K 小/大值问题了。
当然,同样可以查询最小的未出现的正整数。
逻辑也很容易理解。
左子树区间大小与出现的数字个数相同,那显然答案在右区间(左区间是满的),否则肯定在左区间。
int Query(int x, int l, int r){
if(x == 0) return l;
if(V[x] == r - l + 1) return r + 1;
int mid = (l + r) >> 1;
int leftAns = Query(L[x], l, mid);
if(leftAns <= mid) return leftAns;
return Query(R[x], mid + 1, r);
}
复制
四、线段树合并
一个线段树是一个不可拆分的完整的树。
前面插入的点将会影响后面区间的查询。
而面对子树问题时,左子树与右子树往往是没关系的。
这时候就需要对左子树与右子树分别建一个线段树,等分别求出各自的答案后,再合并出当前子树的线段树。
下面的代码是合并的权值线段树,将 y 为根的线段树合并到 x 为根的线段树。
int Merge(int x, int y) {
if(x == 0 || y == 0) return x == 0 ? y : x;
V[x] += V[y];
L[x] = Merge(L[x], L[y]);
R[x] = Merge(R[x], R[y]);
return x;
}
复制
五、动态开点线段树
前面提到需要给每个子树创建一个线段树。
如果每个线段树初始化的时间复杂度是 O(N)
, 那建 n 个线段树的复杂度就是 O(N^2)
的时间了。
所以这里使用普通的线段树是不可行的。
然后就有人提出,既然树可以使用一个一维数组代替二维数组,那自然线段树也可以做到。
具体来说就是,线段树的某个节点存在值时,再开辟节点的内存空间。
这样对于稀疏矩阵,只需要很小的空间就可以表示一个线段树。
有时对于那种区间很大的线段树,离散化也解决不了,就可以采用动态开点的方式来储存线段树。
当然,这里我们使用动态开点方式储存的权值线段树。
非权值线段树应该一样可以表达的。
int L[M], R[M], V[M];
int index;
void Insert(int& x, int l, int r, int v) {
x = ++index;
L[x] = R[x] = 0;
V[x] = 1;
if(l == r) return ;
int mid = (l + r) >> 1;
if(v <= mid) {
Insert(L[x], l, mid, v);
} else {
Insert(R[x], mid + 1, r, v);
}
}
复制
六、查询 MEX
上面介绍了权值线段树、线段树的合并、动态开点线段树。
综合利用这三个数据结构或算法,我们就可以解决线段树查询子树 MEX 的问题了。
一开始,先给所有节点建立一个动态开点的权值线段树。
index = 0;
root.resize(n, 0);
for(int i = 0; i < n; i++) {
Insert(root[i], 1, N, nums[i]);
}
复制
由于每个权值线段树高度是log(n)
,所以内存空间需要申请n log(n)
大小。
const int N = 100000;
const int M = 20 * N;
复制
之后,就可以递归的查询子树字段树,并合并子树线段树,从而递归的求出答案。
void Solver(int x) {
for(auto v: g[x]) {
Solver(v);
root[x] = Merge(root[x], root[v]);
}
ans[x] = Query(root[x], 1, N);
}
复制
七、最后
这次一下认识三个数据结构:权值线段树、线段树合并、动态开点线段树。
可能会有人会有疑问,我曾经作为 ACMER, 为啥会没听说过过这些数据结构呢?
之前我介绍过很多次了,大学的时候天天忙着做项目,只在大一暑假学了基础数据结构与算法,之后就没学习什么算法了。
所以我只会基础的数据结构,比赛就看运气了。
考察高级算法的时候(后缀数组、树套树、树链刨分等),我就不会了。
考察基础数据结构和算法时,我就能挣扎一下可以做出来。
当然,虽然曾经自己没努力学习,但现在依旧不能放弃。
随着打比赛,遇到新的数据结构与算法,努力去学习就是了。
加油,算法人。
《完》
-EOF-
题图:来自朋友圈。
上篇文章:leetcode 第 259 场算法比赛
长按二维码,一起成长学习
▲ 长按关注,天天成长
觉得有帮助可以点击好看与转发,谢谢!