一、图的存储结构
- 极大连通子图:从一个顶点开始作为子图,逐个添加和这个子图有边相连的顶点,直到所有相连的顶点都被纳入图中,所生成的子图就是一个极大连通子图。
1.邻接矩阵
2.邻接表
3.邻接多重表(有点看不懂…)
- 与十字链表类似,由顶点表和边表组成,每一条边用一个结点表示;
- 顶点表:vertex域存储和该顶点相关的信息,firstedge域指示第一条依附于该顶点的边;
- 边表结点:6个域
mark域为标记域:标记该条边是否被搜索过;
ivex和jvex:该边依附的两个顶点在图中的位置;
ilink:指向下一条依附于顶点ivex的边;
jlink:指向下一条依附于顶点jvex的边;
info为指向与边相关的各种信息的指针域;
二、图的遍历
1.深度优先搜索遍历
- DFS类似于二叉树的先序遍历:
首先访问出发点v,并将其标记为已访问过,然后选取与v邻接的未被访问的任意一个结点信息w,并访问他;
选取与w邻接的未被访问的结点并访问他,以此重复进行;
当一个顶点所有的邻接顶点都被访问过,则依次退回到最近被访问过的结点;
若该结点还有其他邻接结点未被访问,则从这些未被访问的结点中选取一个并重复上述过程,直至图中所有顶点都被访问过。 - 算法描述:认取一个结点,访问,检查这个结点的所有邻接结点,递归访问其中未被访问的顶点
int visit[maxSize];
void DFS(AGraph *G,int v)
{
ArcNode *p;
visit[v]=1;
Visit(v);
p=G->adjlist[v].firstarc;
while(p!=NULL)
{
if(visit[p->adjvex]==0)
{
DFS(G,p->adjvex);
p=p->nextarc;
}
}
}
void preorder(BTNode *p)
{
if(!p=NULL)
{
visit(p);
preorder(p->left);
preorder(p->right);
}
}
- 图的深度优先搜索遍历和二叉树的先序遍历的区别:二叉树的先序遍历对于对于每个结点要递归的访问两个分支,图的深度优先搜索遍历则是要递归地访问多个分支;
- 把图的深度优先搜索遍历过程中所经历的边保留,其余的边删掉,就形成一棵树,称为深度优先搜索生存树.
2.广度优先搜索遍历
- 类似于树的层次遍历
- 基本思想:首先访问起始顶点v,然后选取与v邻接的全部顶点w1,…,wn进行访问,再依次访问与w1,…,w2邻接的全部顶点(已经访问的除外),直到所有结点访问过为止.
- 算法过程:
1.取图中一个顶点进行访问,入队,并将这个顶点标记为已访问;
2.当队列不空时循环执行:出队,依次检查出队顶点的所有邻接顶点,访问没有被访问过的邻接顶点并将其入队;
3.当队列为空时跳出循环,广度优先搜索即完成.
void BFS(AGraph *G, int v, int visit[maxSize])
{
ArcNode *p;
int que[maxSize],front=rear=0;
int j;
Visit(v);
visit[v]=1;
rear=(rear+1)%maxSize;
que[rear]=v;
while(front!=rear)
{
front = (front+1)%maxSize;
j=que[front];
p=G->adjlist[j]firstarc;
while(p!=NULL)
{
if(visit[p->adjvex]==0)
{
Visit(p->adjvex);
visit[p->adjvex]=1;
rear=(rear+1)%maxSize;
que[rear]=p->adjvex;
}
p=p->nextarc;
}
}
}
void dfs(AGraph *g)
{
int i;
for(i=0;i<g->n;++i)
{
if(visit[i]==0)DFS(g,i);
}
}
void bfs(AGraph *g)
{
int i;
for(i=0;i<g->n;++i)
{
if(visit[i]==0)BFS(g,i,visit);
}
}
3.例程
int BFS(AGraph *G,int v)
{
ArcNode *p;
int que[maxSize],front=rear=0;
int visit[maxSize];
int i,j;
for(i=0;i<G->n;++i)visit[i]=0;
rear=(rear+1)%maxSize;
que[rear]=v;
visit[v]=1;
while(front!=rear)
{
front=(front+1)%maxSize;
j=que[front];
p=G->adjlist[j].firstarc;
while(p!=NULL)
{
front=(front+1)%maxSize;
j=que[front];
p=G->adjlist[j].firstarc;
while(p!=NULL)
{
if(visit[p->adjvex]==0)
{
visit[p->adjvex]=1;
rear=(rear+1)%maxSzie;
que[rear]=p->adjvex;
}
p=p->nextarc;
}
}
}
return j;
}
void DFS2(AGraph *G,int v,int &vn,int &en)
{
ArcNode *p;
visit[v]=1;
++vn;
p=G->adjvex[v].firstarc;
while(p!=NULL)
{
++en;
if(visit[p->adjvex]==0)DFS2(G,p->adjvex);
p=p->nextarc;
}
}
int GisTree(AGraph *G)
{
int vn=0,en=0,1;
for(i=0;i<G->n;++i)visit[i]=0;
DFS2(G,1,vn,en);
if(vn==G->n&&(G->n-1)==en/2)return 1;
else return 0;
}
int DFSTrave(AGraph *G, int i, int j)
{
int k;
for(k=0;k<G->n;++n)visit[k]=0;
DFS(G,i);
if(visit[j]==1)return 1;
else return 0;
}
三、最小(代价)生成树
1.普里姆算法
- 思想:从图中任意取一个顶点,把他当成一棵树,在与这棵树相接的边中选取一条最短(权值最小)的边,并将这个及其所连接的顶点也并入这棵树中,然后继续找与树相邻的边中最短的边,将边及其顶点并入树中,依此类推,直到图中所有顶点都被并入树中为止.
- vset[ ]数组:存放顶点有没有被并入生存成树中;
- lowcost[ ]数组:存放当前生成树到剩余各顶点最短边的权值,最短边条数对应剩余顶点个数;
- 算法过程:
- 将v0到其他顶点的所有边当作候选边;
- 重复以下步骤n-1次,使得其他n-1个顶点被并入到生成树中:
1)从候选边中挑选出权值最小的边输出,并将与该边另一端相接的顶点v并入到生成树中;
2)考察所有剩余顶点vi,如果(v,vi)的权值比lowcost[vi]小,则用(v,vi)的权值更新lowcost[vi];
void Prim(MGraph g,int v0,int &sum)
{
int lowcost[maxSize],vset[maxSize],v;
int i,j,min;
v=v0;
for(i=0;i<g.n;++i)
{
lowcost[i]=g.edges[v0][i];
vset[i]=0;
}
vset[v0]=1;
sum=0;
for(i=0;i<g.n-1;++i)
{
min=INF;
for(j=0;j<g.n;++j)
{
if(vset[j]==0&&lowcost[j]<min)
{
min=lowcost[j];
k=j;
}
}
vset[k]=1;
v=k;
sum+=min;
for(j=0;j<g.n;++j)
{
if(vset[j]==0&&g.edges[v][j]<lowcost[j])
lowcost[j]=g.edges[v][j];
}
}
}
2.克鲁斯卡尔算法
- 每次找出候选边中权值最小的边,将该边并入生成树中,重复过程直到所有边被检测完成;
- 算法描述:将图中各边按权值从小到大排序,然后从小边开始扫描各边,并检测当前边是否为候选边,如果该边的并入不会构成回路,则将该边并入当前生成树中,直到所有边都被检测完为止。
- 并查集:通过树中的一个结点,可以找到其双亲结点,进而找到根结点。
1.可以快速的将两个含有很多元素的集合并为一个;
2.可以方便的判断两个元素是否属于同一个集合。
typedef struct
{
int a,b;
int w;
}Road;
Road road[maxSize];
int v[maxSize];
int getroot(int a)
{
while(a!=v[a])a=v[a];
return a;
}
void Kruskal(MGraph g,int &sum,Road road[])
{
int i;
int N,E,a,b;
N=g.n;
E=g.e;
sum=0;
for(i=0;i<N;++i)v[i]=i;
sort(road,E);
for(i=0;i<E;++i)
{
a=getRoot(road[i].a);
b=getRoot(road[i].b);
if(a!=b)
{
v[a]=b;
sum+=road[i].w;
}
}
}
四、最短路径
1.迪杰斯特拉算法
- 迪杰斯特斯拉算法用于求解图中某一个顶点到其余各顶点的最短路径
- 思想:集合S(存放图中已找到最短路径的顶点),集合T(存放图中剩余顶点),S每并入一个新的结点vu,都要修改顶点v0到集合T中顶点的最短路径长度值。
- 引入三个辅助数组
dist[],path[],set[]
1.dist[vi]:当前已找到的从v0到每个终点vi的最短路径长度,初态:若v0到vi有边,则dist[vi]为边上的权值,否则置dist[vi]为∞
;
2.path[vi]:保存从v0到vi最短路径上vi得前一个顶点,初态:如果v0到vi有边,则path[vi]=v0,否则path[vi]=-1
;
3.set[vi]:set[vi]=0表示vi在T中,没有被并入最短路径,否则已经被并入路径,初态:set[v0]=1,其余元素全为0
; - 算法描述:
1.从当前dist[]数组选出最小值,假设为dist[vu],set[vu]=1;
2.循环扫描图中顶点:对于当前顶点vj,if set[vj]=1,什么都不做;else if dist[vj]>dist[vu]+w 更新路径,将vu加入vj之前的那个顶点路径;else 什么都不做
3.对1和2循环n-1次,即可得到v0到其余所有顶点的最短路径; - path数组其实保存了一棵用双亲存储结构存储的树,通过这棵树可以打印从源点到任何一个顶点最短路径上所经过的所有顶点。
void printfPath(int path[],int a)
{
int stack[maxSize],top=-1;
while(path[a]!=-1)
{
stack[++top]=a;
a=path[a];
}
stack[++top]=a;
while(top!=-1)
cout<<stack[top--]<<" ";
cout<<endl;
}
void Dijkstra(MGraph g,int v,int dist[],int path[])
{
int set[maxSize];
int min,i,j,u;
for(i=0;i<g.edges[v][i])
{
dist[i]=g.edges[v][i];
set[i]=0;
if(g.edges[v][i]<INF)path[i]=v;
else path[i]=-1;
}
set[v]=1;path[v]=-1;
for(i=0;i<g.n-1;++i)
{
min=INF;
for(j=0;j<g.n;++j)
{
if(set[j]==0&&dist[j]<min)
{
u=j;
min=dist[j];
}
}
set[u]=1;
for(j=0;j<g.n;++j)
{
if(set[j]==0&&dist[u]+g.edges[u][j]<dist[j])
{
dist[j]=dist[u]+g.edges[u][j];
path[j]=u;
}
}
}
}
2.费洛伊德算法
- 费洛伊德算法用来求任意一对顶点间的最短路径,迪杰斯特拉算法求图中某一顶点到其余各顶点的最短路径;
- 求解最短路径的一般过程:
- 设置两个矩阵A和Path,初始化时将图的邻接矩阵赋值给A,将矩阵Path中元素全部设置为-1;
- 以顶点k作为中间结点,k=0~n-1,对图中所有顶点对{i,j}进行如下检测与修改;
if A[i][j]>A[i][k]+A[k][j],则A[i][j]=A[i][k]+A[k][j],Path[i][j]=k;else do nothing;
void Floyd(MGraph g,int Path[][maxSize])
{
int i,j,k;
int A[maxSize][maxSize];
for(i=0;i<g.n;++j)
{
for(j=0;j<g.n;++j)
{
A[i][j]=g.edges[i][j];
Path[i][j]=-1;
}
}
for(k=0;k<g.n;++k)
{
for(i=0;i<g.n;++i)
{
for(j=0;j<g.n;++j)
{
if(A[i][j]>A[i][k]+A[k][j])
{
A[i][j]=A[i][k]+A[k][j];
Path[i][j]=k;
}
}
}
}
}
五、拓扑排序
int TopSort(AGraph *G)
{
int i,j,n=0;
int stack[maxSize],top=-1;
ArcNode *p;
for(i=0;i<G->n;++i)
{
if(G->adjlist[i].count==0)
stack[++top]=i;
}
while(top!=-1)
{
i=stack[top--];
++n;
cout<<i<<" ";
p=G->adjlist[i].firstarc;
while(p!=NULL)
{
j=p->adjvex;
--(G->adjlist[j].count);
if(G->adjlist[j].count==0)
stack[++top]=j;
p=p->nextarc;
}
}
if(n==G->n) return 1;
else return 0;
}
- 拓扑排序序列不唯一:当前步骤有多个入度为0的顶点时,可以任选一个输出,这就造成了拓扑排序序列的不唯一。
- 逆拓扑有序序列:
- 在网中选择一个没有后继的顶点(出度为0)输出;
- 在网中删除该顶点,并删除所有到达该顶点的边;
- 重复上述两步,直到AOV网中已无出度为0的顶点为止。
- 拓扑排序时间复杂度:O(n+e),e为图中边的条数。
六、关键路径
- AOE网和AOV网:
- 相同点:都是有向无环图;
- 不同点:
1)AOE网的边表示活动,边有权值,边代表活动持续时间;顶点代表事件,事件是图中新活动开始或者旧活动结束的标志。
2)AOE网的顶点表示活动,边无权值,边无权值,边代表活动之间的先后关系。
3)AOE网只存在一个入度为0的顶点,即源点,表示整个工程的开始,也只存在一个出度为0的顶点,即汇点,表示工程的结束。
- 关键路径:AOE网中,从源点到汇点的所有路径中,具有最大路径长度的路径,是整个工期所完成的最短时间。
- 剩余时间等于活动的最迟发生时间减去活动的最早发生时间,剩余时间反应了活动完成的一种松弛度。
- 一个工程的完成需要执行图中所有活动,只不过关键路径执行时间是整个图中所有活动所完成的时间。
七、例程
int BFS(AGraph *G,int vi,int vj)
{
ArcNode *p;
int que[maxSize],front=rear=0;
int visit[maxSize];
int i,j;
for(i=0;i<G->n;++i)visit[i]=0;
rear=(rear+1)%maxSize;
que[rear]=vi;
visit[vi]=1;
while(front!=rear)
{
front=(front+1)%maxSize;
j=que[front];
if(j==vj)return 1;
p=G->adjlist[j].firstarc;
while(p!=NULL)
{
if(visit[p->adjvex]==0)
{
rear=(rear+1)%maxSize;
que[rear]=p->adjvex;
visit[p->adjvex]=1;
}
p=p->nextarc;
}
}
return 0;
}
int visit[maxSize],sum;
void DFS(AGraph *G,int v)
{
ArcNode *p;
visit[v]=1;
++sum;
p=G->adjlist[v].firstarc;
while(p!=NULL)
{
if(visit[p->adjvex]==0)
{
DFS(G,p->adjvex);
p=p->nextarc;
}
}
}
void print(AGraph *G)
{
int i,j;
for(i=0;i<G->n;++i)
{
sum=0;
for(j=0;j<G->n;++j)visit[j]=0;
DFS(G,i);
if(sum==G->n)cout<<i<<endl;
}
}