道路与航线
链接
https://www.acwing.com/problem/content/description/344/
题目
农夫约翰正在一个新的销售区域对他的牛奶销售方案进行调查。
他想把牛奶送到T个城镇,编号为1~T。
这些城镇之间通过R条道路 (编号为1到R) 和P条航线 (编号为1到P) 连接。
每条道路 i 或者航线 i 连接城镇Ai到Bi,花费为Ci。
对于道路,0≤Ci≤10,000;然而航线的花费很神奇,花费Ci可能是负数(−10,000≤Ci≤10,000)。
道路是双向的,可以从Ai到Bi,也可以从Bi到Ai,花费都是Ci。
然而航线与之不同,只可以从Ai到Bi。
事实上,由于最近恐怖主义太嚣张,为了社会和谐,出台了一些政策:保证如果有一条航线可以从Ai到Bi,那么保证不可能通过一些道路和航线从Bi回到Ai。
由于约翰的奶牛世界公认十分给力,他需要运送奶牛到每一个城镇。
他想找到从发送中心城镇S把奶牛送到每个城镇的最便宜的方案。
输入格式
第一行包含四个整数T,R,P,S。
接下来R行,每行包含三个整数(表示一个道路)Ai,Bi,Ci。
接下来P行,每行包含三个整数(表示一条航线)Ai,Bi,Ci。
输出格式
第1..T行:第i行输出从S到达城镇i的最小花费,如果不存在,则输出“NO PATH”。
数据范围
\(1≤T≤25000,
1≤R,P≤50000,
1≤Ai,Bi,S≤T\)
输入样例:
6 3 3 4
1 2 5
3 4 5
5 6 10
3 5 -100
4 6 -100
1 3 -10
输出样例:
NO PATH
NO PATH
5
0
-95
-100
思路
题目背景很复杂,但是意思还是很简单的,一个包含单/双向边有负权边的图,并保证:如果有一条航线可以从Ai到Bi,那么保证不可能通过一些道路和航线从Bi回到Ai,求起点为S的单源最短路。有负权只能用SPFA,但这题会卡掉一般的SPFA算法。
两种优化:
1.SLF优化
使用双端队列,如果一个待加入节点的花费小于队头点的花费,则加入对头,否则加入队尾。 这种优化非常好写,但只是能玄学优化,其实还是很容易被卡掉的。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=25010,M=50000;
typedef pair<int,int> PII;
int h[N],cnt,inq[N],dis[N];
struct eg{
int v,c,nex;
}e[M*3];
void addedge(int u,int v,int c){
e[++cnt]=(eg){v,c,h[u]};
h[u]=cnt;
}
void spfa(int st){
deque<PII> q;
memset(dis,0x3f,sizeof dis);
dis[st]=0;inq[st]=1;
q.push_back({st,0});
while(!q.empty()){
int u=q.front().first;q.pop_front();
inq[u]=0;
for(int i=h[u];~i;i=e[i].nex){
int v=e[i].v,c=e[i].c;
if(dis[v]>dis[u]+c){
dis[v]=dis[u]+c;
if(!inq[v]){
inq[v]=1;
if(q.empty()){
q.push_back({v,dis[v]});
}
else if(q.front().second>=dis[v]){
q.push_front({v,dis[v]});
}
else q.push_back({v,dis[v]});
}
}
}
}
}
int main(){
memset(h,-1,sizeof h);
int n,r,p,st;
scanf("%d%d%d%d",&n,&r,&p,&st);
for(int i=1;i<=r;++i){
int u,v,c;
scanf("%d%d%d",&u,&v,&c);
addedge(u,v,c);
addedge(v,u,c);
}
for(int i=1;i<=p;++i){
int u,v,c;
scanf("%d%d%d",&u,&v,&c);
addedge(u,v,c);
}
spfa(st);
for(int i=1;i<=n;++i){
if(dis[i]==0x3f3f3f3f) printf("NO PATH\n");
else printf("%d\n",dis[i]);
}
return 0;
}
2.拓扑序优化
题目给了一个重要的提醒,即:A通过单向边到B,不能通过其他路径从B到A。把双向边连接的点当作块,单向边连接的每个块,只看块和单向边的图具有拓扑序。
对于块内的点用迪杰斯特拉求最短路,块之间的点用拓扑序,每个块跑迪杰斯特拉之前保证被所有可到达的块更新过了。
wa在了两个操作上:
1.不可达点的距离d>INF/2即可,因为有负权边,正无穷也可以更新正无穷。
2.跑迪杰斯特拉时,只有在同一块的点才加入堆中,更新其他块的入度,如果块的入度等于0,加入块的编号到拓扑队列中。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=25010,M=150010,INF=0x3f3f3f3f;
typedef pair<int,int> PII;
int h[N],e[M],w[M],nex[M],idx;
int d[N],st[N],q[N];
int id[N],ind[N],bcnt,hh,tt;
vector<int> blocks[N];
int n,mr,mp,S;
void add(int u,int v,int t){
e[idx]=v;
w[idx]=t;
nex[idx]=h[u];
h[u]=idx++;
}
void dij(int nid){
priority_queue<PII> heap;
for(int i=0;i<(int)blocks[nid].size();++i){
heap.push({-d[blocks[nid][i]],blocks[nid][i]});
}
while(heap.size()){
int u=heap.top().second;
heap.pop();
if(st[u]) continue;
st[u]=1;
for(int i=h[u];~i;i=nex[i]){
int v=e[i];
if(d[v]>d[u]+w[i]){
d[v]=d[u]+w[i];
if(id[v]==id[u]){
heap.push({-d[v],v});
}
}
if(id[v]!=id[u]){
ind[id[v]]--;
if(ind[id[v]]==0){
q[tt++]=id[v];
}
}
}
}
}
void top_sort(){
memset(d,INF,sizeof d);
d[S]=0;
for(int i=1;i<=bcnt;++i){
if(!ind[i]) q[tt++]=i;
}
while(hh!=tt){
dij(q[hh++]);
}
}
void dfs(int u){
id[u]=bcnt;
blocks[bcnt].push_back(u);
for(int i=h[u];~i;i=nex[i]){
int v=e[i];
if(!id[v]) dfs(v);
}
}
int main(){
memset(h,-1,sizeof h);
cin>>n>>mr>>mp>>S;
while(mr--){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
for(int i=1;i<=n;++i){
if(!id[i]){
bcnt++;
dfs(i);
}
}
while(mp--){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
ind[id[v]]++;
}
top_sort();
for(int i=1;i<=n;++i){
if(d[i]>INF/2) cout<<"NO PATH\n";
else cout<<d[i]<<endl;
}
return 0;
}