P1111 修复公路
题目背景
A地区在地震过后,连接所有村庄的公路都造成了损坏而无法通车。政府派人修复这些公路。
题目描述
给出A地区的村庄数N,和公路数M,公路是双向的。并告诉你每条公路的连着哪两个村庄,并告诉你什么时候能修完这条公路。问最早什么时候任意两个村庄能够通车,即最早什么时候任意两条村庄都存在至少一条修复完成的道路(可以由多条公路连成一条道路)
输入输出格式
输入格式:第1行两个正整数N,M
下面M行,每行3个正整数x, y, t,告诉你这条公路连着x,y两个村庄,在时间t时能修复完成这条公路。
输出格式:如果全部公路修复完毕仍然存在两个村庄无法通车,则输出-1,否则输出最早什么时候任意两个村庄能够通车。
输入输出样例
说明
N<=1000,M<=100000
x<=N,y<=N,t<=100000
=========================================
前面分析有点乱,重点还是关注黑体部分以及后面的总结:
拿到题目,明显最小生成树,但应该用哪种解法,刚开始考虑Prim算法,但发现很多问题:
补充:该题所有道路可同时修复,所以要输出最小生成树中最大的那条边;
①、Prim算法的主要思想就是求解出每个点到最近白点的距离,然后根据这些最短距离连接起所有白点;
②、Pirm局限性:无法判断该图是否连通;判断图的连通性,我们可以想到如果最后去重后得到的边数>=n-1,则该图可构成最小生成树,而对于Prim算法,若不连通的图中有一部分形成环,则该条件不再试用。而对于Kruskal来说,见下面......
③、如果硬要做的话(无法摆脱Prim 的局限性):该题有不连通的两点,而且两点间可能有多条线路,所以对于图的构建来说,要有g[i][j]=g[j][i]=min(多条路中最短的一条,去掉耗时长的路),且g初始化为一个极大值。
而对于Kruskal来说,其核心思想是判断当前遍历的一条边是否可以加入到最小生成树中,能加入最小生成树的边,所连接的都是两个不连通的区域,即加入边的过程也是打通两个不连通区域的过程,所以如果生成树中的边数k能够到达n-1,则说明该图可连通(当前边可连接起n个孤立区域\点),如果到达不了n-1,说明该图不能连通。
另外在判断是否可入树时,我们要考虑该边所连两点是否有相同祖先,若没有,则需要合并两点(合并后具有共同祖先),这样也就能保证连接该两点的其他边不会再进入最小生成树中。
所以Prim的90分代码:
#include<bits/stdc++.h>
using namespace std;
int g[1001][1001];
bool flag[1001][1001];
bool u[1001];
int minn[1001];
int main()
{
//freopen("1.txt","r",stdin);
memset(g,0x7f,sizeof(g));//important;
memset(minn,0x7f,sizeof(minn));
memset(u,1,sizeof(u));
int n,m,tm;
cin>>n>>m;
tm=m;//wrong
if(tm<n-1)
{
cout<<"-1"<<endl;
return 0;
}
for(int i=1;i<=m;++i)
{
int x,y,z;
cin>>x>>y>>z;
if(flag[x][y]==0||flag[y][x]==0)
{
flag[x][y]=1;
flag[y][x]=1;
g[x][y]=z;//wrong
g[y][x]=z;
// cout<<x<<" "<<y<<endl;
}
else
{
tm--;
if(tm<n-1)
{
cout<<"-1"<<endl;
return 0;
}
if(z<g[x][y])g[x][y]=z,g[y][x]=z;
}
}
m=tm;
//cout<<g[2][3]<<endl;
//cout<<m<<endl;
minn[1]=0;
for(int i=1;i<=n;++i)
{
int k=0;
for(int j=1;j<=n;++j)
if(u[j]&&minn[j]<minn[k])k=j;
u[k]=0;
//cout<<"k="<<k<<endl;
for(int j=1;j<=n;++j)
{
if(u[j]&&g[k][j]<minn[j])minn[j]=g[k][j];
}
}
int ans=0;
for(int i=1;i<=n;++i){
if(minn[i]>ans)ans=minn[i];//wrong
//cout<<minn[i]<<endl ;
}
cout<<ans<<endl;
return 0;
}
Kruskal的AC代码:
#include<bits/stdc++.h>
using namespace std;
struct point
{
int l,r,v;
}a[1000001];
int fat[1001];
bool cmp(point x,point y)
{
return x.v<y.v;
}
int father(int x)
{
while(x!=fat[x])x=fat[x];//非递归实现 查找 功能;
return x;
}
void unionn(int x,int y)
{
fat[father(x)]=father(y);
}
int main()
{
//freopen("1.txt","r",stdin);
int m,n;
cin>>n>>m;
for(int i=1;i<=m;++i)
{
int x,y,z;
cin>>x>>y>>z;
a[i].l=x;
a[i].r=y;
a[i].v=z;
}
for(int i=1;i<=n;++i)fat[i]=i;
sort(a+1,a+1+m,cmp);
int k=0,ans;
bool flag=1;
for(int i=1;i<=m;++i)
{
if(father(a[i].l)!=father(a[i].r))
{
k++;
unionn(a[i].l,a[i].r);
}
if(k==n-1)
{
ans=a[i].v;
flag=0;//判断图是否可连通;
break;
}
}
if(flag)cout<<"-1"<<endl;
else cout<<ans<<endl;
return 0;
}
最后总结:
目前来看,最小生成树类题目的输入数据有三种给出形式:
1、一种是给出图中所有点的指向(有向图);(例如信息学P511页最优布线问题)
注意点:
Prim算法中要将 邻接矩阵 初始化为极大值,以防止出现有不相连点的情况,同时注意不能构成最小生成树的情况(所以一般不用Prim吧);
Kruskal:无注意点;每有一条单向边,总边数m+1。
2、另一种是只给出能相连的点之间的关系(和 1 类似);(如本题)
Prim算法中上同......
Kruskal:无注意点。
3、还有一种是给出n个孤立点(任意一点都能与该点外的任意一点相连)。
(如 洛谷 公路修建:https://www.luogu.org/problemnew/show/P1265)
Prim中初不初始化无所谓。
Kruskal:无注意点。
(重点)综上所述:
边数较多,用Prim,但需保证所给图能够生成最小生
成树(一般来说比如题目给出好多孤立点);边数较少,用
Kruscal,“一边一m++”,其他暂无注意点。
The end;