【蓝桥杯 2023 省 B】砍树

【蓝桥杯 2023 省 B】砍树


蓝桥杯专栏:2023 省 B

算法竞赛:图论,搜索,差分,最近公共祖先 LCA,树上差分,深度优先搜索,广度优先搜索,拓扑排序

题目链接:洛谷【蓝桥杯 2023 省 B】砍树

题目描述

给定一棵由 n n n 个结点组成的树以及 m m m 个不重复的无序数对 ( a 1 , b 1 ) , ( a 2 , b 2 ) , ... , ( a m , b m ) \left(a_{1},b_{1}\right),\left(a_{2},b_{2}\right),\ldots,\left(a_{m},b_{m}\right) (a1,b1),(a2,b2),...,(am,bm),其中 a i a_{i} ai 互不相同, b i b_{i} bi 互不相同, a i ≠ b j ( 1 ≤ i , j ≤ m ) a_{i} \neq b_{j}(1 \leq i,j \leq m) ai=bj(1≤i,j≤m)。

小明想知道是否能够选择一条树上的边砍断,使得对于每个 ( a i , b i ) \left(a_{i},b_{i}\right) (ai,bi) 满足 a i a_{i} ai 和 b i b_{i} bi 不连通,如果可以则输出应该断掉的边的编号 (编号按输入顺序从 1 1 1 开始),否则输出 -1
输入格式

输入共 n + m n+m n+m 行,第一行为两个正整数 n , m n,m n,m。

后面 n − 1 n-1 n−1 行,每行两个正整数 x i , y i x_{i},y_{i} xi,yi 表示第 i i i 条边的两个端点。

后面 m m m 行,每行两个正整数 a i , b i a_{i},b_{i} ai,bi。

输出格式

一行一个整数,表示答案,如有多个答案,输出编号最大的一个。
数据范围

对于 30 % 30 \% 30% 的数据,保证 1 < n ≤ 10 3 1<n \leq 10^3 1<n≤103。

对于 100 % 100 \% 100% 的数据,保证 1 < n ≤ 10 5 1<n \leq 10^{5} 1<n≤105, 1 ≤ m ≤ n 2 1 \leq m \leq \frac{n}{2} 1≤m≤2n。


题目大意

给出一棵树( n n n 个顶点, n − 1 n-1 n−1 条边)和一些无序数对 ( a i , b i ) , a i , b i ∈ [ 1 , n ] (a_i,b_i),a_i,b_i \in [1,n] (ai,bi),ai,bi∈[1,n],其中 a i , b i a_i,b_i ai,bi 是树上的顶点,让找出一条编号最大的边,满足删去后所以无序数对中的 a i a_i ai 顶点与 b i b_i bi 顶点不相连通,不能相互到达。

题目分析

解决思路:直观分析,找出所有(最短)路径 a i → b i 或 b i → a i a_i \to b_i \text{或} b_i \to a_i ai→bi或bi→ai 都经过的边即可。

这里用到的算法为最近公共祖先(LCA)树上差分

最近公共祖先 LCA

基础知识见:LCA

建议先完成:P3379 【模板】最近公共祖先(LCA)

最近公共祖先的用处

要找出最短 路径 a i → b i 或 b i → a i a_i \to b_i \text{或} b_i \to a_i ai→bi或bi→ai,就要找到 a i a_i ai 和 b i b_i bi 的最近公共祖先,因为最短路径即 a i → l c a ( a i , b i ) + l c a ( a i , b i ) → b i 或 b i → l c a ( a i , b i ) + l c a ( a i , b i ) → a i a_i \to lca(a_i,b_i) + lca(a_i,b_i) \to b_i \text{或} b_i \to lca(a_i,b_i) + lca(a_i,b_i) \to a_i ai→lca(ai,bi)+lca(ai,bi)→bi或bi→lca(ai,bi)+lca(ai,bi)→ai。

树上差分

基础知识见:差分--树上差分

树上差分(边差分)算法:

c i = c i + w , c j = c j + w c_i = c_i + w , c_j = c_j + w ci=ci+w,cj=cj+w

c l c a ( i , j ) = c l c a ( i , j ) − 2 × w c_{lca(i,j)} = c_{lca(i,j)} - 2 \times w clca(i,j)=clca(i,j)−2×w

其中, c x c_x cx 数组表示节点 x x x 与其父节点相连的边出现的次数,记为这条边的边权。

对于本题就是:

c i = c i + 1 , c j = c j + 1 c_i = c_i + 1 , c_j = c_j + 1 ci=ci+1,cj=cj+1

