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\n");
        else printf("%d\n",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\n");
		else printf("%lld\n",dis[i]);
	}
	return 0;
}

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

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

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

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

posted @ 2020-04-30 11:55  liuchanglc  阅读(215)  评论(1编辑  收藏  举报