hdu2874-Connections between cities (LCA/离线tarjan)

1 篇文章 0 订阅

Connections between cities

Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 6783    Accepted Submission(s): 1766


Problem Description
After World War X, a lot of cities have been seriously damaged, and we need to rebuild those cities. However, some materials needed can only be produced in certain places. So we need to transport these materials from city to city. For most of roads had been totally destroyed during the war, there might be no path between two cities, no circle exists as well.
Now, your task comes. After giving you the condition of the roads, we want to know if there exists a path between any two cities. If the answer is yes, output the shortest path between them.
 

Input
Input consists of multiple problem instances.For each instance, first line contains three integers n, m and c, 2<=n<=10000, 0<=m<10000, 1<=c<=1000000. n represents the number of cities numbered from 1 to n. Following m lines, each line has three integers i, j and k, represent a road between city i and city j, with length k. Last c lines, two integers i, j each line, indicates a query of city i and city j.
 

Output
For each problem instance, one line for each query. If no path between two cities, output “Not connected”, otherwise output the length of the shortest path between them.
 

Sample Input
  
  
5 3 2 1 3 2 2 4 3 5 2 3 1 4 4 5
 

Sample Output
  
  
Not connected 6
 

之前没做出来,上网找的题解用vector动态存储,据说因为15年杭电换了服务器MTE了,但代码意思很简洁明了。值得参考:http://blog.csdn.net/a601025382s/article/details/10805869

之后改用静态存储,仍然MTE了几次。参考了许多题解,找到了一发没超的。

借鉴一下大神博客里LCA的说明:LCA算法
对于该问题,最容易想到的算法是分别从节点u和v回溯到根节点,获取u和v到根节点的路径P1,P2,其中P1和P2可以看成两条单链表,这就转换成常见的一道面试题:【判断两个单链表是否相交,如果相交,给出相交的第一个点。】。该算法总的复杂度是O(n)(其中n是树节点个数)。
        本节介绍了两种比较高效的算法解决这个问题,其中一个是在线算法(DFS+ST),另一个是离线算法(Tarjan算法)。
在线算法DFS+ST描述(思想是:将树看成一个无向图,u和v的公共祖先一定在u与v之间的最短路径上):
(1)DFS:从树T的根开始,进行深度优先遍历(将树T看成一个无向图),并记录下每次到达的顶点。第一个的结点是root(T),每经过一条边都记录它的端点。由于每条边恰好经过2次,因此一共记录了2n-1个结点,用E[1, ... , 2n-1]来表示。
(2)计算R:用R[i]表示E数组中第一个值为i的元素下标,即如果R[u] < R[v]时,DFS访问的顺序是E[R[u], R[u]+1, …, R[v]]。虽然其中包含u的后代,但深度最小的还是u与v的公共祖先。
(3)RMQ:当R[u] ≥ R[v]时,LCA[T, u, v] = RMQ(L, R[v], R[u]);否则LCA[T, u, v] = RMQ(L, R[u], R[v]),计算RMQ。
由于RMQ中使用的ST算法是在线算法,所以这个算法也是在线算法。
【举例说明】
T=<V,E>,其中V={A,B,C,D,E,F,G},E={AB,AC,BD,BE,EF,EG},且A为树根。则图T的DFS结果为:A->B->D->B->E->F->E->G->E->B->A->C->A,要求D和G的最近公共祖先, 则LCA[T, D, G] = RMQ(L, R[D], R[G])= RMQ(L, 3, 8),L中第4到7个元素的深度分别为:1,2,3,3,则深度最小的是B。
离线算法(Tarjan算法)描述:
        所谓离线算法,是指首先读入所有的询问(求一次LCA叫做一次询问),然后重新组织查询处理顺序以便得到更高效的处理方法。Tarjan算法是一个常见的用于解决LCA问题的离线算法,它结合了深度优先遍历和并查集,整个算法为线性处理时间。
        Tarjan算法是基于并查集的,利用并查集优越的时空复杂度,可以实现LCA问题的O(n+Q)算法,这里Q表示询问 的次数。更多关于并查集的资料,可阅读这篇文章:《数据结构之并查集》。
        同上一个算法一样,Tarjan算法也要用到深度优先搜索,算法大体流程如下:对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。
