洛谷P3008

题目大意:求源点s到各个点的单源最短路径

由于图中存在负权边,不能用dij;结合数据范围,spfa可能会被卡O(km),可以用SLF、LLL优化,但不一定能过。

解法一:正解(可能是正解):

因为道路之间是双向路径、航线是单向路径且无环(保证如果有一条航线可以从 Ai到 Bi,那么保证不可能通过一些道路和航线从 Bi回到 Ai。)




首先我们根据题意,其实需要先构思清楚到底整个图的结构是怎么样的,我们可以看到,道路都是无向边,故道路里面的点肯定彼此可达的,是联通图。

而航线是单向边且可以有负权边,这道题有一个很重要的性质-> 一旦满足 A 可达 B :A -> B,一定不存在任何通路使得 B 可达 A : B -> A

综合可得——》 有道路的连通块一定彼此可达,且一旦这个连通块中的点 A 向别的点 B 联通一条航线,那么A和B势必不在一个连通块【即没有道路可达】。联向的点 B 所在的连通块【单点也可以看作一个连通块】势必连通块的点没有任何一个点产生一条到 A所在的连通块的航线【即没有 B的连通块到 A的连通块的航线】。

故立马可抽象出,各个连通块抽象成一个点,且这些抽象出来的点构成拓扑图。且每个连通块内部的点,没有负权边,可以使用Dijkstra,而连通块之间可以使用 bfs【拓扑排序】。




可以认为线路形成的情况:
  • 是由双向道路相连的点构成了连通块,由单向路(航线)连接连通块与连通块之间。

  • 把每一个连通块看成一个大点,即一个有向无环图的最短路,可结合拓扑序列进行求解。

在每一个连通块内,由于都是正权边,可用dijkstra求最短路, 在结合拓扑序对块进行处理

