C++ 数据结构—第二章(加油)

目录

一、ST表

1.前置定义:RMQ问题

2.ST表的作用

3.ST表中的存储

4.ST表如何求:st[i][L]=?

5.ST最最最重要的一步:求最值

二、树 

1.定义

 2. LCA最近公共祖先

(1)什么是LCA

(2)solution 1(解法 1):欧拉遍历序与RMQ

(3)solution 2:普通LCA,我愿称之为“散步法”(与solution 3结合)

(4)solution 3:倍增LCA,我愿称之为“弹射法”(与solution 2结合)

(5)最后(多嘴一下)

三、并查集

1.并查集用处

2.并查集怎么搞得(原理)

3.代码实现 

(1)实现找根节点

(2)这就是找根节点的极限吗

(3)实现合并 

(4) 初始化:简简单单

四、最后


一、ST表

1.前置定义:RMQ问题

  • RMQ问题,又称区间最值问题,这是一种多次询问区间最大或最小值的问题。
  • 当询问量不是太多时我们可以用上一章的线段树来解决!

2.ST表的作用

  • O(nlogn)的时间内预处理完O(1)回答RMQ问题的一个询问
  • 但是在询问量非很多时用线段树更快(很多时用ST表)!

3.ST表中的存储

  • st_{i,L}表示以L为开头,长度为2^i的区间的最大(小)值,即对应于[L,L+2^i-1]
  • 由于2^{20}已经算大的了所以存储ST表方式如下。
int st[20][N];

4.ST表如何求:st[i][L]=?

我们可以用st_{i-1,L}st_{i-1,L+2^{i-1}}来求st_{i,L},也可以反过来。

st[i][L]=max⁡(st[i-1][L],st[i-1][L+(1<<(i-1))]);

注:位运算符号“<<”,让某数的二进制左移一位,也就是在二进制后添个0,相当于乘2,所以(1<<x)= 2^x

而初始则需要我们自己写入……

for(int i=1;i<=n;i++){
    st[0][i]=a[i];//a数组为ST表维护的数组。
}

5.ST最最最重要的一步:求最值

再次借用一下老师的图片……

  •  一绿一红的两个区间,这两个区间为长度为2的幂次,且不超出区间的最大区间,详看上图。
  • 它们刚好能罩住我们要查询的[L,R]区间,所以一绿一红两个区间的最值就是[L,R]区间的最值。
  • 而其中k=log(r-l+1)(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,可以是区间[5,10]也可以是区间[7,10](欧拉序中),其中深度最小的为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。
  • 看看都跳2^0(1)到哪到了一个地方,祖先5。

预处理:我们要先生成一个表,倍增表(下文f数组;ST表也运用了倍增),f_{i,j}代表从j节点开始向父亲方向跳2^i次的点是谁,那我们就可以推出f_{i,j}=f_{i-1,(f_{i-1,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.并查集怎么搞得(原理)

  • 并查集实际维护的内容,可以看做是对集合的维护,且支持合并集合。
  • 并查集的维护思路是,将每个集合做成一颗树型,用树的根作为集合的标志。 那么,判断两点是否在一个集合内,只要找到两点所在树的树根,判断是否相同即可。
  • 合并两个集合时,让一颗树的树根父亲设为另一颗树的树根即可。 用这样的结构即可维护点与点间连通性判定,但是,显然最坏这样复杂度会达到O(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,将to_x=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;
	}
}

四、最后

  • 第二章终于肝完了~
  • 总结:各有所长,散了吧~

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值