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分都有可能