图及其应用

定义

多对多的关系。

无向图:每条边都没有方向

有向图:每条边都有方向

完全图:任意两个点都有一条边相连(无向完全图:n个顶点,有n(n-1)/2条边;有向完全图:n个顶点,有n(n-1)条边)

稀疏图:有很少边(无向图)或弧(有向图)的图(e<nlogn)

邻接:有边/弧相连的两个顶点之间的关系。

​ 存在(vi , vj),则称vi和vj互为邻接点

​ 存在<vi , vj>,则称vi邻接到, vj邻接于vi

顶点的度:与该顶点相关联的边的数目。

有向图顶点的度等于该顶点的入度出度之和

顶点v的入度是以v为终点的有向边的条数。

顶点v的出度是以v为始点的有向边的条数。

连通图(强连通图:有向图):从无(有)向图G=(V , {E})中,若对任何两个顶点v、u都存在从v到u的路径,则称G为连通图(强连通图)

image-20230527093719260image-20230527093741420

存储结构

图的存储结构主要使用的分为两种:邻接矩阵、邻接表。

邻接矩阵

无向图的邻接矩阵表示法

image-20230527100936023

有向图的邻接矩阵表示法

image-20230527101137940

注:在有向图的邻接矩阵中,

i行含义:以结点vi为尾的弧(即出度边);

i列含义:以结点vi为头的弧(即入度边)。

顶点i的度 = 第i行元素之和 + 第i列元素之和

代码实现

#define MVNum 100                 //最大顶点数
typedef struct{ //图的结构体定义
    char vexs[MVNum];           //存放顶点的一维数组
    int arcs[MVNum][MVNum];     //邻接矩阵
    int vexnum,arcnum;          //图的顶点数和边数
}MGraph;

void CreatMGraph(MGraph *G){
    scanf("%d %d",&G->vexnum,&G->arcnum); // 输入顶点数和边数
    getchar();
    for(int i = 0;i<G->vexnum;i++){
        scanf("%c",&G->vexs[i]);
    }
    for(int i = 0;i<G->vexnum;i++){
        for(int j = 0;j<G->vexnum;j++){
            G->arcs[i][j] = 0; // 初始化矩阵为0
        }
    }
    getchar();
    for(int i = 0;i<G->arcnum;i++){ // 构造邻接矩阵
        char v1,v2;
        scanf("%c %c",&v1,&v2);
        getchar();
        int x = locate(G,v1);
        int y = locate(G,v2);
        G->arcs[x][y] = 1; // 有路径则矩阵为1
        G->arcs[y][x] = 1; // 无向图具有对称性,路径具有互通性(有向图则去除这行代码)
    }
}

int locate(MGraph *G,char v){ // 查找图中顶点v,存在则返回下标
    for(int i = 0;i<G->vexnum;i++){
        if(v == G->vexs[i]){
            return i;
        }
    }
    return -1; // 否则返回-1
}

邻接表

image-20230527131721524

无向图的邻接表如此实现,其特点为:

  1. 邻接表不唯一
  2. 若无向图中有n个结点、e条边,则其邻接表需要n个头结点和2e个表结点,适宜存储稀疏图。
  3. 无向图中顶点vi的度为第i个单链表中的节点数。

代码实现

#define MVNum 100                                 //最大顶点数
typedef struct ArcNode{                        //表结点
    int adjvex;                                    //邻接点的位置
    struct ArcNode *nextarc;      //指向下一个表结点的指针
  }ArcNode;
typedef struct VNode{
   char data;                                    //顶点信息
   ArcNode *firstarc;         //指向第一个表结点的指针
}VNode, AdjList[MVNum];                 //AdjList表示邻接表类型
typedef struct{
    AdjList vertices;              //头结点数组
    int vexnum, arcnum;     //图的顶点数和边数
}ALGraph;

