目录
(3)solution 2:普通LCA,我愿称之为“散步法”(与solution 3结合)
(4)solution 3:倍增LCA,我愿称之为“弹射法”(与solution 2结合)
一、ST表
1.前置定义:RMQ问题
- RMQ问题,又称区间最值问题,这是一种多次询问区间最大或最小值的问题。
- 当询问量不是太多时我们可以用上一章的线段树来解决!
2.ST表的作用
- 在的时间内预处理完,的回答RMQ问题的一个询问。
- 但是在询问量非很多时用线段树更快(很多时用ST表)!
3.ST表中的存储
- 表示以L为开头,长度为的区间的最大(小)值,即对应于
- 由于已经算大的了所以存储ST表方式如下。
int st[20][N];
4.ST表如何求:st[i][L]=?
我们可以用和来求,也可以反过来。
st[i][L]=max(st[i-1][L],st[i-1][L+(1<<(i-1))]);
注:位运算符号“<<”,让某数的二进制左移一位,也就是在二进制后添个0,相当于乘2,所以。
而初始则需要我们自己写入……
for(int i=1;i<=n;i++){
st[0][i]=a[i];//a数组为ST表维护的数组。
}
5.ST最最最重要的一步:求最值
再次借用一下老师的图片……
- 一绿一红的两个区间,这两个区间为长度为2的幂次,且不超出区间的最大区间,详看上图。
- 它们刚好能罩住我们要查询的区间,所以一绿一红的两个区间的最值就是区间的最值。
- 而其中(k在上图)。
- 而一绿一红的两个区间可以用以下表示。
st[k][r-(1<<k)+1]//绿
st[k][l]//红
二、树
1.定义
- 借第一章图片一用(下图),这就是一棵树。
- 树是N个点,N-1条边的连通图。或者说,没有环的连通图。
- 5和3为父子关系,5为父,3为子。
- 所有父亲的儿子数量为X及X以下,叫X叉树。
- 3和7为兄弟关系。
- 5和0为祖孙关系,5为祖先,0为后代。
2. LCA最近公共祖先
(1)什么是LCA
- 问题:有一颗N个节点的树,有M次询问,每次询问给出两个点u,v,求u,v在树上的最近公共祖先(即深度最深的公共祖先)。
- 节点A是节点B的祖先当且仅当A在B到根的路径上。 反之,如果B在A的子树里,则B是A的后代。
- 借一下老师的图片。
(2)solution 1(解法 1):欧拉遍历序与RMQ
这个遍历顺序就是欧拉序(下图,借用老师图片)。
- 欧拉遍历序(11): 1 2 3 2 5 6 5 2 1 4 1
- 对应点深度(11): 0 1 2 1 2 3 2 1 0 1 0
- 查询5和4的最近公共祖先(详看上面列举),找出5和4,可以是区间也可以是区间(欧拉序中),其中深度最小的为1,所以5和4的最近公共祖先是1。
(3)solution 2:普通LCA,我愿称之为“散步法”(与solution 3结合)
问:9和3的最近公共祖先是谁(上图)?
- 9的深度比3深,9去父亲8那。
- 9的深度还是比3深,9去祖先7那。
- 现在深度一样,9可以在去一次,9去祖先5那。
- 3的深度比9深了,3去父亲5那。
- 9和3位置一样,都在5,所以最近公共祖先是5。
我们可以总结以下内容:
- 首先让深度深的点一直去到深度比另一点浅的祖先那。
- 一直实行如此操作到两点位置相同,所在位置的祖先就是最近公共祖先。
(4)solution 3:倍增LCA,我愿称之为“弹射法”(与solution 2结合)
是solution 2的优化,让我们看看怎么优化的。
问:9和3的最近公共祖先是谁(上图)?
- 9的深度比3深,9跳到与3相同深度自己的祖先7。
- 看看都跳(1)到哪到了一个地方,祖先5。
预处理:我们要先生成一个表,倍增表(下文f数组;ST表也运用了倍增),代表从j节点开始向父亲方向跳次的点是谁,那我们就可以推出,接着我们用一个dfs求出所有点的深度(代码dep数组)与f数组,以下代码。
vector<ll> g[N];//邻接表,每个g[i]中存储与i节点相连的节点。
void dfs(int x, int father){ // 树上遍历建立倍增表。
fa[0][x]=father; // 记录一下。
dep[x]=dep[father]+1; // 根节点深度为 1。
for(int i=1;(1<<i)<=dep[x];i++)
fa[i][x]=fa[i-1][fa[i-1][x]];
for(int i=1;i<=g[x].size();i++)//遍历自己的儿子。
if(g[x][i] != father)//别跑回父亲去了。
dfs(g[x][i],x);
}
接着最重要一步实现跳跃……我们知道任意一个数都可以拆成2的幂次的和,这就是倍增的作用,我们用二进制来拆,6 的二进制是0110,对应了 4 (0100) 和 2(0010) ,上文我们求了深度,我们算出求最近公共祖先的两个节点的深度差,蹦一次让深度一样,接着从大小开始尝试跳,跳不出范围就跳,直到重合,重合点就是最近公共祖先。
int lca(int x,int y) {
if(dep[x]<dep[y]) swap(x,y); // 先保证 x的深度比y的深度深
// 假设 dep[x] - dep[y] = 6 = 4 + 2
// 6 的二进制是 0110,对应了 4 (0100) 和 2(0010)
for(int i=19;i>=0;i--)
if( (dep[x]-dep[y]) & (1<<i)) // 拿2的整数次幂凑出我们要走的距离
// if( (dep[x]-dep[y]) >= (1<<i)) // 等效写法,能走就走
x=fa[i][x];
if(x == y) return x; // 如果某一个人是另一个人的直接祖先,返回
for(int i=19;i>=0;i--) // 倍增找深度最浅不相同的祖先
if(fa[i][x] != fa[i][y])
x=fa[i][x], y=fa[i][y];
return fa[0][x]; // 返回某个的直接父亲就是最近公共祖先
}
(5)最后(多嘴一下)
方法其实还有很多的。
三、并查集
1.并查集用处
- 一开始有N个小朋友,各自一组。每次把两个小朋友所在的组合并成一组,然后询问某两个小朋友是否在一个组。
- 以上这种
超难的题就可以用并查集。
2.并查集怎么搞得(原理)
- 并查集实际维护的内容,可以看做是对集合的维护,且支持合并集合。
- 并查集的维护思路是,将每个集合做成一颗树型,用树的根作为集合的标志。 那么,判断两点是否在一个集合内,只要找到两点所在树的树根,判断是否相同即可。
- 合并两个集合时,让一颗树的树根父亲设为另一颗树的树根即可。 用这样的结构即可维护点与点间连通性判定,但是,显然最坏这样复杂度会达到。
- 当只有一个点的时候也可以当做一个集合,进行合并。
3.代码实现
(1)实现找根节点
很简单只需要一个数组,一个函数。
int to[5005];to[x]为x的父亲。
int go(int p){//根节点没有父亲所以将他的父亲设为自己。
if (to[p] == p) return p;//我已经是根节点了!
else return go(to[p]);//向父亲递归。
}//根节点是谁?
初始化:对于任意x,将也就是将所有店当做一个根节点。
(2)这就是找根节点的极限吗
很显然还可以优化,这个数据结构是干什么的?不就是为了合并集合,查询在哪个集合的吗,所以我们在找根节点时,将根节点的后代全部变成自己儿子。
int to[5005];to[x]为x的父亲。
ll go(ll x){//根节点没有父亲所以将他的父亲设为自己。
if(to[x]==x)return x;//我已经是根节点了!
else {
ll ret=go(to[x]);
to[x]=ret;//向父亲递归,同时认根节点为祖先。
return to[x];
}
}//根节点是谁?
(3)实现合并
我们要引入一个新数组h,树高,别名:秩,字面意思,也就是深度的最大值加1(根节点深度0),还是那句,平衡生长,均匀分配,随机分配出题者也卡不了你。
void join(ll x,ll y){
ll t1=go(x),t2=go(y);
if(h[t1]==h[t2]){//随便让一个为父亲。
to[t2]=t1;
h[t1]++;
}
//秩小则子,秩大则父(以下)。
else if(h[t1]<h[t2]){
to[t1]=t2;
}else {
to[t2]=t1;
}
}
(4) 初始化:简简单单
void init(ll siz){
for(int i=1;i<=siz;i++){
h[i]=1,to[i]=i;
}
}
四、最后
- 第二章终于肝完了~
- 总结:各有所长,散了吧~