有权图
文章目录
有权图的表示
-
邻接矩阵表示有权图:
-
邻接表表示有权图:
-
边的表示:
-
// 边 template<typename Weight> class Edge{ private: int a,b; // 边的两个端点 Weight weight; // 边的权值 public: // 构造函数 Edge(int a, int b, Weight weight){ this->a = a; this->b = b; this->weight = weight; } // 空的构造函数, 所有的成员变量都取默认值 Edge(){} ~Edge(){} int v(){ return a;} // 返回第一个顶点 int w(){ return b;} // 返回第二个顶点 Weight wt(){ return weight;} // 返回权值 // 给定一个顶点, 返回另一个顶点 int other(int x){ assert( x == a || x == b ); return x == a ? b : a; } // 输出边的信息 friend ostream& operator<<(ostream &os, const Edge &e){ os<<e.a<<"-"<<e.b<<": "<<e.weight; return os; } // 边的大小比较, 是对边的权值的大小比较 bool operator<(Edge<Weight>& e){ return weight < e.wt(); } bool operator<=(Edge<Weight>& e){ return weight <= e.wt(); } bool operator>(Edge<Weight>& e){ return weight > e.wt(); } bool operator>=(Edge<Weight>& e){ return weight >= e.wt(); } bool operator==(Edge<Weight>& e){ return weight == e.wt(); } };
-
邻接矩阵和邻接矩阵代码表示有权图:
-
// 稠密图 - 邻接矩阵 template <typename Weight> class DenseGraph{ private: int n, m; // 节点数和边数 bool directed; // 是否为有向图 vector<vector<Edge<Weight> *>> g; // 图的具体数据 public: // 构造函数 DenseGraph( int n , bool directed){ assert( n >= 0 ); this->n = n; this->m = 0; this->directed = directed; // g初始化为n*n的矩阵, 每一个g[i][j]指向一个边的信息, 初始化为NULL g = vector<vector<Edge<Weight> *>>(n, vector<Edge<Weight> *>(n, NULL)); } // 析构函数 ~DenseGraph(){ for( int i = 0 ; i < n ; i ++ ) for( int j = 0 ; j < n ; j ++ ) if( g[i][j] != NULL ) delete g[i][j]; } int V(){ return n;} // 返回节点个数 int E(){ return m;} // 返回边的个数 // 向图中添加一个边, 权值为weight void addEdge( int v, int w , Weight weight ){ assert( v >= 0 && v < n ); assert( w >= 0 && w < n ); // 如果从v到w已经有边, 删除这条边 if( hasEdge( v , w ) ){ delete g[v][w]; if( v != w && !directed ) delete g[w][v]; m --; } g[v][w] = new Edge<Weight>(v, w, weight); if( v != w && !directed ) g[w][v] = new Edge<Weight>(w, v, weight); m ++; } // 验证图中是否有从v到w的边 bool hasEdge( int v , int w ){ assert( v >= 0 && v < n ); assert( w >= 0 && w < n ); return g[v][w] != NULL; } // 邻边迭代器, 传入一个图和一个顶点, // 迭代在这个图中和这个顶点向连的所有边 class adjIterator{ private: DenseGraph &G; // 图G的引用 int v; int index; public: // 构造函数 adjIterator(DenseGraph &graph, int v): G(graph){ this->v = v; this->index = -1; // 索引从-1开始, 因为每次遍历都需要调用一次next() } ~adjIterator(){} // 返回图G中与顶点v相连接的第一个边 Edge<Weight>* begin(){ // 索引从-1开始, 因为每次遍历都需要调用一次next() index = -1; return next(); } // 返回图G中与顶点v相连接的下一个边 Edge<Weight>* next(){ // 从当前index开始向后搜索, 直到找到一个g[v][index]为true for( index += 1 ; index < G.V() ; index ++ ) if( G.g[v][index] ) return G.g[v][index]; // 若没有顶点和v相连接, 则返回NULL return NULL; } // 查看是否已经迭代完了图G中与顶点v相连接的所有边 bool end(){ return index >= G.V(); } }; }; // 稀疏图 - 邻接表 template<typename Weight> class SparseGraph{ private: int n, m; // 节点数和边数 bool directed; // 是否为有向图 vector<vector<Edge<Weight> *> > g; // 图的具体数据 public: // 构造函数 SparseGraph( int n , bool directed){ assert(n >= 0); this->n = n; this->m = 0; // 初始化没有任何边 this->directed = directed; // g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边 g = vector<vector<Edge<Weight> *> >(n, vector<Edge<Weight> *>()); } // 析构函数 ~SparseGraph(){ for( int i = 0 ; i < n ; i ++ ) for( int j = 0 ; j < g[i].size() ; j ++ ) delete g[i][j]; } int V(){ return n;} // 返回节点个数 int E(){ return m;} // 返回边的个数 // 向图中添加一个边, 权值为weight void addEdge( int v, int w , Weight weight){ assert( v >= 0 && v < n ); assert( w >= 0 && w < n ); // 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表 // 我们的程序允许重边的出现 g[v].push_back(new Edge<Weight>(v, w, weight)); if( v != w && !directed ) g[w].push_back(new Edge<Weight>(w, v, weight)); m ++; } // 验证图中是否有从v到w的边 bool hasEdge( int v , int w ){ assert( v >= 0 && v < n ); assert( w >= 0 && w < n ); for( int i = 0 ; i < g[v].size() ; i ++ ) if( g[v][i]->other(v) == w ) return true; return false; } // 邻边迭代器, 传入一个图和一个顶点, // 迭代在这个图中和这个顶点向连的所有边 class adjIterator{ private: SparseGraph &G; // 图G的引用 int v; int index; public: // 构造函数 adjIterator(SparseGraph &graph, int v): G(graph){ this->v = v; this->index = 0; } ~adjIterator(){} // 返回图G中与顶点v相连接的第一个边 Edge<Weight>* begin(){ index = 0; if( G.g[v].size() ) return G.g[v][index]; // 若没有顶点和v相连接, 则返回NULL return NULL; } // 返回图G中与顶点v相连接的下一个边 Edge<Weight>* next(){ index += 1; if( index < G.g[v].size() ) return G.g[v][index]; return NULL; } // 查看是否已经迭代完了图G中与顶点v相连接的所有顶点 bool end(){ return index >= G.g[v].size(); } }; };
最小生成树Minimum Span Tree
生成树
-
在一个有V个节点的图中,存在V-1条边连接了这V个节点,则说这V-1条边组成了这个图的生成树
-
生成树满足2个条件:
- 包含树中的所有节点
- 任意两节点之间仅有一条边
-
所有边的权值和最小的生成树称为最小生成树
切分定理Cut Property
-
把图中的节点分为两部分,称为一个切分
-
如果一个边的两个端点,属于切分不同的两边,这个边称为横切边
-
切分定理:给定任意切分,横切边中权值最小的边必然属于最小生成树
-
上图中,蓝绿色的边为横切边(4-6的边也是),其中红色的边权值最小,属于最小生成树
Lazy Prim
-
算法步骤:
- 1.从起始节点s开始,将他的所有邻边加入到最小堆(优先队列)Q中
- 2.从Q中出队权值最小的边,通过该边的两个端点判断其是否为横切边
- 3.若是,则加入到MST中,遍历与该边相连的另一个节点回到步骤1;否则抛弃之,回到步骤2
-
时间复杂度:O(ElogE),通过优化可以降低到O(ElogV)
-
// 使用Prim算法求图的最小生成树 template<typename Graph, typename Weight> class LazyPrimMST{ private: Graph &G; // 图的引用 MinHeap<Edge<Weight>> pq; // 最小堆, 算法辅助数据结构 bool *marked; // 标记数组, 在算法运行过程中标记节点i是否被访问 vector<Edge<Weight>> mst; // 最小生成树所包含的所有边 Weight mstWeight; // 最小生成树的权值 // 访问节点v void visit(int v){ assert( !marked[v] ); marked[v] = true; // 将和节点v相连接的所有未访问的边放入最小堆中 typename Graph::adjIterator adj(G,v); for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ) if( !marked[e->other(v)] ) pq.insert(*e); } public: // 构造函数, 使用Prim算法求图的最小生成树 LazyPrimMST(Graph &graph):G(graph), pq(MinHeap<Edge<Weight>>(graph.E())){ // 算法初始化 marked = new bool[G.V()]; for( int i = 0 ; i < G.V() ; i ++ ) marked[i] = false; mst.clear(); // Lazy Prim visit(0); while( !pq.isEmpty() ){// O(E) // 使用最小堆找出已经访问的边中权值最小的边 Edge<Weight> e = pq.extractMin();// O(logE) // 如果这条边的两端都已经访问过了, 则扔掉这条边 if( marked[e.v()] == marked[e.w()] ) continue; // 否则, 这条边则应该存在在最小生成树中 mst.push_back( e ); // 访问和这条边连接的还没有被访问过的节点 if( !marked[e.v()] ) visit( e.v() );// O(E) else visit( e.w() ); } // 计算最小生成树的权值 mstWeight = mst[0].wt(); for( int i = 1 ; i < mst.size() ; i ++ ) mstWeight += mst[i].wt(); } // 析构函数 ~LazyPrimMST(){ delete[] marked; } // 返回最小生成树的所有边 vector<Edge<Weight>> mstEdges(){ return mst; }; // 返回最小生成树的权值 Weight result(){ return mstWeight; }; };
Prim
-
Lazy Prim算法存在的问题:
- 将图中所有的边都加入了堆中,而很多边已经不再是横切边
- 虽然有很多横切边,但是通常只需考虑最短的横切边,尤其是和一个节点相连的许多横切边中最短的一个
-
时间复杂度:O(ElogV),对于稠密图改进效果明显
-
使用辅助数据结构:索引堆。该索引堆中只需存储V个节点,每个节点存储和该节点相连的最短的横切边
-
算法步骤:
-
1.从起始节点开始,遍历该节点的邻边,并更新与这些边像连的另一些节点存储的最小的横切边
-
2.选择其中最短的横切边加入到MST中,并从该边的另一个节点开始回到步骤1
-
如上图所示,从节点0开始,遍历其邻边,并更新与之相连的节点(2、4、6、7)在索引堆中存储的最短横切边(分别为0.26、0.38、0.58、0.16);选择其中最短的边0.16加入到MST中,并从节点7开始继续上述步骤。
-
-
// 使用优化的Prim算法求图的最小生成树 template<typename Graph, typename Weight> class PrimMST{ private: Graph &G; // 图的引用 IndexMinHeap<Weight> ipq; // 最小索引堆, 算法辅助数据结构 vector<Edge<Weight>*> edgeTo; // 访问的点所对应的边, 算法辅助数据结构(索引堆中只存储了每个节点的最小横切边权值,没有边相连的节点信息) bool* marked; // 标记数组, 在算法运行过程中标记节点i是否被访问 vector<Edge<Weight>> mst; // 最小生成树所包含的所有边 Weight mstWeight; // 最小生成树的权值 // 访问节点v void visit(int v){ assert( !marked[v] ); marked[v] = true; // 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中 typename Graph::adjIterator adj(G,v); for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ){ int w = e->other(v); // 如果边的另一端点未被访问 if( !marked[w] ){ // 如果从没有考虑过这个端点, 直接将这个端点和与之相连接的边加入索引堆 if( !edgeTo[w] ){ edgeTo[w] = e; ipq.insert(w, e->wt()); } // 如果曾经考虑这个端点, 但现在的边比之前考虑的边更短, 则进行替换 else if( e->wt() < edgeTo[w]->wt() ){ edgeTo[w] = e; ipq.change(w, e->wt()); } } } } public: // 构造函数, 使用Prim算法求图的最小生成树 PrimMST(Graph &graph):G(graph), ipq(IndexMinHeap<double>(graph.V())){ assert( graph.E() >= 1 ); // 算法初始化 marked = new bool[G.V()]; for( int i = 0 ; i < G.V() ; i ++ ){ marked[i] = false; edgeTo.push_back(NULL); } mst.clear(); // Prim visit(0); while( !ipq.isEmpty() ){ // 使用最小索引堆找出已经访问的边中权值最小的边 // 最小索引堆中存储的是点的索引, 通过点的索引找到相对应的边 int v = ipq.extractMinIndex(); assert( edgeTo[v] ); mst.push_back( *edgeTo[v] ); visit( v ); } mstWeight = mst[0].wt(); for( int i = 1 ; i < mst.size() ; i ++ ) mstWeight += mst[i].wt(); } ~PrimMST(){ delete[] marked; } vector<Edge<Weight>> mstEdges(){ return mst; }; Weight result(){ return mstWeight; }; };
Kruskal
-
算法思想:选择图G的所有边E中权值最小的边,判断其是否能在MST中形成环(注意这里并不是判断其是否为横切边),若是,舍弃之并选择下一条权值最小的边;否则,将其加入到MST中,直到所有的节点都在MST中为止
-
如上图所示,0-2这条边虽然不是横切边,但是其并不会使得MST形成环,因此需加入到MST中
-
而1-3这条边若加入到MST中的话,则会在MST中形成环,因此舍弃之
-
若要判断是否能形成环,则需要并查集这种数据结构。每当将一条边加入到MST中时,都要对两端的端点进行一次union操作,这样当对下一条边进行判断的时候,就可以通过判断这条边两端的端点是否已经联通,进而可知是否能形成环
-
时间复杂度:O(ElogE)
-
// Kruskal算法 template <typename Graph, typename Weight> class KruskalMST{ private: vector<Edge<Weight>> mst; // 最小生成树所包含的所有边 Weight mstWeight; // 最小生成树的权值 public: // 构造函数, 使用Kruskal算法计算graph的最小生成树 KruskalMST(Graph &graph){ // 将图中的所有边存放到一个最小堆中 MinHeap<Edge<Weight>> pq( graph.E() ); for( int i = 0 ; i < graph.V() ; i ++ ){ typename Graph::adjIterator adj(graph,i); for( Edge<Weight> *e = adj.begin() ; !adj.end() ; e = adj.next() ) if( e->v() < e->w() ) pq.insert(*e); } // 创建一个并查集, 来查看已经访问的节点的联通情况 UnionFind uf = UnionFind(graph.V()); while( !pq.isEmpty() && mst.size() < graph.V() - 1 ){ // 从最小堆中依次从小到大取出所有的边 Edge<Weight> e = pq.extractMin(); // 如果该边的两个端点是联通的, 说明加入这条边将产生环, 扔掉这条边 if( uf.isConnected( e.v() , e.w() ) ) continue; // 否则, 将这条边添加进最小生成树, 同时标记边的两个端点联通 mst.push_back( e ); uf.unionElements( e.v() , e.w() ); } mstWeight = mst[0].wt(); for( int i = 1 ; i < mst.size() ; i ++ ) mstWeight += mst[i].wt(); } ~KruskalMST(){ } // 返回最小生成树的所有边 vector<Edge<Weight>> mstEdges(){ return mst; }; // 返回最小生成树的权值 Weight result(){ return mstWeight; }; };
最短路径
- 所生成的路径称为单源最短路径,形成了一棵最短路径树
松弛操作Relaxation
-
查看从0先到其他节点(比如2)再到1的距离是否比从0直接到1的距离要短
-
松弛操作是最短路径求解的核心
Dijkstra算法
-
前提:图中不能有负权边
-
时间复杂度:O(ElogV)
-
借助最小索引堆数据结构
-
算法步骤:
- 每次循环从未求出最短路劲的节点中,找出距离起点最短路径的节点Oi
- 判断从原点出发经过Oi到达另一个节点Oj的路径长度,是否小于之前到达Oj的路径长度,若小于则更新Oj对应的最短路径(松弛操作),否则继续遍历Oi所能到达的下一个节点
-
// Dijkstra算法求最短路径 template<typename Graph, typename Weight> class Dijkstra{ private: Graph &G; // 图的引用 int s; // 起始点 Weight *distTo; // distTo[i]存储从起始点s到i的最短路径长度 bool *marked; // 标记数组, 在算法运行过程中标记节点i是否被访问 vector<Edge<Weight>*> from; // from[i]记录最短路径中, 到达i点的边是哪一条 // 可以用来恢复整个最短路径 public: // 构造函数, 使用Dijkstra算法求最短路径 Dijkstra(Graph &graph, int s):G(graph){ // 算法初始化 assert( s >= 0 && s < G.V() ); this->s = s; distTo = new Weight[G.V()]; marked = new bool[G.V()]; for( int i = 0 ; i < G.V() ; i ++ ){ distTo[i] = Weight(); marked[i] = false; from.push_back(NULL); } // 使用索引堆记录当前找到的到达每个顶点的最短距离 IndexMinHeap<Weight> ipq(G.V()); // 对于其实点s进行初始化 distTo[s] = Weight(); from[s] = new Edge<Weight>(s, s, Weight()); ipq.insert(s, distTo[s] ); marked[s] = true; while( !ipq.isEmpty() ){ // 取出最短路径的节点 int v = ipq.extractMinIndex(); // distTo[v]就是s到v的最短距离 marked[v] = true; // 对v的所有相邻节点进行更新 typename Graph::adjIterator adj(G, v); for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ){ int w = e->other(v); // 如果从s点到w点的最短路径还没有找到 if( !marked[w] ){ // 如果w点以前没有访问过, // 或者访问过, 但是通过当前的v点到w点距离更短, 则进行更新 if( from[w] == NULL || distTo[v] + e->wt() < distTo[w] ){ distTo[w] = distTo[v] + e->wt(); from[w] = e; if( ipq.contain(w) ) ipq.change(w, distTo[w] ); else ipq.insert(w, distTo[w] ); } } } } } // 析构函数 ~Dijkstra(){ delete[] distTo; delete[] marked; delete from[0]; } // 返回从s点到w点的最短路径长度 Weight shortestPathTo( int w ){ assert( w >= 0 && w < G.V() ); assert( hasPathTo(w) ); return distTo[w]; } // 判断从s点到w点是否联通 bool hasPathTo( int w ){ assert( w >= 0 && w < G.V() ); return marked[w]; } // 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中 void shortestPath( int w, vector<Edge<Weight>> &vec ){ assert( w >= 0 && w < G.V() ); assert( hasPathTo(w) ); // 通过from数组逆向查找到从s到w的路径, 存放到栈中 stack<Edge<Weight>*> s; Edge<Weight> *e = from[w]; while( e->v() != this->s ){ s.push(e); e = from[e->v()]; } s.push(e); // 从栈中依次取出元素, 获得顺序的从s到w的路径 while( !s.empty() ){ e = s.top(); vec.push_back( *e ); s.pop(); } } };
Bellman-Ford算法
-
Dijkstra算法无法处理存在负权边的图的最短路径问题
-
当图中存在负权环的时候,则不存在最短路径
-
如上图,0->2->0代价为-1,1->2->1代价为-3,0->1->2->0的代价为-2,这些都是负权环。若要找0到任何一个节点的最短路径,则可以在这个环中不断循环使得路径的代价逐渐减小,直到负无穷。
-
Bellman-Ford算法的前提是图中不能有负权环,但可以判断图中是否有负权环;通常处理有向图
-
时间复杂度为O(EV)
-
基本思想:如果一个图没有负权环,则从一个顶点到另一个顶点的最短路径,最多经过所有的V个顶点,有V-1条边,否则,存在顶点经过了两次,即存在负权环。
-
主要步骤:
- 对所有的点进行V-1轮松弛操作,在第n轮松弛操作后,得到的是从原点最多经过n条边到达其他顶点的最短距离;此时,正常情况下能够找到从原点到其余顶点的最短路径
- 最后对所有的点进行第V轮的松弛操作,若还能找到最短路径,则说明图中有负权环
-
可以使用队列进行优化
-
// 使用BellmanFord算法求最短路径 template <typename Graph, typename Weight> class BellmanFord{ private: Graph &G; // 图的引用 int s; // 起始点 Weight* distTo; // distTo[i]存储从起始点s到i的最短路径长度 vector<Edge<Weight>*> from; // from[i]记录最短路径中, 到达i点的边是哪一条 // 可以用来恢复整个最短路径 bool hasNegativeCycle; // 标记图中是否有负权环 // 判断图中是否有负权环 bool detectNegativeCycle(){ for( int i = 0 ; i < G.V() ; i ++ ){ typename Graph::adjIterator adj(G,i); for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ) if( from[e->v()] && distTo[e->v()] + e->wt() < distTo[e->w()] ) return true; } return false; } public: // 构造函数, 使用BellmanFord算法求最短路径 BellmanFord(Graph &graph, int s):G(graph){ this->s = s; distTo = new Weight[G.V()]; // 初始化所有的节点s都不可达, 由from数组来表示 for( int i = 0 ; i < G.V() ; i ++ ) from.push_back(NULL); // 设置distTo[s] = 0, 并且让from[s]不为NULL, 表示初始s节点可达且距离为0 distTo[s] = Weight(); from[s] = new Edge<Weight>(s, s, Weight()); // 这里我们from[s]的内容是new出来的, 注意要在析构函数里delete掉 // Bellman-Ford的过程 // 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离 for( int pass = 1 ; pass < G.V() ; pass ++ ){ // 每次循环中对所有的边进行一遍松弛操作 // 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边 for( int i = 0 ; i < G.V() ; i ++ ){ // 使用我们实现的邻边迭代器遍历和所有顶点相邻的所有边 typename Graph::adjIterator adj(G,i); for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ) // 对于每一个边首先判断e->v()可达 // 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()] // 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离, 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()] if( from[e->v()] && (!from[e->w()] || distTo[e->v()] + e->wt() < distTo[e->w()]) ){ distTo[e->w()] = distTo[e->v()] + e->wt(); from[e->w()] = e; } } } hasNegativeCycle = detectNegativeCycle(); } // 析构函数 ~BellmanFord(){ delete[] distTo; delete from[s]; } // 返回图中是否有负权环 bool negativeCycle(){ return hasNegativeCycle; } // 返回从s点到w点的最短路径长度 Weight shortestPathTo( int w ){ assert( w >= 0 && w < G.V() ); assert( !hasNegativeCycle ); assert( hasPathTo(w) ); return distTo[w]; } // 判断从s点到w点是否联通 bool hasPathTo( int w ){ assert( w >= 0 && w < G.V() ); return from[w] != NULL; } // 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中 void shortestPath( int w, vector<Edge<Weight>> &vec ){ assert( w >= 0 && w < G.V() ); assert( !hasNegativeCycle ); assert( hasPathTo(w) ); // 通过from数组逆向查找到从s到w的路径, 存放到栈中 stack<Edge<Weight>*> s; Edge<Weight> *e = from[w]; while( e->v() != this->s ){ s.push(e); e = from[e->v()]; } s.push(e); // 从栈中依次取出元素, 获得顺序的从s到w的路径 while( !s.empty() ){ e = s.top(); vec.push_back( *e ); s.pop(); } } // 打印出从s点到w点的路径 void showPath(int w){ assert( w >= 0 && w < G.V() ); assert( !hasNegativeCycle ); assert( hasPathTo(w) ); vector<Edge<Weight>> vec; shortestPath(w, vec); for( int i = 0 ; i < vec.size() ; i ++ ){ cout<<vec[i].v()<<" -> "; if( i == vec.size()-1 ) cout<<vec[i].w()<<endl; } } };
最短路径算法总结
- 所有对的最短路径算法:Floyed算法,处理无负权环的图,O(V^3)