【按秩合并并查集/LCA】UVA 11354 Bond 详解

题目链接:https://vjudge.net/problem/SCU-3098(UVA好像挂了,所以其实是SCU上的,这就是vjudge香的地方了)

题目大意:

n个城市,通过m条道路连接,每条道路都有一个危险值,假设从城市s到城市t的一条通路为:道路a→道路b→道路c  则,这条通路的危险值被定义为道路a、b、c中危险值的最大值,现在将进行Q次询问:城市s到城市t的最小危险值为多少?

分析:

①最小生成树包含了任意两点间权值最小的通路,所以城市s到城市t的最小危险值可以由最小生成树求得。因为要进行多次询问,所以想到用一个数据结构来储存信息,也就是并查集,并查集其实会改变树的结构,但是这道题两点间的危险值是由通路上最大的危险值决定的,所以可以用,具体解释如下:

(这一部分略显无聊,且我解释的也不是很清楚,因此可以跳过)

以题目给的样例为例:

4 5
1 2 10
1 3 20
1 4 100
2 4 30
3 4 10

第一步 连接点1和 点2 (以1作为2的子节点)

第二步 连接点3和 点4 (以3作为4的子节点)

第三步 连接点1和 点3 (因为1的根节点是2,而3的根节点是4,所以在并查集中是让2作为 4的子节点)

本来应该连接1-3这条边的,但是其实并查集连接的是2-4这条边。

所以并查集其实会改变最小生成树的结构

但是这道题仍然可以使用并查集:

我们用一个数组danger来记录每个点到其父节点的危险值,则1-3间的danger值被赋到了2-4上

将最小生成树的点集V任意分出两部分V1和V2,则从集合V1和集合V2之间有且只有一条边可以连通

(去掉最小生成树中的任意一条边,连通分量都将增加)

那现在假设并查集要合并集合V1和集合V2了,以上图为例:

集合V1={1,2},集合V2={3,4}

则我们把要连接的1、3之间的边的权值赋给2、4也是无所谓的,因为集合V1和集合V2内部的点互相到达的答案没有改变,而从V1中的某一点到达V2中的某一点终归是要经过这条边的,所以在计算通路上的最大危险值时仍然考虑到了这条边

②求得最小生成树之后如何寻找两个结点通路上的危险最小值?

LCA:lowest common ancestor

最小生成树中任意两点s,t之间有且仅有一条通路,且这条通路必然是:点s→点s和点t的公共祖先→点t

朴素的求lca的做法:O(N)复杂度

从点s向上遍历到根节点t,用vis数组记录被访问过的点,然后从点t向上遍历,如果发现某点vis值为0,则该节点即二者的最小公共祖先

AC代码:

#include <iostream>
#include <cstdio>
#include <string.h>
#include <string>
#include <algorithm>
using namespace std;

typedef struct{
  int a,b,d;
}road;

road r[100005];

int pre[50005];
int rk[50005];
int danger[50005];   //记录结点i和其父节点间的危险值
int vis[50005];

void init(){
   for(int i=1;i<=50000;i++)
        pre[i]=i,rk[i]=0;
}

int root_find(int x){
    return pre[x]==x?x:root_find(pre[x]);
}

bool join(int x,int y,int road_danger){
  int rx=root_find(x);
  int ry=root_find(y);
  if(rx!=ry){
      if(rk[rx]<rk[ry])   pre[rx]=ry,danger[rx]=road_danger;
      else  if(rk[ry]<rk[rx])  pre[ry]=rx,danger[ry]=road_danger;
      else pre[rx]=ry,rk[ry]++,danger[rx]=road_danger;

  }
  else return 0;
  return 1;
}

bool cmp(road x,road y){
  return x.d<y.d;
}

int main()
{
    int N,M;
    while(~scanf("%d%d",&N,&M)){
          init();
          for(int i=0;i<M;i++)
               scanf("%d%d%d",&r[i].a,&r[i].b,&r[i].d);
          sort(r,r+M,cmp);
          int num=0;
          for(int i=0;i<M;i++){
               if(join(r[i].a,r[i].b,r[i].d))  num++;
               if(num==N-1) break;
          }
          int Q;
          scanf("%d",&Q);
          for(int i=1;i<=N;i++)  vis[i]=-1;
          while(Q--){
              int s,t;
              scanf("%d%d",&s,&t);
              int cost_s_to_root=0;
              vis[s]=0;
              int temp=s;
              while(pre[temp]!=temp){
                  cost_s_to_root=max(cost_s_to_root,danger[temp]);
                  temp=pre[temp];
                  vis[temp]=cost_s_to_root;
              }
              temp=t;
              int ans=0;
              while(vis[temp]==-1){
                   ans=max(ans,danger[temp]);
                   temp=pre[temp];
                   if(vis[temp]!=-1) {
                      ans=max(ans,vis[temp]);
                      break;
                   }
              }
              ans=max(ans,vis[temp]);
              printf("%d\n",ans);
              temp=s;
              while(pre[temp]!=temp)
                   vis[temp]=-1,temp=pre[temp];
              vis[temp]=-1;
          }
          printf("\n");
    }
    return 0;
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值