【图论】——最短路问题

【图论】——最短路问题

概述

  • n 为点数,m 为边数
  • 按照源点的个数可以分为:
  1. 单源最短路问题(从图中某一个点到其他点)
    再按是否有负权边分为:
  • 所有边权为整正数:
    Dijkstra(朴素)——> O ( n 2 ) O(n^2) O(n2)——> 用于稠密图
    Dijkstra(堆优化)——> O ( m log ⁡ n ) O(m\log n) O(mlogn)——> 用于稀疏图
  • 存在边权为负
    Bellman-Ford——> O ( m ∗ n ) O(m*n) O(mn)
    SPFA——> O ( m ) O(m) O(m)
  1. 多源汇最短路问题(图中多个起点到其他点)
    Floyd——> O ( n 3 ) O(n^3) O(n3)

单源最短路问题

  • 对于一个有向图,如果所有点都满足三角不等式,则 dis 数组就是所求的最短距离:
    d i s   [   y   ] ≤ d i s   [   t   ]   +   z dis\ [\ y\ ]≤dis\ [\ t\ ]\ +\ z dis [ y ]dis [ t ] + z

不存在负权边

Dijkstra(朴素)——> O ( n 2 ) O(n^2) O(n2)——> 用于稠密图
  • 思路:
    在这里插入图片描述

  • 代码:

int g[M][M];// 邻接矩阵
int dis[M];// 存储 start 到每点的距离
bool st[M];// 存储当前点是否被确定了距离(是否被拿去更新过所有点)
int n, m;
void save_1()// 邻接矩阵的读入
{memset(g 0x3f, sizeof g);
    int a, b;
    scanf("%d%d%d", &a, &b, &c);
    g[a][b] = min(g[a][b], c);}
int dijkstra(int start, int end)
{
    // 初始化为正无穷
    memset(dis, 0x3f, sizeof dis);
    dis[start] = 0;
    for (int i = 0; i < n - 1; i++) {
        // 寻找当前距离 start 最小的点
        int t = -1;
        for (int j = 1; j <= n; j++)
            if (!st[j] && (t == -1 || dis[t] > dis[j]))
                t = j;
        // 现在 t 就是没有被确定过的距离 start 最近的点
        if (t == end)// 如果距离最近的点是 end,找到答案提前结束
            return dis[end];
        for (int j = 1; j <= n; j++)// 用 t 来更新到其他所有点的距离
            dis[j] = min(dis[j], dis[t] + g[t][j]);
        st[t] = true;// 将 t 距离确定
    }
    return dis[end];
}
Dijkstra(堆优化)——> O ( m log ⁡ n ) O(m\log n) O(mlogn)——> 用于稀疏图
  • 思路
    在这里插入图片描述
typedef pair<int,int> PII;// 第二个 int 用来存下标,第一个 int 用来存从 start 到当前点的距离,方便排序
int h[N], e[N], ne[N], w[N], idx;// 用邻接表来存储边
int dis[N];// 存储 start 到每点的距离
bool st[N];// 存储当前点是否被确定了距离(是否被拿去更新过所有点)
memset(h, -1, sizeof - 1);// 表头初始化为空
void add(int a, int b, int c)// 邻接表的读入
{e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    w[idx++] = c;
}
int dijkstra(int start, int end)
{
    // 初始化距离为正无穷
    memset(dis, 0x3f, sizeof dis);
    dis[start] = 0;
    // 使用堆来优化每次寻找最近点的操作(朴素版遍历寻找 t 的过程)
    priority_queue<PII, vector<PII>, greater<PII>> heap;// 小根堆
    heap.push(PII(0, 1));// 将起点入堆
    while (heap.size()) {
        // 取出最近的点
        auto t = heap.top();
        heap.pop()int idx = t.second;// 获得下标(朴素版的 t)
        if (idx == end)// 如果最近的点是 end,提前结束
            return dis[end];
        if (st[idx])// 如果这个点的距离已经被确定过了,跳过
            continue;
        else
            st[idx] = true;// 标记被确定
        for (int i = h[idx]; i != -1; i = ne[i]) {// 遍历这个点所有的邻边,并用这个点来更新
            int j = e[i];
            if (dis[j] > dis[idx] + w[i]) {
                // 如果不满足三角不等式,更新答案,同时入堆
                dis[j] = dis[idx] + w[i];
                heap.push(PII(dis[j], j));}
        }
    }
    return dis[end];
}

