数据结构(C语言版 严蔚敏著)——图

图的定义

· 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示

  为:G(V,E)。其中G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

    -在图中数据元素称之为顶点(Vertex)。

    -顶点集合要有穷非空。

    -任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以空。

· 无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Egde),

  用无序偶(Vi,Vj)来表示。


· 上图G1是一个无向图,G1={V1,E1},其中

    -V1={A,B,C,D},

扫描二维码关注公众号,回复: 169160 查看本文章

    -E1={(A,B),(B,C),(C,D),(D,A),(A,C)}


· 有向边:若从顶点Vi到Vj的边有方向,则称这条边为有向边,也成为弧(Arc),

  用有序偶<Vi,Vj>来表示,Vi称为弧尾,Vj称为弧头。


· 上图G2是一个有向图,G2={V2,E2},其中

    -V2={A,B,C,D}

    -E2={<B,A>,<B,C>,<C,A>,<A,D>}


· 简单图:在图结构中,若不存在顶点到其自身的边,且同一条边不重复

  出现 ,则称这样的图为简单图。

· 以下两个则不属于简单图:



· 无向完全图:在无向完全图中,如果任意两个顶点之间都存在边,则称

  该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。



· 有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的

  两天弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)条边。



· 稀疏图和稠密图:这里的稀疏和稠密是模糊的概念,都是相对而言,通常认为边或

  弧数小于n*logn(n是顶点的个数)的图称为稀疏图,反之称为稠密图。

· 有些图的边或弧带有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight),

  带权的图通常称为网(Network)。



· 假设有两个图G1=(V1,E1)和G2=(V2,E2),如果V2属于V1,E2属于E1,则

  称G2为G1的子图(Subgraph)。



图的顶点与边之间的关系

· 对于无向图G=(V,E),如果边(V1,V2)属于E,则称顶点V1和V2互为邻接点(Adjacent),即V1和V2相

  邻接。边(V1,V2)依附(incident)于顶点V1和V2,或者说(V1,V2)与顶点V1和V2相关联。

· 顶点V的度(Degree)是和V相关联的边的数目,记为TD(V),如下图,顶点A与B互为邻接点,

  边(A,B)依附于顶点A与B上,顶点A的度为3。



· 对于有向图G=(V,E),如果有<V1,V2>属于E,则称顶点V1邻接到顶点V2,顶点V2邻接自顶点V1。

· 以顶点V为头的弧的数目称为V的入度(InDegree),记为ID(V),以V为尾的弧的数目称为

  V的出度(OutDegree),记为OD(V),因此顶点V的度为TD(V)=ID(V)+OD(V)

· 下图顶点A的入度是2,出度是1,所以顶点A的度是3。



路径

· 无向图G=(V,E)中从顶点V1到顶点V2的路径。

· 下图用红线列举了从顶点B到顶点D的四种不同路径:


· 如果G是有向图,则路径也是有向的。

· 下图用红线列举顶点B到顶点D的两种路径,而顶点A到顶点B就不存在路径:


· 路径的长度是路径上的边或弧的数目。

· 第一个顶点到最后一个顶点相同的路径称为回路或者环(Cycle)。

· 序列中顶点不重复出现的路径称为 简单路径,除了第一个顶点和最后一个顶点

  之外,其余顶点不重复出现的回路,称为简单回炉或简单环。

· 下图左侧是简单环,右侧 不是简单环:


连通图

· 在无向图G中,如果匆匆顶点V1到顶点V2有路径,则称V1和V2是联通的,如果

  对于图中任意两个顶点Vi和Vj都是连通的,则称G是连通图。


· 无向图中的极大连通子图称为连通分量

· 注意以下概念:

    -首先要是子图,并且子图是要连通的。

    -连通子图含有极大顶点数。

    -具有极大顶点数的连通子图包含依附于这些顶点的所有边。


· 在有向图G中,如果对于每一队Vi到Vj都存在路径,则称G是强连通图。

· 有向图中的极大强连通子图称为有向图的强连通分量

· 下图左侧并不是强连通图,右侧是。并且右侧是左侧的极大强连通子图,

  也是左侧的强连通分量。