算法伪代码:
LCA(u)
{
Make-Set(u)
ancestor[Find-Set(u)]=u
对于u的每一个孩子v
{
LCA(v)
Union(u,v)
ancestor[Find-Set(u)]=u
}
checked[u]=true
对于每个(u,v)属于P // (u,v)是被询问的点对
{
if checked[v]=true

then {
回答u和v的最近公共祖先为ancestor[Find-Set(v)]
}
      }
}


按照以上思想的代码和备注:

#include <cstdio>
#include <algorithm>
#include <string>
#include <cstring>
#include <stack>
#include <cmath>
#include <queue>
#include <list>
#include <cstdlib>
#include <vector>
#include <set>
#include <map>
#include <sstream>
#include <iostream>
#include <stack>
using namespace std;

int n,m,q;
struct Node
{
    int e,xia,val;
}e1[20001];
struct Node2
{
    int e,xia;
}e2[2000001];
int d1,d2,head1[10001],head2[10001];
bool vis[10001];//表示该节点是否被检查过
int fat[10001],dis[10001],ans[1000001];//所有答案记录在ans数组里。fat数组表示父节点。dis数组表示节点到根节点的距离。

void add1(int s,int e,int val){
    e1[d1].e=e,e1[d1].val=val,e1[d1].xia=head1[s];//表示d1个输入组合中,由s点->e点。head1[s]表示对点的一个索引,因为hear1[s]=d1,与e1[d1]的d1是相同的。
    head1[s]=d1++;//此处s表示的点,但索引的痕迹是d1。通过d1才能索引至e1[d1].e,实现s点->e点的指向。
//仔细思考可以发现xia表示的是s点的其他子树,当s->e时,s无除e以外的子树时,毫无疑问xia就-1,否则,xia为该子树的d1。
}

void add2(int s,int e){
    e2[d2].e=e,e2[d2].xia=head2[s];
    head2[s]=d2++;
}

int find(int num){
    if(fat[num] == num)return num;
    return fat[num]=find(fat[num]);
}

void Union(int s,int e){
    fat[e]=s;
}

void tarjan(int s){
    vis[s]=true;
    for(int i=head1[s];i != -1;i=e1[i].xia){//开始对s点进行深度遍历,如果s点的子节点未被检查过,则可相应计算出子节点dis,并递归继续进行深度遍历,同时将s的子节点的父节点表示为s。
        if(!vis[e1[i].e]){
            dis[e1[i].e]=dis[s]+e1[i].val;
            tarjan(e1[i].e);
            Union(s,e1[i].e);
        }
    }//从此循环外层随着xia遍历,内层递归与s点连接的点可看出为深度遍历
    for(int i=head2[s];i != -1 ;i=e2[i].xia){
        if(vis[e2[i].e]){
            if(dis[e2[i].e] != -1){//此处需注意,在main中进入trajan之前会将dis清空,若不为-1,则必在同一子树内。
                ans[i/2]=dis[s]+dis[e2[i].e]-2*dis[find(e2[i].e)];
            }
            else ans[i/2]=-1;
        }
    }//此循环发生在s点的深度遍历之后。s点深度遍历已经将s的子树节点计算合并完毕,之后在此遍历中处理询问事项。已知此循环中任意相连两点均为询问内容,只需考虑这两点是否在同一子树中。若存在同一子树中,分别已知两点到根节点距离,相加后减去最近公共父节点即可。若不在同一子树中,自然无法连接。
}

int main(){
    int x,y,z;
    while(~scanf("%d %d %d",&n,&m,&q)){
        d1=d2=0;
        memset(head1,-1,sizeof(head1));
        memset(head2,-1,sizeof(head2));
        for(int i=0;i<m;i++){
            scanf("%d %d %d",&x,&y,&z);
            add1(x,y,z);
            add1(y,x,z);
        }
        for(int i=0;i<q;i++){
            scanf("%d %d",&x,&y);
            add2(x,y);
            add2(y,x);
        }
        memset(vis,false,sizeof(vis));
        for(int i=1;i<=n;i++)fat[i]=i;
        for(int i=1;i<=n;i++){
            if(!vis[i])
            {
                memset(dis,-1,sizeof(dis));
                dis[i]=0;
                tarjan(i);
            }
        }
        for(int i=0;i<q;i++){
            if(ans[i] == -1)puts("Not connected");
            else printf("%d\n",ans[i]);
        }
    }
    return 0;
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值