拓扑排序核心算法
在一个有向图中找到一个拓扑排序的过程如下:
1)从一个有向图中选择一个没有前驱(入度为0)的顶点输出;
2)删除1)中的顶点,并删除从该顶点出发的全部边;
3)重复上述两步,直到剩余的图中不存在没有前驱顶点为止(此时图全部顶点都已经输出)
拓扑排序存在的条件是图中不存在环
为使能从途中选取出入度为0的结点并将其进行输出,要对邻接表结构进行修改,可以增加一个统计结点入度的计数器
typedef struct
{
int data;
int count; //此句为新增部分,count用来统计顶点当前的入度
ArcNode *firststarc;
}VNode; //此句前面已经定义过了*/
假设图的邻接表已经生成,并且各个顶点的入度都已经记录在count中,在本算法中要设计一个栈,记录当前图中入度为0的顶点,还需要设计一个计数器
n,用来记录已经输出的顶点的个数
算法开始时置n为0,扫描所有的顶点,将入度为0的顶点入栈。然后在栈部位空的时候循环执行下面的操作:出栈,将栈顶元素进行输出,执行++n,并且
由此顶点引出的边所指向的顶点的入度都减1,并且将入度变为0的顶点入栈;出栈......栈空时退出循环,排序结束。循环退出后判断n是否等于图中顶点
个数,如果相等则返回1,拓扑排序成功;否则返回0,拓扑排序失败。因此可以写出以下的算法代码:
int TopSort(AGraph *G)
{
int i = 0, j = 0, n = 0;
stack<int> topsortstack; //定义并初始化栈
ArcNode *p = NULL;
//下面这个循环将图中入度为0的顶点入栈
for (i = 0; i < G->n; ++i)
if (G->adjlist[i].count == 0)
topsortstack.push(i);
//关键操作开始
while (!(topsortstack.empty()))
{
i = topsortstack.top();
topsortstack.pop(); //顶点出栈
++n; //计数器加一,统计当前顶点
cout << i << " ";
p = G->adjlist[i].firstarc;
//上面这个循环实现了将所有由此顶点引出的边所指向的顶点的入度都减少1。并将这个过程中入度为0的顶点入栈
while (p != NULL)
{
j = p->adjvex;
--(G->adjlist[j].count);
if (G->adjlist[i].count == 0)
{
topsortstack.push(j);
}
p = p->nextarc;
}
}
//关键操作结束
if (n == G->n)
return 1;
else return 0;
}
拓扑排序的时间复杂度为O(n+e),n为图的顶点数,e为图的边数
注意拓扑排序的结果可能不唯一。从算法的过程就可以看出,在选择入度为0的顶点进行输出的时候,对其他顶点是没有要求的
只需要入度为0即可。当当前步骤中有多个入度为0的顶点时,可以选择任意一个进行输出,这就造成了拓扑序列的不唯一
当有向图图中无环时,还可以采用深度优先搜索进行拓扑排序。由于图中无环,当由图中某顶点出发进行DFS遍历的时候,最先退出算法
的顶点即为初度为0的顶点,它是拓扑有序序列中序列当中的最后一个顶点,因此按照DFS算法的先后次序记录下的顶点序列即为逆向的
拓扑有序序列
对这句的详细理解:
1、最先退出算法的顶点即为出度为0的顶点,此处退出算法是指所遍历的顶点退出当前系统栈
2、按照DFS算法的先后次序记录下的顶点序列。此处按照DFS算法先后次序并不是指最终遍历结果序列,而是顶点退出系统栈的顺序
拓展练习:在有向图G中,若r到G的每个顶点都有路径可达,则r为G的根结点。编写一个算法判断有向图中是否存在根,若有,打出根结点
//的值,假设图已经存在邻接表G中
分析:判断r到G中每个顶点是否有路径可达,则可以通过深度优先搜索的方法,以r为为起点进行深度优先遍历,若在函数DFS()推出前已
经访问过所有的顶点,则r为根。要打印出所有的根结点,可以对图中的每个顶点都调用一次函数DFS,若是根则打印。
int DFS_Vedge_visit[MAXSIZE], sum;
//以下是深度优先搜搜遍历算法
void DFS_Judge(AGraph *&G, int v)
{
ArcNode *p = NULL;
DFS_Vedge_visit[v] = 1;
++sum; //此处为DFS算法的修改部分,将函数Visit()换成了++sum,即每次访问一个顶点时计数器加1
p = G->adjlist[v].firstarc;
while ( p != NULL)
{
if (DFS_Vedge_visit[p->adjvex] == 0)
{
DFS_Judge(G, p->adjvex);
}
}
}
void print_DFS_Edges(AGraph *G)
{
int i = 0, j = 0;
for (i = 0; i < G->n; ++i)
{
sum = 0;
for (j = 0; j < G->n; ++j)
DFS_Vedge_visit[j] = 0;
DFS(G, i);
if (sum == G->n)
{
cout << i << endl;
}
}
}
设有向图采用邻接表表示,图中有n个顶点,表示为n。试写一个算法求顶点k的入度
分析:要求顶点k的入度,必须检查表中的所有的边结点。看看边结点所指顶点是否为k,若是则证明k有一个入度,做一次统计。因此需要设计一个技术其来统计k的入度
代码如下:
int count(AGraph *G, int k)
{
ArcNode *p;//p用来扫描每个顶点所发出的边
int i = 0, sum = 0; //sum为顶点k的计数器
for (int i = 0; i < G->n; ++i)
{
p = G->adjlist[i].firstarc;
while ( p != NULL)
{
if (k == p->adjvex)
{
++sum;
break;
}
p = p->nextarc;
}
}
return sum; //返回k的入度
}
试写出一邻接表为存储结构的的深度优先搜索算法的非递归版本
递归的本质就是调用了系统已有的栈来帮助我们解决问题。因此,对于算法的非递归版本的关键在于需要手写一个
栈来代替系统栈。
当问题执行到一个状态,一现有的条件无法完全解决是,必须先要先记下当前状态,然后继续往下进行,等条件成熟之后
在返回来解决。这一类的问题可以用栈解决
根据上述的分析可以写出下面的代码:
void DFS_Non_Recursive(AGraph *&G, int v)
{
ArcNode *p = NULL;
stack<int> dfs_stack; //顶一个栈来记录访问过程中的顶点
int j = 0, k = 0;
int visit[MAXSIZE] = { 0 };//顶点访问标记数组
for (i = 0;i <G->n;++i)
visit[i] = 0;
//访问顶点v的操作
visit[i] = 1; //标记其实顶点v已经被访问过
dfs_stack.push(v); //其实顶点入栈
//以下是本算法的关键步骤
while (!(dfs_stack.empty()))
{
k = dfs_stack.top(); //取栈顶元素
p = G->adjlist[k].firstarc; //p指向该顶点的第一条边
//下面这个循环是p沿着边行走并将图中经过的顶点入栈的过程
while ( p != NULL && 1 == visit[p->adjvex])
{
p = p->nextarc; //找到当前顶点第一个没有访问过的邻接顶点或者p走到当前链表尾部时,while循环停止
}
if (NULL == p)
dfs_stack.pop(); //如果p到达当前链表尾部,则说明当前顶点的所有点都访问完毕,当前顶点出栈
else //否则访问当前邻接点并入栈
{
//Visit(p->adjvex); //即访问顶点p->adjvex的函数
visit[p->adjvex] = 1;
dfs_stack.push(p->adjvex);
}
}
}
省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路到达即可)。
现在得到城镇道路统计表,表中列出了任意两个城镇间修建道路的费用,以及该道路是否修通的状态,全省一共N个村庄,编写程序,计算
出全省需要的最低成本。道路信息存储在Road_Array[]数组中,Road_Array[]数组定义如下:
typedef struct
{
int a, b; //a、b道路两端的两个村庄
int cost;//如果a、b间需要修路,则cost为修路费用
int is; //is为0代表a、b间还未修路,is等于1代表a、b间已经修好了路
}Road_Array; //道路结构体类型
Road road[ M + 1];//村庄间的道路信息已经存储在于road[]中,M为已经定义的常量,假设已修以及待修的道路一共有M条
#define M 1000 //定义所有道路(待修的+已经修的)
typedef struct
{
int a, b; //a、b道路两端的两个村庄
int cost;//如果a、b间需要修路,则cost为修路费用
int is; //is为0代表a、b间还未修路,is等于1代表a、b间已经修好了路
}Road_Array; //道路结构体类型
Road_Array road[ M + 1];//村庄间的道路信息已经存储在于road[]中,M为已经定义的常量,假设已修以及待修的道路一共有M条
int GetRoot(int a, int set[])
{
while (a != set[a] )
{
a = set[a];
}
return a;
}
#define N 1000 //定义村庄数目
int LowCost(Road_Array road[]) //本算法返回最小花费
{
int i = 0;
int a = 0, b = 0,min = 0;
int set[100]; //定义并查集
for (i = 0; i < N; ++i)
set[i] = i; //初始化并查集,各村庄是孤立的,因此自己就是根结点
for (i = 0; i < M; ++i)
{
//下面这个if语句将已经有道路相连的各个村庄合并为一个集合
if (road[i].is == 1)
{
a = GetRoot(road[i].a, set);
b = GetRoot(road[i].b, set);
set[a] = b;
}
}
/*C++中的快排函数,即对road[]中的M条道路按照花费进行递增排序*/
qsort(road, M, sizeof(Road), [](void const *t1,void const * t2)->int
{
return ((Road_Array *)t1)->cost < ((Road_Array *)t2)->cost ? 1 : 0;
}
);
//下面这个循环从road[]数组挑出逐个应该修的道路
for (i = 0; i < M; ++i)
{
if (0 == road[i].is && (a = GetRoot(road[i].a, set)) != (b = GetRoot(road[i].b, set)))
{//当a、b不属于一个集合的时候,并且a、b间没有道路的时候,将a、b并入同一集合,并记录下修建a、b间道路所需要的花费
set[a] = b;
min += road[i].cost;
}
}
return min;
}