【每日算法】图算法(遍历&MST&最短路径&拓扑排序)

图有邻接矩阵和邻接表两种存储方法,邻接矩阵非常easy,这里不讨论,以下我们先看看经常使用的邻接表表示方法。

邻接表经常使用表示方法

指针表示法

指针表示法一共须要两个结构体:

struct ArcNode  //定义边表结点 
{
    int adjvex; //邻接点域
    ArcNode* next;
};

struct VertexNode   //定义顶点表结点
{
    int vertex;
    ArcNode* firstedge;
};

每一个节点相应一个VertexNode,其firstedge指向边表(与当前节点邻接的表构成的链表)的头节点。

vector表示法

使用vector模拟邻接表,十分方便简洁,v是一个数组,相应于上面的顶点表,v[i]是一个vector。存放邻接顶点的下标。
这个方案的缺陷是:要求顶点的编号从0~MAXN-1。

vector<int> v[MAXN];

数组表示法

head[i]:指向编号为i的顶点的邻接表中第一条边的序号;
h:边的序号;
e:边表,每条边相应一个序号,由h给出;

int head[MAXN]; //初始化为-1
int h = 0;

struct Edge
{
    int adjvex;
    int next;
};

Edge e[MAXN<<1];

void add(int a, int b) //a与b邻接
{
    e[h].adjvex = b; //当前边的序号为h,
    e[h].next = head[a]; //头插法
    head[a] = h++;
}

深度优先遍历

图的深度优先遍历(DFS)相似于树的前序遍历:

訪问顶点v。visited[v] = 1;
w = 顶点v的第一个邻接点。
while(w存在)
    if(w未被訪问) 从顶点w出发递归运行DFS;
    w = 顶点v的下一个邻接点;

广度优先遍历

图的广度优先遍历(BFS)相似于树的层次遍历:

初始化队列Q;
訪问顶点v;visited[v] = 1;顶点v入队列Q;
while(队列非空)
    v = 队列Q的队头元素出队。
    w = 顶点v的第一个邻接点。
    while (w存在)
        假设w未被訪问。訪问w;visited[w] = 1;顶点w入队列Q;
        w = 顶点v的下一个邻接点;

注意,我们这里的队列用来存储已被訪问的顶点,即对于每一个顶点。我们是先訪问,再入队,这样能够避免顶点的反复入队。

最小生成树(MST)

最小生成树算法以Prim算法和Kruskal算法最为经典。

Prim算法

初始化:一个顶点。空边集。

其基本思想是。寻找这种边:满足“一个点在生成树中,一个点不在生成树中”的边中权值最小的一条边。

将找到的边增加边集中,顶点加到顶点集中。当全部顶点都增加进来时,算法结束。

初始化: U = {v0}; TE = {};
while (U != V)
{
    在E中寻找最短边(u, v),且满足 u∈U。v∈V-U;
    U = U + {v}。
    TE = TE + {(u, v)};
}

因为Prim须要不断读取随意两个顶点之间边的权值,所以适合用邻接矩阵存储。

为找出最短边,我们定义一个候选最短边的集合。用来存放候选的边(一个点在生成树中,一个点不在生成树中)。我们使用数组shortEdge[n]来表示这个集合。数组元素包括adjvex和lowcost两个域,分别表示邻接点和权值。比方shortEdge[i].adjvex = k, shrotEdge[i].lowcost = w表示下标为i和下标为k的顶点邻接,边的权值为w。

void Prim(Graph G)
{
    for (int i = 1; i < G.vertexNum; ++i) //初始化辅助数组
    {
        shortEdge[i].lowcost = G.arc[0][i];
        shortEdge[i].adjvex = 0;
    }
    shortEdge[0].lowcost = 0; //将顶点0增加集合U
    for (int i = 1; i < G.vertexNum; ++i)
    {
        int k = MinEdge(shortEdge, G.vertexNum); //寻找最短边的邻接点k;
        cout << '(' << k << ',' << shortEdge[k].adjvex << ')' << endl;
        shorEdge[k].lowcost = 0; //将k增加集合U
        for (int j = 1; j < G.vertexNum; ++j) //更新数组shortEdge
        {
            //k新增加U,更新与k邻接的点的lowcost。
            if (G.arc[k][j] < shortEdge[j].lowcost)
            {
                shortEdge[j].lowcost = G.arc[k][j];
                shortEdge[j].adjvex = k;
            }
        }
    }
}

Prim算法的时间复杂度为O(n^2)。与图中的边数无关。适合求稠密图的最小生成树。

Kruskal算法

初始化:n个顶点(即n个连通分量)。空边集。

其基本思想是:每次从未标记的边中选取最小权值的边,假设该边的两个顶点位于两个不同的连通分量。则将该边增加最小生成树。合并两个连通分量,并标记该边。否则,位于同一个连通分量,则去掉该边(相同标记就可以),避免造成回路。

可见,此算法的关键是:怎样考察两个顶点是否位于两个不同连通分量。

最简单的做法是:使用并查集。

此算法须要对边进行操作,所以我们用边集数组存储图的边,为提高最短边查找速度。能够先按权值排序。

const int MaxVertex = 10;
const int MaxEdge = 100;
struct EdgeType
{
    int from, to;
    int weight;
};

template <class DataType>
struct EdgeGraph
{
    DataType vertex[MaxVertex];
    EdgeType edge[MaxEdge];
    int vertexNum, edgeNum;
};

void Kruskal(EdgeGraph G)
{
    for (int i = 0; i < G.vertexNum; ++i)
        parent[i] = -1;
    for (int num = 0, i = 0; i < G.edgeNum; ++i)
    {
        v1 = FindRoot(partent, G.edge[i].from);
        v2 = FindRoot(partent, G.edge[i].to);
        if (v1 != v2) //不同连通分量
        {
            cout << '(' << v1 << ',' << v2 << ')' << endl;
            parent[v2] = v1; //合并
            num++;
            if (num == n-1) return;
        }
    }
}

