蓝桥杯2022(十三届)国赛——机房(细嗦LCA)

机房(LCA)

1.机房 - 蓝桥云课 (lanqiao.cn)

我就是一纯傻逼啊啊啊啊……

真的服了,这道题,本来用自己的方法做出来了,但是提交的时候就过了一个样例。被逼无奈去看了题解,用了一个高深莫测的优化方法,学完之后再去做,发现还是只过了那一个样例,哭了。最后和题解一个一个对,发现其实是题意理解有问题:当查找的u==v时,是延迟1次,而不是延迟1!样例输出1是因为他延迟的一次就是1!!!

比较暴力的LCA(但是仍然AC了):

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
#define ll long long
const int N = 1e5 + 3;

//数据量还是很大的,图论的话,咋一看好像用Dijkstra算法
//但是这个没有边权值,就很迷。虽然可以用DFS骗几分
//但是仔细看会发现,n个节点n-1个边,其实是一个树,最后这是一个LCA问题,记得有一个优化模板的(景区导游)

int n, m;
vector<vector<int>> g(N);	//实际上是以边的形式存的图
int cost[N] = { 0 };
int d[N];	//祖先编号 深度
int pre[N] = { 0 };
int book[N] = { 0 };

void cntDepth()	//统计深度
{
	queue<int>q;
	q.push(1);	//因为是树,那随便哪个都能当根,这里默认1为根
	d[1] = 1;
	book[1] = 1;
	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		for (int i = 0; i < g[cur].size(); i++)
		{
			int nex = g[cur][i];
			if (book[nex] == 1)	continue;
			d[nex] = d[cur] + 1;
			pre[nex] = cur;		//nex的父亲是cur
			book[nex] = 1;
			q.push(nex);
		}
	}
}



int getCost(int a, int b)	//从目标节点向上回溯,直到到达公共祖先,回溯过程中同时累加该节点的权值
{
	//虽然没有用那个非常牛的便利方法,但是这里的思想是类似的
	//先将深度持平
	ll sum = 0;
	if (d[a] < d[b])	//让b的位置保持在高位
		swap(a, b);
	while (d[a] != d[b])	//现在将a向上移动,同时记录移动过程中走过的权值
	{
		sum += cost[a];
		a = pre[a];
	}
	//现在持平了
	if (a == b)	//持平有两种可能,一个是相同了,一个是普通持平
		return sum + cost[a];	//相同,则直接返回即可
	//如果不是兄弟,继续向上走,同步阀
	while (pre[a] != pre[b])
	{
		sum += cost[a] + cost[b];
		a = pre[a], b = pre[b];
	}
	//此时深度相同且父亲相同,说明这个父亲就是最近公共祖先
	return cost[a] + cost[b] + cost[pre[a]] + sum;	
		
}


int main()
{
	cin >> n >> m;
	int u, v;
	for (int i = 1; i <= n - 1; i++)
	{
		cin >> u >> v;
		g[u].push_back(v);
		g[v].push_back(u);
		cost[u]++;
		cost[v]++;
	}

	cntDepth();

	while (m--)
	{
		cin >> u >> v;
		if (u == v)	cout << cost[u] << endl;
		else		cout << getCost(u, v) << endl;
	}

	return 0;
}

优化版:(可以作为LCA模板使用)

如果对 LCA 不太了解,对 f 数组理解欠佳的同学,可以仔细看看我上面那个代码中的 getCost 函数,pre和f数组本质上是一样的,只是f数组跳的更快。

这里的优化有两点:

  1. pre转成了f:pre 只能一步一步走,而 f 可以跳着走,对于寻找最近公共祖先而言,f 的效率是更高的,但是同样也是最难理解的,但是本题之所以 pre 也能做出来,我觉得是因为我在寻找最近公共祖先的时候就已经在为最后答案输出做铺垫了。

  2. 树上前缀和dist:版本1的代码是一点一点加起来的,而版本2则是有一种树上前缀和的感觉,专业一点应该叫做树上差分,感兴趣的同学可以去搜搜。

    #include <iostream>
    #include <queue>
    #include <vector>
    using namespace std;
    
    const int N = 100010, M = N << 1;
    
    int n, m;
    int f[N][18]={0}, d[N] = {0}, cost[N] = {0}, dist[N] = {0};
    int book[N] = { 0 };
    vector<vector<int>> g(N);
    
    void cntDepth()	//统计深度
    {
    	queue<int>q;
    	q.push(1);	//因为是树,那随便哪个都能当根,这里默认1为根
    	d[1] = 1;
    	book[1] = 1;
    	dist[1] = cost[1];
    	while (!q.empty())
    	{
    		int cur = q.front();
    		q.pop();
    		for (int i = 0; i < g[cur].size(); i++)
    		{
    			int nex = g[cur][i];
    			if (book[nex] == 1)	continue;
    			d[nex] = d[cur] + 1;
    			dist[nex] = dist[cur] + cost[nex];	//相当于树上前缀和
    			//pre[nex] = cur;		//nex的父亲是cur
    			f[nex][0] = cur;	//和上面等效
    			book[nex] = 1;
    			q.push(nex);
    
    			for (int j = 1; j <= 17; j++)
    				f[nex][j] = f[f[nex][j - 1]][j - 1];
    			//如:f[nex][1]=f[f[nex][0]][0],也就是:nex的爷爷节点=nex的父节点的父节点
    			//f[nex][2]=f[f[nex][1]][1],也就是:nex的祖父节点=nex的爷爷节点的爷爷节点
    		}
    	}
    }
    
    int lca(int a, int b)
    {
    	if (d[a] < d[b])	swap(a, b);	//将b置于上层
    	for (int i = 17; i >= 0; i--)
    		if (d[f[a][i]] >= d[b])	//如果a有父辈深度比b更深,可以跳转
    			a = f[a][i];
    	if (a == b)	return a;
    	for(int i=17;i>=0;i--)
    		if (f[a][i] != f[b][i])
    		{
    			a = f[a][i];
    			b = f[b][i];
    		}
    	//cout << "LCA=" << f[a][0] << endl;
    	return f[a][0];		//f[a][0]	表示其父节点
    }
    
    
    int main()
    {
    	cin >> n >> m;
    	int u, v;
    	for (int i = 1; i <= n - 1; i++)
    	{
    		cin >> u >> v;
    		g[u].push_back(v);
    		g[v].push_back(u);
    		cost[u]++;
    		cost[v]++;
    	}
    
    	cntDepth();
    
    	while (m--)
    	{
    		cin >> u >> v;
    		//if (u == v)	cout << 1 << endl;
    		if (u == v)	cout << cost[u] << endl;	//注意措辞!是一次延迟,而不是延迟为1!
    		else
    		{
    			int LCA = lca(u,v);
    			cout << dist[u] + dist[v] - 2 * dist[LCA] + cost[LCA] << endl;
    		}
    	}
    
    	return 0;
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值