void CreatMGraph(ALGraph &G){ // 使用邻接表创建无向图
	int x, y;
	char v1, v2;
	cin >> G.vexnum >> G.arcnum;
	for(int i=0; i<G.vexnum; i++){
		cin >> G.vertices[i].data;
		G.vertices[i].firstarc=NULL;//初始化 
	}
	for(int i=0; i<G.arcnum; i++){
		cin >> v1 >> v2;
		int x = locate(G,v1);
        int y = locate(G,v2);
		//头插法 
		ArcNode* p = (ArcNode*)malloc(sizeof(ArcNode));
		p->adjvex = y;
		p->nextarc = G.vertices[x].firstarc;
		G.vertices[x].firstarc = p;
		p = (ArcNode*)malloc(sizeof(ArcNode)); // 无向图具有对称性
		p->adjvex = x;
		p->nextarc = G.vertices[y].firstarc;
		G.vertices[y].firstarc = p;
	}
}

int locate(ALGraph G,char v){ // 获取v头结点的下标
    for(int i = 0;i<G.vexnum;i++){
        if(G.vertices[i].data==v){
            return i;
        }
    }
    return -1;
}

邻接矩阵与邻接表的区别

区别/类型邻接矩阵邻接表
唯一确定的无向图唯一(行列号与顶点编号一致)不唯一(链表次序与顶点编号无关)
空间复杂度O(n 2 ^2 2)O(n+e)
用途多用于稠密图多用于稀疏图

遍历

图的遍历主要分为两种:广度优先搜索(DFS)、深度优先搜索(BFS)

:访问过程中可以使用visited数组来记录访问状态(如,被访问过则该坐标下的visited的值为1,否则为0),防止一个顶点被多次重复访问。

深度优先搜索

遍历方法:

  1. 访问图中某一个起始顶点v后,从v出发,访问其任一邻接顶点w1
  2. 再从w1出发,访问与w1邻接但未被访问过的顶点w2
  3. 然后再从w2出发,重复第二步直至到达所有的邻接顶点都被访问过的顶点u为止。
  4. 从u顶点回退一步至刚访问过的顶点,看是否还有其它未被访问的邻接节点。
  5. 如果有,则访问此顶点,重复2、3步。
  6. 如果没有,则重复第4步。
  7. 重复5、6步直至连通图中所有顶点都被访问过为止。

邻接矩阵表示的无向图深度优先搜索(伪代码)

#define MVNum 100                                 //最大顶点数
typedef struct{ //图的结构体定义
    char vexs[MVNum];           //存放顶点的一维数组
    int arcs[MVNum][MVNum];     //邻接矩阵
    int vexnum,arcnum;          //图的顶点数和边数
}MGraph;

void DFS(MGraph G,int v){ // 访问第v个顶点
    cout << v;
    visited[v] = true;
    for(int i = 0;i<G.vexnum;i++){
        if(G.arcs[v][i]!=0 && (!visited[i])){
            dfs(G,i); // i是v的邻接点,如果i未被访问,则递归调用dfs
        }
    }
}

广度优先搜索

遍历方法:

  1. 从图的某一个节点开始,首先依次访问该节点的所有邻接顶点vi1、vi2、vi3,… ,vin
  2. 再按这些顶点被访问的先后次序依次访问它们想邻接的所有未被访问的顶点。
  3. 重复上述步骤直至所有顶点均被访问为止。

按广度优先非递归遍历图(伪代码)

#define MVNum 100                                 //最大顶点数
typedef struct{ //图的结构体定义
    char vexs[MVNum];           //存放顶点的一维数组
    int arcs[MVNum][MVNum];     //邻接矩阵
    int vexnum,arcnum;          //图的顶点数和边数
}Graph;

void BFS(Graph G,int v){ // 按照广度优先非递归遍历连通图G
    cout << v;
    visited[v] = true; // 访问第v个顶点
    InitQueue(Q); // 使用队列辅助遍历,初始化队列Q
    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的尚未访问过的邻接顶点
            cout << w;
            visited[w] = true;
            EnQueue(Q,w); // w进队
        }
    }
}

