【图论】最短路问题

单源最短路问题

单源最短路问题是指固定一个起点,求它到其他任意一点的最短路问题。

Dijkstra算法(迪克斯拉特算法)

首先要注意的是,这个算法仅仅用于图中没有负边的情况。

原理

Dijkstra本质逻辑其实是动态规划 ,我们要求任意点i到起点(假设为1)的距离,我们不妨用一个数组dist记录这一距离,即disti为1->i的最短路距离,所以现在我们需要考虑的问题就是如何更新并求得dist,这一部分就是算法的重点。

我们将图的点分为两个集合,集合A为最短路径已经确定的点,集合B为最短路径尚未确定的点。

每一次我们从B中选取dist最小的点(设为x),将他转移到A中,同时我们利用x的dist来更新x的邻居(设为p)的dist,即

distp=min(distp,distx+e(x,p))//e(x,p)为边xp的权值

通过反复进行B->A操作,最终得到全部的dist,没看懂没关系,我们用图来演示一遍。

这是图的初始状态,dist简写为d,根据定义我们可得起点的d=0,而其他的dist我们全部设为一个极大的数,方便后续更新,此时我们选取1并入A,同时更新邻居的d值

红色 为这一次发生了更新的d,黄色为已经固定的d,也就是集合A中的元素。接下来我们在B中选择min进行更新,显然这次我们应该选择2,我们会进行如下更新

d3:5->5(d2+4>5)

d4:∞->d2+4=6

d5:∞->d2+10=12

选择最小的d3

d4:6->6(d3+2>6)

不断重复这一操作,最终使得全部d变成黄色,即全部并入集合A

朴素实现

cpp 复制代码
const int INF=0x3f3f3f3f;
const int N=1e3+7;//上限
struct edge{
    int to,w;
    edge(int x,int y){to=x;w=y;}
};
vector<vector<edge>> g(N,vector<edge>());
//邻接表存储图,to为边的终点端,w为权值
bool vis[N];
int dist[N];
void dijkstra(int n,vector<vector<edge>>& g){
    //n为图的点数,N>n
    for(int i=1;i<=n;i++){
        dist[i]=INF;
        vis[i]=false;
    }
    dist[1]=0;
    //初始化
    while(true){
        int v=-1;
        for(int i=1;i<=n;i++){
            if(vis[i]) continue;
            if(v==-1||dist[i]<dist[v]) v=i;
        }
        if(v==-1) break;
        //如果为-1代表全部都被标记,即B集合为空
        vis[v]=true;
        for(auto p:g[v])//更新操作
            dist[p.to]=min(dist[p.to],dist[v]+p.w);
    }
}

注意此处起点设为1,实际操作时我们可以按照要求修改起点。

最终实现复杂度为O(V²)

堆优化

可以发现,复杂度来源主要是最小dist的选取 以及邻居的更新,邻居更新我们无法继续优化,但是最小dist的选取我们可以用最小堆进行优化。

最小堆我们选用STL中优先队列

cpp 复制代码
typedef pair<int,int> P;
const int INF=0x3f3f3f3f;
const int N=1e3+7;//上限
struct edge{
    int to,w;
    edge(int x,int y){to=x;w=y;}
};
vector<vector<edge>> g(N,vector<edge>());
//邻接表存储图,to为边的终点端,w为权值
bool vis[N];
int dist[N];
void dijkstra(int n,vector<vector<edge>>& g){
    //n为图的点数,N>n
    priority_queue< P,vector<P>,greater<P> > q;
    //first是最短距离,sceond是点的编号
    for(int i=1;i<=n;i++)
        dist[i]=INF;
    dist[1]=0;
    q.push(P(0,1));
    //初始化
    while(!q.empty()){
        P x=q.top();q.pop();
        int v=x.second;
        if(dist[v]<x.first) continue;
//如果x的距离大于dist[v]那就不能用它更新邻居
        for(auto p:g[v]){
            if(dist[p.to]>dist[v]+p.w){
                dist[p.to]=dist[v]+p.w;
                q.push(P(dist[p.to],p.to));
            }
        }
    }
}

其实可以发现,我们在这里删除了vis数组,实际上,入队操作实现了这一作用,在更新过程中我们将发生更新的元素入队,也就是上文图中红色元素,而已经固定的黄色元素不会发生更新则被剔除,借此实现了vis数组的功能。

最终实现O(ElogV)。

Bellman-Ford算法(贝尔曼福特算法)

相比于迪克斯拉特算法,这一算法可以处理负边的情况**。**

原理

基本原理

这个原理特别简单,我们维护一个数组d,di代表了从起点s到点i的最短路径,那么我们得到

di=min{dj+e(j,i)}//e(j,i)为j到i的权值

