前言
这一章内容比较多,每一节的知识点都很复杂,需要花很长时间才能搞懂,希望看的人耐心一点。
六、图
目录
图由顶点的有穷非空集合和顶点之间的边集合组成。记作G(V,E),G表示一个图,V是顶点的集合,E是边的集合。
图分为无向图和有向图,无向图的边用(A,B)或者(B,A)表示,可以互换,称为边,而有向图的边用<A,B>表示方向是A到B方向,不能写成<B,A>,称为弧。
在无向图中,如果任意两个顶点之间都存在边,则称为无向完全图,n个顶点,一共有条边。
在有向图中,如果任意两个顶点之间都存在两条有向且相反的两条边,则称为有向完全图,n个顶点,一共有n(n-1)条边。
结论:对于n个节点,e条边数的图,无向图:
;有向图:
有很少边或弧的图称为稀疏图,有较多的边或弧的图称为稠密图,这是一个相对概念,没有绝对的标准。
图的边也可以有数值,称为权。这种带权的图通常称为网。
1、图的顶点和边间的关系
无向图:G=(V,{E}),边(v,v')E,则称为v和v'互为邻接点,即v和v'相连。顶点v的度是和v相关联的边的数目,记为TD(v)。
有向图:G=(V,{E}),弧<v,v'>E,以v为头的弧的数目称为v的入度,记为ID(v),以v为尾的弧的数目称为v的出度,记为OD(v),顶点v的度为TD(v)=ID(v)+OD(v),
路径:即顶点v到顶点v'需经过的顶点序列。路径的长度是路径上的边或弧的数目。第一个顶点到最后一个顶点相同的路径称为回路或环。序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点以外,其余顶点不重复出现的回路,称为简单回路或简单环。
2、连通图
在无向图G中,如果从顶点v到顶点v'有路径,则称v和v'是连通的。如果任意两个顶点都是连通的,则称G是连通图。
无向图中的极大连通子图称为连通分量。(1)要是子图;(2)子图要是连通的;(3)连通子图含有极大顶点数;(4)具有极大顶点数的连通子图包含依附于这些顶点的所有边。
在有向图G中,如果对于每一对vi,vjV、vi
vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称作有向图的强连通分量。
无向图中连通图的生成树是一个极小的连通子图,含有n个顶点和n-1条边,无环路。
有向图中一顶点入度为0其余顶点入度为1的叫做有向树。
一个有向图由若干棵有向图构成森林。
3、图的存储结构
(1)邻接矩阵
用两个数组表示图,一个一维数组存储顶点信息,一个二维数组(称为邻接矩阵)存储边或弧的信息。
1)对于无向图:
图1 无向图
如图1所示,顶点数组:
V0 | V1 | V2 | V3 |
边数组:
V0 | V1 | V2 | V3 | |
V0 | 0 | 1 | 1 | 1 |
V1 | 1 | 0 | 1 | 0 |
V2 | 1 | 1 | 0 | 1 |
V3 | 1 | 0 | 1 | 0 |
根据邻接矩阵,可知(1)当值为1时,即两个顶点间有边;(2)行的和即为顶点的度;(3)求顶点vi的连接点,即把vi这一行扫描一遍,当有值为1即可知道。
2)对于有向图
图2 有向图
边数组:
V0 | V1 | V2 | V3 | |
V0 | 0 | 0 | 0 | 1 |
V1 | 1 | 0 | 1 | 0 |
V2 | 1 | 1 | 0 | 1 |
V3 | 0 | 0 | 0 | 0 |
行:关于顶点的出度
列:关于顶点的入度
3)网
对于网,即是带有权重的图,所以对于网的邻接矩阵,当i=j时,为0;当有边时,值就是边上的权重,当没有边时,值就是。
代码如下:
typedef char VertexesType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
#define MAXVEX 100 //最大顶点数,应由用户定义
#define INFINITY 65535 //用65535来代替无穷大
typedef struct
{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可看作边表
int numVertexes,numEdges; //图中当前的顶点数和边数
}MGraph;
(2)邻接表
当一个图的边数很少的时候,用邻接矩阵存储就会很浪费,所以引入了邻接表。
顶点还是用数组存储,但需要指向第一个邻接点的指针,以便于查找该顶点的边信息。
图中每个顶点的所有邻接点构成一个线性表。
对于无向图:
图3 邻接表表示无向图
1)度:即每个顶点的边表中节点的个数
2)判断vi和vj是否存在边:即找vi的边表中是否有节点j存在
3)求顶点的所有邻接点:对此顶点的边表进行遍历
对于有向图:
一个邻接表要么只能表示每个顶点出度的邻接点或者就是每个顶点入度的邻接点。
对于网:
即邻接节点还需要一个数据域来表示权重信息。
代码如下:
typedef char VertexType; //顶点类型由用户定义
typedef int EdgeType; //边上的权值类型由用户定义
typedef struct EdgeNode //边表节点
{
int adjvex; //存储该顶点对应的下标
EdgeType weight; //存储权值
struct EdgeNode *next; //指向下一个邻接点
}EdgeNode;
typedef struct VertexNode //顶点表节点
{
VertexType data; //存储顶点信息
EdgeNode *firstedge; //边表头指针
}VertexNode,AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; //图中当前顶点数和边数
}GraphAdjList;
(3)十字链表
对于有向表,邻接表只能表示入度或者出度两种情况之一,若想知道另一个信息,必须去遍历整个图才能知道。而十字链表就是将邻接表和逆邻接表结合起来。
顶点表节点结构:
data | firstin | firstout |
firstin:指向该顶点的入边表中第一个节点
firstout:指向该顶点的出边表中第一个节点
边表节点结构:
tailvex | headvex | headlink | taillink |
tailvex:指弧起点在顶点表的下标
headvex:指弧终点在顶点表中的下标
headlink:入边表的指针域
taillink:指向起点相同的下一条边
图4 十字链表表示法
(4)邻接多重表
对于无向图,邻接表似乎没有什么问题,但如果我们对边进行处理时,比如删除边操作,就会比较麻烦,所以在这里,我们引入邻接多重表。
ivex | ilink | jvex | jlink |
ivex和jvex是某条边依附的两个顶点在顶点表中的下标。
ilink:指向依附顶点ivex的下一条边。
jlink:指向依附顶点jvex的下一条边。
图5 邻接多重表
(5)边集数组
由两个一维数组构成,一个存顶点信息,另一个存边的信息,这个边数组每个元素由一条边的起点下标、终点下标和权组成。
begin | end | weight |
4、图的遍历
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这个过程叫做图的遍历。
两种方案:深度优先遍历和广度优先遍历
(1)深度优先遍历,类似于树的前序遍历
const int MAX = 100;
boolean visited[MAX];
//对邻接矩阵使用深度优先递归算法
void DFS(MGraph G, int i)
{
int j;
visited[i] = TRUE;
cout << G.vexs[i];
for (j = 0; j < G.numVertexes; j++)
{
if (G.arc[i][j] == 1 && !visited[j]) //对未访问的节点使用深度优先递归算法
{
DFS(G, j);
}
}
}
//对图使用深度优先遍历算法
void DFSTraverse(MGraph G)
{
for (int i = 0; i < G.numVertexes; i++)
{
visited[i] = FALSE;
}
for (int i = 0; i < G.numVertexes; i++)
{
if (!visited[i])
{
DFS(G, i);
}
}
}
//对邻接矩阵使用深度优先递归算法
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = TRUE;
cout << GL->adjList[i].data; //打印顶点
p = GL->adjList[i].firstedge;
while (p)
{
if (!visited[p->adjvex])
DFS(GL, p->adjvex);
p = p->next;
}
}
void DFSTraverse(GraphAdjList GL)
{
for (int i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE;
for (int i = 0; i < GL->numVertexes; i++)
{
if (!visited[i])
{
DFS(GL, i);
}
}
}
(2)广度优先遍历,类似于树的层次遍历
图6 广度优先遍历的图
在这个问题上,我们用一个队列来解决,假设从V0开始遍历,即让V0入队,与V0相连的节点是V1和V2,则先让队列中队首位置的节点出队,即V0出队,则记为已访问,再让V1和V2入队,与V1相连的节点是V3,则先让队首位置的V1出队,记为已访问,再让V3入队,接着让V2出队,让V4入队,再让V3出队,最后让V4出队。
注:每次入队的节点是与当前出队节点相连的节点,第一次出队节点是:V0,第二次是V1,第三次是V2,第四次是V3
出队的节点即是访问的节点
下面以邻接矩阵的方式存储图的信息进行广度优先遍历:
(其实关于这个算法文字的解释,博主很快就理解了,反而是程序理解了很久,可能是对邻接矩阵不够了解吧)
void BFSTraverse(MGraph G)
{
Queue Q;
InitQueue(&Q); //初始化一个队列辅助
for (int i = 0; i < G.numVertexes; i++)
{
visited[i] = FALSE;
}
for (int i = 0; i < G.numVertexes; i++)
{
if (!visited[i])
{
visited[i] = TRUE; //设置当前节点访问过
cout << G.vex[i]; //打印此节点
EnQueue(&Q, i); //将此节点入队
}
while (!QueueEmpty(Q))
{
DeQueue(&Q, &i); //将队首元素出队,赋值给i
cout << G.vexs[i]; //出队即输出
//寻找与出队元素相连的节点
for (int j = 0; j < G.numVertexes; j++)
{
if (G.arc[i][j] == 1 && !visited[j])
{
visited[j] = TRUE; //将找到的顶点标记为已访问
//cout << G.vexs[j];
EnQueue(&Q, j); //将找到的顶点入队
}
}
}
}
}
下面以邻接表的方式存储图的信息进行广度优先遍历:
void BFSTraverse2(GraphAdjList GL)
{
EdgeNode *p;
Queue Q;
InitQueue(&Q);
for (int i = 0; i < GL->numVertexes; i++)
{
if (!visited[i])
{
cout << GL->adjList[i].data;
EnQueue(&Q, i);
}
while (!QueueEmpty(Q))
{
DeQueue(&Q, &i);
cout << GL->adjList[p->adjvex].data;
p = GL->adjList[i].firstedge;
while (p)
{
if (!visited[p->adjvex])
{
visited[p->adjvex] = TRUE;
EnQueue(&Q, p->adjvex);
}
p = p->next;
}
}
}
}
两种遍历时间复杂度是一样的,只是访问的顺序不同,深度优先更适合目标比较明确的,以找到目标为主要目的;而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
5、最小生成树
我们把构造连通图的最小代价生成树称为最小生成树。
找最小生成树有两个方法:普里姆算法和克鲁斯卡尔算法
(1)普里姆算法
图7 网
对于图7的网,我们写出它的邻接矩阵:
V0 | V1 | V2 | V3 | V4 | |
V0 | 0 | 5 | 7 | ||
V1 | 5 | 0 | 6 | ||
V2 | 7 | 0 | 5 | 12 | |
V3 | 6 | 5 | 0 | 4 | |
V4 | 12 | 4 | 0 |
我们先设定两个矩阵:adjvex[MAXVEX]和lowcost[MAXVEX],第一个矩阵表示开始寻找的起始点,先初始化为0,表示从0开始,寻找权值最小的终点,第二个矩阵初始化为邻接矩阵第0行的值。
第一步:adjvex:X 0 0 0 0 (以X表示此点已找过)
lowcost:0 5 7
判断条件:lowcost!=0,且找到最小值
结果:i=1时,lowcost最小,即让lowcost[1]=0,表示找到此顶点,输出<adjvex[1],1>
第二步:adjvex:X X 0 0 0
lowcost:0 0 7
接下来从V0和V1出发寻找,所以adjvex需要调整起始点,而lowcost需要调整此时到达每个终点的权值
adjvex:X X 0 1 3 (V2还是从V0出发,V3从V1出发,V4依旧是初始状态)
lowcost:0 0 7
与 V1:X X
6
,两个进行比较,更新后:
lowcost:0 0 7 6
判断条件:lowcost!=0,且找到最小值
结果:i=3时,lowcost最小,即让lowcost[3]=0,表示找到此顶点,输出<adjvex[3],3>
第三步:adjvex:X X 0 X 0
lowcost:0 0 7 0
接下来从V0和V3出发寻找,所以adjvex需要调整起始点,而lowcost需要调整此时到达每个终点的权值
adjvex:X X 0 X 3 (V2还是从V0出发,V4从V3出发)
lowcost:0 0 7 0 与 V3:X X 5 X 4,两个进行比较,更新后:
lowcost:0 0 5 0 4
判断条件:lowcost!=0,且找到最小值
结果:i=4时,lowcost最小,即让lowcost[4]=0,表示找到此顶点,输出<adjvex[4],4>
第四步:adjvex:X X 0 X 3
lowcost:0 0 5 0 4
接下来从V0、V3和V4出发寻找,所以adjvex需要调整起始点,而lowcost需要调整此时到达每个终点的权值
adjvex:X X 3 X X
lowcost:0 0 5 0 0
即当从V3出发找到V2这条路径最短,此时寻找完毕。
图8 最小生成树
程序如下:
//Prim算法生成最小生成树
void MiniSpanTree_Prim(MGraph G)
{
int min;
int adjvex[MAXVEX];
int lowcost[MAXVEX];
for (int i = 0; i < G.numVertexes; i++)
{
lowcost[i] = G.arc[0][i]; //初始化lowcost为V0这一行的权值
adjvex[i] = 0; //初始化起始点都为V0
}
for (int i = 0; i < G.numVertexes; i++)
{
min = INFINITY; //初始化最小权值为无穷大
int j = 0,k;
while (j < G.numVertexes)
{
if (lowcost[j] != 0 && lowcost[j] < min)
{//如果权值不为0,即是还没被找到
min = lowcost[j];
k = j;
}
j++;
}
//找到了下一个顶点
cout << adjvex[k] << " " << k;
//找到的顶点后,需要让它的权值为0,表示已找到
lowcost[k] = 0;
//下面即是对Vk这一行和lowcost进行比较,保留较小值
for (int j = 0; j < G.numVertexes; j++)
{
if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j])
{
lowcost[j] = G.arc[k][j];
adjvex[j] = k; //改变此时达到节点k的起始点
}
}
}
}
(2)克鲁斯卡尔算法
以边为目标去构建,因为权值在边上,直接去找最小权值的边来构建生成树,只是需要考虑是否会形成环路而已。
首先以edge边集数组结构定义代码:
typedef struct
{
int begin;
int end;
int weight;
}Edge;
图9 图7的边集数组
这个边集数组是已经按照从小到大的顺序排好了,为了方便查找。在这里还需要定义一个parent数组,用来存每个顶点的终点信息,先初始化为0,也可以表示所有顶点都没有连接,接下来就开始依次连接,找最短的路径了。
第一步:找到第一条边,begin=3,end=4,此时parent为:
parent: | 0 | 0 | 0 | 0 | 0 |
对应下标: | 0 | 1 | 2 | 3 | 4 |
下标我们可以认为是起始点,parent对应的值可以认为是终点。
即现在3和4的终点都为0,即从3和4出发,还没走就已经止步,表示可以连接,即选择连接3和4。此时从3出发可以走到4,即4是3的终点,所以更新parent[3]=4
parent: | 0 | 0 | 0 | 4 | 0 |
对应下标: | 0 | 1 | 2 | 3 | 4 |
第二步:继续往下找第二条边,begin=0,end=1,发现parent[0]=parent[1]=0,表示两者可以连接,此时从0出发的终点是1,所以更新parent[0]=1
parent: | 1 | 0 | 0 | 4 | 0 |
对应下标: | 0 | 1 | 2 | 3 | 4 |
第二步:继续往下找第三条边,begin=2,end=3,发现parent[2]=0,parent[3]=4,两者不等,可以相连,此时从2出发的终点是3,所以更新parent[2]=3,由于前面从3出发可以走到4,现在可以从3走到3,即从2也可以走到4,再次更新parent[2]=4
parent: | 1 | 0 | 4 | 4 | 0 |
对应下标: | 0 | 1 | 2 | 3 | 4 |
博主注:parent每次的更新,需更新到从起始点出发走到最终的终点下标
第四步:继续往下找第四条边,begin=1,end=3,parent[1]=0,parent[3]=4,两者不等,可以连接,此时从1出发的终点是3,所以更新parent[1]=3,由于前面从3出发可以走到4,所以从1也可以走到4,再次更新parent[1]=4
parent: | 1 | 4 | 4 | 4 | 0 |
对应下标: | 0 | 1 | 2 | 3 | 4 |
第五步:继续往下找第五条边,begin=0,end=2,parent[0]=parent[2]=4!=0,所以两者会形成回路,不能连接
第六步:继续往下找第六条边,begin=2,end=4,parent[2]=4,可以到达4,而parent[4]=0,即终止,而从4出发,也是到达4终止,因此两个会形成回路,不能连接。
扫描完毕!
下面来看程序:
int Find(int *parent, int f)
{
while (parent[f] > 0) //循环过程就是不断寻找终点,找到最终终点
{
f = parent[f];
}
return f;
}
void MiniSpanTree_Kruskal(MGraph G)
{
Edge edges[MAXEDGE];
int parent[MAXVEX];
//此处省略将邻接矩阵G转化为边集数组edges并按权从小到大排序代码
for (int i = 0; i < G.numVertexes; i++)
{
parent[i] = 0;
}
for (int i = 0; i < G.numEdges; i++)
{
int begin1, end1;
begin1 = Find(parent, edges[i].begin);
end1 = Find(parent, edges[i].end);
if (begin1 != end1) //若begin1=end1,则会形成环路
{
parent[begin1] = end1;//跟新起始点的终点
cout << edges[i].begin << " " << edges[i].end << " " << edges[i].weight << endl;
}
}
}
普里姆算法的时间复杂度:O(
) (n为顶点数)
克鲁斯卡尔算法的时间复杂度:O(eloge) (e为边数)
6、最短路径
对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。
下面介绍两种从某源点到其余顶点的最短路径问题。
(1)迪杰斯特拉算法
基本想法:一步步根据已求出的最短路径基础上,求得更远顶点的最短路径,最终得到想要的结果。
图10 网
如图10所示的网,假设V0为源点,现在求解从V0出发到达V1的最短路径,很容易发现为1,而计算从V0出发到V2的最短路径,首先反应过来的就是5,但是你会发现从V0出发经过V1再到V2,这时路径的长度为1+3=4,比5短,对于这个算法的核心思想就是,目前我们已经找到了V0和V1,现在分别计算从这两点出发到达终点的路径和,记住从V1出发的基础路径已经有长度为1了。
下面看下程序:
#define MAXVEX 9
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX]; //用来存储最短路径下标的数值
typedef int ShortPathTable[MAXVEX]; //用来存储各点最短路径的权值和
//P[v]的值为前驱顶点的下标,D[v]表示v0到v的最短路径和
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx *p, ShortPathTable *D)
{
int flag[MAXVEX]; //当flag[i]=1表示已求得v0到vi的最短路径
//初始化所有数据
for (int i = 0; i < G.numVertexes; i++)
{
flag[i] = 0; //全部顶点还未求解最短路径
(*D)[i] = G.matirx[v0][i]; //将与v0有连线的顶点加上权值
(*p)[i] = 0; //初始化所有起始点都为0
}
(*D)[v0] = 0; //v0到v0的最短路径为0
flag[v0] = 1; //v0到v0不需要求解路径
//开始求解v0到每一个顶点的最短路径
int min,min_k;
for (int j = 1; j < G.numVertexes; j++)
{
min = INFINITY;
for (int k = 0; k < G.numVertexes; k++)
{//寻找离v0最近的顶点,按照网中所示,会先找到v1
if (!flag[k] && (*D[k]) < min)
{
min_k = k;
min = (*D)[k];
}
}
flag[min_k] = 1; //将目前找到的最近的顶点设置为1
//修正当前最短路径及距离,即目前有两个起始点,v0和vmin_k
for (int k = 0; k < G.numVertexes; k++)
{
if (!flag[k] && (min + G.matirx[min_k][k] < (*D)[k]))
{
//说明以min_k出发找到了更短的路径,修改D[k]和P[k]
(*D)[k] = min + G.matirx[min_k][k];
(*p)[k] = min_k; //修改此时的起始点
}
}
}
}
下面根据程序来详细解释下算法的过程:
1)先写出网的邻接矩阵
V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | |
V0 | 0 | 1 | 5 | ||||||
V1 | 1 | 0 | 3 | 7 | 5 | ||||
V2 | 5 | 3 | 0 | 1 | 7 | ||||
V3 | 7 | 0 | 2 | 3 | |||||
V4 | 5 | 1 | 2 | 0 | 3 | 6 | 9 | ||
V5 | 7 | 3 | 0 | 5 | |||||
V6 | 3 | 6 | 0 | 2 | 7 | ||||
V7 | 9 | 5 | 2 | 0 | 4 | ||||
V8 | 7 | 4 | 0 |
2)先初始化参数:flag=[0 0 0 0 0 0 0 0 0],D=[65535 1 5 65535 65535 65535 65535 65535 65535],p=[0 0 0 0 0 0 0 0 0]
后来令D[v0]=0,令flag[v0]=1,即从V0出发。
3)开始遍历每个顶点与V0的最短路径,所有下标从1开始。
4)下面这一层循环就是从V0出发,找距离V0最短的路径,找到D[1]=1为最短,即找到V1顶点,令flag[1]=1,此时flag=[1 1 0 0 0 0 0 0 0],D=[0 1 5 65535 65535 65535 65535 65535 65535]
5)下面又是一层循环,即是更新D,从V1出发重新计算最短路径,不过要加上之前的路径长度1,由邻接矩阵第二行知:V1=[X X 3 7 5 65535 65535 65535 65535],加上1为:[X X 4 8 6 65535 65535 65535 65535]与之前的D=[0 1 5 65535 65535 65535 65535 65535 65535]一一比较更新为最短路径:D=[0 1 4 8 6 65535 65535 65535 65535],而对应的p也需要更新,之前的D都是从V0出发计算而得,现在的D有部分是根据从V1出发计算而得,所以p相应的位置需要更新为1,即p=[0 0 1 1 1 0 0 0 0]
6)就是这样重复循环,已经找到的顶点,即flag=1的顶点不参与比较路径值。
(2)费洛伊德算法
图11
如图1所示,我们有两个矩阵和
,
代表顶点到顶点的最短路径权值和矩阵,
代表对应顶点的最小路径的前驱矩阵。
首先来分析下所有顶点经过V0后达到另一个顶点的最短路径,因为只有三个顶点,因此需要查看v1-->v0-->v2,得到[1][0]+
[0][2]=2+1=3,而
[1][2]=5,我们发现
[1][2]>
[1][0]+
[0][2],通俗地讲就是v1-->v0-->v2比直接v1-->v2距离还要近,所以我们让
[1][2]=
[1][0]+
[0][2]=3,于是就有了
,
也需要做相应的改变,
接下来就是在和
的基础上继续处理所有顶点经过v1和v2后到达另一个顶点的最短路径,得到
和
、
和
完成所有顶点到所有顶点的最短路径计算。
以上即是算法的整个过程。
程序如下:
//求网图中各顶点v到其余顶点w最短路径p[v][w]及带权长度D[v][w]
void ShortestPath_Floyd(MGraph G, Pathmatirx *p, ShortPathTable *D)
{
//初始化D和p
for (int v = 0; v < G.numVertexes; v++)
{
for (int w = 0; w < G.numVertexes; w++)
{
(*D)[v][w] = G.matirx[v][w];
(*p)[v][w] = w;
}
}
for (int k = 0; k < G.numVertexes; k++)
{
for (int v = 0; v < G.numVertexes; v++)
{
for (int w = 0; w < G.numVertexes; w++)
{
if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w])
{//如果经过下标为k顶点路径比原两点间路径更短
//将当前两点间权值设为更小的一个
(*D)[v][w] = (*D)[v][k] + (*D)[k][w];
(*p)[v][w] = (*p)[v][k];//路径设置经过下标为k的顶点
}
}
}
}
}
这个算法特别简洁,就是一个二重循环初始化加上一个三重循环权值修正。
两个算法的时间复杂度都是:O(
)
7、拓扑排序
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网(Activity On Vertex Network)。
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1,v2,...,vn,满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必在顶点vj之前,则我们称为这样的顶点序列为拓扑序列。
所谓拓扑序列,其实就是对一个有向图构造拓扑序列的过程。
解决一个工程是否能顺利进行的问题。
拓扑排序算法:
基本思想:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。
由于有删除顶点操作,所以选择邻接表数据结构,更为方便。
in | data | firstedge |
in就是入度的个数
图12 拓扑排序
在拓扑算法中,涉及到结构代码:
typedef struct EdgeNode //边表节点
{
int adjvex; //储存该顶点对应的下标
int weight; //用于存储权值
struct EdgeNode *next; //指向下一个邻接点
}EdgeNode;
typedef struct VertexNode //顶点表节点
{
int in; //顶点入度
int data; //边表头指针
EdgeNode *firstedge;
}VertexNode,AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; //图中当前顶点数和边数
}graphAdjList,*GraphAdjList;
//拓扑排序,若GL无回路,则输出拓扑排序序列并返回OK,若有回路返回ERROR
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i, k, gettop;
int top = 0; //用于栈指针下标
int count = 0; //用于统计输出顶点的个数
int *stack; //建栈存储入度为0的顶点
stack = (int *)malloc(GL->numVertexes * sizeof(int));
for (i = 0; i < GL->numVertexes; i++)
{
if (GL->adjList[i].in == 0)
{
stack[++top] = i; //将入度为0的顶点入栈
}
}
while (top != 0)
{
gettop = stack[top--]; //出栈
cout << GL->adjList[gettop].data;
count++; //统计输出顶点数
for (e = GL->adjLisrt[gettop].firstedge; e; e = e->next)
{//对此顶点弧表遍历
k = e->adjvex;
if (!(--GL->adjList[k].in)) //将k号顶点邻接点的入度减一
{
stack[++top] = k; //若为0则入栈,以便于下次循环输出
}
}
}
if (count < GL->numVertexes) //如果count小于顶点数,说明存在环
return ERROR;
else
return OK;
}
此算法的过程就是:不断将入度为0的顶点先压栈,再将栈顶元素出栈,即输出,并将与输出元素相连的顶点入度数减一,这样往返出栈,减少与之相连的入度数,当入度为0时,即可压栈,进行同样的操作
时间复杂度:O(n+e)
8、关键路径
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网(Activity On Edge Network)。
我们把AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点。
我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫关键活动。
为了缩短整个工程的活动时间,则需找到关键路径,然后缩短关键路径上的活动时间。
下面详细讲解关键路径算法原理:
做法:需要找到所有活动的最早开始时间和最晚开始时间,并且比较他们,如果相等就意味着此活动是关键活动,活动间的路径为关键路径,如果不等,则就不是。
为此,我们定义几个参数:
1)etv:顶点vk的最早发生时间
2)ltv:顶点vk的最晚发生时间,也就是每个顶点对应的事件最晚需要开始时间,超过此时间会延误整个工期
3)ete:弧ak的最早发生时间
4)lte:弧ak的最晚发生时间,也就是不推迟工期的最晚开工时间
我们是由1和2可以求得3和4,然后再根据ete[k]是否等于lte[k]相等来判断ak是否是关键活动。
图13
与拓扑排序时邻接表结构不同得地方在于,这里弧链表增加了weight域,用来存储弧得权值。
求事件的最早发生时间etv的过程,就是我们从头至尾找拓扑序列的过程,因此,在求关键路径之前,需要先调用一次拓扑序列算法的代码来计算etv和拓扑序列列表,为此,首先在程序开始处声明几个全局变量。
int *etv,*ltv; //事件最早发生事件和最迟发生事件数组
int *stack2; //用于存储拓扑序列的栈,以便后面求关键路径时使用
int top2; //用于stack2的指针
下面是改进的求拓扑序列算法:
//拓扑排序,若GL无回路,则输出拓扑排序序列并返回OK,若有回路返回ERROR
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i, k, gettop;
int top = 0; //用于栈指针下标
int count = 0; //用于统计输出顶点的个数
int *stack; //建栈存储入度为0的顶点
stack = (int *)malloc(GL->numVertexes * sizeof(int));
for (i = 0; i < GL->numVertexes; i++)
{
if (GL->adjList[i].in == 0)
{
stack[++top] = i; //将入度为0的顶点入栈
}
}
top2 = 0; //初始化为0
etv = new int[GL->numVertexes]; //事件最早发生时间,并初始化为0
stack2 = new int[GL->numVertexes]; //初始化
while (top != 0)
{
gettop = stack[top--]; //出栈
cout << GL->adjList[gettop].data;
count++; //统计输出顶点数
stack2[++top2] = gettop; //将弹出的顶点元素压入拓扑序列的栈
for (e = GL->adjLisrt[gettop].firstedge; e; e = e->next)
{//对此顶点弧表遍历
k = e->adjvex;
if (!(--GL->adjList[k].in)) //将k号顶点邻接点的入度减一
{
stack[++top] = k; //若为0则入栈,以便于下次循环输出
}
if ((etv[gettop] + e->weight) > etv[k])
{//求各顶点事件最早发生时间值
etv[k] = etv[gettop] + e->weight;
}
}
}
if (count < GL->numVertexes) //如果count小于顶点数,说明存在环
return ERROR;
else
return OK;
}
图14 求解etv[k]的示意图
,其中p[k]表示所有到达顶点vk的弧的集合,比如p[3]就是图中<v1,v3>和<v2,v3>两条弧,len<vi,vk>是弧<vi,vk>上的权值
下面看看求解关键路径算法:
//求关键路径,GL为有向图,输出GL的各项关键活动
void CriticalPath(GraphAdjList GL)
{
EdgeNode *e;
int i, gettop, k, j;
int ete, lte; //声明活动最早发生时间和最迟到达时间变量
TopologicalSort(GL); //求拓扑序列,计算数组etv和stack2的值
ltv = new int[GL->numVertexes];
//计算ltv
while (top2 != 0)
{
gettop = stack2[top2--];
for (e = GL->adjList[gettop].firstedge; e; e = e->next)
{//求各顶点事件的最迟发生事件ltv
k = e->adjvex;
if (ltv[k] - e->weight < ltv[gettop])
{//求各顶点事件最晚发生事件ltv
ltv[gettop] = ltv[k] - e->weight;
}
}
for (j = 0; j < GL->numVertexes; j++)
{//求ete,lte和关键活动
for (e = GL->adjList[j].firstedge; e; e = e->next)
{
k = e->adjvex;
ete = etv[j]; //活动最早发生时间
lte = ltv - e->weight; //活动最迟发生时间
if (ete == lte)
{
cout << GL->adjList[j].data << GL->adjList[k].data << e->weight;
}
}
}
}
}
当etv[k]=ltv[k]时,即是关键路径