实现思路
  1. 读入所有的道路(双向),在处理每个点时,用id[i]记录每个点属于哪个联通块,同时记录这个连通块中有哪些点。

  2. 读入所有的航线,同时维护一下航线到达站的入度值,便于拓扑排序使用

  3. 进行拓扑排序,遍历每个连通块,将入度为0的点入放入队列中

  4. 对于队列中放入的连通块,对连通块中所有的点做dijkstra

  5. dij的处理,将这个连通块中所有点的放进堆中,取出队头,弹出,遍历其所有出边,判断这个点是否属于与队头节点属于同一个连通块。如果不属于,则出边所在的点所处的连通块入度减1,当所在连通块入度为0时,入队;属于同一个连通块,则将其放入堆中,同时更新dist的值。

    #include<bits/stdc++.h>
    #define x first
    #define y second
    using namespace std;
    //无向边 50000*2+有向边50000 
    const int N=25010,M=150010,INF=0x3f3f3f3f;
    typedef pair<int,int> PII;
     
    int n,mr,mp,s; 
    int id[N],dist[N],din[N];//id记录当前这个点数与哪个连通块 
    vector<int> block[N];//记录这个连通块里面有哪些点 
    bool st[N];//判重数组 
    int bcnt=0;//连通块的个数 
    queue<int>q;
    
    int h[N],e[M],w[M],ne[M],idx;
    void add(int a,int b,int c)
    {
    	e[idx]=b;
    	w[idx]=c;
    	ne[idx]=h[a];
    	h[a]=idx++;
    }
    
    void dfs(int u, int bid)
    {
        id[u] = bid, block[bid].push_back(u);
    
        for (int i = h[u]; ~i; i = ne[i])
        {
            int j = e[i];
            if (!id[j])
                dfs(j, bid);
        }
    }
    
    void dij(int bid)
    {
    	//将当前所有连通块中的点都插入到堆中
    	priority_queue<PII,vector<PII>,greater<PII> >heap;
    	//将这个连通块中所有的点放进堆中 
    	for(auto u:block[bid])   heap.push({dist[u],u});
    	
    	while(heap.size())
    	{
    		auto t=heap.top();
    		heap.pop();
    		
    		int ver=t.y,distance=t.x;
    		if(st[ver])  continue;
    		st[ver]=true;
    		
    		for(int i=h[ver];~i;i=ne[i])
    		{
    			int j=e[i];	
    			//如果不属于同一个连通块,就是不同连通块间的边 
    			if(id[j]!=id[ver] && --din[id[j]]==0)
    			{
    				q.push(id[j]);
    			}
    			if(dist[j]>dist[ver]+w[i])
    			{
    				dist[j]=dist[ver]+w[i];
    				if(id[j]==id[ver])//是同一个连通块就放进堆中 
    				{
    					heap.push({dist[j],j});
    				}
    			}
    		
    		}
    		
    	}
    	
     } 
    
    
    void topsort()
    {
    	//初始化 
    	memset(dist,0x3f,sizeof dist);
    	dist[s]=0;
    	//遍历连通块 
    	for(int i=1;i<=bcnt;i++)
    	{
    		if(!din[i])
    		q.push(i);
    		
    	}
    	while(q.size())
    	{
    		int t=q.front();
    		q.pop();
    		dij(t);
    	 } 
    }
    int main()
    {
    	scanf("%d%d%d%d",&n,&mr,&mp,&s);
    	
    	memset(h,-1,sizeof h);
    	//
    	while(mr--)
    	{
    		int a,b,c;
    		scanf("%d%d%d",&a,&b,&c);
    		add(a,b,c);
    		add(b,a,c);		 
    	 } 
    	 //暴搜一个连通块
    
    	 for(int i=1;i<=n;i++)
    	 {
    	 	if(!id[i])
    	 	{
    	 		dfs(i,++bcnt);
    		 }
    	  } 
    	  
    	  while(mp--)
    	  {
    	  	int a,b,c;
    	  	scanf("%d%d%d",&a,&b,&c);
    		add(a,b,c);
    	  	din[id[b]]++;//b所在的连通块的入度++ 
    	  }
    	  
    	  topsort(); 
    	  
        for(int i=1;i<=n;i++)
    	  {
    	  	   //无穷大的边判断就是大于一个非常大的数即可 
    	  	   if(dist[i]>INF/2)    puts("NO PATH");
    	  	   else printf("%d\n",dist[i]); 
    	  }
    	  return 0;
    }

    解法二:SLF优化SPFA

  • SPFA 的主要操作是把变化的点入队。这些点差不多是随机入队的,如果改变入队和出队的顺序,是否能加快所有点的最短路径计算?下面给出两个小优化。
    1)   入队的优化:SLF(Small Label First)
           需要使用STL的双端队列 deque。队头出队后,需要把它的有变化的邻居入队。把入队的点u 与新队头v进行比较,如果 dis[u]<dis[v],将 u 插入队头,否则插入队尾。这个优化使队列弹出的队头都是路径较短的点,从而加快所有点的最短路径的计算。
    2)   出队的优化:LLL(Large Label Last)
           计算队列中所有点的距离的平均值 x,每次选一个小于  的点出队。具体操作是:如果队头 的 dis[u]>x,把 弹出然后放到队尾去,然后继续检查新的队头 ,直到找到一个 dis[z]<x 为止。这个优化也是先处理了更短的点。

  • SLF 和LLL可以一起使用。

    #include <bits/stdc++.h>
    using namespace std;
    const int N=400000 +100;
    int head[N],ver[N],Next[N],edge[N],vis[N],dis[N],tot;
    void add_edge(int a,int b,int c)
    {
        edge[tot]=b;
        ver[tot]=c;
        Next[tot]=head[a];
        head[a]=tot++;
    }
    void spfa(int s)
    {
        memset(dis,0x3f,sizeof(dis));
        memset(vis,false,sizeof(vis));
        deque<int> q;
        dis[s]=0;
        vis[s]=true;
        q.push_back(s);
        while(q.size())
        {
            int now=q.front();
            vis[now]=false;
            q.pop_front();
            for(int i=head[now]; ~i; i=Next[i])
            {
                int j=edge[i];
                if (dis[j]>dis[now]+ver[i])
                {
                    dis[j]=dis[now]+ver[i];
                    if (!vis[j])
                    {
                        vis[j]=true;
                        if (q.size() && dis[j]<dis[q.front()])
                            q.push_front(j);
                        else
                            q.push_back(j);
                    }
                }
            }
        }
    }
    int main()
    {
        int t,r,p,s,x,y,z;
        ios::sync_with_stdio(false);
        cin.tie(0);
        cin>>t>>r>>p>>s;
        memset(head,-1,sizeof(head));
        for(int i=1; i<=r; i++)
        {
            cin>>x>>y>>z;
            add_edge(x,y,z);
            add_edge(y,x,z);
        }
        for(int i=1; i<=p; i++)
        {
            cin>>x>>y>>z;
            add_edge(x,y,z);
        }
        spfa(s);
        for(int i=1; i<=t; i++)
        {
            if (dis[i]==0x3f3f3f3f)
                cout<<"NO PATH"<<endl;
            else
                cout<<dis[i]<<endl;
        }
        return 0;
    }
    

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值