最近公共祖先刷题笔记:

1.模板题:AcWing 1172. 祖孙询问 - AcWing

传统做法:倍增+lca

思路: 1)先预处理出每个点的祖先

         2)再求LCA:先拿到层数最深的一个点a,然后吧他的层数增加到和另一个b层数一样,

           特判: if a==b,b就是a,b的最近公共祖先

           else 就两个点一起向上,继续找公共祖先,--一定 会存在,因为最后都会回到根节点

初始化代码:

怎么理解fa[j][k] = fa[fa[j][k - 1]][k - 1] ?

fa[i][k] --表示从i开始,向上走2^k步所能走到的节点

我们从 j 跳到他前面的的2 ^k个位置,需要跳2^k步,我们可以先跳2^(k-1)步跳到到他的2^(k-1),即fa[j][k-1]这个节点,再从这个点跳2^(k-1)步就到达目的地

(2^(k-1)+2^(k-1) =2*2^(k-1)=2^k)

//使用倍增初始化来提速
int depth[N], fa[N][16];//2^16 >=40000
//depth,fa同时初始化
//注意fa[i][k]含义: i的第2^k位祖先
void bfs(int root) {
	memset(depth,0x3f,sizeof depth);
	depth[0] = 0, depth[root] = 1;
	int hh = 0, tt = 0;
	q[0] = root;
	while (hh <= tt) {
		int t = q[hh++];
		for (int i = h[t]; ~i ; i = ne[i])
		{
			int j = e[i];
			if (depth[j] > depth[t] + 1) {
				depth[j] = depth[t] + 1;
					q[++tt] = j;
					fa[j][0] = t;
					for (int k = 1; k <= 15; ++k)
						fa[j][k] = fa[fa[j][k - 1]][k - 1];
			}
		
		}

	
	}


}

//求lca代码:

int lca(int a, int b) {
	//先让深度更深的点,跳到同一层
	if (depth[a] < depth[b])swap(a, b);
	for (int k = 15; k >= 0; --k) 
	if(depth[fa[a][k]]>=depth[b]){
		a = fa[a][k];
	}
	//然后特判一下,两个点是否是相同的,若是,则b是a的祖先
	if (a == b)return a;
	//然后两个点现在处于同一层,两个点一起往上跳,直到找到公共祖先
	for(int k=15;k>=0;--k)
		//因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,
		if (fa[a][k] != fa[b][k]) {//判断二者祖先 是否相同
			a = fa[a][k];
			b = fa[b][k];
		}

	return fa[a][0];
}

解法二: Tarjan:是强制离线算法

思路:

先很自然地深优遍历下去,
如果当前节点x涉及到某一个询问且询问的另一个点已经访问过了,那么就可以得出答案了;反之标记点x已经访问过,直到访问到另一个节点。
,在访问到一个点时,我们可以一并解决所有与这个点有关的询问,所以我们的询问也要用邻接表来存贮

Tarjan--离线求LCA  O(n+m)
在dfs时将所有点分为三类:
1)已经遍历过且回溯过的点--2
2)正在搜索的分支上的点--1
3)还未搜索到的点--0

我们发现绿圈中的点与红线上的点满足一定规律,即一颗子树上的根节点为二者lca 

例子:

我们从上到下 分出3颗子树: a.:绿2  b: 绿4  c:绿1

我们发现绿2 上的所有点与红线上点的lca为这棵子树的根节点,即与绿2连接的红线上的点 

同样下面的绿4,绿1也是,so这启发我们求lca的时候可以使用集合合并的思路--并查集'

那么聪明的 小朋友已经发现我们遗漏了一种情况:

 我们只是把左边的绿色点加入到集合中了,那红线上的怎么处理,

比如我现在就想求2,3的lca怎么求呢,其实我们遍历顺序是从1到5:

观察发现: 线上2与绿4的lca为1,3与绿1的lca为2,5与三绿的lca依次为3,2,1(从下到上)

因为最后会从5回溯回去,我们只需要回溯的时候更新5的lca的同时顺便求出当前lca所能求的点即可.

因为并查集每个查询和插入都是O(1),且我们需要插入最多n个点,查询m个点,so总时间复杂度为

O(n+m)

Tarjan代码如下: 

 

附赠练习题目:

2.AcWing 1171. 距离(算法提高课) - AcWing

//思路: 求一颗树上两个点的最短距离那么就是求这两个点到他们lca的距离之和

怎么求,我们可以先初始化d[x]表示x到根节点的距离

我们所求可以变为 d[x]+d[y]-2*d[lca(x,y)]

如图:p为lca(x,y)

 

注意: 本题没有指出root,是因为求两点最短距离,是相对距离的意思,选谁做根节点均可,我们默认1位根节点咯

ok,热身结束,让我们来接触一下比较复杂的一题吧:

3.356. 次小生成树 - AcWing题库

求次小生成树: 这题当然可以用dfs去求我们的d1,d2--AcWing 1148. 秘密的牛奶运输 - AcWing

这头的dist求的是任意两点之间的边权最大值

但本题正解是用最近公共祖先去维护的d1,d2