DFS与BFS算法效率比较:

  1. 空间复杂度相同,都是O(n)(借用了堆栈或队列)
  2. 空间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关。

应用

图的应用分为了主要四个部分:最小生成树、最短路径问题、拓扑排序、关键路径问题。

最小生成树

定义:生成树是指所有顶点均由边连接在一起,但不存在回路的图。

一个图可以有许多棵不同的生成树。

所有生成树都具有以下共同特点:

  • 生成树的顶点个数与图的顶点数相同。
  • 生成树是图的极小连通子图,去掉一条边则非连通。
  • 一个有n个顶点的连通图的生成树有 n-1 条边。
  • 在生成树中再加一条边必然会形成一个回路。

设图G=(V, E)是个连通图,当从图任一顶点出发遍历图 G 时,将
边集E(G)分成两个集合T(G)和B(G)。其中T(G)是遍历图时所经过的边的集合,B(G) 是遍历图时未经过的边的集合。显然,G1(V, T)是图 G
的极小连通子图。即子图G1是连通图 G 的生成树。

两种遍历所形成的生成树:

image-20230527174710707

image-20230527174902268

最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那颗生成树称为该网的最小生成树,也叫做最小代价生成树

构造最小生成树(Minimum Spanning Tree,简称MST)

构造最小生成树的算法有很多,其中多数的算法都利用了MST的性质。其中主要的两种算法,一种是Prim算法、另一种是Kruskal算法。

MST性质:N= (V, E)是一个连通网,U是顶点集V的一个非空子集。若边(u,v)是一条具有最小权值的边,其中u∈U, v∈V-U,则必存在一棵包含边(u, v)的最小生成树。

普里姆(Prim)算法

算法思路:

  1. 设N=(V , E)是连通图,是N上最小生成树中边的集合
  2. 初始化令U={u0},(u0∈V),TE={}。
  3. 在所有u∈V,v∈V-U的边(u , v)∈E中,找一条代价(权值)最小的边(u0 , v0)。
  4. 将(u0 , v0)并入集合TE,同时v0并入U。

代码实现

#include <iostream>
#include <climits>
using namespace std;

#define MVNum 100
#define MaxInt INT_MAX

typedef struct {
    char vexs[MVNum];
    int arcs[MVNum][MVNum];
    int vexnum, arcnum;
} AMGraph;

struct edge {
    char adjvex;
    int lowcost;
} closedge[MVNum];

int LocateVex(AMGraph G, char v) {
    for (int i = 0; i < G.vexnum; ++i) {
        if (G.vexs[i] == v) {
            return i;
        }
    }
    return -1; // 如果未找到对应的顶点,返回-1表示错误
}

void CreateUDN(AMGraph& G) {
    cout << "请输入顶点个数和边个数:" << endl;
    cin >> G.vexnum >> G.arcnum;
    cout << "请输入顶点信息:" << endl;
    for (int i = 0; i < G.vexnum; ++i) {
        cin >> G.vexs[i];
    }
    // 初始化邻接矩阵
    for (int i = 0; i < G.vexnum; ++i) {
        for (int j = 0; j < G.vexnum; ++j) {
            G.arcs[i][j] = MaxInt;
        }
    }
    // 输入边的信息
    cout << "请输入边的信息(起点、终点、权值):" << endl;
    for (int k = 0; k < G.arcnum; ++k) {
        char start, end;
        int weight;
        cin >> start >> end >> weight;
        int i = LocateVex(G, start);
        int j = LocateVex(G, end);
        G.arcs[i][j] = weight;
        G.arcs[j][i] = weight; // 无向图,权值对称
    }
}


int Min(AMGraph G) {
    int i;
    int index = -1;
    int min = MaxInt;
    for (i = 0; i < G.vexnum; ++i) {
        if (!closedge[i].lowcost) {
            min = closedge[i].lowcost;
            index = i;
        }
    }
    return index;
}

