2023第14届蓝桥杯大赛软件赛省赛C/C++大学A组第7题题解:网络稳定性

目录

问题描述:

题意分析:

方法一:Kruscal最大生成树+LCA最近公共祖先

        Kruscal最大生成树算法代码,需要用到并查集算法。由于本题点的数目很多,边的数目够小,存储图使用的是邻接链表结构。

        dfs代码:

        lca代码:

        完整代码:

结语:


问题描述:

        有一个局域网,由 n 个设备和 m 条物理连接组成,第 i 条连接的稳定性为wi。 对于从设备 A 到设备 B 的一条经过了若干个物理连接的路径,我们记这条路径的稳定性为其经过所有连接中稳定性最低的那个。 我们记设备 A 到设备 B 之间通信的稳定性为 A B 的所有可行路径的稳定性中最高的那一条。给定局域网中的设备的物理连接情况,求出若干组设备 xi yi 之间的通信稳定性。如果两台设备之间不存在任何路径,请输出 −1。

输入格式:

        输入的第一行包含三个整数 n, m, q ,分别表示设备数、物理连接数和询问数。 接下来 m 行,每行包含三个整数 ui , vi ,wi ,分别表示 ui vi 之间有一条稳定性为 wi 的物理连接。接下来 q 行,每行包含两个整数 xi , yi ,表示查询 xi yi 之间的通信稳定性。

输出格式:

        输出 q 行,每行包含一个整数依次表示每个询问的答案。

评测用力规模与规定:

        对于 30% 的评测用例,n, q 500m 1000

        对于 60% 的评测用例,n, q 5000m 10000

        对于所有评测用例,2 ≤ n, q 10^51 m 3 × 10^51 ui , vi , xi , yi n,1 ≤ wi 10^6ui ≠ vixi ≠ yi。

题意分析:

        可以将本题理解为有n个点m条边的图,设备对应点,连接对应边。如果一条路径将两点连通,这条路径的稳定性为所有经过的边的最小值。两点间的稳定性为所有联通两点的路径的稳定性的最大值。

方法一:Kruscal最大生成树+LCA最近公共祖先

        分析后可以得知,代表两点间通信稳定性的那条路径,就是该图最大生成树中连通这两点的路径。

        什么是最大生成树呢?只保留原图中的一部分边,使得剩余的边仍能使整个图连通,并且不存在任意一条回路。这样的图是原图的生成树,n个点的图的生成树有n-1条边。最大生成树也就是在所有可能的生成树中,所有边权值和加起来最大的那棵生成树。由于不存在回路,生成树中任意两点间只有一条路径连通。

        为什么最大生成树中两点间的唯一路径就是原图中代表两点间通信稳定性最大那条路径呢?当使用kruscal算法生成一棵最大生成树时,我们将所有边按权值从大到小排序,如果边的两点还没有连通的话就把这条边加入最大生成树,否则继续判断下一条边。这样最后剩下的没有选择的边权值都是小于等于我们选择加入最大生成树的边的权值的,将其中任意一条加入最大生成树的话,经过这条边的路径的稳定性都等于这条新加入的小边。而题目中的定义要求的是两点间稳定性最大的那条路径,新路径都小于最大生成树中的路径,因此不能是代表两点间通信稳定性的路径。

        这样证明了最大生成树中的路径可以代表两点间最大稳定性的正确性,可做题的时候是怎么想到所有最大稳定性的路径组成的图其实就是最大生成树的呢?这个真的挺难想的。。。

        Kruscal最大生成树算法代码:

        需要用到并查集算法。由于本题点的数目很多,边的数目够小,存储图使用的是邻接链表结构。

