链式前向星+拓扑排序+最短路径(Dijkstra+Floyd+Bellman-Ford+SPFA)

本文深入讲解了几种重要的图算法,包括链式前向星、拓扑排序、Dijkstra算法、Floyd算法及Bellman-Ford算法等。通过实例演示了算法的具体实现过程,并探讨了它们在解决实际问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

链式前向星是图的一种静态链表存储,用边集数组和邻接表相结合,
可以快速访问一个顶点的所有邻接点。
链式前向星和邻接表一样,采用头插法进行链接,所以,边输入的顺序不同,
创建的链式前向星也不同;对于无向图,可通过与1异或得到反向边。

链式前向星包含两种结构:
(1)边集数组: edge[], edge[i]表示第i条边;
(2)头结点组数: head[], head[i]存以i为起点的第一条边的下标(在edge[]中的下标)

struct node {
    int to;
    int next;
    int w;
} edge[MAXN*MAXN];

int head[MAXN];

添加一条边(u, v, w)
void add(int u, int v, int w)
{
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].next = head[u];  // 采用头插法
    head[u] = cnt++;
}
如果是有向图,每输入一条边,执行一次add(u, v, w)即可; 
如果是无向图,则需要执行两次{add(u, v, w); add(v, u, w)}
 

对一个有向无环图G进行拓扑排序,是将G中所有顶点排成一个线性序列,
使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。

拓扑排序可以用来判断图中是否有环。

拓扑排序基本思想
(1)从有向图中选一个入度为0的顶点输出
(2)将此顶点和以它为起点的弧删除
(3)重复(1)和(2),直到不存在无前驱的顶点

算法:
(1)求所有顶点的入度
(2)把所有入度为0的顶点入队列
(3)当队列不空时
  a. 取队列顶点为u,输出顶点u
  b. 顶点u的所有邻接点入度-1,如果有入度为0的顶点,则入队列

#include <queue>
#include <vector>
#include <iostream>

const unsigned int MAXVER = 10;
std::vector<int> adj[MAXVER];
unsigned int indegree[MAXVER];
unsigned int MAXID = 0;

bool topsort()
{
    std::queue<int> qu;
    for (unsigned int i = 1; i <= MAXID; ++i)
    {
        if (indegree[i] == 0)
            qu.push(i);
    }

    std::vector<int> ans;

    while (!qu.empty())
    {
        int tmp = qu.front();
        qu.pop();
        //std::cout << tmp << "   ";
        ans.push_back(tmp);
        int size = adj[tmp].size();
        for (int t = 0; t < size; ++t)
            if (--indegree[adj[tmp][t]] == 0)
                qu.push(adj[tmp][t]);
    }

    if (ans.size() == MAXID)
    {
        for (int j = 0; j < MAXID; ++j)
            std::cout << ans[j] << "   ";
        std::cout << std::endl;
        return true;
    }
    else
        return false;
}

int main()
{
    int arr[][2] = { {1, 2}, {1, 3}, {1, 4}, {2, 4}, {2, 5}, {3, 6}, {4, 3}, {5, 4}, {5, 7}, {7, 6} };
    int num = sizeof(arr) / sizeof(int) / 2;
    for (int i = 0; i < num; ++i)
    {
        std::cout << arr[i][0] << "->" << arr[i][1] << std::endl;
        adj[arr[i][0]].push_back(arr[i][1]);
        ++indegree[arr[i][1]];
        if (arr[i][0] > MAXID)
            MAXID = arr[i][0];
        if (arr[i][1] > MAXID)
            MAXID = arr[i][1];
    }

    topsort();

    return 0;
}

1->2
1->3
1->4
2->4
2->5
3->6
4->3
5->4
5->7
7->6
1   2   5   4   7   3   6 

Dijkstra 算法核心思想:

将结点分成两个集合:
已确定最短路长度的点集(记为S集合)的和
未确定最短路长度的点集(记为 V-S 集合)。
一开始所有的点都属于 V-S 集合。

初始化distTo[s] = 0.0;,其他点的最短路径均为无穷大。

然后重复如下操作:
(1)从 V-S 集合中,选取一个最短路长度最小的结点,移到S集合中。
(2)对那些刚刚被加入 S 集合的结点的所有出边执行松弛操作。

直到 V-S 集合为空为止。

使用优先队列维护(1)操作中最短路长度最小的结点,
如果同一个点的最短路被更新多次,
因为先前更新时插入的元素不能被删除,也不能被修改,只能留在优先队列中,
故优先队列内的元素个数是O(E)  的,时间复杂度为O(E*log(E)) 。