void MiniSpanTree_Prim(AMGraph G, char u) {
    int k, j, i;
    char u0, v0;
    k = LocateVex(G, u);
    for (j = 0; j < G.vexnum; ++j) {
        if (j != k) {
            closedge[j].adjvex = G.vexs[k];
            closedge[j].lowcost = G.arcs[k][j];
        }
    }
    closedge[k].lowcost = 0;
    for (i = 1; i < G.vexnum; ++i) {
        k = Min(G);
        u0 = closedge[k].adjvex;
        v0 = G.vexs[k];
        cout << u0 << "->" << v0 << endl;
        closedge[k].lowcost = 0;
        for (j = 0; j < G.vexnum; ++j) {
            if (G.arcs[k][j] < closedge[j].lowcost) {
                closedge[j].adjvex = G.vexs[k];
                closedge[j].lowcost = G.arcs[k][j];
            }
        }
    }
}

int main() {
    AMGraph G;
    CreateUDN(G);
    char u;
    cin >> u;
    MiniSpanTree_Prim(G, u);
    return 0;
}

克鲁斯卡尔(Kruskal)算法

算法思路:

  1. 设连通网N= (V E),令最小生成树初始状态为只有n个顶点而无边的非连通图T=(V { }),
    每个顶点自成一个连通分量。
  2. 在E中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),
    则将此边加入到T中;否则,舍去此边,选取下条代价最小的边。
  3. 依此类推,直至T中所有顶
    点都在同一连通分量上为止。

代码实现

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

#define MVNum 100
#define MaxInt 32767

struct Edge {
    int start;
    int end;
    int weight;
};

typedef struct {
    char vexs[MVNum];
    int vexnum, arcnum;
    vector<Edge> edges;
} AMGraph;

int parent[MVNum]; // 并查集数组

int Find(int x) {
    if (parent[x] == x)
        return x;
    return Find(parent[x]);
}

void Union(int x, int y) {
    int rootX = Find(x);
    int rootY = Find(y);
    if (rootX != rootY)
        parent[rootY] = rootX;
}

bool CompareEdges(Edge a, Edge b) {
    return a.weight < b.weight;
}

void CreateUDN(AMGraph& G) {
    cout << "请输入顶点个数和边个数:" << endl;
    cin >> G.vexnum >> G.arcnum;
    cout << "请输入顶点信息:" << endl;
    for (int i = 0; i < G.vexnum; ++i) {
        cin >> G.vexs[i];
    }
    // 输入边的信息
    cout << "请输入边的信息(起点、终点、权值):" << endl;
    for (int k = 0; k < G.arcnum; ++k) {
        char start, end;
        int weight;
        cin >> start >> end >> weight;
        Edge edge;
        edge.start = start - 'A';
        edge.end = end - 'A';
        edge.weight = weight;
        G.edges.push_back(edge);
    }
}

void MiniSpanTree_Kruskal(AMGraph G) {
    // 初始化并查集数组
    for (int i = 0; i < G.vexnum; ++i) {
        parent[i] = i;
    }
    // 对边按权值进行排序
    sort(G.edges.begin(), G.edges.end(), CompareEdges);

    int edgeCount = 0; // 记录选中的边数
    int treeWeight = 0; // 记录最小生成树的权值

    cout << "最小生成树的边:" << endl;

    for (int i = 0; i < G.arcnum; ++i) {
        int start = G.edges[i].start;
        int end = G.edges[i].end;
        int weight = G.edges[i].weight;
        if (Find(start) != Find(end)) { // 判断边的两个顶点是否属于同一个连通分量
            Union(start, end); // 合并连通分量
            cout << char(start + 'A') << " - " << char(end + 'A') << " : " << weight << endl;
            treeWeight += weight;
            edgeCount++;
            if (edgeCount == G.vexnum - 1) // 边数达到最小生成树的边数上限,停止构造
                break;
        }
    }

    cout << "最小生成树的权值:" << treeWeight << endl;
}

int main() {
    AMGraph G;
    CreateUDN(G);
    MiniSpanTree_Kruskal(G);
    return 0;
}