· 最后我们再来看连通图的生成树定义

· 所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个

  顶点,但只有足以构成一棵树的n-1条边。




· 如果一个有向图恰有一个顶点入度为0,其余顶点入度均为1,则是一棵有向树。

左边的有向图,能拆分成右边两棵有向树。



图的存储结构

· 因为任意两个顶点之间都可能村长联系,因此无法以数据元素在内存中的物理

  位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)。

· 如果用多重链表来描述倒是可以做到,但是纯粹用多重链表导致的浪费是无法

  想象的(如果各个顶点的度数相差太大,就会造成巨大的浪费)。

邻接矩阵(无向图)

· 考虑到图是由顶点和边或弧两部分组成,自然会用到两个结构体来存储。

· 顶点因为不区分大小、主次,所以用一个一维数组来存储。

· 边或弧由于是顶点与顶点之间的关系,可以考虑二维数组来存储。


· 图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点

  信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。


· vertex[4]={v0,v1,v2,v3},边数组arc[4][4]为对称矩阵(由于无向图不分先后)

  0表示不存在顶点间的边,1表示顶点间存在边。

· 对称矩阵:所谓对称矩阵就是n阶矩阵的元满足a[i][j]=a[j][i](0<=i,j<=n)。

· 有了这个二维数组组成的对称矩阵,可以很容易地知道图中的信息:

    -要判定任意两顶点是否有边无边就非常容易。

    -某个顶点的度就为这个顶点Vi在邻接矩阵中第i行(列)的元素之和。

    -顶点Vi的所有邻接点就是矩阵中第i行元素扫描一遍,为1就是邻接点。

邻接矩阵(有向图)


· 可见顶点数组vertex[4]={V0,V1,V2,V3},弧树组arc[4][4]也是一个矩阵,但因为是有

  向图,所以这个矩阵并不对称,例如由V1到V0有弧,得到arc[1][0]=1,而V0到V1

  没有弧,因此arc[0][1]=0。

· 关于入度和出度,顶点V1的入度为1,整合是第V1列各数之和,

  顶点V1的出度为2,正好是第V1行的各数之和。

邻接矩阵(网)


· 当i=j时书上是用∞表示,有的地方用0表示

· 这里“∞”表示一个计算机允许的,大于所有边上权值的值(int最大值65535)。

图的数组表示法,结构体:

#define INFINITY 65535      //表示无穷大-->在带权的图中用到,即网
#define MAX_VERTEX_NUM 20   //图的最大顶点数
#define MAX_INFO 20         //最大信息数
typedef char InfoType;     //附加信息类型
typedef int VRType;        //顶点关系类型
typedef int VertexType;    //顶点数据类型
//图的种类:有向图,有向网,无向图,无向网
typedef enum {
    DG, DN, UDG, UDN
} GraphKind;
typedef struct ArcCell {
    VRType adj;     //顶点关系类型,对无权图用1或0表示是否相邻
    //对带权图,则为权值类型
    InfoType *info; //附加信息指针
} ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

typedef struct {
    VertexType vexs[MAX_VERTEX_NUM];//顶点向量
    AdjMatrix arcs;     //邻接矩阵
    int vexnum, arcnum;  //当前顶点数和弧数
    GraphKind kind;     //图的种类
} MGraph;

邻接表(无向图)

· 我们可以发现,对于边数相对较少的图,上面的邻接矩阵是存在巨大浪费的。如下图


· 因此可以采用数组和链表组合起来存储,这里称为邻接表。

· 邻接表的处理方法是这样的:

    -图中顶点用一个一维数组存储。

    -图中每个顶点Vi的所有邻接点构成一个线性表,由于个数不确定,所以用单链表存储。


邻接表(有向图)

· 类似地,下面是把顶点当弧尾建立的邻接表,这样容易得到每个顶点的出度。


· 有时为了确定顶点的入度或以顶点为弧头的弧,可以建立一个有向图的逆邻接表。


邻接表(网)

· 对于带权值的网图,可以在边表结点定义中再增加一个数据域来存储权值即可。


图的邻接表存储,结构体

