• P3008 [USACO11JAN]Roads and Planes G 拓扑排序+Dij


    题目描述

    Farmer John正在一个新的销售区域对他的牛奶销售方案进行调查。他想把牛奶送到T个城镇 (1 <= T <= 25,000),编号为1T。这些城镇之间通过R条道路 (1 <= R <= 50,000,编号为1到R) 和P条航线 (1 <= P <= 50,000,编号为1到P) 连接。每条道路i或者航线i连接城镇A_i (1 <= A_i <= T)到B_i (1 <= B_i <= T),花费为C_i。对于道路,0 <= C_i <= 10,000;然而航线的花费很神奇,花费C_i可能是负数(-10,000 <= C_i <= 10,000)。道路是双向的,可以从A_i到B_i,也可以从B_i到A_i,花费都是C_i。然而航线与之不同,只可以从A_i到B_i。事实上,由于最近恐怖主义太嚣张,为了社会和谐,出台 了一些政策保证:如果有一条航线可以从A_i到B_i,那么保证不可能通过一些道路和航线从B_i回到A_i。由于FJ的奶牛世界公认十分给力,他需要运送奶牛到每一个城镇。他想找到从发送中心城镇S(1 <= S <= T) 把奶牛送到每个城镇的最便宜的方案,或者知道这是不可能的。

    输入格式

    第1行:四个空格隔开的整数: T, R, P, and S 第2到R+1行:三个空格隔开的整数(表示一条道路):A_i, B_i 和 C_i 第R+2到R+P+1行:三个空格隔开的整数(表示一条航线):A_i, B_i 和 C_i

    输出格式

    第1到T行:从S到达城镇i的最小花费,如果不存在输出"NO PATH"。

    样例

    样例输入

    6 3 3 4

    1 2 5

    3 4 5

    5 6 10

    3 5 -100

    4 6 -100

    1 3 -10

    样例输入解释:一共六个城镇。在1-2,3-4,5-6之间有道路,花费分别是5,5,10。同时有三条航线:3->5, 4->6和1->3,花费分别是-100,-100,-10。FJ的中心城镇在城镇4。

    样例输出

    NO PATH

    NO PATH

    5

    0

    -95

    -100

    样例输出解释:FJ的奶牛从4号城镇开始,可以通过道路到达3号城镇。然后他们会通过航线达到5和6号城镇。 但是不可能到达1和2号城镇。

    分析

    正解

    我们可以发现题目中有两种边,一种是双向边,边权非负,另一种是单向边,边权可能为正

    如果我们用Dij直接去跑最短路,显然是不可以的,因为题目中有负权

    如果我们用SPFA 呢,显然会被卡掉

    所以我们考虑一下它所具有的的某种性质

    双向建的边是非负的,跑Dij是没有问题的,但是问题就是题目中还有单项负权边

    我们仔细读一下题就可以发现一个重要的性质:负权的边不会出现环

    那么我们就可以把强连通分量缩点,这样缩点之后的图就变成了一个有向无环图

    这样就可以在强连通分量内使用Dij,分量外使用拓扑排序更新答案

    代码

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #include<cmath>
    #include<queue>
    #include<vector>
    #define INF 0x3f3f3f3f
    using namespace std;
    const int maxn=150005;
    int t,r,p,s,head[maxn],tot=1,cnt;
    struct asd{
        int to,next,val;
    }b[maxn];
    void ad(int aa,int bb,int cc){
        b[tot].to=bb;
        b[tot].val=cc;
        b[tot].next=head[aa];
        head[aa]=tot++;
    }
    bool vis[maxn];
    int shuyu[maxn],dis[maxn];
    vector<int> jl[maxn];
    void dfs(int now){
        shuyu[now]=cnt,vis[now]=1,jl[cnt].push_back(now);
        for(int i=head[now];i!=-1;i=b[i].next){
            int u=b[i].to;
            if(vis[u])continue;
            dfs(u);
        }
    }
    struct jie{
        int num,jz;
        jie(int aa=0,int bb=0){
            num=aa,jz=bb;
        }
        bool operator < (const jie& A) const {
            return jz>A.jz;
        }
    };
    int ru[maxn];
    queue<int> q;
    priority_queue<jie> Q;
    void dij(){
        dis[s]=0;
        while(!q.empty()) {
            int xx=q.front();
            q.pop();
            for(int i=0;i<jl[xx].size();i++){
                Q.push(jie(jl[xx][i],dis[jl[xx][i]]));
            }
            while(!Q.empty()){
                int now = Q.top().num;
                Q.pop();
                if(vis[now]) continue;
                vis[now]=1;
                for(int i=head[now];i!=-1;i=b[i].next){
                    int u=b[i].to;
                    if(dis[u]>dis[now]+b[i].val){
                        dis[u]=dis[now]+b[i].val;
                        if(shuyu[now]==shuyu[u]) Q.push(jie(u,dis[u]));
                    }
                    if(shuyu[u]!=shuyu[now] && (--ru[shuyu[u]]==0)) q.push(shuyu[u]); 
                }
            }
        }
    }
    int main(){
        memset(head,-1,sizeof(head));
        scanf("%d%d%d%d",&t,&r,&p,&s);
        for(int i=1;i<=r;i++){
            int aa,bb,cc;
            scanf("%d%d%d",&aa,&bb,&cc);
            ad(aa,bb,cc);
            ad(bb,aa,cc);
        }
        for(int i=1;i<=t;i++){
            if(!vis[i]) cnt++,dfs(i);
        }
        for(int i=1;i<=p;i++){
            int aa,bb,cc;
            scanf("%d%d%d",&aa,&bb,&cc);
            ad(aa,bb,cc);
            ru[shuyu[bb]]++;
        }
        memset(vis,0,sizeof(vis));
        memset(dis,0x7f,sizeof(dis));
        for(int i=1;i<=cnt;i++) if(ru[i]==0) q.push(i);
        dij();
        for(int i=1;i<=t;i++){
            if(dis[i]>INF) printf("NO PATH
    ");
            else printf("%d
    ",dis[i]);
        }
        return 0;
    }
    

    水过

    当然,如果是在考试的时候,正解肯定是很难想到

    即使是想到了,又是缩点又是拓扑排序,又是Dij,分起来写还好点,如果合在一起,难免有点代码量

    而且,如果最后你调试了半天还没有暴力分高,岂不是很尴尬

    所以我们就尝试这用SPFA水一下

    其实SPFA本质上还是Bellman Ford的优化版本

    那么Bellman Ford是怎么运作的呢

    实际上它是使用全部的边对于起点到其他n-1个点的路径进行松弛,重复n-1次

    算法复杂度为O(VE)

    这样的复杂度几千条边还勉强可以接受,但是十万以上的边是肯定不可以的

    于是就有了优化版本SPFA,它的优化之处在哪里呢

    实际上我们来想一下,对于普通的Bellman Ford,其实有些边是根本松弛不动的

    所以我们优化的方向就是把肯定不能松弛其它节点的节点排除在外

    我们不去考虑哪些节点不能松弛其它节点,而是考虑哪些节点可以松弛其它节点

    很显然,只有当前已经松弛成功过的节点才有可能松弛其它的节点

    因此这时,我们就用一个栈来记录那些已经松弛成功的节点

    每次我们只要从栈中取出节点松弛就可以了

    那么时间复杂度呢?

    SPFA算法的时间复杂度是不可靠的,一般情况下为O(E),而在极限情况下也有可能达到Bellman-ford算法的复杂度,即O(V*E)

    其实,想要把SPFA卡掉还是很容易的,我们可以随便建一个网格图

    因为网格图中的边比较稠密,所以SPFA稍有不慎变会误入歧途,然后多走很多条边

    但是,网上也有很多关于SPFA的优化,其实就是想让普通的栈更接近优先队列

    因为如果你的栈里有很多个已经松弛过的节点,你肯定希望拿出一个值比较小的节点去松弛其他的节点

    因为这样一次松弛成功的几率比较大

    所以,我们尽量使维护的栈更接近一个优先队列,也就是权值小的先出栈

    目前比较常见的有两种方法,一种是把要插入的元素的值和栈顶元素比较,如果比栈顶元素小,那么就把这个元素放在占栈顶,否则放在栈底

    另一种方法就是把与队首元素比较改成了与队中元素的平均值比较,思路差不多

    对于这道题,我们可以用第一种方法水过

    #include<cstdio>
    #include<iostream>
    #include<queue>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    typedef long long ll;
    const int maxn=150005;
    struct asd{
    	ll from,to,next,val;
    }b[maxn];
    ll head[maxn],tot=1;
    void ad(ll aa,ll bb,ll cc){
    	b[tot].from=aa;
    	b[tot].to=bb;
    	b[tot].next=head[aa];
    	b[tot].val=cc;
    	head[aa]=tot++;
    }
    deque<ll> q;
    bool vis[maxn];
    ll dis[maxn];
    void SPFA(ll xx){
    	memset(dis,0x3f,sizeof(dis));
    	dis[xx]=0,vis[xx]=1;
    	q.push_back(xx);
    	while(!q.empty()){
    		ll now=q.front();
    		q.pop_front();
    		vis[now]=0;
    		for(ll i=head[now];i!=-1;i=b[i].next){
    			ll u=b[i].to;
    			if(dis[u]>dis[now]+b[i].val){
    				dis[u]=dis[now]+b[i].val;
    				if(!vis[u]){
    					if(!q.empty()&&dis[u]>=dis[q.front()]) q.push_back(u);
                        else q.push_front(u);
    					vis[u]=1;
    				}
    			}
    		}
    	}
    }
    int main(){
    	memset(head,-1,sizeof(head));
    	ll t,r,p,s;
    	scanf("%lld%lld%lld%lld",&t,&r,&p,&s);
    	for(ll i=1;i<=r;i++){
    		ll aa,bb,cc;
    		scanf("%lld%lld%lld",&aa,&bb,&cc);
    		ad(aa,bb,cc),ad(bb,aa,cc);
    	}
    	for(ll i=1;i<=p;i++){
    		ll aa,bb,cc;
    		scanf("%lld%lld%lld",&aa,&bb,&cc);
    		ad(aa,bb,cc);
    	}
    	SPFA(s);
    	for(ll i=1;i<=t;i++){
    		if(dis[i]==0x3f3f3f3f3f3f3f3f) printf("NO PATH
    ");
    		else printf("%lld
    ",dis[i]);
    	}
    	return 0;
    }
    

    其实大家还可以想一下,如果我们把用优先队列,改成用小根堆去维护,会变成什么样子

    或者是裸的Bellman Ford加点特判,又会是什么样子

    最后提醒大家一下,正权最短路尽量用DIJ

    负权最短路和最长路千万不要用DIJ,卡成0分都有可能

  • 相关阅读:
    HTML事件处理程序---内联onclick事件
    js的width函数
    了解跨站请求伪造CSRF
    离线百度地图
    GetOverlappedResult 函数
    OVERLAPPED 结构
    SetupDi系列函数
    Linux 各个命令的缩写原型
    Linux grep命令
    Linux if[......] then ......else...... fi
  • 原文地址:https://www.cnblogs.com/liuchanglc/p/12807954.html
Copyright © 2020-2023  润新知