注:最小生成树可能不是唯一的。


两种算法进行比较

算法名Prim算法Kruskal算法
算法思想选择点选择边
时间复杂度O(n 2 ^2 2) (n为顶点数)O(eloge) (e为边数)
适应范围稠密图稀疏图

最短路径

在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,称为最短路径。与最小生成树不同的是,最短路径上不一定包含 n 个顶点,也不一定包含 n - 1 条边。

两种常见的最短路径问题:单源最短路径、所有顶点间的最短路径。

迪杰斯特拉(Dijkstra)算法 - 单源最短路径

算法思路:

  1. 初始化:先找出从源点v0到各终点vk的直达路径(v0 , vk),即通过一条弧到达的路径。
  2. 选择:从这些路径中找出一条长度最短的路径(v0 , u)。
  3. 更新:然后对其余各条路径进行适当调整:若在图中存在弧(u , vk),且(v0 , u) + (u , vk) < (v0 , vk),则以路径(v0 , u , vk)代替(v0 , vk)。在调整后的各条路径中,再找长度最短的路径,依次类推。

image-20230527195830423image-20230527203447204

代码实现

#include <iostream>
#include <vector>
#include <climits>
using namespace std;

#define MVNum 100
#define MaxInt INT_MAX

typedef char VerTexType;
typedef int ArcType;

typedef struct {
    VerTexType vexs[MVNum];
    ArcType arcs[MVNum][MVNum];
    int vexnum, arcnum;
} AMGraph;

int LocateVex(AMGraph G, VerTexType v) {
    for (int i = 0; i < G.vexnum; ++i) {
        if (G.vexs[i] == v)
            return i;
    }
    return -1; // 未找到对应顶点
}

void CreateUDN(AMGraph& G) {
    cout << "请输入顶点个数和边个数:" << endl;
    cin >> G.vexnum >> G.arcnum;
    cout << "请输入顶点信息:" << endl;
    for (int i = 0; i < G.vexnum; ++i) {
        cin >> G.vexs[i];
    }
    // 初始化邻接矩阵
    for (int i = 0; i < G.vexnum; ++i) {
        for (int j = 0; j < G.vexnum; ++j) {
            if (i == j)
                G.arcs[i][j] = 0;
            else
                G.arcs[i][j] = MaxInt;
        }
    }
    // 输入边的信息
    cout << "请输入边的信息(起点、终点、权值):" << endl;
    for (int k = 0; k < G.arcnum; ++k) {
        char start, end;
        int weight;
        cin >> start >> end >> weight;
        int i = LocateVex(G, start);
        int j = LocateVex(G, end);
        G.arcs[i][j] = weight;
        G.arcs[j][i] = weight; // 无向图的边是双向的
    }
}

void Dijkstra(AMGraph G, int v0, int* dist, int* path) {
    int n = G.vexnum;

    vector<bool> visited(n, false); // 记录顶点是否已被访问
    dist[v0] = 0; // 初始顶点的最短路径为0

    for (int i = 0; i < n; ++i) {
        int minDist = MaxInt;
        int u = -1; // 选取的顶点

        // 选取距离最短且未被访问的顶点
        for (int j = 0; j < n; ++j) {
            if (!visited[j] && dist[j] < minDist) {
                minDist = dist[j];
                u = j;
            }
        }

        if (u == -1)
            break; // 所有顶点已被访问,结束算法

        visited[u] = true; // 标记顶点u为已访问

        // 更新以顶点u为中间顶点的最短路径
        for (int v = 0; v < n; ++v) {
            if (!visited[v] && G.arcs[u][v] != MaxInt && dist[u] + G.arcs[u][v] < dist[v]) {
                dist[v] = dist[u] + G.arcs[u][v];
                path[v] = u;
            }
        }
    }
}

void DisplayPath(AMGraph G, int v0, int dest, int* path) {
    if (dest != v0) {
        DisplayPath(G, v0, path[dest], path);
        cout << " -> ";
    }
    cout << G.vexs[dest];
}