#define MAX_VERTEX_NUM 20
typedef char InfoType;     //附加信息类型
typedef int VertexType;    //顶点数据类型
typedef struct ArcNode{
    int adjvex;             //该弧所指向的顶点位置
    struct ArcNode *nextarc;//下个结点
    InfoType *info;         //当前结点(弧)的信息
}ArcNode;
typedef struct VNode{
    VertexType data;//顶点信息
    ArcNode *firstarc;//指向第一个依附于该顶点弧的指针
}VNode,*AdjList[MAX_VERTEX_NUM];
typedef struct {
    AdjList vertices;
    int vexnum,arcnum;//图的当前顶点数和弧数
    int kind;        //图的种类标志
}ALGraph;

十字链表(是对于有向图来说的,关心出入度问题,所以把两个邻接表结合一起)

· 邻接表固然优秀,但也有不足,例如对有向图的处理上,有时需要再建立一个逆邻接表。

· 这时候就出现了十字链表。

· 为此重新定义顶点表结构结点结构:


· 接着重新定义边表结点结构:



· 可以发现,蓝线是邻接表的指向,红线是逆邻接表的指向,是把他们合起来了。

· 十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到Vi为

  尾的弧,也容易找到以Vi为头的弧,因而容易求得顶点的出度和入度。

· 十字链表除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的。

  因此,在有向图的应用中,十字链表也是非常好的数据结构模型。

//十字链表
#define MAX_VERTEX_NUM 20
typedef char InfoType;     //附加信息类型
typedef int VertexType;    //顶点数据类型
typedef struct ArcBox{
    int tailvex,headvex;    //该弧的尾和头顶点的位置
    struct ArcBox *hlink,*tlink;//分别为弧头相同回合弧尾相同的弧的链域
    InfoType *info;     //该弧相关信息的指针
}ArcBox;
typedef struct VexNode{
    VertexType data;
    ArcBox *firstin,*firstout;//分别指向该顶点第一条弧和出弧
}VexNode;
typedef struct {
    VexNode xlist[MAX_VERTEX_NUM];//表头向量
    int vexnum,arcnum;      //有向图的当前顶点数和弧数 
}OLGraph;

邻接多重表(对无向图而言)

· 如果在无向图的应用中,关注的重点是顶点的话,那么邻接表是不错的选择,但如果更挂关注的是边的操作,

  比如对已经访问过的边做标记,或者删除某一条边等操作,邻接表就显得不方便了(如下图)


· 因此,这里仿造十字链表的方式,对边表结构进行改装,重新定义的边表结构如下:


· 其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。

  iLink指向依附点iVex的下一条边,jLink指向依附顶点jVex的下一条边。

· 也就是说在邻接多重表里边,边表存放的是一条边,而不是一个顶点。


//无向图的邻接多重表存储结构
#define MAX_VERTEX_NUM 20
typedef char InfoType;     //附加信息类型
typedef int VertexType;    //顶点数据类型
typedef enum {unvisited,visited}VisitIf;
typedef struct EBox{
    VisitIf mark;   //访问标记
    int ivex,jvex;  //该边依附的两个顶点的位置
    struct EBox *ilink,*jlink;//分别指向依附这两个顶点的下一条边
    InfoType *info; //该边信息指针
}EBox;
typedef struct VexBox{
    VertexType data;
    EBox *firstedge;//指向第一条依附该顶点的边
}VexBox;
typedef struct {
    VexBox adjmulist[MAX_VERTEX_NUM];
    int vexnum,edgenum;//无向图的当前顶点数和边数
}AMLGraph;

图的遍历(这里以邻接矩阵表示)

· 对于图的遍历,因为它的任一顶点都可以和其余的所有顶点相邻接,

  因此极有可能村长重复走过某个顶点或漏了某个顶点的遍历过程。

· 如果要避免这种情况,就需要合理科学地遍历,常见的有两种遍历:

  深度优先遍历和广度优先遍历。

深度优先遍历

· 也称为深度优先搜索,简称为DFS。

· 从图的某一点按照某个原则进行深度遍历,把遍历过的点作一个标记,在一个分支上

  遍历到底后,逐个顶点退回,遍历其他分支,直到退回到原点,遍历完毕。