Djikstra 算法,从bfs演化而来,优先队列的bfs,
我们每次寻找的点就是当下最好的,贪心地扩大图中的点,
每次找到的tmp就是在vis集合中能到达的且离源点s最近的点,它的最短路就可以确认。 
一个点可以进队多次,但是取出来只有剩下的在队伍中作废(因为vis置为1了)。

最短路径算法通常依赖于一个性质,一条两结点间的最短路径包含路径上的其他的最短路径。Dijkstra算法不允许图中存在负权边
1.单源最短路径:从某点s到其他所有结点的最短路径
2.松弛技术:松弛边(u,v),检测当前从s到v的最优路径是否有必要经过s到u,如有必要,则取边(u,v)

G = (V,E) where
 V is a set of vertices and
 E is a set of edges.
Dijkstra's algorithm keeps two sets of vertices:

S   the set of vertices whose shortest paths from the source have already been determined and
V-S   the remaining vertices.
(1) Set S to empty,
(2)While there are still vertices in V-S,
a. Sort the vertices in V-S according to the current best estimate of their distance from the source,
b. Add u, the closest vertex in V-S, to S,
c. Relax all the vertices still in V-S connected to u

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <vector>
#include <list>
#include <queue>
#include <stack>
#include <string>

using std::string;
using std::priority_queue;
using std::vector;
using std::stack;
using std::list;
using std::pair;
using std::make_pair;
//using std::greater;
using std::cout;
using std::endl;
using namespace std;

const double MAX = 10000.0;
class DirectedEdge
{
public:
    DirectedEdge(int u, int v, double w)
    {
        if (u >= 0 && v >= 0)
        {
            this->u = u;
            this->v = v;
            weight = w;
        }
    }

    int from()
    {
        return u;
    }

    int to()
    {
        return v;
    }

    double getWeight()
    {
        return weight;
    }

    string toString()
    {
        char result[64] = "";
        sprintf_s(result, "%d -- %d: %8.2f\n", u, v, weight);
        string str = result;
        return str;
    }

private:
    int u;
    int v;
    double weight;
};

class EdgeWeightedDigraph
{
public:
    EdgeWeightedDigraph(int nVertex, int nEdge, int arr[][2], double weight[]);
    ~EdgeWeightedDigraph();
    list<DirectedEdge *> getAdj(int v);
    list<DirectedEdge *> edges();
    int getV() { return V; }

private:
    int V;
    int E;
    list<DirectedEdge *> *adj;
};

EdgeWeightedDigraph::EdgeWeightedDigraph(int nVertex, int nEdge, int arr[][2], double weight[])
{
    V = nVertex;
    E = nEdge;
    adj = new list<DirectedEdge *>[nVertex];
    for (int i = 0; i < nEdge; ++i)
    {
        DirectedEdge *e = new DirectedEdge(arr[i][0], arr[i][1], weight[i]);
        adj[arr[i][0]].push_back(e);
    }
}

EdgeWeightedDigraph::~EdgeWeightedDigraph()
{
    list<DirectedEdge *> ls = edges();
    for (list<DirectedEdge *>::iterator it = ls.begin(); it != ls.end(); ++it)
    {
        delete *it;
        *it = NULL;
    }
    delete[]adj;
    adj = NULL;
}

list<DirectedEdge *> EdgeWeightedDigraph::getAdj(int v)
{
    if (v >= 0 && v < V)
        return adj[v];
}

list<DirectedEdge *> EdgeWeightedDigraph::edges()
{
    list<DirectedEdge *> ls;
    for (int i = 0; i < V; ++i)
    {
        list<DirectedEdge *> tmpList = getAdj(i);
        for (list<DirectedEdge *>::iterator it = tmpList.begin(); it != tmpList.end(); ++it)
            ls.push_back(*it);
    }

    return ls;
}

//greater<pair<int, double> > 
//使用greater<>后,小根堆(升序),top()返回的是最小值而不是最大值
using Ty = std::pair<int, double>;

struct myGreater {
    bool operator() (Ty a, Ty b) {
        return a.second > b.second;
    }
};

class DijkstraSP
{
public:
    DijkstraSP(EdgeWeightedDigraph *g, int s);
    ~DijkstraSP();
    double getDistTo(int v) { return distTo[v]; }
    bool hasPathTo(int v) { return distTo[v] < MAX; }
    stack<DirectedEdge *> pathTo(int v);

private:
    void relax(DirectedEdge *e);

private:
    bool *vis;
    double *distTo;      // distTo[v]=distance of shortest s --> v path
    DirectedEdge **edgeTo;// edgeTo[v]=last edge on shortest s --> v path
    //priority_queue<pair<int, double>, vector<pair<int, double> >, greater<pair<int, double> > > pq;
    priority_queue<pair<int, double>, vector<pair<int, double> >, myGreater > pq;
};