int n,m,q;
struct line{//存储边的数据结构
	int x,y,val;
	line(int a,int b,int c){x=a,y=b,val=c;}
};
vector<line>link;//存储输入的所有边
vector<pair<int,int>>node[N];//存储图的邻接链表
int root[N];//并查集中代表某一元素根(祖先)的数组
int find(int x)//并查集算法
{
	if(root[x]==0||root[x]==x) return root[x]=x;
	return root[x]=find(root[x]);
}
void fk()//Kruscal算法
{
	int cnt=0;
	for(int i=0;i<link.size();i++)//在主函数中先将边从大到小排序
	{
		int x=link[i].x,y=link[i].y;
		if(find(x)==find(y)) continue;//边的两点没有连通的话才加入这条边
		node[x].push_back(make_pair(y,link[i].val));//将点y加入点x的链表
		node[y].push_back(make_pair(x,link[i].val));//将点x加入点y的链表
		cnt++;//已加入生成树中边的总数
		x=find(x),y=find(y);
		root[y]=x;//将x,y两点连通
		if(cnt>=n-1) break;//边数超过n-1,生成树已建成
	}
}

        构建完最大生成树,就是找了代表两点间最大稳定性的路径之后,接下来的任务就是根据输入求输入的两点间这条路径的稳定性。只有一条路径的稳定性遍历一遍就能简单求得,但是如何处理大量的查询呢?之后需要使用到lca算法,也就是最近公共祖先。在知道本题需要使用lca算法之后,我在网上学习了一个晚上,做了一道模版题,被博大精深的各种lca算法深深震撼到(折磨够)了,TAT。

        最近公共祖先,指一棵树中两个节点的所有公共祖先中距离两个点最近那个祖先。除了暴力最简单的方法是倍增求法。本题中我们建立的最大生成树还是用图存储的,只要将第一个节点当作根节点进行dfs遍历就能得到一棵树进而使用lca算法。求两点间路径的稳定性就是这两点到最近公共祖先的稳定性的最小值。

        简单说一下我一晚上学习lca的理解,倍增算法采用动态规划的思想,用dp[i][j]表示节点i往上第2^j个节点,这样dp[i][j]就等于dp[dp[i][j-1]][j-1],也就是往前2^j个节点等于往前2^(j-1)个节点的节点再往前2^(j-1)个节点,dp[i][0]也就是父节点。从根节点进行dfs遍历,遍历前将子节点的dp[i][0]指为父节点,遍历时对每个节点x循环执行dp[x][j]=dp[dp[x][j-1]][j-1]的过程,j从1增加直到2^j超过了x节点的深度,因此还需要定义一个深度数组存储每个节点的深度。

        dfs代码:

bool vis[N];//标志是否遍历过
int dep[N];//深度
int dp[N][20];//动态规划存储第i个节点往前2^j个节点
int val[N][20];//动态规划存储第i个节点到往前2^j个节点中最小的边权值即稳定性
void dfs(int x)//对x节点进行遍历
{
	for(int i=1;(1<<i)<dep[x];i++)//当2^i大于x的深度时结束状态转移
	{
		dp[x][i]=dp[dp[x][i-1]][i-1];//等于往前2^(i-1)个节点的节点往前2^(i-1)个节点
		val[x][i]=min(val[x][i-1],val[dp[x][i-1]][i-1]);
	}
	for(int i=0;i<node[x].size();i++)
	{
		int t=node[x][i].first;
		if(vis[t]) continue;
		vis[t]=1;//标志为已遍历
		dep[t]=dep[x]+1;//子节点深度+1
		dp[t][0]=x;//子节点的父节点表示为x
		val[t][0]=node[x][i].second;//子节点往前1个节点中最小边权值为与父节点相连的边权值
		dfs(t);//遍历子节点
	}
}

        在执行完动态规划的过程得到dp[i][j]后,当我们想要求x,y两点的最近公共祖先,假设x的深度更深,只需要先将x节点向上移动到与y节点同一深度,如果与y相同说明y是x的祖先;否则将两点同时向上遍历,这时步幅先从大步向小步循环,如果移动了2^j步后两点的祖先相同则将步数减少,因为这个祖先不一定是最近的公共祖先,这样当我们结束循环后x和y的上一个节点就是他们的最近公共祖先。

        由于本题要求的是稳定性,光找到lca是不够的,还需要计算点到lca的稳定性。在dfs进行动态规划的时候同步进行稳定性的动态规划,val[i][j]表示从i节点到往前2^j节点中所有边中的最小值,它的状态转移方程就是val[x][i]=min(val[x][i-1],val[dp[x][i-1]][i-1]),就是等于2^j个节点中前2^(j-1)节点稳定性和后2^(j-1)节点中稳定性的最小值。

        lca代码:

int lca(int x,int y)//求x和y的最近公共祖先lca
{
	int ans=inf;//设置稳定性ans的初始值	
	if(dep[x]<dep[y]) swap(x,y);//令x节点为较深的节点
	int k=log2(dep[x]-dep[y]);//移动步幅的最大值
	for(int i=k;i>=0;i--)//将x向上移动
	{
		if(dep[dp[x][i]]>=dep[y])//x移动2^j步后深度仍然不浅与y
		{
			ans=min(ans,val[x][i]);
			x=dp[x][i];
		}
	}//移动后x与y深度相同
	if(x==y) return ans;//y是x的祖先
	k=log2(dep[x]);//移动步幅的最大值
	for(int i=k;i>=0;i--)
	{
		if(dp[x][i]==dp[y][i]) continue;//移动2^i步后是x和y的祖先,跳过
		ans=min(ans,val[x][i]);
		ans=min(ans,val[y][i]);//移动2^i步后更新稳定性
		x=dp[x][i];
		y=dp[y][i];//移动x和y
	}//移动后x和y的上一个节点是lca
	ans=min(ans,val[x][0]);
	ans=min(ans,val[y][0]);//比较最后两条边,x和y到lca的边权值
	return ans;
}

        完整代码:

