最近写了个作业,需求是“输出有向无环图的所有拓扑序列”。经过一番折腾,终于完成。之后回想起来觉得应该写一份简单易懂的文章,帮助其他人理解。
细细分析一下,可以发现这次任务我们需要解决以下问题:
(1)如何通过邻接表创建一个图
(2)什么是拓扑排序
(3)如何对图进行拓扑排序
(4)如何输出所有的拓扑序列
对这些问题,我想先一一从算法方面先解决,之后再上代码。
(1)如何通过邻接表创建一个图?
对图有过了解的朋友们应该都知道,图大体来说,有邻接矩阵和邻接表这两种主流的储存结构。
所谓邻接矩阵(不带权值),就是假设图中有n个顶点,采用n*n的矩阵A来表示。
这样的储存结构,大家应该也都发现了,邻接矩阵的优点在于可以通过二维数组A[i][j]直接引用边(i,j),即很容易确定顶点之间的关系。但是同样,缺点也很明显。它消耗的内存空间是顶点数的平方,如果图的边较少的话,就会浪费大量空间。并且,在手动进行输入的时候,也很麻烦,所以,为了偷懒节约空间。我还是选择了邻接表。
那么什么又是邻接表呢?就是将邻接点构成链表
如图,1的邻接点有2,3,4三个,所以可以让1的邻接点指向2,3,4.这样就可以轻松的取到邻接点了。如果是有向图的话,则需要加一个表示入度的变量就行了。
那了解这样一个储存结构之后,怎样创建一个图呢?首先,我们要有一个顶点表,装着顶点。然后需要一个储存顶点信息的结构体vnode,里面装上节点的度,节点的数据和指向邻接点的第一条边。
struct vnode { enode* firstedge; int data; int du; };
之后,也需要一个结构体enode,它储存啥呢?它储存当前顶点邻接顶点在顶点表里的下标。还有一个enode型的指针,指向下一个邻接点。
struct enode { int adjvex; enode* next; };
如上图,在class graph里面的私有成员里,定义一个vnode型顶点表(数组),表中储存的就是上图的v0到v4。而vnode型顶点中enode型的指针,指向第一个邻接点,第一个邻接点的adjex则储存着邻接顶点在顶点表中的下标,enode型指针next则指向下一个邻接点。
这样之后,就可以定义类了。
class graph { public: graph();//无参构造函数 int local(int val);//定位函数 void create();//创建邻接表的图 void show(); void tuopu(); void tuopu(vector<int> order); //bool tuopusort(); private: vnode vertex[maxsize]; int numedge, numvertex; };
如图可见,类的私有成员里定义了我所说的顶点表和顶点和边的数量。之后写一个create函数,在里面就可以开始创建图了。
很多人不知道图应该怎么输入!
其实,只要在你在纸上画出一个有向图,然后在create函数中让用户自行输入边数和顶点数,就可以开始创建了。具体代码如下:
void graph::create() { enode* e, * p, * q; cout << "请输入图的顶点数和边数:" << endl; cin >> numvertex >> numedge; for (int i = 0; i < numvertex; i++) { cout << "第" << i + 1 << "个顶点:"; cin >> vertex[i].data; vertex[i].firstedge = NULL; } for (int i = 0; i < numedge; i++) { cout << "请分别输入" << numedge << "条边上的信息(包括两段顶点和方向)(左指向右)"; \ int v1, v2; cin >> v1 >> v2; int a = local(v1); int b = local(v2); if (vertex[a].firstedge == NULL) { e = new enode; e->adjvex = b; e->next = NULL; vertex[a].firstedge = e; //e->du = 1; vertex[b].du += 1; } else { p =vertex[a].firstedge; while (p->next != NULL) { p = p->next; } e = new enode; e->adjvex = b; e->next = NULL; p->next = e; //e->du = 1; vertex[b].du += 1; } } }
其中local函数定义如下(为了返回顶点对应下标的函数):
int graph::local(int val) { for (int i = 0; i < numvertex; i++) { if (vertex[i].data == val) return i; } return -1; }
下面解释create函数:
在输入顶点数和边数之后,要开始输入顶点的值,于是构建了个for循环分别输入。但是因为不知道顶点间的关系,所以firstedge置为空,度也置为0。之后要求用户输入那numedge条边的两端分别是哪两个顶点,以此来建立顶点间的联系。因为建立的是有向图,所以我要求用户将被指的顶点放在后面,主动指向的顶点放在前面。
在用户输入顶点关系之后,就可以对被指向顶点进行入度加一的操作,并且可以将顶点指向调整,指向邻接点。
到此,就完成了图的创建。成果如下:
到此,解决了第一个问题。
(2)什么是拓扑排序
拓扑排序的定义就是由一个集合的偏序得到一个全序的过程。
听起来有些复杂,但是用算法表示就是:
①找一个入度为0的顶点v输出;
②删除v及相关弧;(将v的后继顶点减一即可)
③重复①,②直到无入度为0的顶点为止。
案例如下:
这样最后就实现了四组拓扑序列。
(3)如何对图进行拓扑排序
知道了拓扑排序的内容,那么如何对一个构建好的图进行拓扑排序呢?
我们知道,求解方法设计到入度,因而需要保存各顶点的入度,那怎么通过入度实现拓扑排序呢?我们可以将入度为0 并且未输出的顶点放在一个结构里,需要时直接取出,当新的入度为0的顶点出现时,也要将其存放进来。对此,我们采用栈结构储存入度为0的顶点,可以通过包含stack头文件调用STL中的栈,而我选择的是自己构建一个栈。有了栈之后,接下来的操作就简单了:
FIRST:初始化栈为空
SECOND:将AOV网中所有入度为0的顶点放入栈S中
THIRD:若S不空,则pop,并输出pop掉的顶点V
FOURTH:将V的每个后继顶点的入度减一,如果其中有减少一之后入度变成了0,则将其放入S中
FIFTH:转到THIRD继续执行
下面分别给出伪码和可运行代码
伪码:
Bool toposort(graph G){ get_Ind(G,Ind); stack s; int count=0; for(i=1;i<=n;i++) if ( Ind[i] == 0 ) s.push(i); while ( !s.empty() ){ v=s.pop(); cout<<v; count++; w=firstadj(G,v); while(w!=0){ Ind[w]--; if ( Ind[w] == 0 ) s.push(w); w=nextadj(G,v,w); } } if ( count == n ) return TURE: else returen FALSE; }
可运行代码:
bool graph::tuopusort() { stack<int> S; for (int i = 0; i < numvertex; i++) { if (vertex[i].du == 0)//挑出入度为0的顶点入栈 S.push(i); } int count = 0;//计数判断是否为无环图 while (!S.empty()) { int tmp; tmp = S.top(); S.pop(); ++count; if (vertex[tmp].firstedge != NULL) { vertex[vertex[tmp].firstedge->adjvex].du--;//度为0的顶点的邻接顶点入度减一 if (vertex[vertex[tmp].firstedge->adjvex].du == 0)//如果入度减一之后变成零 { S.push(vertex[tmp].firstedge->adjvex);//入栈 } while (vertex[tmp].firstedge->next != NULL)//把当前顶点的所有邻接顶点都遍历一遍 { vertex[tmp].firstedge = vertex[tmp].firstedge->next; vertex[vertex[tmp].firstedge->adjvex].du--; if (vertex[vertex[tmp].firstedge->adjvex].du == 0) S.push(vertex[tmp].firstedge->adjvex); } } } if (count < numvertex)//如果count小于numvertex说明为有环图,返回false return false; else return true; }
(代码解释见注释)
运行结果如下:
这时候我们发现,已经输出了拓扑序列了,别得意。需求是输出所有拓扑序列...
咋办呢???
(4)如何输出所有的拓扑序列
开门见山吧,回溯法!
首先先简单的说一下回溯法是啥:回溯法简单来说就是按照深度优先的顺序,穷举所有可能性的算法,但是回溯算法比暴力穷举法更高明的地方就是回溯算法可以随时判断当前状态是否符合问题的条件。一旦不符合条件,那么就退回到上一个状态,省去了继续往下探索的时间。
最基本的回溯法是在解空间中穷举所有的解。
就比如在上面求解拓扑序列时输出的这四条线就是通过回溯法。具体怎么实现呢?
首先我们要建立一个bool类型的数组visit[maxsize],干啥用呢?对应每个顶点有没有被访问过,初始化为false。
之后再想想用啥储存结果并输出呢?思来想去我觉得用vector性价比更高点,于是
void graph::tuopu(vector<int> order)
就这样定义了个输出所有拓扑序列的函数。
首先我们知道,当有一个拓扑序列完成时要直接输出,然后再进行第二个拓扑序列的处理。所以首先要设置输出的代码段。
之后遍历顶点表,找到入度为0并且没有被访问过的顶点。之后呢?肯定要将visit[i]变成true呀!然后将该顶点的data放进容器内待输出,之后就是和(3)中描述的相同的,邻接顶点入度减一的操作。
但是!在这之后,与之不同的是再次调用tuopu(order)形成递归,为的就是能够一次输出所有序列。递归完成后,则说明已经输出了拓扑序列,现在则需要开始回溯,咋回溯呢?就是要将访问过的顶点统统设置为未访问,并且将减一的度数加回来。之后将原本存入容器的顶点值pop出来,就完成了代码的编写了。
void graph::tuopu(vector<int> order) { if (order.size() == numvertex) { for (int i = 0; i < numvertex; i++) { cout << order[i] << " "; } cout << endl; } for (int i = 0; i < numvertex; i++) { if (vertex[i].du == 0 && !visit[i]) { visit[i] = true; order.push_back(vertex[i].data); if (vertex[i].firstedge!=NULL) { enode* p = vertex[i].firstedge; while (p != NULL) { vertex[p->adjvex].du--; p = p->next; } } tuopu(order); if (vertex[i].firstedge != NULL) { enode* q = vertex[i].firstedge; while (q != NULL) { vertex[q->adjvex].du++; q = q->next; } } visit[i] = false; order.pop_back(); } } }
运行结果如下:
那么到这里,就完成了任务的需求啦!!!
完整源代码如下:
stack.h
#pragma once template<class T> class stack { public: stack(int max=1000); ~stack(); bool empty(); bool full(); T top(); bool push(T x); bool pop(); private: T* p; int max_len; int count; };
stack.cpp
#include"stack.h" #include<iostream> using namespace std; template<class T> stack<T>::stack(int max) { max_len = max; count = 0; p = new T[max_len]; } template<class T> stack<T>::~stack() { delete[]p; } template<class T> bool stack<T>::empty() { return count == 0; } template<class T> bool stack<T>::full() { return count == max_len - 1; } template<class T> T stack<T>::top() { if (empty()) { cout << "栈为空" << endl; return -1; } else { T x; x = *(p + count); return x; } } template<class T> bool stack<T>::push(T x) { if (full()) return false; else { count++; *(p + count) = x; return true; } } template<class T> bool stack<T>::pop() { if (empty()) return false; else { count--; return true; } }
graph.h
#pragma once #include<iostream> #include<vector> using namespace std; #define elementtype int #define maxsize 20 //邻接链表初始化图 struct enode { int adjvex; enode* next; }; struct vnode { enode* firstedge; int data; int du; }; class graph { public: graph();//无参构造函数 //graph(adjlink v[maxsize], int _current);//有参构造函数 //~graph();//析构函数 int local(int val); void create();//创建邻接表的图 void show(); void tuopu(); void tuopu(vector<int> order); bool tuopusort(); private: vnode vertex[maxsize]; int numedge, numvertex; };
graph.cpp
#include"graph.h" #include<vector> #include"stack.h" #include"stack.cpp" vector<int> result; bool visit[maxsize]; int ans[maxsize]; int graph::local(int val) { for (int i = 0; i < numvertex; i++) { if (vertex[i].data == val) return i; } return -1; } graph::graph() { numedge = 0, numvertex = 0; for (int i = 0; i < maxsize; i++) { vertex[i].data = 0; vertex[i].du = 0; vertex[i].firstedge = NULL; } } void graph::create() { enode* e, * p, * q; cout << "请输入图的顶点数和边数:" << endl; cin >> numvertex >> numedge; for (int i = 0; i < numvertex; i++) { cout << "第" << i + 1 << "个顶点:"; cin >> vertex[i].data; vertex[i].firstedge = NULL; } for (int i = 0; i < numedge; i++) { cout << "请分别输入" << numedge << "条边上的信息(包括两段顶点和方向)(左指向右)"; \ int v1, v2; cin >> v1 >> v2; int a = local(v1); int b = local(v2); if (vertex[a].firstedge == NULL) { e = new enode; e->adjvex = b; e->next = NULL; vertex[a].firstedge = e; //e->du = 1; vertex[b].du += 1; } else { p =vertex[a].firstedge; while (p->next != NULL) { p = p->next; } e = new enode; e->adjvex = b; e->next = NULL; p->next = e; //e->du = 1; vertex[b].du += 1; } } } void graph::show() { for (int i = 0; i < numvertex; i++) { cout << "第"<<i+1<<"个节点信息及后继为:" << endl; cout << vertex[i].data << "->"; if (vertex[i].firstedge != NULL) { cout << vertex[vertex[i].firstedge->adjvex].data << "->"; while (vertex[i].firstedge->next != NULL) { vertex[i].firstedge = vertex[i].firstedge->next; cout << vertex[vertex[i].firstedge->adjvex].data << "->"; } } cout << "NULL"; cout << endl; } cout << endl; } void graph::tuopu() { vector<int> order; for (int i = 0; i < numvertex; i++) { visit[i] = false; } tuopu(order); } void graph::tuopu(vector<int> order) { if (order.size() == numvertex) { for (int i = 0; i < numvertex; i++) { cout << order[i] << " "; } cout << endl; } for (int i = 0; i < numvertex; i++) { if (vertex[i].du == 0 && !visit[i]) { visit[i] = true; order.push_back(vertex[i].data); if (vertex[i].firstedge!=NULL) { enode* p = vertex[i].firstedge; while (p != NULL) { vertex[p->adjvex].du--; p = p->next; } } tuopu(order); if (vertex[i].firstedge != NULL) { enode* q = vertex[i].firstedge; while (q != NULL) { vertex[q->adjvex].du++; q = q->next; } } visit[i] = false; order.pop_back(); } } } bool graph::tuopusort() { stack<int> S; for (int i = 0; i < numvertex; i++) { cout << "第" << i+1 << "个点的入度为:" << vertex[i].du << endl; if (vertex[i].du == 0) S.push(i); } int count = 0; while (!S.empty()) { int tmp; tmp = S.top(); S.pop(); cout << vertex[tmp].data << "->"; ++count; if (vertex[tmp].firstedge != NULL) { vertex[vertex[tmp].firstedge->adjvex].du--; if (vertex[vertex[tmp].firstedge->adjvex].du == 0) { int temp = vertex[tmp].firstedge->adjvex; S.push(temp); } while (vertex[tmp].firstedge->next != NULL) { vertex[tmp].firstedge = vertex[tmp].firstedge->next; vertex[vertex[tmp].firstedge->adjvex].du--; if (vertex[vertex[tmp].firstedge->adjvex].du == 0) S.push(vertex[tmp].firstedge->adjvex); } } } if (count < numvertex) return false; else return true; }
main.cpp
#include"graph.h" void test() { graph map; map.create(); //map.show(); //cout << "拓扑序列分别为:" << endl; map.tuopusort(); } int main() { test(); system("pause"); return 0; }
希望看官们能够动动小手,给鄙人留下一个赞~