· 类似于树的先根遍历,是树的先根遍历的推广。


假设先从A遍历


代码实现:

int FirstAdjVex(MGraph G, int v) {
    //返回v(序号)的第一个相邻节点(序号)
    if (v > G.vexnum || v < 0)
        return -1;
    int i, j;
    j = 0;
    //如果是网
    if (G.kind == DN || G.kind == UDN)
        j = INFINITY;
    for (i = 0; i < G.vexnum; i++)
        if (G.arcs[v][i].adj != j)
            return i;
    return -1;
}

int NextAdjVex(MGraph G, int v, int w) {
    //w是v的相邻节点,返回v相对w的下一个节点的序号,否则返回-1
    int i, j;
    j = 0;
    //如果为网
    if (G.kind == DN || G.kind == UDN)
        j = INFINITY;
    //两顶点不相邻
    if (G.arcs[v][w].adj == j)
        return -1;
    //从w之后的结点开始
    for (i = w + 1; i < G.vexnum; i++)
        if (G.arcs[v][i].adj != j)
            return i;
    return -1;
}


int visited[MAX_VERTEX_NUM];//访问标记数组

void DFS(MGraph G, int v) {
    //从第v个顶点出发递归地深度优先遍历图G
    visited[v] = 1;
    printf("%d", G.vexs[v]);//访问第v个顶点
    for (int w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w))
        if (!visited[w])//对未访问的邻接顶点w递归调用DFS
            DFS(G, w);
}

void DFSTraverse(MGraph G) {

    for (int i = 0; i < G.vexnum; ++i) {
        visited[i] = 0;   //访问标志数组初始化
    }
    for (int j = 0; j < G.vexnum; ++j) {
        if (!visited[j])
            DFS(G, j);   //对未访问的顶点调用DFS
    }
}
int main() {
    MGraph G;
    CreateGraph(G);
    DFSTraverse(G);
    return 0;
}

广度优先遍历

· 又称为广度优先搜索,简称BFS。


· 要实现广度优先遍历,可以利用队列来实现。

· 访问A后,把与A相邻的结点入队列,A标记已入过列。访问B,把B相邻的C,I,G入列

  并标记已入列。访问F,把F相邻的G,E入列,因为G已被标记,所以只要E入列,依次类推。


代码实现:

void BFSTraverse(MGraph G) {
    //按广度优先非递归遍历图G,使用辅助队列Q和访问标志数组visited
    int v, w;
    VertexType u;
    LinkQueue Q;
    for (v = 0; v < G.vexnum; ++v)//标志数组初始化
        visited[v] = 0;
    IniteQueue(Q);  //生成队列
    for (v = 0; v < G.vexnum; ++v)
        if (!visited[v]) {
            visited[v] = 1;
            printf("%d", G.vexs[v]);
            EnQueue(Q, v);   //v入队列
            while (!QueueEmpty(Q)) {
                DeQueue(Q, u);   //队头元素出队并至为u
                for (w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w))
                    if (!visited[w]) {    //w为u的尚未访问的邻接点
                        visited[w] = 1;
                        printf("%d", G.vexs[w]);
                        EnQueue(Q, w);
                    }
            }
        }
}


最小生成树

· 普里姆算法:寻找起始顶点邻接的最短边,并入集合,再寻找邻接的最短边 ,不能有环。




· 克鲁斯卡尔算法,寻找最短边,不能有环。




直接上两种算法的代码:

//普里姆算法
struct {
    VertexType adjvex;
    VRType lowcost;
}closedge[MAX_VERTEX_NUM];
int minmum(MGraph G){

    int min=INFINITY;
    int index=-1;
    for (int i = 0; i < G.vexnum; ++i) {
        //最小值大于 该边的权值 且 该边的权值不为 0
        if(min>closedge[i].lowcost&&closedge[i].lowcost!=0){
            min=closedge[i].lowcost;
            index=i;
        }
    }
    return index;
}

