题目描述

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. mysql基础之-mysql数据控制语言DCL(七)

    0x01 mysql中的权限分布 mysql中的用户信息都存储在系统数据库mysql的user表中 查询语句:select host,user,password from mysql.user; 创建 ...

  2. Spring Cloud 系列之 Alibaba Nacos 注册中心(一)

    前言 从本章节开始,我们学习 Spring Cloud Alibaba 相关微服务组件. Spring Cloud Alibaba 介绍 Spring Cloud Alibaba 致力于提供微服务开发 ...

  3. RabbitMQ系列之【centos6 服务开启自启脚本】

    #!/bin/sh## rabbitmq-server RabbitMQ broker## chkconfig: - 80 05# description: Enable AMQP service p ...

  4. Alink漫谈(七) : 如何划分训练数据集和测试数据集

    Alink漫谈(七) : 如何划分训练数据集和测试数据集 目录 Alink漫谈(七) : 如何划分训练数据集和测试数据集 0x00 摘要 0x01 训练数据集和测试数据集 0x02 Alink示例代码 ...

  5. @topcoder - SRM603D1L3@ SumOfArrays

    目录 @desription@ @solution@ @accepted code@ @details@ @desription@ 给定两个长度为 n 的数列 A, B.现你可以将两数列重排列,然后对 ...

  6. 在CentOS8下利用seafile软件搭建私有云

    一.安装前准备工作 1. 安装EPEL源 EPEL (Extra Packages for Enterprise Linux)是基于Fedora的一个项目,为“红帽系”的操作系统提供额外的软件包,适用 ...

  7. Excel只想显示一部分日期,怎样把其余部分隐藏起来?

      问题:只想显示一部分日期,怎样把其余部分隐藏起来? 方法:分列 Step1:选中需要修改的单元格——数据——分列. Step2:固定宽度——点击下一步. Step3:在建立分列处单击鼠标(若想取消 ...

  8. Arduino控制超声波检测与0.96OLED及串口显示

    Arduino控制超声波检测与0.96OLED及串口显示代码使用库共享(包括超声波检测与U8glib): 使用元件: 0.96寸 12864 I2C OLED 128x64规格 超声波检测模块 湿度模 ...

  9. Python实现二分法和黄金分割法

    运筹学课上,首先介绍了非线性规划算法中的无约束规划算法.二分法和黄金分割法是属于无约束规划算法的一维搜索法中的代表. 二分法:$$x_{1}^{(k+1)}=\frac{1}{2}(x_{R}^{(k ...

  10. 从零开始手把手教你使用原生JS+CSS3实现幸运水果机游戏

    项目体验地址 免费视频教程 游戏介绍 幸运水果机是一款街机游戏,游戏界面由24个方格拼接成一个正方形,每个方格中都有一个不同的水果图形,方格下都有一个小灯.玩家使用游戏币选择希望押注的目标,按下开始后 ...