如果是**DAG(有向无环图)**我们就可以按照拓扑序遍历。但如果有环,如果不是负环,这也并不影响算法运作,如果有负环这就是我们需要着重考虑的部分了。

负环:环上所有的边权和为负数。

如果有负环,我们将一条最短路径简化成三个点

起点------>负环------>终点

由于负环的权值为负,所以起点到终点的距离可以无限小,因为每走一次负环距离都会缩短(有点神奇不是吗),实现起来就是

起点------>负环环环环环环........------>终点

负环处理原理

所以我们需要检测是否存在负环,有负环就无解(最短路可以无穷小)。

对于任意有V个顶点的图,起点到终点的最短路最多经过(v-1)条边,因为最短路不会同时经过同一顶点两次,除了一种情况,就是负环。因为对于负环来说,经过同一顶点的代价是负的。我们就可以借助这一特性来检测负环。

实现

cpp 复制代码
struct edge{
    int from,to,w;
};
vector<edge> g;//我们选择记录边
int V,E,s;//顶点数,边数,起点数
int d[MAXV];
bool check(){
    for(int i=1;i<=V;i++) d[i]=0;//初始化为0,为了应对负边
    for(int i=1;i<=V;i++){
        for(int j=1;j<=E;j++){
            edge e=g[j];
            if(d[e.to]>d[e.from]+e.w){
                d[e.to]=d[e.from]+e.w;
                if(i==V) return false;
//如果第V次都更新了,就代表有负环,因为正常图最多更新V-1次
            }
        }
    }
    return true;
}
void Bellman(){
    for(int i=1;i<=V;i++) d[i]=INF;
    d[s]=0;
    while(true){
        bool update=false;
        for(int i=1;i<=E;i++){
            edge e=g[i];
            if(d[e.from]!=INF&&d[e.to]>d[e.from]+e.w){
                d[e.to]=d[e.from]+e.w;
                update=true;
            }
        }
        if(!update) break;
    }
}

由于while最多循环(V-1)次所以实现为O(V²)

任意两点最短路问题

Floyd-Warshall算法(弗洛伊德-沃舍尔)

对于任意两点的问题,我们用Floyd算法来解决,本质是动态规划。

原理

我们定义状态 fkij ,表示为在只使用编号1-k的顶点当作中间节点的情况下,i到j的最短路。

而f0ij则代表e(i,j),没有就设为无穷大。

所以我们就可以把只用1-k顶点的问题约束到只用1-k-1顶点的问题中

对于顶点k,我们有两种情况,经过k,不经过k,于是我们就可以得到状态转移方程

fkij = min( fk-1ij , fk-1ik + fk-1kj )

实际操作中,由于第一维不会影响算法进行,我们可以去掉第一位减小空间开销。

而Floyd对于负环处理比较简单,只需要检测e(i,j)是否小于0即可,这里不再展示。

实现

cpp 复制代码
struct edge{
    int from,to,w;
};
int n,E;//点数,边数
int f[N][N];
void init(){
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            f[N][N]=INF;
        }
    }
    for(int i=1;i<=E;i++){
        int u,v;cin>>u>>v;
        cin>>f[u][v];
    }
}
void Floyd(){
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
            }
        }
    }
}

最终实现O(n³)

相关推荐
AZaLEan__1 分钟前
图论:拓扑排序
算法·深度优先
悠仁さん2 分钟前
数据结构 排序
数据结构·算法·排序算法
阿文的代码库4 分钟前
机器学习之精确率和召回率的关系
人工智能·算法·机器学习
咸鱼翻身小阿橙12 分钟前
高斯模糊降噪/磨皮算法降噪图像
前端·opencv·算法·webpack·c#
代码中介商18 分钟前
数据结构进阶(五):最短路径——Dijkstra 与 Floyd 算法
数据结构·算法
fengxin_rou20 分钟前
LeetCode链表经典五题:从相交到环形,双指针的妙用
算法·leetcode·链表
KaMeidebaby21 分钟前
卡梅德生物技术快报|抗原如何自己检测?FAdV-4 重组抗原制备与 ELISA 体系技术调试指南
前端·人工智能·物联网·算法·百度
却道天凉_好个秋24 分钟前
HEVC(二):如何实现并行处理
人工智能·算法·计算机视觉·hevc·瓦片技术·波前并行处理wpp
wayz1125 分钟前
Momentum:QQE(定量定性估计)技术指标详解
算法·金融·数据分析·量化交易·特征工程
Dontla26 分钟前
聚类找不到簇原因分析(聚类失败)(DBSCAN聚类算法、eps参数、Epsilon参数、最大允许距离)
算法·数据挖掘·聚类