void MiniSpanTree_PRIM(MGraph G,VertexType u){
    //书本P174表格
    int k=LocateVex(G,u);//该顶点位置      k=0
    for (int j = 0; j < G.vexnum; ++j)//辅助数组初始化
        if(j!=k)//相当于u顶点 边的信息
            closedge[j]={u,G.arcs[k][j].adj};//{adjvex,lowcost}
    closedge[k].lowcost=0;//初始,U={u},把u并入集合
    for (int i = 1; i < G.vexnum; ++i) {//选择其余G.vexnum-1个顶点
        k=minmum(G);    //求出T的下一个结点,第k顶点
        printf("%d-->%d\n",closedge[k].adjvex,G.vexs[k]);//输出 生成树边
        closedge[k].lowcost=0;//第k顶点并入U集
        for(int j=0;j<G.vexnum;++j)
            if(G.arcs[k][j].adj<closedge[j].lowcost)
                //新顶点并入U后重新选择最小边
                closedge[j]={G.vexs[k],G.arcs[k][j].adj};
    }
}


//克鲁斯卡尔算法,自己写的
typedef struct {
    int beginNum;   //存放顶点下标
    int endNum;
    VertexType begin;//顶点名称
    VertexType end;
    VRType weight;//权重
}Edge[MAX_EDGE_NUM];//边集数组

void MiniSpanTree_Kruskal(MGraph G){
    Edge edge[G.arcnum];
    int set[G.vexnum];

    //初始化辅助数组
    for (int i = 0; i < G.vexnum; ++i) {
        set[i]=i;//{0,1,2,3,4,5,....}
    }
    int k=0;
    //得到边集数组
    for (int i = 0; i < G.vexnum; ++i)
        for (int j = 1; j < G.vexnum; ++j)
            if (i<j&&G.arcs[i][j].adj!=INFINITY){
                edge[k]->beginNum=i;
                edge[k]->endNum=j;
                edge[k]->begin=G.vexs[i];
                edge[k]->end=G.vexs[j];
                edge[k]->weight=G.arcs[i][j].adj;
                k++;
            }
    VertexType temp;
    VRType adj;
    //边集数组,按权重从小到大
    for (int i = 0; i < G.arcnum; ++i)
        for (int j = i+1; j < G.arcnum; ++j)
            if(edge[i]->weight>edge[j]->weight){
                temp=edge[i]->begin;
                edge[i]->begin=edge[j]->begin;
                edge[j]->begin=temp;
                temp=edge[i]->end;
                edge[i]->end=edge[j]->end;
                edge[j]->end=temp;
                adj=edge[i]->weight;
                edge[i]->weight=edge[j]->weight;
                edge[j]->weight=adj;
                k=edge[i]->beginNum;
                edge[i]->beginNum=edge[j]->beginNum;
                edge[j]->beginNum=k;
                k=edge[i]->endNum;
                edge[i]->endNum=edge[j]->endNum;
                edge[j]->endNum=k;
            }
    int a,b;
    for (int i = 0; i < G.arcnum; ++i) {
        if(set[edge[i]->beginNum]!=set[edge[i]->endNum]){
            //如果辅助数组中,对应数字不同
            //输出该边,修改辅助数组中数字
            //如果set[edge[i]->beginNum]为a
            //set[edge[i]->endNum]为b
            //那么把辅助数组中所有为b的改为a
            printf("%d->%d  weight:%d\n",edge[i]->begin,edge[i]->end,edge[i]->weight);
            a=set[edge[i]->beginNum];
            b=set[edge[i]->endNum];
            for (int j = 0; j < G.vexnum; ++j)
                if(set[j]==b)
                    set[j]=a;
        }
    }
}

· 普里姆算法的时间复杂度为O(n^2),与网中的边数无关,适用于求边稠密的最小生成树。

· 库鲁斯卡尔的时间复杂度为O(eloge),e为网中边的数目,适合于求边稀疏的最小生成树。


拓扑排序

· 一个无环的有向图称为无环图(Directed Acyclic Graph),简称DAG图。

· 所有的工程或者某种流程都可以分为若干个小的工程或者阶段,

  称这些小的工程或阶段为"活动"。

· 在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,

  这样的有向图为顶点表示活动的网,称之为AOV网。

· AOV网中的弧表示活动之间存在某种制约关系,且不能存在回路。

