题目描述

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

P3008 [USACO11JAN]Roads and Planes G 拓扑排序+Dij的更多相关文章

  1. P3008 [USACO11JAN]Roads and Planes G (最短路+拓扑排序)

    该最短路可不同于平时简单的最短路模板. 这道题一看就知道用SPFA,但是众所周知,USACO要卡spfa,所以要用更快的算法. 单向边不构成环,双向边都是非负的,所以可以将图分成若干个连通块(内部只有 ...

  2. [USACO11JAN]Roads and Planes G【缩点+Dij+拓补排序】

    题目 Farmer John正在一个新的销售区域对他的牛奶销售方案进行调查.他想把牛奶送到T个城镇 (1 <= T <= 25,000),编号为1T.这些城镇之间通过R条道路 (1 < ...

  3. 【图论】USACO11JAN Roads and Planes G

    题目内容 洛谷链接 Farmer John正在一个新的销售区域对他的牛奶销售方案进行调查.他想把牛奶送到\(T\)个城镇 (\(1 <= T <= 25,000\)),编号为\(1\)到\ ...

  4. [USACO11JAN]Roads and Planes

    嘟嘟嘟 这道题他会卡spfa,不过据说加SLF优化后能过,但还是讲讲正解吧. 题中有很关键的一句,就是无向边都是正的,只有单向边可能会有负的.当把整个图缩点后,有向边只会连接在每一个联通块之间(因为图 ...

  5. Wannafly挑战赛2 D.Delete(拓扑排序 + dij预处理 + 线段树维护最小值)

    题目链接  D.Delete 考虑到原图是个DAG,于是我们可以求出每个点的拓扑序. 然后预处理出起点到每个点的最短路$ds[u]$, 和所有边反向之后从终点出发到每个点的最短路$dt[u]$. 令点 ...

  6. 拓扑排序(三)之 Java详解

    前面分别介绍了拓扑排序的C和C++实现,本文通过Java实现拓扑排序. 目录 1. 拓扑排序介绍 2. 拓扑排序的算法图解 3. 拓扑排序的代码说明 4. 拓扑排序的完整源码和测试程序 转载请注明出处 ...

  7. 拓扑排序(二)之 C++详解

    本章是通过C++实现拓扑排序. 目录 1. 拓扑排序介绍 2. 拓扑排序的算法图解 3. 拓扑排序的代码说明 4. 拓扑排序的完整源码和测试程序 转载请注明出处:http://www.cnblogs. ...

  8. 拓扑排序(一)之 C语言详解

    本章介绍图的拓扑排序.和以往一样,本文会先对拓扑排序的理论知识进行介绍,然后给出C语言的实现.后续再分别给出C++和Java版本的实现. 目录 1. 拓扑排序介绍 2. 拓扑排序的算法图解 3. 拓扑 ...

  9. P3008 [USACO11JAN]道路和飞机Roads and Planes

    P3008 [USACO11JAN]道路和飞机Roads and Planes Dijkstra+Tarjan 因为题目有特殊限制所以不用担心负权的问题 但是朴素的Dijkstra就算用堆优化,也显然 ...

随机推荐

  1. 阿里云专有网络配置以及交换机配置+ip、子网掩码、ip网段计算原理讲解

    在阿里云上购买ECS或者其他服务,如redis.polardb时,需要配置专有网络,阿里的文档写的总体上还是比较抽象的,没有一定的网络基础,会一脸懵. 所以这里我来进行专有网络和交换机的配置,以及ip ...

  2. Swift 语法总结

    1,用 var 定义变量 ,与js类似. let 用于定义常量,定义完后不能修改. var 用于定义变量,可以修改. swift可以自动识别属性类别. 2,使用 import 语句来引入任何的 Obj ...

  3. QToolTip 设置提示信息

    import sys from PyQt5.QtWidgets import (QWidget, QToolTip, QPushButton, QApplication) from PyQt5.QtG ...

  4. matlab 梯度法(最速下降法)

    norm(A,p)当A是向量时norm(A,p) Returns sum(abs(A).^zhip)^(/p), for any <= p <= ∞.norm(A) Returns nor ...

  5. 01 . MongoDB简介及部署配置

    简介 什么是MongoDB? MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统. 在高负载的情况下,添加更多的节点,可以保证服务器性能. MongoDB 旨在为WEB应用 ...

  6. 本地代码提交到远程仓库(git)

    [准备环境] 我没有在Linux搭建gitlab私有云服务器,用的是开源的 gitee托管平台 1.在gitee注册账号 2.本地下载git客户端 [步骤] 1  本地新建1个文件夹  进入文件夹后 ...

  7. 1169A+B问题终结版(高精度计算)

    描述 给定两个整数A和B,输出A+B的值.A和B的值可能会很大很大,甚至达到100位.现在请你解决这一问题. 输入 两行,分别是两个整数A,B,换行隔开.A和B会很大很大. 输出 一个整数,即A+B的 ...

  8. Jmeter Response Data 乱码问题解决方法

    1. 进入jmeter\apache-jmeter-4.0\bin,打开jmeter.properties 2. 搜索“sampleresult.default.encoding” 设置sampler ...

  9. 一文搞懂:Adaboost及手推算法案例

    boosting Boosting 算法的特点在于:将表现一般的弱分类器通过组合变成更好的模型.代表自然就是我们的随即森林了. GBDT和Adaboost是boost算法中比较常见的两种,这里主要讲解 ...

  10. Nginx具体配置(三)

    一:Nginx配置实例 - 反向代理 实例一: 1.1:实现效果 在Windows浏览器地址栏中输入www.123.com,跳转到Linux系统中的tomcat主页面 访问Nginx:192.168. ...