图论——最短路径问题

先赞再看,养成习惯:D

Dijkstra

什么是Dijkstra

Dijkstra采用了贪心的策略。

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

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

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


理解贪心策略

这里举一个简单的栗子

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

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

他该去哪个超市呢?

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

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


思考实现代码

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

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

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

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

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

原来那个点怎么办?

丢进垃~圾~桶~


代码实现

题目:
输出从 s s s t t t的最短路,并换行输出最短路径
I n p u t Input 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

O u t p u t Output Output

60
1 4 3 5

C o d e Code Code

#include 
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]); // 倒过来输出就行了
    }
}

注意:

D i j k s t r a Dijkstra Dijkstra采用的是贪心的策略,所以遇上有负边的图时,它就会陷入自环中。


SPFA

什么是SPFA

相对 D i j k s t r a Dijkstra Dijkstra来讲,在随机生成数据中,会比 D i j k s t r a Dijkstra Dijkstra会更快一点。

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

理解邻接表

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

举个粒子

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

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


代码实现

题目:给一堆数据,输出1到 n n n的最短路
I n p u t Input Input

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

O u t p u t Output Output

66

C o d e Code Code

#include 
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有一定的亲密度,为了使他们的亲密度更近,需要一个中介来帮忙。通过中介的帮忙,他们是否能提升之间的亲密度呢?所以我们就枚举一下。

代码实现

C o d e Code 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 ) O(n^3) O(n3)

《没 逝 , 就 慢 了 亿 点 点》


时间复杂度讨论

随机数据下:
D i j k s t r a (堆优化) ≤ S P F A < D i j k s t r a (无优化) < F l o y d Dijkstra(堆优化) \leq SPFA < Dijkstra(无优化) < Floyd Dijkstra(堆优化)SPFA<Dijkstra(无优化)<Floyd
其他数据:

有负边权就选 S P F A SPFA SPFA

全是正边权就选 D i j k s t r a 堆优化版 Dijkstra堆优化版 Dijkstra堆优化版


D i j k s t r a Dijkstra Dijkstra的堆优化版仍在学习中,希望本贴对各位有帮助)

你可能感兴趣的:(算法,贪心算法,图论)