· 拓扑序列:设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2,...,Vn

  满足若从顶点Vi到Vj有一条路径,则在顶点序列中顶点Vi必在顶点Vj之前。则称

  这样的顶点序列为拓扑序列。

· 拓扑排序:就是对一个有向图构造拓扑序列的过程。



· 拓扑序列(其中一种):

1,13,4,8,15,5,2,3,10,11,12,7,6,9

注:前面的必须指向后面的。

· 对AOV网进行拓扑排序的方法和步骤如下:

    -从AOV网中选择一个没有前驱的顶点(入度为0)并输出他。

    -从网中删去该顶点,并且删去从该顶点发出的全部有向边。

    -重复上述两步,直到剩余网中不再存在没有前驱的顶点为止。

· 由刚才我们那幅AOV网图,我们可以用邻接表(因为需要删除顶点,所以

  选择邻接表会更加方便)数据结构表示:


代码实现:

int TopologicalSort(ALGraph G){
    //有向图G采用邻接表存储结构
    //若G无回路,则输出G的顶点的一个拓扑序列并返回1,否则0
    SqStack S;
    ArcNode *p;
    int i,k;
    InitStack(S);
    for ( i = 0; i < G.vexnum; ++i) {
        if(G.vertices[i]->in==0)
            Push(S,i);  //入度为0的顶点下标入栈
    }
    int count=0;    //对输出顶点计数
    while (!StackEmpty(S)){
        Pop(S,i);   //输出i号顶点,并计数
        printf("%d,%d\n",i,G.vertices[i]->data);
        ++count;
        for(p=G.vertices[i]->firstarc;p;p=p->nextarc){
            k=p->adjvex;    //对i号顶点的每个邻接点的入度减1
            if(!(--G.vertices[k]->in))  //若入度为0,则入栈
                Push(S,k);
        }
    }
    if(count<G.vexnum)
        return 0;
    else
        return 1;
}

算法时间复杂度:

    -对一个具有n个 顶点,e条边的网来说,初始建立入度为零的顶点栈,

      要检查所有顶点一次,执行时间为O(n)。

    -排序中,若AOV网无回路,则每个顶点入出栈各一次次,每个表结点

      被检查一次,因而执行时间是O(n+e)。

    -所以整个算法时间复杂度是O(n+e)。


关键路径

· AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,

  用边上的权值表示活动的持续时间,这种有向图的边表示活动的网称为AOE网。

· 把AOE网中入度为零的顶点称为始点或源点,出度为零的顶点称为终点或汇点。


· 路径长度最长的路径叫做关键路径。

    -etv(Earliest Time Of Vertex):事件最早发生时间,就是顶点的最早发生时间。

    -ltv(Latest Time Of Vertex):事件最晚发生时间,就是每个顶点对应的事件最晚

      需要开始的时间,如果超出此时间将会延误整个工期。

      从后面往前看,减去这个工时就为最晚发生时间

      例如:C8的etv为12,C6的ltv为12-4=8

    -ete(Earliest Time Of Edge):活动的最早开工时间,就是弧的最早发生时间。

    -lte(Latest Time Of Edge):活动的最晚发生时间,就是不推迟工期的最晚开工时间。

      类似ltv算法,也要倒着过来。



代码实现:

//拓扑排序
int TopologicalSort(ALGraph G) {
    //有向图G采用邻接表存储结构
    //若G无回路,则输出G的顶点的一个拓扑序列并返回1,否则0
    SqStack S;
    ArcNode *p;
    int i, k;
    InitStack(S);
    for (i = 0; i < G.vexnum; ++i) {
        if (G.vertices[i]->in == 0)
            Push(S, i);  //入度为0的顶点下标入栈
    }
    int count = 0;    //对输出顶点计数
    while (!StackEmpty(S)) {
        Pop(S, i);   //输出i号顶点,并计数
        printf("%d,%d\n", i, G.vertices[i]->data);
        ++count;
        for (p = G.vertices[i]->firstarc; p; p = p->nextarc) {
            k = p->adjvex;    //对i号顶点的每个邻接点的入度减1
            if (!(--G.vertices[k]->in))  //若入度为0,则入栈
                Push(S, k);
        }
    }
    if (count < G.vexnum)
        return 0;
    else
        return 1;
}