存在负权边

Bellman-Ford——> O ( m ∗ n ) O(m*n) O(mn)
  • 用结构体存储每条边即可
  • 可以用来计算限制最大步数的最短路
  • 思路:
    在这里插入图片描述
int dis[M], bkup[M];// 创建 dis 和 dis 的备份,备份的用处在下文
int n, m, k;//n 为点数,m 为边数,k 为最大合法步数
struct Edge// 用于存储每条边的信息
{int a, b, w;} edges[N];
// 读入每条边:
int a,b,w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};

int bellman_ford(int start,int end)
{
    // 首先初始化为正无穷
    memset(dis,0x3f,sizeof dis);/
    dis[start] = 0;
    for (int i = 0; i < k; i++) {memccpy(bkup, dis, sizeof dis);
        /* 为什么要备份:
            在运用三角不等式更新 dis 时,由于边的枚举不是按序的,可能会将需要用到的上一点的 dis 更新,导致在更新当前点时用到的上一点的 dis 出错,
            因此在循环枚举每条边的时候要先将 dis 备份
        */
        // 枚举所有边
        for (int j = 0; j < m;j++){int a = edges[j].a;
            int b = edges[j].b;
            int w = edges[j].w;
            dis[b] = min(dis[b], bkup[a] + w);// 若用 dis[a]+w, 由于枚举不是按序,如果之前有存在可以更新 dis[a] 的边会导致当前的 dis[b] 出错,很有可能 b 与之前更新过 a 的点没有连通。
        }
    }
    return dis[end];
}
SPFA——> O ( m ) O(m) O(m)(队列优化的 Bellman-Ford 算法)
  • Bellman 是每次遍历所有的边,在遍历过程中其实真正对答案起到作用的是当某点不满足三角不等式时的更新,换言之,dis[a] 变小是因为他的前一个点 dis[b] 变小造成的
  • 因此,类似于 BFS 的方式,用队列来优化这个过程可以将复杂度优化到线性
  • 思路:
    在这里插入图片描述
int h[N], e[N], ne[N], w[N], idx;// 邻接表的存储
int dis[N];// 存储 start 到其他点的距离
bool st[N];// 判断当前点是否入队
int n, m;//n 为点数,m 为边数
queue<int> q;// 队列
void add(int a, int b, int c)
{e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    w[idx++] = c;
}
memset(h,-1,sizeof h);// 表头初始化为空
int spfa(int start, int end)
{
    // 初始化为正无穷
    memset(dis, 0x3f, sizeof dis);
    dis[start] = 0;
    // 将起点入队
    q.push(start);
    st[start] = true;
    while (q.size()) {auto t = q.front()// 取出队头
        q.pop();
        st[t] = false;
        for (int i = h[t]; i != -1; i = ne[i]) {// 更新队头的所有邻边
            int j = e[i];
            if (dis[j] > dis[t] + w[i]) {// 如果不满足三角不等式,更新该点
                dis[j] = dis[t] + w[i];
                if (st[j])// 如果已经在队中跳过
                    continue;
                q.push(j);// 否则入队
                st[j] = true;
            }
        }
    }
    return dis[end];// 当队中所有元素都更新完,返回 dis(不能提前返回,因为不知道仍在队中的元素是否会把当前答案更新)
}
SPFA 判断负环
  • 记录一下从 start 到每个点所经过的边数,由抽屉原理可知,如果 cnt≥n 则证明一定存在 n+1 个点,但是只有 n 个点,证明有边经过了多次,只有存在负环才会让 dis 无休止的小下去
  • 思路:
    在这里插入图片描述
int e[M], ne[M], h[M], w[M], idx;//邻接表存储
int dis[N], cnt[N];//cnt记录每个点被经过的次数
bool st[N];//判断是否在队中
int n, m;//n为点数,m为边数
queue<int> q;
void add(int a, int b, int c)//邻接表读入
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    w[idx++] = c;
}
bool judge_spfa()//spfa判断负环
{
    memset(dis, 0x3f, sizeof dis);//初始化
    for (int i = 1; i <= n; i++) {//将每个点都入队当作起点
        dis[i] = 0;
        q.push(i);
        st[i] = true;
    }
    while (q.size()) {
        auto t = q.front();
        q.pop();
        st[t] = false;
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dis[j] > dis[t] + w[i]) {//如果不满足三角不等式
                dis[j] = dis[t] + w[i];//更新距离
                cnt[j] = cnt[t] + 1;//更新经过该点最多次数
                if (cnt[j] >= n)//如果cnt≥n则证明至少有n+1个点,矛盾,有负环
                    return true;
                if (st[t])//已经在队中就不用加了
                    continue;
                q.push(j);//否则入队
                st[j] = true;
            }
        }
    }
    return false;
}

