最短路

· · 个人记录

定义

在一个图中,从起点到终点路径选择方案中,所有路径 权值之和最小的路径选择方案。

他们的特点如下

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_idis_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; } ```