DijkstraSP::DijkstraSP(EdgeWeightedDigraph *g, int s)
{
    int v = g->getV();
    distTo = new double[v];
    edgeTo = new DirectedEdge *[v];
    vis = new bool[v];
    for (int i = 0; i < v; ++i)
    {
        distTo[i] = MAX;
        edgeTo[i] = NULL;
        vis[i] = false;
    }
    distTo[s] = 0.0;
    // relax vertices in order of distance from s, 这里的relax还有很多改进的地方
    pq.push(make_pair(s, distTo[s]));

    while (!pq.empty())
    {
        int t = pq.top().first;
        pq.pop();
        if (vis[t])
            continue;
        vis[t] = true;
        list<DirectedEdge *> ls = g->getAdj(t);
        for (list<DirectedEdge *>::iterator it = ls.begin(); it != ls.end(); ++it)
        {
            relax(*it);
        }
    }
}

DijkstraSP::~DijkstraSP()
{
    if (NULL != distTo)
    {
        delete[]distTo;
        distTo = NULL;
    }

    if (NULL != edgeTo)
    {
        delete[]edgeTo;
        edgeTo = NULL;
    }

    if (nullptr != vis)
    {
        delete[] vis;
        vis = nullptr;
    }
}

//To relax an edge u->v means to test whether the best known way from s to v is to go from s to u,
//then take the edge from u to v, and, if so, update our data structures
void DijkstraSP::relax(DirectedEdge *e)
{
    int u = e->from();
    int v = e->to();
    double w = e->getWeight();
    if (distTo[v] > distTo[u] + w)
    {
        distTo[v] = distTo[u] + w;
        edgeTo[v] = e;
        pq.push(make_pair(v, distTo[v]));
    }
}

stack<DirectedEdge *> DijkstraSP::pathTo(int v)
{
    stack<DirectedEdge *> path;
    if (hasPathTo(v))
    {
        for (DirectedEdge *e = edgeTo[v]; e != NULL; e = edgeTo[e->from()])
        {
            path.push(e);
        }
    }
    return path;
}

int main()
{
    int arr[15][2] = { { 4, 5 },{ 5, 4 },{ 4, 7 },{ 5, 7 },{ 7, 5 },
    { 5, 1 },{ 0, 4 },{ 0, 2 },{ 7, 3 },{ 1, 3 },
    { 2, 7 },{ 6, 2 },{ 3, 6 },{ 6, 0 },{ 6, 4 } };
    double weight[15] = { 0.35, 0.35, 0.37, 0.28, 0.28, 0.32, 0.38, 0.26, 0.39,
        0.29, 0.34, 0.40, 0.52, 0.58, 0.93 };
    EdgeWeightedDigraph g(8, 15, arr, weight);
    int s = 0;

    DijkstraSP sp(&g, s);

    for (int t = 0; t < 8; ++t)
        if (sp.hasPathTo(t))
        {
            cout << s << " to " << t << " (" << sp.getDistTo(t) << ")" << endl;

            stack<DirectedEdge *> path = sp.pathTo(t);
            while (!path.empty())
            {
                DirectedEdge *e = path.top();
                cout << e->toString();
                path.pop();
            }

        }
        else
            cout << s << " to " << t << " has no path" << endl;

    return 0;
}


output:
0 to 0 (0)
0 to 1 (1.05)
0 -- 4:     0.38
4 -- 5:     0.35
5 -- 1:     0.32
0 to 2 (0.26)
0 -- 2:     0.26
0 to 3 (0.99)
0 -- 2:     0.26
2 -- 7:     0.34
7 -- 3:     0.39
0 to 4 (0.38)
0 -- 4:     0.38
0 to 5 (0.73)
0 -- 4:     0.38
4 -- 5:     0.35
0 to 6 (1.51)
0 -- 2:     0.26
2 -- 7:     0.34
7 -- 3:     0.39
3 -- 6:     0.52
0 to 7 (0.6)
0 -- 2:     0.26
2 -- 7:     0.34