int main() {
    AMGraph G;
    CreateUDN(G);

    VerTexType start, dest;
    cout << "请输入起点和终点:" << endl;
    cin >> start >> dest;

    int v0 = LocateVex(G, start);
    int v1 = LocateVex(G, dest);

    if (v0 == -1 || v1 == -1) {
        cout << "起点或终点不存在!" << endl;
        return 0;
    }

    int* dist = new int[G.vexnum]; // 记录起点到各顶点的最短路径长度
    int* path = new int[G.vexnum]; // 记录最短路径中各顶点的前驱顶点

    for (int i = 0; i < G.vexnum; ++i) {
        dist[i] = MaxInt;
        path[i] = -1;
    }

    Dijkstra(G, v0, dist, path);

    cout << "从起点 " << start << " 到终点 " << dest << " 的最短路径为:";
    DisplayPath(G, v0, v1, path);
    cout << endl;
    cout << "最短路径长度为:" << dist[v1] << endl;

    delete[] dist;
    delete[] path;

    return 0;
}

弗洛伊德(Floyd)算法 - 所有顶点间的最短路径(了解)

求最短路径步骤:

  1. 初始时设置一个n阶方阵,
    令其对角线元素为0,若存在弧
    <Vi , Vj>,则对应元素为权值;
    否则为∞。
  2. 逐步试着在原直接路径中增加中间顶点,若加入中间顶点后路径变短,则修改之;否则,维持原值。所有顶点试探完毕,算法结束。

代码实现

#include <iostream>
#include <vector>

const int INF = 999999; // 代表无穷大

// 矩阵相乘函数
void multiply(std::vector<std::vector<int>>& a, std::vector<std::vector<int>>& b, std::vector<std::vector<int>>& c, int n) {
    std::vector<std::vector<int>> temp(n, std::vector<int>(n, INF));

    // 矩阵相乘
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            for (int k = 0; k < n; ++k) {
                if (a[i][k] != INF && b[k][j] != INF) {
                    temp[i][j] = std::min(temp[i][j], a[i][k] + b[k][j]);
                }
            }
        }
    }

    // 将相乘结果拷贝回矩阵c
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            c[i][j] = temp[i][j];
        }
    }
}

// Floyd算法
void floyd(std::vector<std::vector<int>>& graph, int num_vertices) {
    std::vector<std::vector<int>> dist(num_vertices, std::vector<int>(num_vertices));

    // 初始化距离矩阵
    for (int i = 0; i < num_vertices; ++i) {
        for (int j = 0; j < num_vertices; ++j) {
            dist[i][j] = graph[i][j];
        }
    }

    // 进行n次矩阵相乘操作
    for (int k = 0; k < num_vertices; ++k) {
        multiply(dist, dist, dist, num_vertices);
    }

    // 输出最终的距离矩阵
    for (int i = 0; i < num_vertices; ++i) {
        for (int j = 0; j < num_vertices; ++j) {
            if (dist[i][j] == INF) {
                std::cout << "INF ";
            } else {
                std::cout << dist[i][j] << " ";
            }
        }
        std::cout << std::endl;
    }
}

int main() {
    int num_vertices = 4;
    std::vector<std::vector<int>> graph = {
        {0, 3, INF, 7},
        {8, 0, 2, INF},
        {5, INF, 0, 1},
        {2, INF, INF, 0}
    };

    // 调用Floyd算法计算最短路径
    floyd(graph, num_vertices);

    return 0;
}

拓扑排序

在一个AOV图(顶点表示活动,有向边表示活动之间的先后关系的图)没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧<i , j>存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序

拓扑排序的方法
  1. 在有向图中选择一个没有前驱的顶点并输出。
  2. 从图中删除该顶点和所有以它为尾的弧。
  3. 重复1、2步,直至全部顶点均已输出(或者当图中不存在无前驱的顶点为止)。