理论铺垫:

最小生成树中加入一条非数边,一定会形成一个环,因为所有点都已经连通,再加入一条任意两点之间的边,那么这两个点就会配合其他点形成一个环,so我们只需要去掉环上 的最大边即可求出次小生成树了

思路:

1)先求出一颗最小生成树--Kruskal 

2)再枚举每条非数边,加入判断

我们此时的 d1,d2--这样定义:

d1[i][k],从i开始,向上条2^k步得到的 最大边权
d2[i][k],从i开始,向上条2^k步得到的 次大边权

那么次数的d1,d2是怎么更新的呢?

来看一张图片:

 我们发现d1要在4者中取最大,d2要在4者中取次大,而这步又是在lca中体现,那么我们只需要在bfs插入初始化fa,d1,d1的操作即可

代码如下:

void bfs() {
    memset(depth,0x3f,sizeof depth);
    depth[0] = 0,depth[1]=1;
    int hh = 0, tt = 0;
    q[0] = 1;
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (depth[j] > depth[t] +1) {
                depth[j] = depth[t] + 1;
                q[++tt] = j;
                fa[j][0] = t;
                d1[j][0] = w[i], d2[j][0] = -INF;
                for (int k = 1; k <= 16; ++k) {
                    int anc = fa[j][k - 1];
                    fa[j][k] = fa[anc][k - 1];
                    int dis[4] = {d1[j][k-1],d2[j][k-1],d1[anc][k-1],d2[anc][k-1]};
                    for (int u = 0; u < 4; ++u) {
                        int d = dis[u];
                        if (d > d1[j][k])d2[j][k] = d1[j][k], d1[j][k] = d;
                        else if (d != d1[j][k] && d > d2[j][k])d2[j][k] = d;
                    }
                }
            }
        }
    }
}

但是,这样的d1d2还不是我们要求的a,b到lca的最大距离和次大距离:

我们还需要把a到lca,b到lca的没个点的d1,d2加入,最后取最大,次大即可:

代码如下:

int lca(int a, int b, int w) {
    static int dis[2 * N];
    int cnt = 0;
    if (depth[a] < depth[b])swap(a,b);
    for (int k = 16; k >= 0; --k) 
    if(depth[fa[a][k]]>=depth[b])
    {
        dis[cnt++] = d1[a][k];
        dis[cnt++] = d2[a][k];
        a = fa[a][k];
    }
    if (a != b) {
        for (int k = 16; k >= 0; --k) {
            if (fa[a][k] != fa[b][k])
            {
                dis[cnt++] = d1[a][k];
                dis[cnt++] = d2[a][k];
                dis[cnt++] = d1[b][k];
                dis[cnt++] = d2[b][k];
                a = fa[a][k], b = fa[b][k];
            }
        }
        dis[cnt++] = d1[a][0];
        dis[cnt++] = d1[b][0];
    }
    int dist1 = -INF, dist2 = -INF;
    for (int i = 0; i < cnt; ++i) {
        int d = dis[i];
        if (d > dist1)dist2 = dist1, dist1 = d;
        else if (d != dist1 && d > dist2)dist2 = d;
    }
    
    if (w > dist1)return w - dist1;
    if (w > dist2)return w - dist2;
    return INF; //取min不会用到
}

哎呀,我们的code农场遭到wa大魔王的进攻啦?

AC勇士快去打败他吧:

4.352. 闇の連鎖 - AcWing题库

题意:求砍断一颗生成树的方法数,我们每次先可以砍一条树边,才可以砍一条非树边

如图:红色边为非树边 

我们发现:左红和与他连接的数边构成的环上的边,我们砍完之后还需要再砍一条非数边(就是左红了)才能切断这个树,我们再遍历右边的环,一次 累积 +1 到每条环中边

思路:

我们遍历所有边和他们经过的环上的数边(遍历一次+1)最后得到环上的数字 c,表示,要砍了这条边使得数不连通的话还需要我们砍掉c条非树边

so我们的方法数== 遍历整颗树上的数字c:

分类:

c==0: ans+=m(都没有非树边的环经过他,把他砍了+砍1条非树边(共m条)--一共m中方案)

c==1:ans+=1(砍了还需要再砍一条非树边,只有一种方案)

c>1:     ans+=0(因为我们只能砍一刀非树边,so这个边不能砍)

难点: 如何快速的给每一条边 + 一个数--差分:快速的给某一个区间+一个数-O(1)
使用 树上差分:
p =cla(x,y)
d[x]+=c, d[y]+=c,d[p]-=2*c

我们发现这样操作只会给x,y在cla(x,y)下的祖先们的边权+c

 so我们求ans的时候只需要dfs一下,从下往上遍历即可

代码如下:

int dfs(int u,int fa) {//返回每颗子树的和是多少
    int res = d[u]; //继承父亲的差分的求前缀和操作
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (j != fa) {
            int s = dfs(j,u);
            //s==0 || 1 只有 遍历到单边的时候会出现
            if (s==0)ans += m;
            else if (s == 1)ans++;
            res += s; //体现了差分求前缀和
        }
    }
    return res;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值