//关键路径
int ve[];//顶点最早开始时间
int vl[];//顶点最晚开始时间

int TopologicalOrder(ALGraph G, SqStack &T) {
    //有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve(全局变量)
    //T为拓扑序列顶点栈,S为零入度顶点栈
    //若G无回路,则用栈T返回G的一个拓扑序列,返回1,否则0
    SqStack S;
    ArcNode *p;
    int i, k, j;
    InitStack(S);
    InitStack(T);
    for (i = 0; i < G.vexnum; ++i) {
        if (G.vertices[i]->in == 0)
            Push(S, i);  //入度为0的顶点下标入栈
        ve[i] = 0;    //初始化
    }
    int count = 0;
    while (!StackEmpty(S)) {
        Pop(S, j);      //j号顶点入T栈并计数
        Push(T, j);
        ++count;
        for (p = G.vertices[j]->firstarc; p; p = p->nextarc) {
            k = p->adjvex;  //对j号顶点的每个邻接点的入度减1
            if (!(--G.vertices[k]->in)) //若入度减为0,则入栈
                Push(S, k);
            //当前出栈j号顶点的 最早发生时间+ 当前循环连接k号顶点的权值 >k号顶点最早 发生时间
            if (ve[j] + *(p->info) > ve[k])
                //k号顶点的最早发生时间=出栈顶点号的最早发生时间+与之相连边的权值
                ve[k] = ve[j] + *(p->info);
        }
    }
    if (count < G.vexnum)   //该有向网有回路
        return 0;
    else
        return 1;
}

int CriticalPath(ALGraph G) {
    //G为有向网,输出G的各项关键活动
    int j, k, dut, ee, el;
    char tag;
    SqStack T;
    ArcNode *p;
    if (!TopologicalOrder(G, T))
        return 0;
    for (int i = 0; i < G.vexnum; ++i)  //初始化顶点事件的最迟发生时间
        vl[i] = ve[G.vexnum - 1];
    while (!StackEmpty(T))      //按拓扑逆序求各顶点的vl值
        //依附于j号顶点的第一条边(包含边权重和指向的顶点)
        for (Pop(T, j), p = G.vertices[j]->firstarc; p; p = p->nextarc) {
            //该弧指向顶点的位置k
            k = p->adjvex;
            //该弧权重
            dut = *(p->info);
            //k的最迟发生时间-该弧权重<初始顶点最迟发生时间
            if (vl[k] - dut < vl[j])
                vl[j] = vl[k] - dut;
        }
    for (j = 0; j < G.vexnum; ++j)
        for (p = G.vertices[j]->firstarc; p; p = p->nextarc) {
            k = p->adjvex;
            dut = *(p->info);
            //注意ve,vl是顶点的最早和最晚发生时间
            //ee,el才是弧的最早和最晚发生时间
            //关键路径是对弧而言
            ee = ve[j];//该弧最早发生时间=该弧起始顶点最早发生时间
            el = vl[k] - dut;//该弧最晚发生时间=该弧结束顶点最晚发生时间-权重
            //带* 为关键
            tag = (ee == el) ? '*' : '';
            printf("起始顶点:%d 结束顶点:%d 该弧权重:%d 最早开始时间:%d 最晚开始时间:%d 是否关键:%c", j, k, dut, ee, el, tag);
        }
}

· 这两种算法的时间复杂度均为O(n+e),前一种算法的常数因子要小些。由于计算弧的活动

  最早开始时间和最迟开始时间的复杂度为O(e),所以总的求关键路径的时间复杂度O(n+e)


最短路径

· 在网图和非网图中,最短路径的含义是不同的。

    -网图是两顶点经过的边上权值之和最少的路径。

    -非网图是两顶点之间经过的边数最少的路径。可以看成网图,权值都为1。

· 把路径起始的第一个顶点称为源点,最后一个顶点称为终点。

· 关于最短路径,分为两种算法:

迪杰斯特拉算法