image-20230527203248155

检测AOV网中是否存在环的方法:

对有向图构造其顶点的拓扑排序有序序列,若网中所有顶点都在它的拓扑有序序列中,则AOV网必定不存在环。否则就有环。

代码实现

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

#define MVNum 100

typedef char VerTexType;
typedef struct ArcNode {
    int adjvex;
    struct ArcNode* next;
} ArcNode;

typedef struct VNode {
    VerTexType data;
    ArcNode* firstarc;
} VNode, AdjList[MVNum];

typedef struct {
    AdjList vertices;
    int vexnum, arcnum;
} ALGraph;

int LocateVex(ALGraph G, VerTexType v) {
    for (int i = 0; i < G.vexnum; ++i) {
        if (G.vertices[i].data == v)
            return i;
    }
    return -1; // 未找到对应顶点
}

void CreateDG(ALGraph& G) {
    cout << "请输入顶点个数和有向边个数:" << endl;
    cin >> G.vexnum >> G.arcnum;
    cout << "请输入顶点信息:" << endl;
    for (int i = 0; i < G.vexnum; ++i) {
        cin >> G.vertices[i].data;
        G.vertices[i].firstarc = NULL;
    }
    cout << "请输入有向边的信息(起点、终点):" << endl;
    for (int k = 0; k < G.arcnum; ++k) {
        VerTexType start, end;
        cin >> start >> end;
        int i = LocateVex(G, start);
        int j = LocateVex(G, end);

        ArcNode* arcNode = new ArcNode;
        arcNode->adjvex = j;
        arcNode->next = G.vertices[i].firstarc;
        G.vertices[i].firstarc = arcNode;
    }
}

bool TopologicalSort(ALGraph G, vector<VerTexType>& result) {
    vector<int> indegree(G.vexnum, 0); // 保存顶点的入度
    queue<int> q; // 保存入度为0的顶点

    // 计算每个顶点的入度
    for (int i = 0; i < G.vexnum; ++i) {
        ArcNode* p = G.vertices[i].firstarc;
        while (p != NULL) {
            indegree[p->adjvex]++;
            p = p->next;
        }
    }

    // 将入度为0的顶点入队列
    for (int i = 0; i < G.vexnum; ++i) {
        if (indegree[i] == 0)
            q.push(i);
    }

    while (!q.empty()) {
        int v = q.front();
        q.pop();
        result.push_back(G.vertices[v].data); // 将顶点加入结果列表

        // 更新与顶点v相邻的顶点的入度
        ArcNode* p = G.vertices[v].firstarc;
        while (p != NULL) {
            int adjvex = p->adjvex;
            indegree[adjvex]--;
            if (indegree[adjvex] == 0)
                q.push(adjvex);
            p = p->next;
        }
    }

    // 判断是否存在环路
    if (result.size() != G.vexnum)
        return false;

    return true;
}

int main() {
    ALGraph G;
    CreateDG(G);

    vector<VerTexType> result;
    if (TopologicalSort(G, result)) {
        cout << "拓扑排序结果:";
        for (int i = 0; i < result.size(); ++i) {
            cout << result[i] << " ";
        }
        cout << endl;
    } else {
        cout << "图中存在环路,无法进行拓扑排序!" << endl;
    }

    return 0;
}

关键路径

将工程计划表示为边表示活动的网络,即AOE网,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。路径长度是路径上各活动持续时间之和。

关键路径指路径长度最长的路径。

image-20230527210911357

代码演示

#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;

#define MVNum 100
#define INF 999999

typedef char VerTexType;
typedef struct ArcNode {
    int adjvex;
    int weight;
    struct ArcNode* next;
} ArcNode;

typedef struct VNode {
    VerTexType data;
    int early; // 最早开始时间
    int late;  // 最晚开始时间
    int duration; // 持续时间
    ArcNode* firstarc;
} VNode, AdjList[MVNum];

typedef struct {
    AdjList vertices;
    int vexnum, arcnum;
} ALGraph;