多源汇最短路问题

Floyd——> O ( n 3 ) O(n^3) O(n3)

  • 算法本质为 DP,状态表示为: d p ( k , i , j ) dp(k,i,j) dp(k,i,j)
  • 表示:经过若干个序号不超过 k k k 的节点,从 i i i 走向 j j j 的最短长度
  • 可以将其化为两个子集合:
  1. 经过编号不超过 k − 1 k-1 k1 的节点从 i i i 走向 j j j 的最短长度
  2. 经过编号不超过 k − 1 k-1 k1 的节点从 i i i 走向 k k k,再由 k k k 走向 j j j 的最短长度
  • 转移方程:
    d p (   k   ,   i   ,   j   ) = m i n   {   d p (   k − 1   ,   i   ,   j   )   ,   d p (   k − 1   ,   i   ,   k   )   +   d p (   k − 1   ,   k   ,   j   ) } dp(\ k\ ,\ i\ ,\ j\ ) = min\ \{\ dp(\ k-1\ ,\ i\ ,\ j\ )\ ,\ dp(\ k-1\ ,\ i\ ,\ k\ )\ +\ dp(\ k-1\ ,\ k\ ,\ j\ )\} dp( k , i , j )=min { dp( k1 , i , j ) , dp( k1 , i , k ) + dp( k1 , k , j )}
  • 状态压缩,由于每次更新只会用到 k − 1 k-1 k1 的值,因此可以用滚动数组压缩,去掉 k k k 层。
    d p (   i   ,   j   ) = m i n   {   d p (   i   ,   j   )   ,   d p (   i   ,   k   )   +   d p (   k   ,   j   ) } dp(\ i\ ,\ j\ ) = min\ \{\ dp(\ i\ ,\ j\ )\ ,\ dp(\ i\ ,\ k\ )\ +\ dp(\ k\ ,\ j\ )\} dp( i , j )=min { dp( i , j ) , dp( i , k ) + dp( k , j )}
  • 代码
int dis[N][N];// 邻接矩阵
int n, m;//n 为点数,m 为边数
memset(dis, 0x3f, sizeof dis);// 初始化为正无穷
void floyd()
{
    // 初始化为原地距离为 0
    for (int i = 1; i <= n;i++)
        dis[i][i] = 0;
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
引用\[1\]提供了使用Python的networkx库绘制网络图和计算最短加权路径的示例代码。该代码使用了一个包含顶点和边的列表,并使用add_nodes_from和add_weighted_edges_from方法将它们添加到图中。然后,使用nx.shortest_path_length方法计算了从顶点v1到顶点v11的最短加权路径长度为13。\[1\] 引用\[2\]提供了一个计算最短路径的Python程序示例。该程序使用了numpy和networkx库。首先,定义了一个包含顶点和边的列表,并使用add_nodes_from和add_weighted_edges_from方法将它们添加到图中。然后,使用nx.shortest_path_length方法计算了最短路径长度,并将结果存储在一个字典中。接下来,使用numpy创建了一个6x6的零矩阵,并使用两个嵌套的for循环将最短路径长度填充到矩阵中。最后,使用矩阵乘法计算了运力,并找到了最小运力和对应的位置。\[2\] 引用\[3\]提供了关于Dijkstra算法的一些背景信息。Dijkstra算法是一种寻找最短路径的算法,适用于所有权重大于等于0的情况。它可以用于解决从一个起始点到任意一个点的最短路径问题。\[3\] 综上所述,如果你想在Python中计算图论中的最短路径,可以使用networkx库和Dijkstra算法。你可以根据引用\[1\]和引用\[2\]中的示例代码进行操作。 #### 引用[.reference_title] - *1* *3* [运筹学——图论与最短距离(Python实现)](https://blog.csdn.net/weixin_46039719/article/details/122521276)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [数学建模:图论模型 — 最短路模型示例 (Python 求解)](https://blog.csdn.net/qq_55851911/article/details/124776487)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Siriu_Sky

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值