c l c a ( i , j ) = c l c a ( i , j ) − 2 c_{lca(i,j)} = c_{lca(i,j)} - 2 clca(i,j)=clca(i,j)−2

但我们可以改变一下,使用 e x e_x ex 数组记录节点 x x x 的出现次数,用 c x c_x cx 数组记录节点 x x x 与其父节点相连的边的出现次数,由恒成立关系 c x = e x c_x = e_x cx=ex 我们可以对点权进行加减操作,再赋值给边权,即

e i = e i + 1 , e j = e j + 1 e_i = e_i + 1 , e_j = e_j + 1 ei=ei+1,ej=ej+1

e l c a ( i , j ) = e l c a ( i , j ) − 2 e_{lca(i,j)} = e_{lca(i,j)} - 2 elca(i,j)=elca(i,j)−2

e f a i = e f a i + e i e_{fa_i} = e_{fa_i} + e_i efai=efai+ei

c i = e i c_i = e_i ci=ei

其中, f a i fa_i fai 表示节点 i i i 的父节点。

差分后可以通过 dfs 遍历树来求每条边的边权,但差分后往回加应该是父节点加子节点的点权,子节点的点权转移到与父节点相连的边的边权上。

也可以通过每个点的深度来判断父子关系,从而使(同上)父节点加子节点的点权,子节点的点权转移到与父节点相连的边的边权上。

最后,边权等于 m m m 且编号最大的边即为所求, a n s ans ans 初始化为 − 1 -1 −1 更方便。

AC Code 记录深度

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int n,m,t,f[N][25],d[N],c[N],e[N],p[N],ans=-1;//c[]--边权,e[]--点权,d[]--深度,p[]--与父节点相连的边的编号
vector<pair<int,int>>G[N];
vector<int>k;
queue<int>q;
void bfs()
{
	q.push(1);//根节点指定为 1 即可
	d[1]=1;
	while (q.size()>0)
	{
		int x=q.front();
		q.pop();
		for (auto [y,id] : G[x])
			if (!d[y])
			{
				d[y]=d[x]+1;
				f[y][0]=x;
				p[y]=id;
				for (int j=1; j<=t; j++)
					f[y][j]=f[f[y][j-1]][j-1];//lca 预处理
				q.push(y);
			}
	}
}
int lca(int x,int y)
{
	if (d[x]>d[y]) swap(x,y);
	for (int i=t; i>=0; i--)//需要逆向进行
		if (d[f[y][i]]>=d[x])
			y=f[y][i];
	if (x==y) return x;//注意返回
	for (int i=t; i>=0; i--)
	{
		if (f[y][i]!=f[x][i])//不等于,下面返回 f[x][0]
			y=f[y][i],x=f[x][i];
	}
	return f[x][0];
}
int main()
{
	scanf("%d%d",&n,&m);
	t=ceil(log(n)/log(2));//亦可 ceil(log2(n))
	for (int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		G[x].push_back({y,i});
		G[y].push_back({x,i});
	}
	memset(d,0,sizeof d);
	bfs();
	for (int i=0; i<m; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		e[x]++,e[y]++;
		e[lca(x,y)]-=2;//边差分
	}
	for (int i=1; i<=n; i++) k.push_back(i);
	sort(k.begin(),k.end(),[&](int x,int y){return d[x]>d[y];});//深度排序
	for (int i : k)
	{
		if (i==1) continue;
		e[f[i][0]]+=e[i];//父节点加子节点点权
		c[p[i]]=e[i];//子节点点权转移到与父节点相连的边的边权上
	}
	for (int i=1; i<n; i++)//可逆向,找到立即 break
		if (c[i]==m) ans=i;
	printf("%d",ans);
	return 0;
}

