图 基于邻接表的图的ADT实现 class Graph { public : int VerticesNum () ; int EdgesNum () ; Edge FirstEdge (int oneVertex) ; Edge NextEdge (Edge preEdge) ; bool setEdge (int fromVertex, int toVertex, int weight) ; bool delEdge (int fromVertex, int toVertex) ; bool IsEdge (Edge oneEdge) ; int FromVertex (Edge oneEdge) ; int ToVertex (Edge oneEdge) ; int Weight (Edge oneEdge) ; }; class Edge { public : int from, to, weight; Edge () { from = -1 ; to = -1 ; weight = 0 ; } Edge (int f, int t, int w) { from = f; to = t; weight = w; } bool operator > (Edge oneEdge) { return weight > oneEdge.weight; } bool operator < (Edge oneEdge) { return weight < oneEdge.weight; } }; class Graphl : public Graph { public : int numVertex, numEdge; int *Mark, *indegree; Graph (int numVert) { numVertex = numVert; numEdge = 0 ; indegree = new int [numVertex]; Mark = new int [numVertex]; for (int i = 0 ; i < numVertex; i++) { Mark[i] = UNVISITED; Indegree[i] = 0 ; } } ~Graph () { delete [] Mark; delete [] indegree; } virtual Edge FirstEdge (int oneVertex) = 0 ; virtual Edge NextEdge (Edge preVertex) = 0 ; int verticesNum () { return numVertex; } int EdgesNum () { return numEdge; } bool isEdge (Edge oneEdge) { if (oneEdge.weight > 0 && oneEdge.weight < INFINITY && oneEdge.to >= 0 ) return true ; else return false ; } int FromVertex (Edge oneEdge) { return oneEdge.from; } int ToVertex (Edge oneEdge) { return oneEdge.to; } int Weight (Edge oneEdge) { return oneEdge.weight; } virtual void setEdge (int from, int to, int weight) = 0 ; virtual void delEdge (int from, int to) = 0 ; }; struct listUnit { int vertex; int weight; }; template <class Elem > class link {public : Elem element; link *next; link (const Elem& elemval, Link *nextval = NULL ) { element = elemval; next = nextval; } link (link *nextval = NULL ) { next = nextval; } }; template <class Elem > class LList {public : Link <Elem> *head; LList () { head = new Link <Elem>(); } void removeall () { Link<Elem> *fence; while (head != NULL ) { fence = head; head = head->next; delete fence; } } ~LList () { removeall (); } }; class Graphl : public Graph {private : LList <listUnit> *graList; public : Graphl (int numVert) : Graph (numVert) { graList = new LList<listUnit>[numVertex]; } ~Graphl () { delete [] graList; } }; Edge firstEdge (int oneVertex) { Edge myEdge; myEdge.from = oneVertex; myEdge.to = -1 ; Link<listUnit> *temp = graList[oneVertex].head; if (temp->next != NULL ) { myEdge.to = temp->next->element.vertex; myEdge.weight = temp->next->element.weight; } return myEdge; } Edge nextEdge (Edge preVertex) { Edge myEdge; myEdge.from = preEdge.from; myEdge.to = -1 ; Link<listUnit> *temp = graList[preEdge.from].head; while (temp->next != NULL && temp->next->element.vertex <= preEdge.to) { temp = temp->next; } if (temp->next != NULL ) { myEdge.to = temp->next->element.vertex; myEdge.weight = temp->next->element.weight; } return myEdge; } void setEdge (int from, int to, int weight) { Link <listUnit> *temp = graList[from].head; while (temp->next != NULL && temp->next->element.vertex < to) { temp = temp->next; } if (temp->next == NULL ) { temp->next = new Link<listUnit>; temp->next->element.vertex = to; temp->next->element.weight = weight; numEdge++; indegree[to]++; return ; } if (temp->next->element.vertex == to) { temp->next->element.weight = weight; return ; } if (temp->next->element.vertex > to) { Link <listUnit> *other = temp->next; temp->next = new Link<listUnit>; temp->next->element.vertex = to; temp->next->element.weight = weight; temp->next->next = other; numEdge++; indegree[to]++; } } void delEdge (int from, int to) { Link <listUnit> *temp = graList[from].head; while (temp->next != NULL && temp->next->element.vertex < to) { temp = temp->next; } if (temp->next == NULL ) return ; if (temp->next->element.vertex > to) return ; if (temp->next->element.vertex == to) { Link <listUnit> *other = temp->next->next; delete temp->next; temp->next = other; numEdge--; indegree[to]--; return ; } }
图的周游 dfs 递归版本:
非递归版本
#include <iostream> #include <vector> #include <stack> using namespace std;struct Gnode { int i; vector<Gnode *> neighbornodes; bool visited = false ; }; void dfs (Gnode *a) { stack<Gnode *> st; st.push (a); while (!st.empty ()) { Gnode *tmp = st.top (); st.pop (); cout << tmp->i << " " ; tmp->visited = true ; for (int i = 0 ; i < tmp->neighbornodes.size (); i++) { if (!tmp->neighbornodes[i]->visited) { st.push (tmp->neighbornodes[i]); } } } } int main () { Gnode node0, node1, node2, node3; node0. i = 0 ; node1. i = 1 ; node2. i = 2 ; node3. i = 3 ; node0. neighbornodes.push_back (&node1); node0. neighbornodes.push_back (&node2); node1. neighbornodes.push_back (&node2); node2. neighbornodes.push_back (&node0); node2. neighbornodes.push_back (&node3); node3. neighbornodes.push_back (&node3); dfs (&node2); return 0 ; }
通过检查是否存在backward edges判断是否存在环
bfs
拓扑排序
先决条件:是指以某种线性顺序来组织多项任务,以便能够在满足先决条件的情况下逐个完成各项任务
有向无环图能够模拟先决条件
拓扑排序方法
从图中选择一个入度为0的顶点并输出
从图中删掉此顶点及其所有的出边(出边关联顶点的入度减1)
回到第一步继续执行
若环路存在
排序结束,仍有顶点没有输出
但在剩下的图中找不到入度为0的顶点
基于邻接矩阵表示的拓扑排序
一个点的入度等于这个点所在的列的和
基于邻接表的bfs
算法 void TopsortbyQueue (Graph &G) { for (int i = 0 ; i < G.VerticesNum (); i++) { G.Mark[i] = UNVISITED; } using std::queue; queue<int > Q; for (i = 0 ; i < G.verticesNum (); i++) { if (G.Indegree[i] == 0 ) { Q.enqueue (i); } } while (!G.empty ()) { V = Q.dequeue (); Visit (G, V); G.Mark[V] = VISITED; for (Edge e = G.FirstEdge (V); G.IsEdge (e); e = G.NextEdge (e)) { G.Indegree[G.ToVertex (e)]--; if (G.Indegree[G.ToVertex (e)] == 0 ) Q.enqueue (G.ToVertex (e)); } } for (i = 0 ; i < G.VerticesNum (); i++) { if (G.Mark[i] == UNVISITED) { Print ("图有环" ); break ; } } }
基于邻接表的dfs-topsort
算法 原理:假设在DAG中有一条有向路径从$v_i$到$v_j$,根据拓扑排序的规则,$v_i$一定排在$v_j$之前。在DFS中利用类似后序访问的规则,当$v_i$所有可以达到的节点被访问完以后,$v_i$才会被访问,这样节点被访问的顺序,恰好是拓扑排序的逆序。
void TopsortbyDFS (Graph& G) { for (int i=0 ;i<G.VerticesNum ();i++){ G.Mark[i]=UNVISITED; } int *result=new int [G.VerticesNum ()]; int tag=0 ; for (i=0 ;i<G.VerticeNum ();i++){ if (G.Mark[i]==UNVISITED){ Do_topsort (G,i,result,tag); } for (i=G.VerticesNum ()-1 ;i>=0 ;i--){ Visit (G,result[i]); } } } void Do_topsort (Graph& G,int V,int *result,int & tag) { G.Mark[V]=VISITED; for (Edge e=G.FirstEdge (V);G.IsEdge (e);e=G.NextEdge (e)){ if (G.Mark[G.ToVertex (e)]==UNVISITED){ Do_topsort (G,G.ToVertex (e),result,tag); } } result[tag++]=V; }
==注意==:深度优先拓扑排序不能判断环的存在
思考题
:给出一种实现判断深度优先拓扑排序环存在的算法!
给edge加上一个标志位,在
void Do_topsort (Graph& G,int V,int *result,int & tag) { G.Mark[V]=VISITED; for (Edge e=G.FirstEdge (V);G.IsEdge (e);e=G.NextEdge (e)){ if (G.Mark[G.ToVertex (e)]==UNVISITED){ Do_topsort (G,G.ToVertex (e),result,tag); } } result[tag++]=V; }
最短路径问题
分类
单源最短路径(Dijkstra算法
) 注意:边权非负
基本思想:
每次从距离已生成最短路径的节点集合 “一步之遥 ”的节点中,选择距离远点$V_0$最近的边进行延伸
结果由近到远以起始点$V_0$为根的有向树
贪心算法
最短路径的表示方法:
实现:
class Dist { public : int index; int length; int pre; }; void Dijkstra (Graph& G, int s, Dist* &D) { D = new Dist[G.VerticesNum ()]; for (int i = 0 ; i < G.VerticesNum (); i++) { G.Mark[i] = UNVISITED; D[i].length = INFINITY; D[i].index = i; D[i].pre = s; } D[s].length = 0 ; MinHeap<Dist> H (G.EdgesNum()) ; H.Insert (D[s]); for (int i = 0 ; i < G.VerticesNum (); i++) { bool FOUND = false ; Dist d; while (!H.empty ()) { d = H.RemoveMin (); if (G.Mark[d.index] == UNVISITED) { FOUND = true ; break ; } } if (!FOUND) break ; int v = d.index; G.Mark[v] = VISITED; Visit (v); for (Edge e = G.FirstEdge (v); G.IsEdge (e); e = G.NextEdge (e)) { if (D[G.ToVertex (e)].length > D[v].length + G.Weight (e)) { D[G.ToVertex (e)].length = D[v].length + G.Weight (e); D[G.ToVertex (e)].pre = v; H.insert (D[G.ToVertex (e)]); } } } }
时间复杂性:
Floyd算法
基本思想:
从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点X到B。
递推公式
假设已求得矩阵$adj^{(k-1)}$,那么从顶点$V_i$到顶点$V_j$中间顶点的序号不大于k的路径有两种情况:
中间不经过顶点$V_k$,那么$adj^{(k)}[i,j]=adj^{(k-1)}[i,j]$
中间经过顶点$V_k$,那么$adj^{(k)}[i,j]<adj^{(k-1)}[i,j],且adj^{(k)}[i,j]=adj^{(k-1)}[i,k]+adj^{(k-1)}[k,j]$
这一页的解释
在这一页幻灯片中,描述了如何确定最短路径的具体方法:
**使用路径矩阵 path
**:
定义一个 ( $n \times n$ ) 的矩阵 path
,其中 path[i][j]
表示从顶点 ($ v_i $) 到顶点 ( $v_j$ ) 的最短路径上紧接在 ( $v_j$ ) 之前的那个顶点。
这个矩阵在更新过程中会逐步填充,记录路径中的前驱顶点。
路径更新规则 :
如果在迭代过程中找到一个新的路径,使得从 $( v_i ) 到 ( v_j )$ 的距离更小,那么更新 path[i][j]
为使得距离最小的中间顶点 k
的前驱,即 path[i][j] = path[k][j]
。
无路径情况 :
如果当前没有从 ( v_i ) 到 ( v_j ) 的路径,则将 path[i][j]
置为 -1。
这页主要说明了在求最短路径时如何使用路径矩阵来追踪路径信息,以便在计算出最短路径长度的同时,也可以获得具体的路径序列。
提取并合并的 Floyd 算法实现代码如下:
void Floyd (Graph& G, Dist** &D) { int i, j, v; D = new Dist*[G.VerticesNum ()]; for (i = 0 ; i < G.VerticesNum (); i++) { D[i] = new Dist[G.VerticesNum ()]; for (j = 0 ; j < G.VerticesNum (); j++) { if (i == j) { D[i][j].length = 0 ; D[i][j].pre = i; } else { D[i][j].length = INFINITY; D[i][j].pre = -1 ; } } } for (v = 0 ; v < G.VerticesNum (); v++) { for (Edge e = G.FirstEdge (v); G.IsEdge (e); e = G.NextEdge (e)) { D[v][G.ToVertex (e)].length = G.Weight (e); D[v][G.ToVertex (e)].pre = v; } } for (v = 0 ; v < G.VerticesNum (); v++) { for (i = 0 ; i < G.VerticesNum (); i++) { for (j = 0 ; j < G.VerticesNum (); j++) { if (D[i][v].length + D[v][j].length < D[i][j].length) { D[i][j].length = D[i][v].length + D[v][j].length; D[i][j].pre = D[v][j].pre; } } } } }
代码说明
初始化 D
数组 :
为 D
分配空间,并初始化每个顶点到自身的距离为 0,路径前驱为自身。
如果顶点之间没有直接边,则设置距离为无穷大,前驱为 -1。
邻接顶点的初始化 :
权值、路径更新 :
通过三重循环更新 D
数组。对于每一对顶点 (i) 和 (j),检查是否可以通过中间顶点 (v) 找到更短路径,如果可以则更新距离和前驱。
时间复杂度 此算法的时间复杂度为 (O(n^3)),适合稠密图。
最小支撑(生成)树
概念:
提取并合并的 Prim 算法实现代码如下:
void Prim (Graph& G, int s, Edge* &MST) { int MSTtag = 0 ; Edge* MST = new Edge[G.VerticesNum () - 1 ]; MinHeap<Edge> H (G.EdgesNum()) ; for (int i = 0 ; i < G.VerticesNum (); i++) { G.Mark[i] = UNVISITED; } int v = s; G.Mark[v] = VISITED; do { for (Edge e = G.FirstEdge (v); G.IsEdge (e); e = G.NextEdge (e)) { if (G.Mark[G.ToVertex (e)] == UNVISITED) { H.Insert (e); } } bool Found = false ; Edge e; while (!H.empty ()) { e = H.RemoveMin (); if (G.Mark[G.ToVertex (e)] == UNVISITED) { Found = true ; break ; } } if (!Found) { Print ("不存在最小支撑树。" ); delete [] MST; MST = NULL ; return ; } v = G.ToVertex (e); G.Mark[v] = VISITED; AddEdgetoMST (e, MST, MSTtag++); } while (MSTtag < (G.VerticesNum () - 1 )); }
代码说明
初始化 :
定义最小支撑树的边集合 MST
,以及最小堆 H
。
初始化所有顶点为未访问状态,并设置起始顶点 v
为已访问。
构建最小支撑树 :
将 v
连接的、另一端未访问的边加入到最小堆 H
中。
从堆中选出权值最小且另一端未被访问的边 e
,将该边的终点设为已访问,并将该边加入 MST
。
边检查 :
如果无法找到满足条件的边,说明不存在最小支撑树,释放 MST
空间并退出。
循环终止条件 :
重复上述步骤直到 MST
包含 V-1
条边,即构成最小支撑树。
该代码实现了以最小堆优化的 Prim 算法,适用于稠密图。
证明Prim算法的正确星
Kruskal
算法基本思想
对于图G=(V,E)开始时,将顶点集分为|V|个等价类,每个等价类包括一个顶点
然后,以权的代销为顺序处理各条边,如果某条边连接两个不同等价类的顶点,则这条边被合并为一个:
反复执行此过程,直到剩下一个等价类
提取并合并的 Kruskal 算法实现代码如下:
void Kruskal (Graph& G, Edge* &MST) { Partree A (G.VerticesNum()) ; MinHeap<Edge> H (G.EdgesNum()) ; MST = new Edge[G.VerticesNum () - 1 ]; int MSTtag = 0 ; for (int i = 0 ; i < G.VerticesNum (); i++) { for (Edge e = G.FirstEdge (i); G.IsEdge (e); e = G.NextEdge (e)) { if (G.FromVertex (e) < G.ToVertex (e)) { H.Insert (e); } } } int EquNum = G.VerticesNum (); while (EquNum > 1 ) { Edge e = H.RemoveMin (); int from = G.FromVertex (e); int to = G.ToVertex (e); if (A.differ (from, to)) { A.UNION (from, to); AddEdgetoMST (e, MST, MSTtag++); EquNum--; } } }
代码说明
初始化 :
创建等价类 A
用于管理顶点的联通性,使用最小堆 H
来保存边,并初始化最小生成树 MST
。
边集合入堆 :
遍历图的所有边,将每条边插入最小堆 H
中,确保只插入每条边一次。
合并等价类 :
从堆中提取权值最小的边 e
,判断其两端点是否属于不同的等价类。
如果是不同的等价类,合并它们并将边加入 MST
。
减少等价类的数量,直到只剩一个连通分量,即构成最小生成树。
Kruskal 算法通过最小堆和等价类合并实现,适用于稀疏图。
v2V_1v_3V_4