Floyd算法又称插点法,可以求所有顶点对之间的最短路径的长度,边权可正可负。
其算法核心是在结点i与结点j之间插入结点k,
看看是否可以缩短结点i与结点j之间的距离(松弛操作)。
算法核心思想:
(1)采用邻接矩阵G[][]存储图,dist[i][j]记录从结点i到结点j的最短路径长度,
pre[i][j]记录结点i到结点j的最短路径上的结点j的直接前驱。
(2)初始化 dist[i][j]=G[i][j], 如果结点i到结点j有边相连,pre[i][j]=i,
否则pre[i][j]=-1。
(3)插点:在结点i,j之间插入结点k,看是否可以缩短结点i,j之间的距离(松弛操作)
   if  (dist[i][j] > dist[i][k] + dist[k][j])
   {
        dist[i][j] = dist[i][k] + dist[k][j];
        pre[i][j] = pre[k][j];
   }

#include <iostream>

const int MAXD = 0x5FFF; 
const int MAXN = 100+2;
int dist[MAXN][MAXN] = {  };
int pre[MAXN][MAXN] = { };
int G[MAXN][MAXN] = { };

void Floyd(int n)
{
    int i = 0, j = 0, k = 0;
    for (i = 1; i <= n; ++i)
        for (j = 1; j <= n; ++j)
        {
            if (i == j)
                dist[i][j] = 0;
            else
                dist[i][j] = G[i][j];

            if (dist[i][j] < MAXD && i != j)
                pre[i][j] = i;
            else
                pre[i][j] = -1;
        }

    for (i = 1; i <= n; ++i)
        for (j = 1; j <= n; ++j)
            for (k= 1; k <= n; ++k)
                if (dist[i][j] > dist[i][k] + dist[k][j])
                {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    pre[i][j] = pre[k][j];
                }
}

void printpath(int u, int v)
{
    if (pre[u][v] != -1)
    {
        printpath(u, pre[u][v]);
        std::cout << pre[u][v] << "    ";
    }
}

int main()
{
    int arr[][3] = { { 1, 2, 2 },
                     { 1, 3, 5 },
                     { 2, 3, 2 },
                     { 2, 4, 6 },
                     { 3, 4, 7 },
                     { 4, 3, 2 },
                     { 3, 5, 1 },
                     { 4, 5, 4 } };
    for (int i = 1; i <= 5; ++i)
        for (int j = 1; j <= 5; ++j)
        {
            G[i][j] = MAXD;
            dist[i][j] = MAXD;
            pre[i][j] = -1;
        }


    int m = sizeof(arr)/sizeof(arr[0]);
    for (int i = 0; i < m; ++i)   // 处理重复的边,保留较小的权值
        G[arr[i][0]][arr[i][1]] = G[arr[i][0]][arr[i][1]] < arr[i][2] ? G[arr[i][0]][arr[i][1]]: arr[i][2];

    Floyd(5);

    for (int i = 1; i <= 5; ++i)
    {
        for (int j = 1; j <= 5; ++j)
            std::cout << dist[i][j] << "         ";
        std::cout << std::endl;
    }

    return 0;
}

0              2          4         8         5
24575          0          2         6         3
24575         24575       0         7         1
24575         24575       2         0         3
24575         24575      24575    24575       0

Dijkstra算法贪心选取未被处理的具有最小最短路径的结点,然后对其邻接点进行松弛操作。
Bellman-Ford算法也可求单源最短路径,对所有边进行松弛操作,共n-1次,
由于负环可以无限制的减少最短路径长度,所以,如果第n次操作扔可松弛,则一定存在负环。
算法核心思想:
(1)采用边集数组存储图, (u, v, w)
(2)松弛操作,
dist[v]表示从源点到结点v的最短路径长度
对所有的边i(u, v, w), 如果dist[e[i].v] > dist[e[i].u] + e[i].w,
则 dist[e[i].v] = dist[e[i].u] + e[i].w
(3)重复松弛n-1次
(4)再执行一次,如果仍然可以松弛,则说明有负环。


#include <iostream>

const int MAXN = 100 + 2;
const int MAXW = 0x5ffff;
int dist[MAXN];
struct node {
    int u;
    int v;
    int w;
    node(int u1, int v1, int w1) 
        : u(u1), v(v1), w(w1) {}
    node() : u(0), v(0), w(0) {}
};
node edge[MAXN*MAXN];
int n; // 结点个数
int m; // 边数量
bool Bellman_Ford(int s)
{
    dist[s] = 0;
    for (int index = 1; index < n; ++index)
    {
        bool flag = false; // 松弛优化
        for (int i = 0; i < m; ++i)
            if (dist[edge[i].u] != MAXW && (dist[edge[i].v] > dist[edge[i].u] + edge[i].w))
            {
                dist[edge[i].v] = dist[edge[i].u] + edge[i].w;
                flag = true;
            }

        if (!flag)
            return false;
    }

    for (int i = 0; i <m; ++i)
        if (dist[edge[i].u] != MAXW && (dist[edge[i].v] > dist[edge[i].u] + edge[i].w))
            return true;

    return false;
}

