最短路
algorith
·
·
个人记录
定义
在一个图中,从起点到终点路径选择方案中,所有路径
权值之和最小的路径选择方案。
他们的特点如下
dfs枚举
这是最没思路,和时间复杂度最高的,也就是暴力。
其思路就是枚举所有可能,最后找最小值
void dfs(int i,int now){
if(i==end){
ans=min(ans,now);
return;
}
for(int j=1;j<=n;++j)
if(!vis[j]&&g[i][j]){
vis[j]=1;
dfs(j,now+g[i][j]);
vis[j]=0;
}
}
Dijkstra
Dijkstra主要采用贪心策略,当所有边权都是非负数
时,从起点(此处记做 s )出发,把 i 到起点的最
短路路径的长度记为dis_i,dis_s初始为0,在
未标记(初始)点中寻找距离起始点路径最短的顶点,并
将其标记(即确定了起点到它的最短路,因为此时是距离
起点最近,所有没有其他到这个点更近的路径,所有即
确定了它),直到所有顶点都被标记为止。
时间复杂度O(n^2)
实现
首先初始化
memset(vis,0,sizeof vis)//标记数组,初始全没被标记
memset(dis,0x3f,sizeof dis)//最短路记录,初始化为无穷大
dis[s]=0;//起点到自己的距离为0
主体
for(int i=1;i<=n;++i){
int mi=0x7fffffff,k;
for(int j=1;j<=n;++j){
if(!vis[j]&&dis[j]<mi){ //寻找此时没被标记离起点最近的点
mi=a[1][j];
k=j;
}
}
vis[k]=1; //标记他
for(int j=1;j<=n;++j){
if(!vis[j]&&a[j][k]&&dis[j]>a[j][k]+dis[k]) //更新出与其相邻的未被标记的点的此时的最短路长度
dis[j]=a[j][k]+dis[k];
}
}
堆优化Dijkstra
在朴素Dijkstra中,每次寻找距离起点最近的点的时间
复杂度为O(n),寻找一个集合的最小值我们可以想到
数据结构 "堆"(或优先队列),它可以用O(log_n)的
时间复杂度寻找出一个集合最小值,此处我们可以用上
它,来找每次距离起点最近的点。
同时该程序用了链式前向星优化了,每个点直接记录下
了它相邻的点直接用,而不是遍历n个点,再在过程中
判断相不相邻。
时间复杂度O(mlogn)
struct node{
int t,z;
};
int n,m,dis[505],vis[505];
vector <node> q[505]; //用vector来链式前向星
void dij(){
memset(dis,0x3f,sizeof dis);
priority_queue<pair<int,int> > v; //大根堆
v.push({0,1}); //路径长度,点编号,用pair的话,堆会直接把第一个数(first)当做关键字
dis[1]=0;
while(!v.empty()){ //只要堆不为空
int cur=v.top().second; //找出最近的点编号
v.pop();
if(vis[cur])continue;
for(int i=0;i<q[cur].size();++i){
int to=q[cur][i].t;
if(dis[to]>dis[cur]+q[cur][i].z){
dis[to]=dis[cur]+q[cur][i].z;
v.push({-dis[to],to}); //大根堆存其负数相当于小根堆
}
}
}
}
例题P4779 【模板】单源最短路径(标准版)
为什么普通Dijkstra不能处理有负权的图
这就因为其贪心策略,默认是后面加会越来越大,而不
会变小,所以点会直接确定
比如上面这个图,如果按 Dijkstra的策略来的话
是$1->3->4->2:-5$,所以Dijkstra处理不了有负权的
图
# Bellman-Ford
每次把每个点都尝试用其的最短路径的值更新一次其相
邻点的最短路的值(或可以说是把每条边上的两个点更新
对方),最后所有点的最短路长度就更新出来了
时间复杂度$O(nm)
其核心代码只有4行
//单向边
for(int k=1;k<n;++k) //只可能更新n-1次
for(int i=1;i<=m;++i) //m条边
if(dis[v[i]]>dis[u[i]]+w[i])
dis[v[i]]=dis[u[i]]+w[i];
SPFA
其是Bellman-Ford的一个优化过来的算法,我们可以发
现Bellman-Ford中,每次都要拿每个点去更新与其相邻
的点,而实际上,如果几次循环,一个点的值一直没
变,那么它更新其他点也只会更新一次,之后是更新不
了的。于是我们就可以只把值有改变的点用来更新。于
是我们用一个队列来存这些需要改变的点,如果一个点
的值有改变,且队列里没有它(此处是一个小优化,因为
队列存的是编号,而用来改变的值是一样的所以,没必
要该两次)就将其入队。当用它更新完其他点后,就让它
出队。
时间复杂度O(km),k:用点次数,一般时间复杂度
O(m)$,最坏时间复杂度$O(nm)
while(!q.empty()){
for(int i=head[q.front()];i;i=edges[i].next){
if(ans[q.front()]+edges[i].z<ans[edges[i].to]){
ans[edges[i].to]=ans[q.front()]+edges[i].z;
if(!inq[edges[i].to])
q.push(edges[i].to),inq[edges[i].to]=1;
}
}
inq[q.front()]=0;
q.pop();
}
判负环
即几个负边权组成的环,一直转下去会让答案无限小,
无解
例题P3385 【模板】负环
首先我们要知道,对于一个不存在负环的图,从起点到
任意一个点最短距离经过的点最多只有 n 个,所以一
个点它最多只会被更新n次,当一个点更新次数超过
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,w,head[100005],cnt,ans[100005],t,ma,tot[10005];
struct node{
int from,to,next=0,z;
}edges[5000005];
void add(int a,int b,int c){
edges[++cnt].from=a;
edges[cnt].to=b;
edges[cnt].z=c;
edges[cnt].next=head[a];
head[a]=cnt;
}
int spfa(){
queue<int> q;
ans[1]=0;
q.push(1);
tot[1]++;
while(!q.empty()){
for(int i=head[q.front()];i;i=edges[i].next){
if(ans[q.front()]+edges[i].z<ans[edges[i].to]){
ans[edges[i].to]=ans[q.front()]+edges[i].z;
tot[edges[i].to]++; //更新次数
if(tot[edges[i].to]>n)return 1;
q.push(edges[i].to);
}
}
q.pop();
}
return 0;
}
int main(){
cin>>t;
for(int k=1;k<=t;k++){
cin>>n>>m;
memset(head,0,sizeof(head));
memset(edges,0,sizeof(edges));
memset(tot,0,sizeof tot);
cnt=0;
for(int i=1;i<=n;i++)ans[i]=0x7fffffff;
for(int i=1;i<=m;i++){
int a,b,c;
cin>>a>>b>>c;
if(c>=0){
add(a,b,c);
add(b,a,c);
}else{
add(a,b,c);
}
}
if(spfa())cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
return 0;
}
```
# floyd
floyd的主要思想是动态规划,而求最短路径需要不断松
弛。它就是一直把每个点作为一个中间点去松弛其它两
点之间的距离。把每个点都尝试去松弛其它两点。
时间复杂度$O(n^3)
优点:代码好写,思路简单易理解
if(dis[i][j]<dis[i][k]+dis[k][j]) //松弛操作
dis[i][j]=dis[i][k]+dis[k][j];
核心代码如下
for(int k=1;k<=n;++k)
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
if(dis[i][j]>dis[i][k]+dis[k][j])
dis[i][j]=dis[i][k]+dis[k][j];
最开始只允许经过1号顶点进行松弛,接下来只允许经过
1和2号顶点进行松弛…允许经过1…n号所有顶点进
行中转,求任意两点之间的最短路程。用一句话概括就
是:从i号顶点到j号顶点只经过前k号点的最短
路程
例题:B3647 【模板】Floyd 算法
但其时间复杂度不如堆优化dij O(n^2logn),有时不
如spfa
O(knm)
其它
建反边
有时题目会给出一个有向图,问其它所有点到其中某一个点的最短路长度总和。
常规思路的话可能就是跑n遍dij或spfa或floyd,可其时间复杂度达到了
且我们可以发现,其实我们只用
算出其它点到那某一个点的距离就可以了,而不用去算其它点和其它点的距离,
但就算是我们用dij,算到那个某个点
就结束时间复杂度仍旧很高。于是就有了
**建反边**,就如字面意思,把边反着建,我们可以发现,此时a到b的距离就
等于之前b到a的距离,所以,之前的其它点到某一个点就成了一个点到其它所有
点,也就是单源最短路模板了。
例题:[P1342请柬](https://www.luogu.com.cn/problem/P1342)
这道题就可以直接正着跑一遍再反着跑一遍最后加起来
```cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1000005;
int n,m,dis[MAXN],vis[MAXN];
long long ans;
struct node{
int t,z;
};
vector<node> q[MAXN];
vector<node> q1[MAXN];
void dij(){
memset(dis,0x3f,sizeof(dis));
priority_queue<pair<int,int> > v;
v.push({0,1});
dis[1]=0;
while(!v.empty()){
int cur=v.top().second;
v.pop();
if(vis[cur])continue;
vis[cur]=1;
for(int i=0;i<q[cur].size();++i){
if(dis[q[cur][i].t]>dis[cur]+q[cur][i].z){
dis[q[cur][i].t]=dis[cur]+q[cur][i].z;
v.push({-dis[q[cur][i].t],q[cur][i].t});
}
}
}
}
void dij2(){
memset(dis,0x3f,sizeof(dis));
priority_queue<pair<int,int> > v;
v.push({0,1});
dis[1]=0;
while(!v.empty()){
int cur=v.top().second;
v.pop();
if(vis[cur])continue;
vis[cur]=1;
for(int i=0;i<q1[cur].size();++i){
if(dis[q1[cur][i].t]>dis[cur]+q1[cur][i].z){
dis[q1[cur][i].t]=dis[cur]+q1[cur][i].z;
v.push({-dis[q1[cur][i].t],q1[cur][i].t});
}
}
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;++i){
int u,v,z;
scanf("%d%d%d",&u,&v,&z);
q[u].push_back(node{v,z});
q1[v].push_back(node{u,z});
}
dij();
for(int i=1;i<=n;++i){
ans+=dis[i];
}
memset(vis,0,sizeof(vis));
dij2();
for(int i=1;i<=n;++i){
ans+=dis[i];
}
cout<<ans;
return 0;
}
```
例题2:[P1821 [USACO07FEB] Cow Party S](https://www.luogu.com.cn/problem/P1821)
思路和上一题大同小异
## 权值不一样
例题:[P2888 [USACO07NOV]Cow Hurdles S](https://www.luogu.com.cn/problem/P2888)
此题中,问的是任意两点的距离,
所
以
需
要
用到floyd,
并且一条路的大小不是看总和,而是看最大
值
所以把floyd中的+换成max即可
```cpp
//FLOYD
#include<iostream>
#include<algorithm>
using namespace std;
int ans[301][301]={0x7fffffff},n,m,q;
int main(){
cin>>n>>m>>q;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
ans[i][j]=0x7fffffff;
}
}
for(int i=1;i<=m;i++){
int a,b,c;
cin>>a>>b>>c;
ans[a][b]=c;
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(ans[i][k]!=0x7fffffff&&ans[k][j]!=0x7fffffff){
if(ans[i][j]>max(ans[i][k],ans[k][j])){
ans[i][j]=max(ans[i][k],ans[k][j]);
}
}
}
}
}
for(int i=1;i<=q;i++){
int a,b;
cin>>a>>b;
if(ans[a][b]==0x7fffffff){
cout<<"-1"<<endl;
}else{
cout<<ans[a][b]<<endl;
}
}
return 0;
}
```