#include<bits/stdc++.h>
using namespace std;
const int N=100005;
const int inf=1000010;
int n,m,q;
struct line{//存储边的结构体
	int x,y,val;
	line(int a,int b,int c){x=a,y=b,val=c;}
};
vector<line>link;//存储输入的所有边
int root[N];//并查集中代表某一元素根(祖先)的数组
bool vis[N];//标志是否遍历过
int dep[N];//深度
int dp[N][20];//动态规划存储第i个节点往前2^j个节点
int val[N][20];//动态规划存储第i个节点到往前2^j个节点中最小的边权值即稳定性
bool cmp(line a,line b) {return a.val>b.val;}
vector<pair<int,int>>node[N];//存储图的邻接链表
int find(int x)//并查集算法
{
	if(root[x]==0||root[x]==x) return root[x]=x;
	return root[x]=find(root[x]);
}
void fk()//Kruscal算法
{
	int cnt=0;
	for(int i=0;i<link.size();i++)//在主函数中先将边从大到小排序
	{
		int x=link[i].x,y=link[i].y;
		if(find(x)==find(y)) continue;//边的两点没有连通的话才加入这条边
		node[x].push_back(make_pair(y,link[i].val));//将点y加入点x的链表
		node[y].push_back(make_pair(x,link[i].val));//将点x加入点y的链表
		cnt++;//已加入生成树中边的总数
		x=find(x),y=find(y);
		root[y]=x;//将x,y两点连通
		if(cnt>=n-1) break;//边数超过n-1,生成树已建成
	}
}
void dfs(int x)//对x节点进行遍历
{
	for(int i=1;(1<<i)<dep[x];i++)//当2^i大于x的深度时结束状态转移
	{
		dp[x][i]=dp[dp[x][i-1]][i-1];//等于往前2^(i-1)个节点的节点往前2^(i-1)个节点
		val[x][i]=min(val[x][i-1],val[dp[x][i-1]][i-1]);
	}
	for(int i=0;i<node[x].size();i++)//遍历所有子节点
	{
		int t=node[x][i].first;
		if(vis[t]) continue;
		vis[t]=1;//标志为已遍历
		dep[t]=dep[x]+1;//子节点深度+1
		dp[t][0]=x;//子节点的父节点表示为x
		val[t][0]=node[x][i].second;//子节点往前1个节点中最小边权值为与父节点相连的边权值
		dfs(t);//遍历子节点
	}
}
int lca(int x,int y)//求x和y的最近公共祖先lca
{
	int ans=inf;//设置稳定性ans的初始值	
	if(dep[x]<dep[y]) swap(x,y);//令x节点为较深的节点
	int k=log2(dep[x]-dep[y]);//移动步幅的最大值
	for(int i=k;i>=0;i--)//将x向上移动
	{
		if(dep[dp[x][i]]>=dep[y])//x移动2^j步后深度仍然不浅与y
		{
			ans=min(ans,val[x][i]);
			x=dp[x][i];
		}
	}//移动后x与y深度相同
	if(x==y) return ans;//y是x的祖先
	k=log2(dep[x]);//移动步幅的最大值
	for(int i=k;i>=0;i--)
	{
		if(dp[x][i]==dp[y][i]) continue;//移动2^i步后是x和y的祖先,跳过
		ans=min(ans,val[x][i]);
		ans=min(ans,val[y][i]);//移动2^i步后更新稳定性
		x=dp[x][i];
		y=dp[y][i];//移动x和y
	}//移动后x和y的上一个节点是lca
	ans=min(ans,val[x][0]);
	ans=min(ans,val[y][0]);//比较最后两条边,x和y到lca的边权值
	return ans;
}
int main()
{
	cin>>n>>m>>q;
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		cin>>x>>y>>z;
		link.push_back(line(x,y,z));
	}
	sort(link.begin(),link.end(),cmp);//从大到小排序所有边
	fk();//kruscal建立最大生成树
	for(int i=1;i<=n;i++)
	{
		if(vis[i]) continue;
		vis[i]=1;
		dep[i]=1;//根节点深度为1
		dfs(i);//动态规划求lca
	}
	for(int i=1;i<=q;i++)
	{
		int x,y;
		cin>>x>>y;
		if(find(x)!=find(y)) cout<<"-1"<<endl;//并查集检查是否两点是否连通
		else cout<<lca(x,y)<<endl;//lca算法求稳定性
	}
	return 0;
}

结语:

        本题难点有两个,一是判断出两点间的稳定性其实就是最大生成树中两点间的唯一通路,二是lca算法的书写,并且结合本题对稳定性的求法,在比赛的时候是看不到自己交的代码得了多少分的,如此复杂的代码也不好检查因此不好得分。为了做出本题我学习了一晚上lca的各种求法,不知不觉本文已经写了6200+字了,希望能够对你有帮助!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值