git图演示


    -初始所有顶点路径长为∞,设源点为0。

    -搜索该源顶点的路径,依次标上花费路径长度,然后归入S集(表示已遍历)

    -逐个搜索除S集合外所有已经标有花费路径长度的顶点,无需遍历归入S集顶点

      遍历到其他顶点,若花费小于其标值,则修改之,遍历完成后,并入S集。

    -重复上步步骤。

//最短路径,迪杰斯特拉算法
typedef int Patharc[MAX_VERTEX_NUM];//用于存储最短路径下标的数组
typedef int ShortPathTable[MAX_VERTEX_NUM];//用于存储到各点最短路径的权值和

void ShortestPath_DIJ(MGraph G, int V0, Patharc &P, ShortPathTable &D) {
    int v, w, min, k = NULL;
    int final[MAX_VERTEX_NUM];  //final[w]=1 表示已经求得顶点V0到Vw的最短路径
    //初始化数据
    for (v = 0; v < G.vexnum; v++) {
        final[v] = 0;             //全部顶点初始化为未找到最短路径
        D[v] = G.arcs[V0][v].adj; //将与V0点有连线的顶点加上权值
        P[v] = 0;                 //初始化路径数组P为0
    }
    D[V0] = 0;    //V0至V0路径为0
    final[V0] = 1;//V0至V0不需要求路径
    //开始主循环,每次求得V0到某个v顶点的最短路径
    for (v = 1; v < G.vexnum; v++) {
        min = INFINITY;
        //循环后得到一个已知最短路径的顶点,作为发散修正的顶点
        for (w = 0; w < G.vexnum; w++)
            if (!final[w] && D[w] < min) {
                k = w;
                min = D[w];
            }
        final[k] = 1; //将目前找到的最近的顶点置1
        //修正当前最短路径 及距离
        //从该顶点发散出去的各个顶点距离修正
        for (w = 0; w < G.vexnum; w++)
            //如果经过v顶点的路径比现在这条路径的长度短的话,更新
            if (!final[w] && (min + G.arcs[k][w].adj < D[w])) {
                D[w] = min + G.arcs[k][w].adj;//修改当前路径长度
                P[w] = k;     //存放前驱顶点
            }

    }
}

· 第一个FOR循环的时间复杂度是O(n),第二个FOR循环共进行n-1次,

  每次执行时间是O(n),所以总的时间复杂度是O(n^2)。

· 如果用带权的邻接表作为有向图的存储结构,则虽然修改D的时间可以减少,

  但由于在D向量中选择最小分量的时间不变,所以总时间仍为O(n^2)。

· 如果只希望找到源点到某一个特定终点的最短路径,这个问题和求

 源点到其他所有顶点的最短路径一样复杂,其时间复杂度也是O(n^2)。


佛洛依德算法

· 从任意阶段i到任意节点j的最短路径只有2种可能,一种是直接从i到j另一种是

  从i经过若干个节点k到j。所以,假设Dis(i,j)为节点u到节点v的最短路径的距离,

  对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,

  证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),

  这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。

· 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。

· 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。

十字交叉法

方法:两条线,从左上角开始计算一直到右下角如图所示

给出矩阵,其中矩阵A是邻接矩阵,而矩阵Path记录u,v两点之间最短路径所必须经过的点。






算法实现:

//最短路径,佛洛依德算法
typedef int PathMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef int DistancMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

void ShortestPath_FLOYD(MGraph G, PathMatrix &P, DistancMatrix &D) {
    int v, w, k;
    //初始化D和P
    for (v = 0; v < G.vexnum; v++)
        for (w = 0; w < G.vexnum; w++) {
            D[v][w] = G.arcs[v][w].adj;
            P[v][w] = -1;
        }
    //算法核心
    for (k = 0; k < G.vexnum; k++)
        for (v = 0; v < G.vexnum; v++)
            for (w = 0; w < G.vexnum; w++)
                if (D[v][w] > D[v][k] + D[k][w]) {
                    D[v][w] = D[v][k] + D[k][w];
                    P[v][w] = P[v][k];
                }
}
· 此算法时间复杂度为O(n^3)。



猜你喜欢

转载自blog.csdn.net/super_sloppy/article/details/79765880
今日推荐