int main()
{
    int arr[][3] = { { 2, 3, 2 },
                     { 1, 2, -3 },
                     { 1, 5, 5 },
                     { 4, 5, 2 },
                     { 3, 4, 3} };
    m = sizeof(arr) / sizeof(arr[0]);
    n = 5;
    for (int i = 1; i <= n; ++i)
        dist[i] = MAXW;
    for (int i = 0; i < m; ++i)
    {
        edge[i] = node(arr[i][0], arr[i][1], arr[i][2]);
        std::cout << edge[i].u << "  " << edge[i].v << "  " << edge[i].w << "  " << std::endl;
    }
        

    bool hasCircle = Bellman_Ford(1);
    std::cout << "hasCircle: " << hasCircle << std::endl;
    for (int i = 1; i <= n; ++i)
        std::cout << dist[i] << std::endl;

    return 0;
}

2  3  2
1  2  -3
1  5  5
4  5  2
3  4  3
hasCircle: 0
0
-3
-1
2
4
 

队列优化的Bellman - Ford算法,又称为SPFA算法,松弛操作只会发生在最短路径松弛过的前驱结点上,
用一个队列记录松弛过的结点,可以避免冗余的计算。
通常用于求解包含负边的单源最短路径,以及判负环。
在最坏的情况下(稠密图),SPFA算法的时间复杂度和Bellman - Ford算法相同,为O(nm);
但在稀疏图上运行效率较高,为O(km), 其中k是一个较小的常数。

算法核心思想
(1)采用链式前向星存储图,dist[i]记录从源点到结点i的最短路径长度,vis[i]标记结点i是否在队列中,
sum[i]记录结点i入队次数
(2)创建一个队列,源点s入队,标记s在队列中,sum[s]的入队次数加1
(3)松弛操作,取出对头t, 标记t不在队列中。考察t的所有出边i(u, v, w),
如果 dist[v] > dist[t] + edge[i].w,
则 dist[v] = dist[t] + edge[i].w。
如果结点v不在队列中,如果v的入队次数加1后大于等于n, 则说明存在负环,退出;
否则v入队,标记v在队列中。
(4)重复松弛操作,直到队列为空。

#include <iostream>
#include <deque>

const int MAXN = 10 + 2;
struct node {
    int to;
    int next;
    int w;
} edge[MAXN*MAXN];

int head[MAXN];
bool vis[MAXN];
int sum[MAXN];
int dist[MAXN];

int n = 0, m = 0, cnt = 0;

void add(int u, int v, int w)
{
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].next = head[u];  // 采用头插法
    head[u] = cnt++;
}

bool Spfa(int s)
{
    std::deque<int> qu;
    vis[s] = true;
    dist[s] = 0;
    ++sum[s];
    qu.push_back(s);

    while (!qu.empty())
    {
        int t = qu.front();
        qu.pop_front();
        vis[t] = false;
        for (int i = head[t]; ~i; i = edge[i].next)
        {
            int v = edge[i].to;
            if (dist[v] > dist[t] + edge[i].w)
            {
                dist[v] = dist[t] + edge[i].w;
                if (!vis[v])
                {
                    if (++sum[v] >= n) // has minus circle
                        return true;
                    if (!qu.empty() && dist[qu.front()] > dist[v]) // 优化:待插结点的最短路径dist > 队首结点的最短路径dist, 尾插;否则,头插 
                            qu.push_front(v);
                    else
                        qu.push_back(v);
                    vis[v] = true;
                }
            }
        }
    }

    return false;
}

int main()
{
    int arr[][3] = { { 1, 2, 2 },
                     { 2, 3, 2 },
                     { 2, 4, 1 },
                     { 1, 3, 5 },
                     { 3, 4, 3 },
                     { 1, 4, 4 } };
    n = 4;
    m = sizeof(arr) / sizeof(arr[0]);

    for (int j = 1; j <= n; ++j)
    {
        vis[j] = false;
        sum[j] = 0;
        dist[j] = 0x5ffff;
        head[j] = -1;
    }

    for (int i = 0; i < m; ++i)
        add(arr[i][0], arr[i][1], arr[i][2]);
    
    Spfa(1);
    for (int i = 1; i <= n; ++i)
        std::cout << dist[i] << "   ";
    std::cout << std::endl;

    return 0;
}

0   2   4   3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值