/*
* 线性结构: 一对一
树性结构: 一对多
图结构: 多对多
* 【部分算法参考于:http://kjwy.5any.com/sjjg/index1.htm】
*
* 【图】
* 【一】图的定义:【有一堆基本术语和五种存储结构:http://kjwy.5any.com/sjjg/index1.htm】
【二】图的遍历:
①深度优先搜索(DFS): 【类似于树的先根遍历:遍历后生成的树叫“深度优先生成树”】
从指定的V0点开始访问,访问邻节点,再访问该邻节点的其他邻接点,直到所有的邻接点都访问完毕
void DFSTraverse(Graph G, Status(*visit)(int v))
{
VisitFunc = Visit;
for (v=0; v<G.vexnum; ++v)
{
visited[v] = FALSE; //访问标志数组初始化
}
for (v=0; v<G.vexnum; ++v)
{
if (!visited[v]) //如果没有访问过
DFS(G, v); //对尚未访问的顶点调用DFS
}
}
void DFS(Graph G, int v)
{
visited[v] = TRUE;
VisitFunc(v);
for (w=FirstAdjVex(G, v); w!=0; w=NextAdjVex(G, v, w))
if (!visited[w]) //对v的尚未访问的邻接点w递归调用DFS
DFS(G, w);
}
②广度优先搜索(BFS):【搜索生成的树叫做“广度优先生成树”】
从图中的某个顶点V0开始,在访问此顶点之后依次访问V0的所有未访问的邻接点,之后按这些邻接点被访问
的先后次序【这里要用队列,存下先后顺序】依次访问它们的邻接点, 直至图中所有和V0有路径想通的顶点
都被访问到,若此时途中尚有顶点未访问到,则另选图中一个未曾被访问的顶点做起始点,重复上述操作,直到
图中所有顶点都被访问到为止!
void BFSTraverse(Graph G, status (*Visit)(int v))
{
for (v=0; v<G.vexnum; ++v)
{
visited[v] = FALSE;
}
InitQueue(Q); //置空的辅助队列Q
for (v=0; v<G.vexnum; ++v)
{
if (!visited[v]) //如果未被访问
{
EnQueue(Q, v);
visited[v] = TRUE;
Visit(v);
while (!QueueEmpty(Q))
{
DeQueue(Q, u); //队头元素出队并置为u
for (w=FirstAdjVex(G, u); w!=0; w=NextAdjVex(G, u, w))
{
if (!visited[w])
{
EnQueue(Q, w);
visited[w] = TRUE;
Visit(w);
}
}
}
}
}
}
③图遍历的应用:
Ⅰ 求一条从顶点i到顶点s的简单路径【在深度优先搜索基础上得出】
Ⅱ 求两个顶点之间的路径长度最短的路径【在广度优先搜索基础上做比较简单】
【三】图的算法
①最小生成树
问:假设要在n个城市之间建立通讯网络,则联通n个城市只需要修n-1条线路
如何在最节省经费的前提下建立这个通讯网?
【分析:构造网的一颗最小生成树,即:在e条带权边中选取n-1条(不构成回路),使“权值之和”最小】
算法一:【普利姆算法(prim算法)】 O(n^2),适用于稠密图
一、普里姆算法的基本思想:
从连通网N={V,E}中的某一顶点U0出发,选择与它关联的具有最小权值的边(U0,v),将其顶点加入到生成树
的顶点集合U中。以后每一步从一个顶点在U中,而另一个顶点不在U中的各条边中选择权值最小的边(u,v),
把它的顶点加入到集合U中。如此继续下去,直到网中的所有顶点都加入到生成树顶点集合U中为止。
二、 Prim算法
void minispantree_prim(mgraph G,vertextype u){
//用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。
//记录从顶点集U到V-U的代价最小的边的辅助数组定义:
//struct{
// Vertextype adjvex;
// VRType lowcost;
// }closedge[MAX_VERTEX_NUM];
k=locatevex(G,u);
for(j=0;j<G.vexnum;++j) //辅助数组初始化
if(j!=k)closedge[j]={u,G.arcs[k][j].adj};
closedge[k].lowcost=0;
for(I=1;I<G.vexnum;++i){
k=minimum(closedge);
printf(closedge[k].adjvex,G.vexs[k]);
closedge[k].lowcost=0;
for(j=0;j<G.vexnum;++j)
if(G.arcs[k][j].adj<closedge[j].lowcost)
closedge[j]={G.vexs[k],G.arcs[k][j].adj};
}
}
算法二:【克鲁斯卡尔算法(Kruskal算法)】: 需对e条边按权值进行排序,O(eloge),则适用于稀疏图
一、克鲁斯卡尔算法的基本思想:
设有一个有n个顶点的连通网N={V,E},最初先构造一个只有n个顶点,没有边的非连通图T={V, E},
图中每个顶点自成一个连通分量。当在E中选到一条具有最小权值的边时,若该边的两个顶点落在
不同的连通分量上,则将此边加入到T中;否则将此边舍去,重新选择一条权值最小的边。
如此重复下去,直到所有顶点在同一个连通分量上为止。
二、Kruskal算法
void kruskal (edgeset ge, int n, int e)
// ge为权按从小到大排序的边集数组
{ int set[MAXE], v1, v2, i, j;
for (i=1;i<=n;i++)
set[i]=0; // 给set中每个元素赋初值
i=1; // i表示获取的生成树中的边数,初值为1
j=1; // j表示ge中的下标,初始值为1
while (j<n && i<=e)
// 检查该边是否加入到生成树中
{
v1=seeks(set,ge[i].bv);
v2=seeks(set,ge[i].tv);
if (v1!=v2) // 当v1,v2不在同一集合,该边加入生成树
{
printf(“(%d,%d)”,ge[i].bv,ge[i].tv);
set[v1]=v2;
j++;
}
i++;
}
}
② 重(双)连通图和关节点
若从一个连通图中删除任何一个顶点和其相关的边,它仍为一个
连通图的话,则该连通图称为重(双)连通图
若连通图中的某个顶点和其相关的边被删除之后,该连通图
被分割成两个或两个以上的连通分量,则称此顶点为关节点
---->综上:【没有关节点的连通图为双(重)连通图】
关节点的特征:【从深度优先生成树上找】
假设从某个顶点V0出发对连通图进行深度优先搜索遍历,则可以得到
一颗深度优先生成树,树上包含图的所有顶点
1.若生成树的根节点,有两个或两个以上的分支,则此顶点必为关节点
2.对生成树上的任意一个顶点,若其子树的根或子树中的其他顶点
没有和其祖先想通的回边,则该顶点必为关节点!
那如何求关节点就很重要了:
③ 从源头到其余各点的最短路径【Dijkstra算法】:http://kjwy.5any.com/sjjg/index1.htm
一、 单源点的最短路径问题:
给定一个带权有向图D与源点v,求从v到D中其它顶点的最短路径。
二、求最短路径
Dijkstra提出按路径长度的递增次序,逐步产生最短路径的算法。首先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从顶点v到其它各顶点的最短路径全部求出为止。
假设Dist[k]表示 当前所求得的从源点到顶点K的最短路径
则一般情况下
Dist[k] = <源点到顶点k的弧上的权值>
或者 Dist[k] = <源点到其他顶点的路径长度> + <其他顶点到该顶点k弧上的权值>
【注意:此处的其他顶点,必定是已经求得最短路径的顶点】
Min = 当前选出的最短路径长度
//更新其他顶点的当前最短路径值
if (!final[w] && (min + G.arcs[v][w] < Dist[w]))
{
Dist[w] = min + G.arcs[v][w];
Path[w] = Path[v] + <v,w>;
}
④ 求两个顶点间的最短路径【求每一对顶点间的最短路径】:
一、每一对顶点之间的最短路径
已知一个各边权值均大于0的带权有向图,对每一对顶点Vi≠Vj,要求求出Vi与Vj之间的最短路径和最短路径长度。
二、求解每一对顶点之间的最短路径的方法
一是采用迪杰斯特拉算法(Dijkstra),每次以一个顶点为源点,重复执行Dijkstra算法n次。这样,便可以求得每一对顶点之间得最短路径。总的执行时间为O(n3);
二是采用弗洛伊德Floyd算法。
⑤ 拓扑排序【解决有向图的问题:不允许出现回路(这个算法来检查是否有回路)】
对有向图进行如下操作:按照有向图给出的次序关系,将图中顶点排成一个线性序列
对于有向图没有限定次序关系的顶点,可以人为加上任意的次序关系,由此得到的顶点线性
序列称为“拓扑有序序列”
【得不出“拓扑有序序列”的有向图就是存在了回路!】
如何进行拓扑排序:
1.从有向图中选取一个没有前驱的顶点【即该顶点入度为0】,并输出
2.从有向图中删除此顶点以及所有以它为尾的弧(删掉弧,是为了让弧头指向的顶点入度-1【代码就是让它的入度-1】)
重复上述两步,直至图空【存在拓扑排序序列,无回路】,
或者图不空但找不到无前驱的顶点【不存在拓扑排序序列,有回路】为止
代码:
部分算法:
取入度为0的顶点v;
while (v == 0)
{
printf(v);
++m;
w = FirstAdj(v);
while (w == 0)
{
inDegree[w]--; //入度-1
w = nextAdj(v,w);
}
//取下一个入度为0的顶点v;
}
if(m < n) //取到的入度为0的个数m == 图中顶点个数就是没回路,否则有回路
printf("图中有回路");
完整算法:【利用栈来存储入度为0的顶点,那么出栈的时候要进行统计个数】
CountInDegree(G, indegree);//对各顶点求入度
InitStack(S);
for (i=0; i<G.vexnum; ++i)
{
if (!indegree[i]) //如果入度为0
Push(S, i); //放入栈中
}
count = 0; //对输出顶点计数
while (!EmptyStack(S))
{
Pop(S, v);
++count;
printf(v);
for (w = FirstAdj(v); w; w=NextAdj(G, v, w))
{ //扫描刚出栈的v的所有邻接点,将它们的入度-1
--indegree[w];
if (!indegree[w])
{ //如果入度-1之后,入度为0了,那么也就可以入栈了
Push(S, w);
}
}
}
if (count < G.vexnum) //如果出栈(或入栈)的数目不足图中顶点个数
printf("图中有回路");
⑥ 关键路径
引出问题: 弧上的权值表示完成工程所需的时间,哪些工序是完成这个工程的关键
即:影响整个工程完成期限的子工程
【整个工程完成的时间:从有向图的源点到汇点的最长路径】
【“关键活动”是该弧上的权值增加将使得有向图上最长路径的长度增加(总工程时间增加)】
如何求“关键活动”:【根据“拓扑有序序列”的次序来求】
“事件(顶点)”的最早发生事件 ve(j) = 从源点到顶点j的最长路径长度
“事件(顶点)”的最迟发生事件 vl(k) = 从顶点k到汇点的最短路径长度
假设第i条弧为<j,k>
则第i项活动: "活动弧"的最早开始时间 ee(i) = ve(j);
"活动弧"的最迟开始时间 el(i) = vl(k) - dut(<j,k>)【减去弧的权值】
事件发生的时间计算公式:
ve(源点) = 0;
ve(k) = Max{ve(j) + dut(<j,k>)}
vl(汇点) = ve(汇点);
vl(j) = Min{vl(k) - dut(<j, k>)}
算法的实现要点:
求ve的顺序应该按拓扑有序的次序,而求vl的顺序要按拓扑逆序的次序
【拓扑排序的时候用"栈"记下拓扑有序序列,到时候出栈就可以得到拓扑逆序序列了】
*
*
*
*
*
*
*/