AC Code dfs

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int n,m,t,f[N][25],d[N],c[N],e[N],ans=-1;//c[]--边权,e[]--点权,d[]--深度
vector<int>G[N];
queue<int>q;
map<string,int>mp;//存边的编号
void bfs()
{
	q.push(1);//根节点指定为 1 即可
	d[1]=1;
	while (q.size()>0)
	{
		int x=q.front();
		q.pop();
		for (int y : G[x])
			if (!d[y])
			{
				d[y]=d[x]+1;
				f[y][0]=x;
				for (int j=1; j<=t; j++)
					f[y][j]=f[f[y][j-1]][j-1];//lca 预处理
				q.push(y);
			}
	}
}
int lca(int x,int y)
{
	if (d[x]>d[y]) swap(x,y);
	for (int i=t; i>=0; i--)//需要逆向进行
		if (d[y]-d[x]>>i&1) 
			y=f[y][i];
	if (x==y) return x;//注意返回
	for (int i=t; i>=0; i--)
	{
		if (f[y][i]!=f[x][i])//不等于,下面返回 f[x][0]
			y=f[y][i],x=f[x][i];
	}
	return f[x][0];
}
void dfs(int x,int fa)
{
	for (int y : G[x])
	{
		if (y==fa) continue;//由 fa 转移来的
		dfs(y,x);//先递归
		e[x]+=e[y];//父节点加子节点点权
		c[mp[to_string(x)+"to"+to_string(y)]]=e[y];//子节点点权转移到与父节点相连的边的边权上
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	t=ceil(log2(n));
	for (int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		G[x].push_back(y);
		G[y].push_back(x);//正反存图
		mp[to_string(x)+"to"+to_string(y)]=i;
		mp[to_string(y)+"to"+to_string(x)]=i;
	}
	bfs();
	for (int i=0; i<m; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		e[x]++,e[y]++;
		e[lca(x,y)]-=2;//边差分
	}
	dfs(1,0);
	for (int i=1; i<n; i++)//可逆向,找到立即 break
		if (c[i]==m) ans=i;
	printf("%d",ans);
	return 0;
}

TLE LCA 朴素 dfs 求边权

(初代代码)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int n,m,f[N][25],d[N],ind[N],root,a[N],b[N],fl,ans=-1,t;
vector<int>G[N],k;
queue<int>q;
map<string,int>mp1,mp2;
void bfs_root()
{
	for (int i=1; i<=n; i++)
		if (ind[i]==1) q.push(i),ind[i]--;
	while (q.size()>0)
	{
		int x=q.front();
		q.pop();
		root=x;
		for (int y : G[x])
			if (--ind[y]==1) q.push(y);
	}
}
void bfs()
{
	q.push(root);
	d[root]=1;
	while (q.size()>0)
	{
		int x=q.front();
		q.pop();
		for (int y : G[x])
			if (!d[y])
			{
				d[y]=d[x]+1;
				f[y][0]=x;
				for (int j=1; j<=t; j++)
					f[y][j]=f[f[y][j-1]][j-1];
				q.push(y);
			}
	}
}
int lca(int x,int y)
{
	if (d[x]>d[y]) swap(x,y);
	for (int i=t; i>=0; i--)
		if (d[y]-d[x]>>i&1)
			y=f[y][i];
	if (x==y) return x;
	for (int i=t; i>=0; i--)
	{
		if (f[y][i]!=f[x][i])
			y=f[y][i],x=f[x][i];
	}
	return f[x][0];
}
void dfs(int x,int fa,int tar)
{
	if (x==tar)
	{
		for (int i=1; i<=n; i++)
			b[i]+=a[i];
		fl=1;
		return;
	}
	for (int y : G[x])
	{
		if (fl) return;
		if (y==fa) continue;
		a[mp1[to_string(x)+"to"+to_string(y)]]++;
		dfs(y,x,tar);
		a[mp1[to_string(x)+"to"+to_string(y)]]--;
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	t=ceil(log(n)/log(2));
	for (int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		G[x].push_back(y);
		G[y].push_back(x);
		ind[x]++,ind[y]++;
		mp1[to_string(x)+"to"+to_string(y)]=i;
		mp1[to_string(y)+"to"+to_string(x)]=i;
	}
	bfs_root();
	//printf("%d",root);
	//memset(f,-1,sizeof f);
	memset(d,0,sizeof d);
	//q.clear();
	bfs();
	int tem=0,now;
	for (int i=0; i<m; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		k.push_back(x);
		k.push_back(y);
		now=lca(x,y);
		if (tem==0) tem=now;
		else tem=lca(tem,now);
	}
	for (int tar : k)
	{
		fl=0;
		memset(a,0,sizeof a);
		dfs(tem,0,tar);
	}
	for (int i=1; i<=n; i++)
	{
		//printf("%d\n",b[i]);
		if (b[i]==m&&i>ans) ans=i;
	}
	printf("%d",ans);
	return 0;
}

End

感谢观看,如有问题欢迎指出。

更新日志

  1. 2025/8/22 开始书写本篇 CSDN 博客,并完稿发布。

本篇博客最早由本人发布于洛谷文章广场,本篇博客对其进行了修改调整与完善丰富。