最近公共祖先

(1)倍增法(在线处理,读入一个询问即处理一个答案)

1172. 祖孙询问 - AcWing

预处理o(nlogn)  询问o(logn)

①fa[i][j]表示节点向上跳2^{j}步的节点是谁,depth[i]表示节点i的深度,通过bfs初始化(具体方法看下面代码逻辑) 

注意:fa[i][j]=fa[ fa[i][j-1] ][j-1]

 ②先将较低的点向上跳,直到两点在同一层,此时如果两点重合,则其最近公共祖先为该点,否则让两点再同时向上跳,直到跳到其最近公共祖先的下一层(判断条件为fa[a][k] == fa[b][k]

 ③此时两者的最近公共祖先即为向上再跳一步的点,即fa[a][0]

//N为节点个数,深度为logN
int depth[N]; //logN = 16 
void bfs(int root)
{
    memset(depth,0x3f,sizeof depth);
    // 哨兵depth[0] = 0: 如果从i开始跳2^j步会跳过根节点 
    // fa[fa[j][k-1]][k-1] = 0
    // 那么fa[i][j] = 0 depth[fa[i][j]] = depth[0] = 0
    depth[0] = 0,depth[root] = 1;
    queue<int> q;
    q.push(root);
    while(q.size())
    {
        int t = q.front();
        q.pop();
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j = e[i];
            if(depth[j]>depth[t]+1)//说明j还没被搜索过
            {
                depth[j] = depth[t]+1;
                q.push(j);//把第depth[j]层的j加进队列
                fa[j][0] = t;//j往上跳2^0步后就是t
                for(int k=1;k<=15;k++)
                    fa[j][k] = fa[fa[j][k-1]][k-1];
            }
        }
    }
}
//询问a b 的最近公共祖先 
int lca(int a, int b)
{
    //让a在下面 
    if (depth[a] < depth[b]) swap(a, b);
    //让a往上跳 
    //当a跳完2^k依然在b下面 我们就一直跳
    for (int k = 15; k >= 0; k -- )
        if (depth[fa[a][k]] >= depth[b])
            a = fa[a][k];
    //如果跳到了b
    if (a == b) return a;
    //a,b同层但不同节点
    //a,b同时往上跳
	//如果a,b都跳出根节点,fa[a][k]==fa[b][k]==0 不符合更新条件 
    for (int k = 15; k >= 0; k -- )
        if (fa[a][k] != fa[b][k])
        {
            a = fa[a][k];
            b = fa[b][k];
        }
    //循环结束,到达最近公共祖先的子节点 
    //lca(a,b) = 再往上跳1步即可
    return fa[a][0];
}
 
    bfs(root);//建fa[i][j]

 二叉树

 3555. 二叉树 - AcWing题库

题意:二叉树中两个结点之间的最短路径长度

