图论——最短路径问题

update:2023.07.19 更新了堆优化版本的 Dijkstra
update:2023.09.21 更新了时间复杂度讨论并修改了堆优化 Dij

Dijkstra

什么是Dijkstra

Dijkstra采用了贪心的策略。

在一张连通图中,我们将点分类为已知点未知点

什么是已知点?就是已经遍历过的,例如原点(出发点)就是一个已知点。

什么是未知点?顾名思义,就是未遍历到的点(除了已知点,剩下的就是未知点)


理解贪心策略

这里举一个简单的栗子

夏福要去supermarket,他面前可以选择3个商店:

  • 1.壹加壹超市,据夏福1000m
  • 2.美宜佳商店,据夏福500m
  • 3.711超市,据夏福200m

他该去哪个超市呢?

这里很容易看出来,肯定是选择第3种方案。

没错,这就是从夏福(原点)到超市(终点)的最短路径。也就是贪心策略。


思考实现代码

因为贪心只能保证局部最优,不能保证全局最优,所以我们还是需要去遍历,但是我们可以缩小遍历的范围。

假想现在从已知点可以到达三个中转站,最后都可以到终点。

那么从起点到终点的路径就是:起点到中转站的路径 + 中转站到终点的路径

我们想让起点到中转站的距离尽可能的小,那肯定是选择据起点最近的中转站作为新的起点(新的已知点)

我们就可以把那个点当作起点,继续找最短路径就好了。

原来那个点怎么办?

丢进垃~圾~桶~


代码实现

题目:
输出从\(s\)\(t\)的最短路,并换行输出最短路径
\(Input\)

5 7
1 2 10
1 4 30
1 5 100
2 3 50
3 5 10
4 3 20
4 5 60
1 5

\(Output\)

60
1 4 3 5

\(Code\)

#include <bits/stdc++.h>
using namespace std;

int n, m;
int edge[505][505];
int ss, tt;
bool vis[505];
pair<int, int> dis[505]; // 相当于一个变量里有两个成员:一个是距离,另一个是数组下标所连接的点
vector<int> q[505];
vector<int> ans;

bool cmp(int a, int b)
{
    return a < b;
}

int main()
{
    scanf("%d%d", &n, &m); // 输入点的个数n,边的数量m

    for (int i = 1; i <= m; i++)
    {
        int l, r, v;
        scanf("%d%d%d", &l, &r, &v); // 输入边的信息:边连接的两个点l、r,和边的权值v

        edge[l][r] = v; // 表示从l到r这个点的权值是v
	/* 因为这个代码是有向图的最短路问题,如果是无向图的话应该再加上下面这行代码:

	edge[r][l] = v;

	这个表示r到l的路径边权是v */
    }

    scanf("%d%d", &ss, &tt); // 输入ss(原点)和tt(终点),这个代码是输出从ss到tt的最短路径

    for (int i = 1; i <= n; i++)
        dis[i].first = 1e9; // 初始化从原点到第i个点的路径全部为正无穷(这里只要数据够大就行了)
    fill(vis, vis + n + 1, 0); // 初始化所有的点都没被访问过
    dis[ss].first = 0; // 从原点到原点的距离肯定是0
    dis[ss].second = ss; // 默认原点和原点相连

    for (int i = 1; i <= n; i++)
    {
        int tmp = -1; // 存储下标的临时变量

        for (int j = 1; j <= n; j++)
        {
            if (!vis[j] && (tmp == -1 || dis[j] < dis[tmp]))
            {
		// 只要 (j未被访问过) 并且只要达到 [(tmp未赋值) 或 (找到离i点有更近的点)] 的条件之一
                tmp = j;
		// tmp就赋值为j
            }
        }

        vis[tmp] = 1; // 完事之后,备注tmp已经被访问过了,丢进**垃~圾~桶~**里
        for (int j = 1; j <= n; j++)
        {
            if (dis[tmp].first + edge[tmp][j] < dis[j].first && tmp != j && edge[tmp][j])
            {
		/*
		  如果 (到tmp的距离 + tmp到j的距离 < 原来到j的距离)
		  并且
		  (tmp和j不是同一个点)
		  并且
		  (tmp到j的距离不是0)
		  的话
        	*/
		dis[j].first = dis[tmp].first + edge[tmp][j];
                dis[j].second = tmp;
		// 到j的距离重新赋值,与j相邻的点就变为tmp
            }
        }
    }

    printf("%d\n", dis[tt].first); // 输出终点的距离

    int temp = tt;
    while (dis[temp].second != ss) // 只要与temp这个临时变量相邻的不是起点,就进行
    {
        ans.push_back(temp); // 把temp存储进ans数组里
        temp = dis[temp].second; // temp就重新赋值
    }
    ans.push_back(temp); // 最后一个点会跳出循环,所以要存储
    ans.push_back(ss); // 把原点也放进去

    for (int i = ans.size() - 1; i >= 0; i--)
    {
        printf("%d ", ans[i]); // 倒过来输出就行了
    }
}

注意:

\(Dijkstra\)采用的是贪心的策略,所以遇上有负边的图时,它就会陷入自环中。


SPFA

什么是SPFA

相对\(Dijkstra\)来讲,在随机生成数据中,会比\(Dijkstra\)会更快一点。