int LocateVex(ALGraph G, VerTexType v) {
    for (int i = 0; i < G.vexnum; ++i) {
        if (G.vertices[i].data == v)
            return i;
    }
    return -1; // 未找到对应顶点
}

void CreateAOE(ALGraph& G) {
    cout << "请输入顶点个数和有向边个数:" << endl;
    cin >> G.vexnum >> G.arcnum;
    cout << "请输入顶点信息:" << endl;
    for (int i = 0; i < G.vexnum; ++i) {
        cin >> G.vertices[i].data;
        G.vertices[i].early = 0;
        G.vertices[i].late = INF;
        G.vertices[i].duration = 0;
        G.vertices[i].firstarc = NULL;
    }
    cout << "请输入有向边的信息(起点、终点、持续时间):" << endl;
    for (int k = 0; k < G.arcnum; ++k) {
        VerTexType start, end;
        int duration;
        cin >> start >> end >> duration;
        int i = LocateVex(G, start);
        int j = LocateVex(G, end);

        ArcNode* arcNode = new ArcNode;
        arcNode->adjvex = j;
        arcNode->weight = duration;
        arcNode->next = G.vertices[i].firstarc;
        G.vertices[i].firstarc = arcNode;
    }
}

void TopologicalSort(ALGraph G, vector<int>& result) {
    vector<int> indegree(G.vexnum, 0); // 保存顶点的入度
    queue<int> q; // 保存入度为0的顶点

    // 计算每个顶点的入度
    for (int i = 0; i < G.vexnum; ++i) {
        ArcNode* p = G.vertices[i].firstarc;
        while (p != NULL) {
            indegree[p->adjvex]++;
            p = p->next;
        }
    }

    // 将入度为0的顶点入队列
    for (int i = 0; i < G.vexnum; ++i) {
        if (indegree[i] == 0)
            q.push(i);
    }

    while (!q.empty()) {
        int v = q.front();
        q.pop();
        result.push_back(v); // 将顶点加入结果列表

        // 更新与顶点v相邻的顶点的入度
        ArcNode* p = G.vertices[v].firstarc;
        while (p != NULL) {
            indegree[p->adjvex]--;
            if (indegree[p->adjvex] == 0)
                q.push(p->adjvex);
            p = p->next;
        }
    }
}

void CriticalPath(ALGraph G) {
    vector<int> result;
    TopologicalSort(G, result);

    // 计算最早开始时间
    for (int i = 0; i < G.vexnum; ++i) {
        int v = result[i];
        ArcNode* p = G.vertices[v].firstarc;
        while (p != NULL) {
            int w = p->adjvex;
            if (G.vertices[v].early + p->weight > G.vertices[w].early)
                G.vertices[w].early = G.vertices[v].early + p->weight;
            p = p->next;
        }
    }

    // 计算最晚开始时间和关键路径
    for (int i = G.vexnum - 1; i >= 0; --i) {
        int v = result[i];
        if (G.vertices[v].firstarc == NULL) {
            G.vertices[v].late = G.vertices[v].early;
        } else {
            ArcNode* p = G.vertices[v].firstarc;
            while (p != NULL) {
                int w = p->adjvex;
                if (G.vertices[w].late - p->weight < G.vertices[v].late)
                    G.vertices[v].late = G.vertices[w].late - p->weight;
                p = p->next;
            }
        }
        G.vertices[v].duration = G.vertices[v].late - G.vertices[v].early;

        // 输出关键活动
        if (G.vertices[v].early == G.vertices[v].late) {
            ArcNode* p = G.vertices[v].firstarc;
            while (p != NULL) {
                int w = p->adjvex;
                if (G.vertices[v].late == G.vertices[w].early - p->weight) {
                    cout << "(" << G.vertices[v].data << ", " << G.vertices[w].data << ") ";
                }
                p = p->next;
            }
        }
    }
}

int main() {
    ALGraph G;
    CreateAOE(G);
    CriticalPath(G);
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值