倍增寻找最近公共祖先的过程中,记录跳的距离,即为两点间路径长度

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=1e3+5,M=N<<1;
int n,m;
int h[N],e[M],ne[M],idx;
void add(int a,int b){
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
int fa[N][12];
int depth[N];
void bfs(int root){
	//注意初始化 depth[0]和 depth[root]
	memset(depth,0x3f,sizeof depth);
	depth[0]=0;
	depth[root]=1;
	queue<int> q;
	q.push(root);
	while(!q.empty()){
		int t=q.front();q.pop();
		for(int i=h[t];~i;i=ne[i]){
			int j=e[i]; 
			if(depth[j]>depth[t]+1){//如果j没被搜索到才更新 
				depth[j]=depth[t]+1;
				fa[j][0]=t;
				for(int k=1;k<=10;k++){
					fa[j][k]=fa[fa[j][k-1]][k-1];
				}
				q.push(j);
			}
		}
	}
}
int query(int a,int b){
	int res=0;
	if(depth[a]<depth[b]) swap(a,b);
	for(int k=10;k>=0;k--){
		if(depth[fa[a][k]]>=depth[b]){
			a=fa[a][k];
			res+=pow(2,k);//往上跳2^k步 
		}
	}
	if(a==b) return res;
	for(int k=10;k>=0;k--){
		if(fa[a][k]!=fa[b][k]){
			a=fa[a][k];
			b=fa[b][k];
			res+=2*pow(2,k);
		}
	}
	//树里的两个点一定有公共祖先(最远为根节点)
	//但在其他图里最后需要判断a是否等于b 
	a=fa[a][0];
	b=fa[b][0];
	return res+2;
}
int main(){
	int t;cin>>t;
	while(t--){
		memset(h,-1,sizeof h);
		idx=0;
		cin>>n>>m;
		for(int i=1;i<=n;i++){
			int x,y;cin>>x>>y;
			if(x!=-1) add(i,x);
			if(y!=-1) add(i,y);
		}
		bfs(1);
		while(m--){
			int x,y;cin>>x>>y;
			cout<<query(x,y)<<endl;
		}
	}
}

(2)tarjan算法(离线处理,需要先读入所有询问)

1171. 距离 - AcWing题库

整体复杂度o(n+m)    n为节点数量,m为询问次数

① dfs所有的点,过程中将所有的点分成三类

        第一类为已经访问且回溯过的点(即访问过且不在搜索栈中),标记为2

        第二类为正在搜索的分支,标记为1

        第三类为还未搜索到的点,标记为0 

②可以看出,第一类点(绿色),即被访问且回溯过的点,第二类节点x(红色)与第一类点中的一整个分支的最近公共祖先都是相同的,即X与Y所在分支的所有点的最近公共祖先均为Z

③所以我们可以使用并查集,将第一类的点都合并到其根节点上,即将Y所在分支的所有点都合并到Z上,当询问X与Y的最近公共祖先时,即查询Y被合并到了哪个点上

④将所有的询问存储到vector<PII> query[u]中,下标u表示关于点u的询问,存储的PII first表示除了u的另外一点,second表示询问的编号 

⑤对于某一点u,对于u的所有子节点j进行dfs,然后将子节点合并到u上

⑥当处理完某一点u后,处理u的询问,然后更新u的状态

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int,int> PII;
const int N=1e4+5,M=N<<1;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
vector<PII> query[N];//query[i]存i的相关询问  {另一节点,问题编号} 
int res[M];//问题答案 
int p[N];
int dis[N];//点i到根节点的距离 
int st[N];//点i在tarjan搜索的状态 
void add(int a,int b,int c){
    e[idx]=b;
    w[idx]=c;
    ne[idx]=h[a];
    h[a]=idx++;
}
int find(int x){
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
//dfs搜索每个点到根节点的距离 
void dfs(int u,int fa){ 
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==fa) continue;
		dis[j]=dis[u]+w[i];
		dfs(j,u);
	}
}
void tarjan(int u){
	st[u]=1;//u正在搜 
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(!st[j]){//如果子节点未搜,对子节点tarjan,并将子节点并到u 
			tarjan(j);
			p[j]=u;
		}
	}
	//处理u相关询问 
	for(int i=0;i<query[u].size();i++){
		PII t=query[u][i];
		int y=t.first,id=t.second;
		if(st[y]==2){//如果另一节点已搜完,find(y)即为最近公共祖先 
			int lca=find(y);
			res[id]=dis[u]+dis[y]-dis[lca]*2;//两点最短距离=两点各自到根节点距离-最近公共祖先到根节点距离*2 
		}
	}
	st[u]=2;//u搜完了 
}
int main(){
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for(int i=1;i<=n;i++) p[i]=i;
    for(int i=0;i<n-1;i++){
        int a,b,c;cin>>a>>b>>c;
        add(a,b,c),add(b,a,c);
    }
    //存询问
    for(int i=0;i<m;i++){
        int a,b;cin>>a>>b;
        if(a!=b){
            query[a].push_back({b,i});
            query[b].push_back({a,i});
        }
    }
    
    dfs(1,-1);//选择任意一个点为根节点,此处选1 
    tarjan(1);//从根节点开始tarjan 
    for(int i=0;i<m;i++){
    	cout<<res[i]<<endl;
    }
    return 0;
}

机房 

机房 - 蓝桥云课

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
typedef long long ll;
typedef pair<int,int> PII;
int h[N],e[N<<1],ne[N<<1],idx;
void add(int a,int b){
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
int n,m;
vector<PII> query[N];
int res[N];
int dis[N];
int w[N];
void dfs(int u,int fa){
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==fa) continue;
		dis[j]=dis[u]+w[j];
		dfs(j,u);
	}
}
int p[N];
int find(int x){
	if(p[x]!=x) p[x]=find(p[x]);
	return p[x];
}
int st[N];
void tarjan(int u){
	st[u]=1;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(!st[j]){
			tarjan(j);
			p[j]=u;
		}
	}
	for(int i=0;i<query[u].size();i++){
		int y=query[u][i].first,id=query[u][i].second;
		if(st[y]==2){
			int lca=find(y);
			res[id]=dis[u]+dis[y]-2*dis[lca]+w[lca];
		}
	}
	st[u]=2;
}
int main(){
	cin>>n>>m;
	memset(h,-1,sizeof h);
	for(int i=1;i<=n;i++) p[i]=i;
	for(int i=1;i<n;i++){
		int x,y;cin>>x>>y;
		add(x,y);add(y,x);
		w[x]++;w[y]++;
	}
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		if(u==v){
			res[i]=w[u];
			continue;
		}
		query[u].push_back({v,i});
		query[v].push_back({u,i});
	}
	dis[1]=w[1];
	dfs(1,-1);
	tarjan(1);
	for(int i=1;i<=m;i++){
		cout<<res[i]<<endl;
	}
}

次小生成树

