最小生成树
在 n 个城市之间,选择建造 n-1 条线路,使得总的耗费最少;各边代价和最小的那棵生成树称为该连通网的最小代价生成树,简称最小生成树
-
普利姆算法(加点法)
思路
假设 N = (V,E) 是连通网,TE 是最小生成树中边的集合
①U = {u0}(u0∈ V),TE = { }
②在所有 u ∈ V,v ∈ V - U 的边中找一条权值最小的边(u0,v0) 并入集合 TE 中,同时 v0 并入 U
③重复 ②,直至 U = V 为止实现
假设一个无向网 G 以邻接矩阵形式存储,从顶点 u 出发构造 G 的最小生成树 T,要求输出 T 的各条边;为实现这个算法需附设一个数组 closedge,记录从 U 到 V - U 具有最小权值的边;对于每个顶点 vi ∈ V - U ,在辅助数组中存在一个相应分量 closedge[i-1],它包括两个域:lowcost 和 adjvex ,其中 lowcost 存储最小边上的权值,adjvex 存储最小边在 U 中的那个顶点
struct
{
VexTexType adjvex; /* 最小边在 U 中的那个顶点 */
ArcType lowcost; /* 最小边上的权值 */
}closedge[MVNum];
①首先将初始顶点 u 加入 U 中,对其余的每一个顶点 vj,将 closedge[j] 均初始化为到 u 的边信息
②循环 n-1 次,做如下处理:
·从各组边 closedge 中选出最小边 closedge[k],输出此边
·将 k 加入 U 中
·更新剩余的每组最小边信息 closedge[j],对于 V - U 中的边,新增加了一条从 k 到 j 的边,如果新边的权值比 closedge[j].lowcost 小,则将 closedge[j].lowcost 更新为新边的权值
/* 普利姆算法 */
void MiniSpanTree_Prim ( AMGraph G, VerTexType u )
{
k = LocateVex(G,u); /* 定位顶点 u 的下标 */
for(int j=0;j<G.vexnum;j++)
if(j!=k) closedge[j] = {u,G.arcs[k][j]} /* 对V - U中的每个顶点初始化 closedge */
closedge[k].lowcost = 0;
for(int i=1;i<G.vexnum;i++) /* 对连通图加入n-1条边 */
{
k=Min(closedge); /* 第 k 个顶点的 closedge[k] 中存有最小边 */
u0 = closedge[k].adjvex; v0 = G.vexs[k];
printf("(%d,%d)",u0,v0); /* 输出当前最小边 */
closedge[k].lowcost = 0; /* 该顶点并入 U 集 */
for(int j=0;j<G.vexnum;j++)
if(G.arc[k][j] < closedge[j].lowcost) /* 更新最小边 */
closedge[j] = {G.vexs[k],G.arcs[k][j]};
}
}
-
克鲁斯卡尔算法(加边法)
思路
假设 N = (V,E) 是连通网,将 N 中的边按权值从小到大排序
①初始状态为只有 n 个顶点而无边的非连通图 T =(V,{}),图中每个顶点自成一个连通分量
②在 E 中选择权值最小的边,若该边依附的顶点落在 T 中的不同的连通分量上(即不会形成回路),则将此边加入 T 中,否则舍去此边而选择下一条权值最小的边
③重复 ②,直至 T 中所有顶点都在同一连通分量上为止实现
算法的实现需要引入以下辅助的数据结构
①结构体数组 Edge:存储边的信息,包括边的两个顶点信息和边的权值
②Vexset[i]:标识各个顶点所属的连通分量;对于每个顶点 vi ∈ V,在辅助数组中都存在一个相应的元素 Vexset[i] 表示该顶点所在的连通分量;初始时 Vexset[i] = i,表示各顶点自成一个连通分量
/* 辅助数组 Edge 的定义 */
struct
{
VexTexType Head; /* 边的始点 */
VexTexType Head; /* 边的终点 */
ArcType lowcost; /* 边上的权值 */
}Edge[arcnum];
/* 辅助数组 Vexset 的定义
int Vexset[MTNum];
①将数组 Edge 中的元素按权值从小到大排序
②依次查看数组 Edge 中的边,循环执行以下操作:
·依次从排好序的数组 Edge 中选出一条边(U1,U2)
·在 Vexset 中分别查找 v1 和 v2 所在的连通分量 vs1 和 vs2,进行判断:
> 如果 vs1 和 vs2 不等,表明两个顶点分属不同的连通分量,输出此边,并合并 vs1 和 vs2 两个连通分量
> 如果 vs1 和 vs2 相等,表明所选的两个顶点属于同一个连通分量,舍去此边而选择下一条权值最小的边
/* 克鲁斯卡尔算法 */
void MiniSpanTree_Kruskal ( AMGraph G )
{
Sort(Edge); /* 排序 */
for(int i=0;i<G.vexnum;i++)
Vexset[i] = i; /* 每一顶点自成连通分量 */
for(int i=1;i<G.arcnum;i++) /* 依次查看Edge中的边 */
{
v1 = LocateVex(G,Edge[i].Head); /* 定位v1为边始点下标 */
v2 = LocateVex(G,Edge[i].Tail); /* 定位v2为边终点下标 */
vs1 = Vexset[v1]; vs2 = Vexset[v2]; /* 获取两顶点所在连通分量 */
if(vs1 != vs2)
{
printf("(%d,%d)",Edge.Head[i],Edge.Tail[i]); /* 输出此边 */
for(int j=0;j<G.vexnum;j++)
if(Vexset[j] == vs2) Vexset[j] = vs1; /* 合并vs1 和vs2 两个分量 */
}
}
}
普利姆算法的时间复杂度为 O(n2),与网中的边数无关,适合求稠密网的最小生成树
克鲁斯卡尔算法的时间复杂度为 O(elog2e),与网中的边数有关,适用于求稀疏网的最小生成树
最短路径
-
迪杰斯特拉算法(求从某个源点到其余各顶点的最短路径)
思路
对于网 N = (V,E),将 N 中的顶点分为两组
①第一组 S :已求出的最短路径的终点集合(初始只包含源点 v0 )
②第二组 V - S : 尚未求出最短路径的顶点集合
③算法按各顶点与 v0 间最短路径长度递增的次序,逐个将集合 V - S 中的顶点加入到集合 S 中去;这一过程中,总保持从 v0 到集合 S 中各顶点的路径长度始终不大于到集合 V - S 中各顶点的路径长度实现
假设用带权的邻接矩阵 arcs 来表示带权有向网 G,G.arcs[i][j] 表示弧 <vi,vj> 上的权值,若 <vi,vj> 不存在,则置为 ∞ ,源点为 v0
①一维数组 S[i]:记录从源点 v0 到终点 vi 是否已经确定最短路径长度
②一维数组path[i]:记录从源点 v0 到终点 vi 的当前最短路径上 vi 的直接前驱顶点序号;其初值为:若 v0 到 vi 有弧,则 path[i] = v0,否则为 -1
③一维数组 D[i]:记录从源点 v0 到 终点 vi 的当前最短路径长度;其初值为:若 v0 到 vi 有弧,则 D[i] 为弧上权值,否则为 ∞
算法
①初始化
·将源点 v0 加入 S 中,即 S[v0] = true
·将 v0 到各个终点的最短路径长度初始化为权值,即 D[i] = G.arcs[v0][vi]
·如果 v0 和顶点 vi 之间有弧,则将 vi 的前驱置为 v0,否则 path[i] = -1
②循环 n-1 次,执行以下操作
·选择下一条最短路径的终点 vk,
·将 vk 加到 S 中,即 S[vk] = true
·根据条件更新从 v0 出发到集合 V - S 上任一顶点的最短路径的长度,若条件 D[k] + G.arcs[k][i] < D[i] 成立,则更新 D[i] = D[k] + G.arcs[k][i] ,同时更改 vi 的前驱为 vk ,path[i] = k
/* 迪杰斯特拉算法 */
void ShortestPath_DIJ ( AMGraph G,int v0 )
{
/* 初始化 */
for(int i=0;i<G.vexnum;i++)
{
S[i] = false;
D[i] = G.arcs[v0][i];
if(D[v]<MaxInt) path[i] = v0; /* 有弧 */
else path[i] = -1;
}
S[v0] = true; D[v0] = 0; /* v0 加入 S */
/* 开始主循环 */
for(int i=1; i<G.vexnum; i++)
{
int v, min;
/* 循环比较以选取当前最短路径 */
for(int w=0;w<G.vexnum;w++)
{
if(!S[w]&&D[w]<MaxInt)
{
v = w; min = D[w];
}
}
S[v] = true; /* 将 v 加入 S */
/* 更新最短路径 */
for(int w=0;w<G.vexnum;w++)
{
if(!S[w]&&(D[v] + G.arcs[v][w] < D[w]))
{
D[w] = D[v] + G.arcs[v][w] ;
path[w] = v;
}
}
}
}
- 迪杰斯特拉算法(每一对顶点之间的最短路径)
另一种方法是分别以图中的每一个顶点为源点调用 n 次迪杰斯特拉算法
思路
仍然使用带权的邻接矩阵 arcs 来表示有向网 G,求从顶点 vi 到 vj 的最短路径
且引入以下辅助的数据结构
①二维数组 path[i][j]:最短路径上顶点 vj 的前一顶点序号
②二维数组 D[i][j]:记录顶点 vi 和 vj 之间的最短路径长度
算法
将 vi 到 vj 的最短路径长度初始化,即 D[i][j] = G.arcs[i][j],然后进行 n 次比较和更新
/* 弗洛伊德算法 */
void ShortestPath_Floyd( AMGraph G )
{
/* 初始化 */
for(int i=0; i<G.vexnum;i++)
for(int j=0; j<G.vexnum;j++)
{
D[i][j] = G.arcs[i][j];
/* i 和 j 之间有弧,则将 j 的前驱置为 i */
if( D[i][j]<MaxInt && i!=j ) path[i][j] = i;
else path[i][j] = -1;
}
/* 求每一对顶点间最短路径 */
for(int k=0; k<G.vexnum;k++)
for(int i=0; i<G.vexnum;i++)
for(int j=0; j<G.vexnum;j++)
{
if(D[i][k] + D[k][j] < D[i][j])
{
D[i][j] = D[i][k] + D[k][j] ;
path[i][j] = path [k][j];
}
}
}
拓扑排序
对于一般的工程,都可以分成若干个称作活动的子工程,而这些子工程之间,通常存在一定条件的约束,如某些子工程的开始必须在另一些子工程的完成之后
用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的图(Activity On Vertex Network),简称 AOV - 网
所谓拓扑排序就是将 AOV - 网中所有顶点排成一个线性序列,该序列满足:若在 AOV - 网中由顶点 vi 到顶点 vj 有一条路径,则在该线性序列中的顶点 vi 必定在 vj 之前
思路
①在有向图中选择一个无前驱的顶点并输出它
②在图中删除该顶点和所有以它为尾的弧
③重复①、②,直至图中不再存在无前驱的点
④若此时输出的顶点数夏鸥i有向图中的顶点数,则说明有向图中存在环,否则输出的顶点序列即为一个拓扑排序
实现
采用邻接表作为图的存储结构
①一维数组 indegree[i]:存放各顶点入度,没有前驱的顶点就是入度为 0 的顶点,删除顶点及以它为尾的弧的操作,可不必真正对图的存储结构进行改变,可用弧头顶点入度减 1 的办法来实现
②栈 S:暂存所有入度为 0 的顶点,这样可以避免重复扫描数组 indegree 检测入度为 0 的顶点,提高算法的效率
③一维数组 topo[i]:记录拓扑序列的顶点序号
算法
①求出各顶点的入度存入数组 indegree[i] 中,并将入度为 0 的顶点入栈
②只要栈不空,则重复以下操作
·将栈顶顶点 vi 出栈并保存在拓扑序列数组 top 中
·将顶点 vi 的每个邻接点的 vk 的入度减 1,如果 vk 的入度变为 0,则将 vk 入栈
③如果输出顶点个数少于 AOV - 网的顶点个数,则网中存在有向环,无法进行拓扑排序,否则拓扑排序成功
/* 拓扑排序 */
boolean TopologicalSort ( AMGraph G, int topo[] )
{
FindInDegree(G,indegree); /* 求出各顶点入度 */
InitStack(S);
for(int i=0;i<G.vexnum;i++)
if(indegree[i] == 0) Push(S,i); /* 入度为 0 者入栈 */
int num = 0; /* 对输出顶点计数,初始个数为 0 */
for( !StackEmpty(S) )
{
Pop(S,i); /* 栈顶顶点出栈 */
topo[num] = i; num++; /* 将 vi保存在拓扑数组topo中 */
p = G.vertices[i].firstarc; /* p指向第一个邻接点 */
while(p!=NULL)
{
k = p ->adjvex; /* vk 为 vi 的邻接点 */
indegree[k] --; /* 邻接点入度减 1 */
if(indegree[k] == 0) Push(S,k); /* 入度减为 0, 则入栈 */
p = p->nextarc; /* 指向下一个邻接点 */
}
}
if(m<G.vexnum) return false; /* 说明图有回路 */
else return true;
}
时间复杂度为 O(n+e)
关键路径
与 AOV - 网对应的是 AOE - 网,即以边表示活动的网;其中,顶点表示事件,弧表示活动,权表示活动持续的时间;通常,AOE - 网可用来估算整个工程的完成时间
(1)估算整项工程完成至少需要多少时间
(2)判断哪些活动是影响工程进度的关键
要估算整项工程完成的最短时间,就是要找一条从源点到汇点的带权路径长度最长的路径,称为关键路径,关键路径上的活动叫关键活动,这些活动是影响工程进度的关键,它们的提前或拖延将使整个工程提前或拖延
在一定范围内,非关键活动的提前完成对于整个工程的进度没有直接的好处,它的稍许拖延也不会影响整个工程的进度,工程指挥者可以把人力物力资源暂时调给关键活动,加速其进展速度,以使整个工程提前完工
4 个描述量
①事件 vi 最早发生时间 ve(i):进入事件 vi 的每一活动都结束,vi 才可发生,所以 ve(i) 是从源点到 vi 的最长路径长度; ve(0) = 0
②事件 vi 最迟发生时间 vl(i):事件 vi 的发生不得延误 vi 的每一后继事件的最迟发生时间;为了不拖延工期, vi 的最迟发生事件不得迟于其后继事件 vk 的最迟发生事件减去活动 <vi,vk> 的持续时间;对于汇点 vl(n-1) = ve(n-1); vl(i) = Min{vl(k)-wi,k}
③活动 ai = <vj,vk> 最早发生时间 e(i):只有 vj 发生了,活动 ai 才可以开始,活动 ai 的最早开始时间等于时间 vj 的最早发生时间 vej
④活动 ai = <vj,vk> 最晚发生时间 l(i): 活动 ai 的开始时间需保证不延误时间 vk 的最迟发生时间; 所以活动 ai 的最晚开始时间 l(i) 等于事件 vk 的最迟发生事件 vl(k) 减去活动 ai 的持续时间 wj,k ,即 l(i) = vl(k) - wj,k
显然,对于关键活动而言,e(i) = l(i)
求解过程
①对图中顶点进行排序,在排序过程中按拓扑排序求出每个事件的最早发生事件ve(i)
②按逆拓扑序列求出每个事件的最迟发生事件 vl(i)
③由 ve(i) 得出每个活动 ai 的最早开始时间 e(i)
④求出每个活动 ai 的最晚开始时间 l(i)
⑤找出 e(i) = l(i) 的活动 ai ,即为关键活动; 由关键活动形成的从源点到汇点的每一条路径就是关键路径,关键路径有可能不止一条
六度空间
把六度空间理论中的人际关系网络图抽象称一个不带权值的无向图 G ,用图 G 的一个顶点表示一个人,两个人认识与否,用代表这两个人的顶点之间是否有一条边来表示,这样六度空间理论问题便可描述为:在图 G 中,任意两个顶点之间都存在一条路径长度不超过 7 的路径;且实际验证过程中,可以通过测试满足要求的数据达到一定的百分比来进行验证
思路
比较简单的一种验证方案是:利用广度优先搜索方法,对于任意一个顶点,通过对图 G 的 “7”层遍历,就可以统计出所以路径长度不超过 7 的顶点数,从而得到这些顶点在所有顶点中的所占比例
实现
①完成系列初始化工作:设变量 Visit_Num 用来记录路径长度不超过 7 的顶点个数,初值为 0 ;数组 level 用来记录遍历时不同层次下入队的顶点个数; start 为指定的一个起始顶点,置 visit[start] 的值为 true,即将 start 标记为六度顶点的始点;辅助队列 Q 初始化为空,然后将 start 入队
②当队列 Q 非空,且循环次数小于 7 时,循环执行以下操作(统计路径长度不超过 7 的顶点个数):
·队头顶点 u 出队
·依次检查 u 的所有邻接点 w,如果 visited[w] 为 false,则将 w 标记为六度顶点
·路径长度不超过 7 的顶点个数 Visit_Num 加 1,该层次的顶点个数加 1
·w 进队
③退出循环时输出从顶点 start 出发,到其他顶点长度不超过 7 的路径的百分比
/* 通过广度优先搜索验证六度空间理论 */
void SixDegree_BFS( Graph G, int start )
{
Visit_Num = 0;
Visited[start] = true; /* 始点 */
InitQueue(Q); EnQueue(Q,start); /* 辅助队列 Q 初始化 */
level[0] = 1; /* 第一层入队的顶点个数初始化为 1 */
/*开始统计路径长度不超过 7 的顶点个数 */
for(int len=1;len<=6 && !QueueEmpty(Q);len++)
{
for(int i=0;i<level[len-1];i++)
{
DeQueue(Q,u);
for(w=FirstAdjVex(G,u);w>=0;w=FirstAdjVex(G,u,w))
{
if(!visited[w])
{
visited[w] = true;
Visit_Num ++; level[len] ++; /* 路径长度不过 7、该层次的顶点数加 1 */
Enqueue(Q,w);
}
}
}
}
printf("%d",100*Visit_Num/G.vexnum); /* 输出路径长度不超过 7 的顶点数百分比 */
}