int FindRoot(int parent[], int v)
{
    int t = v;
    if (parent[t] != -1) t= parent[t]; //一直往上找到根节点
    return t;
}

最短路径

最短路径经典的算法有Dijkstra算法(单源最短路,不能处理负权),SPFA算法(单源最短路,可处理负权)。Floyd算法(任一对顶点间的最短路)。

Dijkstra算法

此算法使用贪心策略,这里作为复习。仅仅讲一讲实现。不再讨论原理了。以下贴出一张图。方便大家回顾。

这里写图片描写叙述

当中黑色顶点之间的灰色边为已选的边,黑色与灰色顶点之间的灰色边为候选边。

每一次增加一条边就更新顶点的最短路径值,贪心策略为每次选取值最小的点。

因为此算法须要高速求得随意两顶点之间边的权值,所以用邻接矩阵存储。

为记录每一个顶点的最短路径值。须要辅助数组dist[n]:dist[i]表示当前所找到的从源点v到终点vi的最短路径长度。
初始值:若从v到vi有弧,则dist[i]为弧上的权值。否则为正无穷。

为记录路径,须要辅助数组path[n]:path[i]为一个字符串。表示当前所找到的从源点v到终点vi的最短路径。


初始值:若从v到vi有弧,则path[i]为“v vi”。否则为“”。

数组s[n]:存放源点和已经生成的终点。

伪代码:

初始化数组dist,path和s
while (s中元素个数 < n)
{
    在dist[n]中找最小值。其编号为k;
    输出dist[k]和path[k];
    改动数组dist和path;
    将顶点k增加到数组s中。
}

C++实现:

void Dijkstra(MGraph G, int v)
{
    for (int i = 0; i < G.vertexNum; ++i)
    {
        s[i] = 1;
        dist[i] = G.arc[v][i];
        if (path[i] != INF)
            path[i] = G.vertex[v]+G.vertex[i];
        else
            path[i] = "";
    }
    s[v] = 0; //将源点增加集合S中
    dist[v] = 0;
    int num = 1;
    while (num < G.vertexNum)
    {
        int k = 0;
        for (int i = 0; i < G.vertexNum; ++i)
            if (s[i] && (dist[i] < dist[k]))
                k = i;
        cout << dist[k] << ' ' << path[k] <<endl;
        s[k] = 0;
        ++num;
        for (int i = 0; i < G.vertexNum; ++i)
            if (dist[i] > dist[k] + G.arc[k][i])
            {
                dist[i] = dist[k] + G.arc[k][i];
                path[i] = path[k] + G.vertex[i];
            }
    }
}

时间复杂度为O(n^2)。

Floyd算法

Floyd算法用于求每一对顶点之间的最短路径问题。其算法复杂度为O(n^3)。

void Floyd(MGraph G)
{
    for (int i = 0; i < G.vertexNum; ++i)
        for (int j = 0; j < G.vertexNum; ++j)
        {
            dist[i][j] = G.arc[i][j];
            if (dist[i][j] != INF)
                path[i][j] = G.vertex[i]+G.vertex[j];
            else
                path[i][j] = "";
        }
    for (int k = 0; k < G.vertexNum; ++k)
        for (int i = 0; i < G.vertexNum; ++i)
            for (int j = 0; j < G.vertexNum; ++j)
            {
                if (dist[i][k]+dist[k][j] < dist[i][j])
                {
                    dist[i][j] = dist[i][k]+dist[k][j];
                    path[i][j] = path[i][k] + path[k][j];
                }
            }
}

注意k要放在最外层。因为dist[i][j] 依赖于 dist[i][k]和dist[k][j], 所以k小的须要先计算。

SPFA

SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源最短路径的一种算法,它另一个重要的功能是判负环(在差分约束系统中会得以体现),在Bellman-ford算法的基础上加上一个队列优化,降低了冗余的松弛操作,是一种高效的最短路算法。

bool SPFA(MGraph G, int s)
{
    for (int i = 0; i < G.vertexNum; ++i)
    {
        dist[i] = G.arc[v][i];
    }
    dist[v] = 0;
    memset(vis, 0, sizeof(vis));
    deque<int> q;
    q.push_back(s);
    vis[s] = true;
    while (!q.empty())
    {
        int k = q.front();
        q.pop_front();
        vis[k] = false;
        for (int i = 0; i < G.vertexNum; ++i)//存在负权的话。就须要创建一个COUNT数组,当某点的入队次数超过vertexNum(顶点数)返回。
        {
            if (dist[i] > dist[k] + G.arc[k][i])
            {
                dist[i] = dist[k] + G.arc[k][i];
                if (!vis[i])
                {
                    q.push_back(i);
                    vis[i] = true;
                }
            }
        }
    }
}

拓扑排序

參加另一篇博文:

拓扑排序算法的实现——Kahn算法及基于dfs的算法

结语

从前阵子開始,我就打算好好梳理一下学过的基础算法。一来为接下来的面试做准备,二来能够跟大家分享自己学到的知识。

最近比較忙。所以说好的【每日算法】没能做到每日更新。

今天匆匆忙忙将几个图算法回顾了一遍。上面的代码也大多数是伪代码。或者是用C++实现的大体思路(没有经过測试,请见谅。)。

接下来可能临时停更,等到过阵子忙完再好好写几篇质量高一点的博文~


每天进步一点点,Come on!

(●’◡’●)

本人水平有限,如文章内容有错漏之处,敬请各位读者指出,谢谢。

posted on 2017-07-24 16:24  ljbguanli  阅读(295)  评论(0编辑  收藏  举报