它是基于邻接表的基础写的。

理解邻接表

在一张连通图中,一个点并不总是只连着一个点。

举个粒子

小A家处于十字路口,可以从小A家到小B家、小C家、小D家等多个good friends的家。

而对于小\(A\)家的邻接表就是:
\(B->C->D\)


代码实现

题目:给一堆数据,输出1到\(n\)的最短路
\(Input\)

4 7
1 2 68
1 3 19
1 4 66
2 3 23
3 4 65
3 2 57
4 1 68

\(Output\)

66

\(Code\)

#include <bits/stdc++.h>
using namespace std;

int n, m;
struct edge
{
    int s, e, val;
};
int maxx = INT_MIN;
int step;
vector<edge> b[5005];
queue<int> q;
int dis[5005];
bool vis[5005];

int main()
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; i++)
    {
        int x, y, v;
        edge ee;
        scanf("%d%d%d", &x, &y, &v);

        ee.s = x, ee.e = y, ee.val = v;

        b[x].push_back(ee); // 存储x的邻接表
    }

    fill(dis, dis + n + 1, 1e9); // 初始化路径全部为正无穷(数据足够大就行)

    q.push(1);
    dis[1] = 0;
    vis[1] = 1;
    while (!q.empty())
    { // STL宽搜
        step++;
        if (step > m)
        { // 出现负环,直接退出
            printf("No Solution");
            return 0;
        }

        int u = q.front();
        q.pop();

        for (int i = 0; i < b[u].size(); i++)
        { // 对于当前这个u点的邻接点
            int vv = b[u][i].val;
            int en = b[u][i].e;

            if (dis[u] + vv < dis[en])
            { // 如果 (到u点距离) + (从u点到它的邻接点的距离) < (原来的到en的距离)
                dis[en] = dis[u] + vv;

                if (!vis[en])
                { // 在压入队列之前进行标记,否则会陷入死循环
                    vis[en] = 1;
                    q.push(en);
                }
            }
        }

        vis[u] = 0; // 标记这个点《 免 费 》了(free这个点)
    }

    printf("%d", dis[n]);
}

Floyd

什么是Floyd

就是运用枚举中间点进行松弛(专业术语,指令这个点距离最小)每个点的距离。

理解枚举中间点

还是举个梨子

小A和小B有一定的亲密度,为了使他们的亲密度更近,需要一个中介来帮忙。通过中介的帮忙,他们是否能提升之间的亲密度呢?所以我们就枚举一下。

代码实现

\(Code\)

for (int k = 1; k <= n; k++)
{
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            if (i != j && i != k && j != k)
            {
                if (dis[i][k] + dis[k][j] < dis[i][j])
                {
                    dis[i][j] = dis[i][k] + dis[k][j];
                    // 这样写的话,无论是有向图还是无向图都成立
                }
            }
        }
    }
}

但是这个时间复杂度是

\(O(n^3)\)

《没 逝 , 就 慢 了 亿 点 点》


堆优化Dijkstra

思想

因为Dijkstra类似于贪心的策略,每次都选择边权最小的边。如果我们用小根堆来维护呢?

是的,所以堆优化Dijkstra用到了优先队列优化。

因为曾有“关于SPFA——他死了”的一说,所以这个就很吃香。更好的是,这个和SPFA代码实现很像,所以直接入手是很容易的。

代码实现

code

#include<bits/stdc++.h>
using namespace std;

#define int long long
#define pb push_back

const int MAXN=500+5,MAXM=500+5,INF=0x3f3f3f3f3f3f3f3f;

int n,m;
int dis[MAXN];
bool vis[MAXN];
int su,en[MAXM<<1],vl[MAXM<<1],hd[MAXN],lt[MAXM<<1];
struct node
{
    int id,dis;
    bool operator>(const node &T)const
    {
        return dis>T.dis;
    }
};

void add(int u,int v,int w)
{
    en[++su]=v,vl[su]=w,lt[su]=hd[u],hd[u]=su;
}

int Dij()
{
    priority_queue<node,vector<node>,greater<node>> q;
    memset(vis,0,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    dis[1]=0,vis[1]=1;
    q.push({1,0});
    while(!q.empty())
    {
        int u=q.top().id;q.pop();
        for(int i=hd[u];i;i=lt[i])
        {
            int v=en[i],w=vl[i];
            if(dis[v]>dis[u]+w)
            {
                dis[v]=dis[u]+w;
                if(!vis[v])
                {
                    vis[v]=1;
                    q.push({v,dis[v]});
                }
            }
        }
        vis[u]=0;
    }
    if(dis[n]==INF)
        return -1;
    return dis[n];
}

signed main()
{
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%lld%lld%lld",&u,&v,&w);
        add(u,v,w),add(v,u,w);
    }
    printf("%lld\n",Dij());
    return 0;
}

时间复杂度讨论

堆优化Dij:\(O(m\log m)\)
朴素Dij:\(O(n^2)\)
SPFA:\(O(nm)\)(最坏情况)
Floyd:\(O(n^3)\)

一般来说,无负边权就选择 Dij,对于是否采用堆优化,取决于图的稀疏程度。

关于 SPFA……

posted @ 2022-06-02 20:32  WerChange  阅读(142)  评论(1编辑  收藏  举报