356. 次小生成树 - AcWing题库

思路:① 首先Kruskal求出最小生成树,并建图

           ② 然后倍增法BFS预处理出depth,以及所有路径离根节点的最大边和次大边

           ③ 对于每个非树边,利用lca,求出这两点与公共祖先的路径上的最大边和次打边,然后替换

// 建树,建造出最小生成树
void build() {
    memset(head, -1, sizeof head);
    for (int i = 0; i < m; i ++)
        if (edges[i].used) {
            int a = edges[i].a, b = edges[i].b, w = edges[i].w;
            add(a, b, w);
            add(b, a, w);
        }
}

// 用最近公共祖先来求所有路径离根节点的最大的一条边,和次大的一条边
void bfs() {
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[n] = 1;
    q[0] = n;
    int hh = 0, tt = 0;
    while (hh <= tt) {
        int t = q[hh ++];
        for (int i = head[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (depth[j] > depth[t] + 1) {
                depth[j] = depth[t] + 1;  // bfs更新层次
                q[++ tt] = j;  // 宽度优先
                fa[j][0] = t;  // 更新一下 fa(i, 0);
                d1[j][0] = w[i], d2[j][0] = -INF;  // 更新一下d1, d2
                // 只有一条边不存在次长路

                // 都已经更新到这个点了, 之前路径上的距离也都算出来了
                // 所以都可以更新出d1, d2 fa,
                for (int k = 1; k <= 16; k ++) {  // 开始跳
                    int anc = fa[j][k - 1];  // 这anc是跳一半的位置
                    fa[j][k] = fa[anc][k - 1];  // 求得跳2^k之后的位置

                    // distance存储的是j开始跳一半的数据
                    // d1[j][k - 1] 跳一般的最大边
                    // d2[j][k - 1] 跳一半的次大边
                    // d1[anc][k - 1]  跳另一半的最大边
                    // d2[anc][k - 1]  跳另一半的次大边
                    int distance[4] = {d1[j][k - 1], d2[j][k - 1], 
                                       d1[anc][k - 1], d2[anc][k - 1]};

                    d1[j][k] = d2[j][k] = -INF;  // 整条路径的最大值和次大值先初始化为-INF
                    for (int u = 0; u < 4; u ++) {
                        // 更新一下最大值和次大值
                        int d = distance[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;
                    }
                }
            }
        }
    }
}

// a -> b w是一条非树边
int lca(int a, int b, int w) {
    static int distance[N * 2];
    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]) {
            distance[cnt ++] = d1[a][k];  // 每跳一次,记录一下这路径上的最大值
            distance[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]) {  // 把这所有东西都存下来
                distance[cnt ++] = d1[a][k];
                distance[cnt ++] = d2[a][k];
                distance[cnt ++] = d1[b][k];
                distance[cnt ++] = d2[b][k];
                a = fa[a][k], b = fa[b][k];
            }
        // 已经找到公共祖先了,公共祖先就是 fa[a][0] = fa[b][0] = anc
        // 把这边也存下来
        distance[cnt ++] = d1[a][0];
        distance[cnt ++] = d1[b][0];
    }

    // 找到公共祖先的
    int dist1 = -INF, dist2 = -INF;
    for (int i = 0; i < cnt; i ++) {
        int d = distance[i];
        // 这里规定dist1 一定严格大于 dist2 (dist2是严格次小值)
        if (d > dist1) dist2 = dist1, dist1 = d;
        // 如果这里 d = dist 而且 d = dist2, 令dist2 = d的话,就会有 dist1 = dist2
        else if (d != dist1 && d > dist2) dist2 = d;
    }

    if (w > dist1) return w - dist1;  // 看一下这个非树边是否大于dist1, 是的话就替换
    // 返回变大了多少
    // w 一定是 >= dist1 > dist2
    // 如果第一个if不满足, 那一定有 w = dist1
    if (w > dist2) return w - dist2;  // 看一下这个非树边是否大于dist2, 是的话就替换
    // 返回变大了多少
}

LL kruskal() {
    for (int i = 1; i <= n; i ++ ) p[i] = i;
    sort(edges, edges + m);
    LL res = 0;

    for (int i = 0; i < m; i ++) {
        int a = find(edges[i].a), b = find(edges[i].b), w = edges[i].w;
        if (a != b) {
            res += w;
            p[a] = b;
            edges[i].used = true;
        }
    }

    return res;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i ++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edges[i] = {a, b, c};
    }

    LL sum = kruskal();
    build();
    bfs();

    LL res = 1e18;
    for (int i = 0; i < m; i ++) {
        if (!edges[i].used) {  // 枚举一下所有非树边
            int a = edges[i].a, b = edges[i].b, w = edges[i].w;
            res = min(res, sum + lca(a, b, w));  // 看看替换后的结果, 取最小值
        }
    }

    printf("%lld", res);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vic.GoodLuck

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值