图
图的逻辑结构
图的定义:
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G=(V,E)其中:G表示一个图,V是图G中顶点的集合,E是图G中顶点之间边的集合。
基本定义:
有向图
无向图
简单图
邻接、依附(有向图和无向图不同)
有向完全图
无向完全图
稀疏图
稠密图
顶点的度
顶点的入度
顶点的出度
权
网
路径
…
深度优先遍历:
基本思想 :
⑴ 访问顶点v;
⑵ 从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;
⑶ 重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。
(深度优先搜索是带回溯的许多问题的解决
都是通过深度优先搜索方法解决的)
广度优先遍历
基本思想:
⑴ 访问顶点v;
⑵ 依次访问v的各个未被访问的邻接点v1, v2, …, vk;
⑶ 分别从v1,v2,…,vk出发依次访问它们未被访问的邻接点,并使“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”被访问。直至图中所有与顶点v有路径相通的顶点都被访问到。
图的存储结构及实现
邻接矩阵表示方法
基本思想:
用一个一维数组存储图中顶点的信息
用一个二维数组(称为邻接矩阵)存储图中各顶点之间的邻接关系。
假设图G=(V,E)有n个顶点,则邻接矩阵是一个n×n的方阵,定义为:
arc[i][j]=1 (若(vi, vj)∈E(或<vi, vj>∈E))
arc[i][j]=0 (其它)
特点:
主对角线为 0 且一定是对称矩阵。
1.如何求顶点i的度?
邻接矩阵的第i行(或第i列)非零元素的个数。
2.如何判断顶点 i 和 j 之间是否存在边?
测试邻接矩阵中相应位置的元素arc[i][j]是否为1。
3.如何求顶点 i 的所有邻接点?
将数组中第 i 行元素扫描一遍,若arc[i][j]为1,则顶点 j 为顶点 i 的邻接点。
4.如何求顶点 i 的出度?
邻接矩阵的第 i 行元素之和。
5.如何求顶点 i 的入度?
邻接矩阵的第 i 列元素之和。
6.如何判断从顶点 i 到顶点 j 是否存在边?
测试邻接矩阵中相应位置的元素arc[i][j]是否为1
构造函数
template <class T>
MGraph::MGraph(T a[ ], int n, int e) {
vertexNum=n; arcNum=e;
for (i=0; i<vertexNum; i++)
vertex[i]=a[i];
for (i=0; i<vertexNum; i++) //初始化邻接矩阵
for (j=0; j<vertexNum; j++)
arc[i][j]=0;
for (k=0; k<arcNum; k++) {
cin>>i>>j; //边依附的两个顶点的序号
arc[i][j]=1; arc[j][i]=1; //置有边标志
}
}
深度优先遍历
int visited[MaxSize];
template <class T>
void MGraph::DFSTraverse(int v)
{
cout<<vertex[v]; visited [v]=1;
for (j=0; j<vertexNum; j++)
if (arc[v][j]==1 && visited[j]==0)
DFSTraverse( j );
}
广度优先遍历:
int visited[MaxSize];
template <class T>
void MGraph::BFSTraverse(int v){
front=rear=-1; //假设采用顺序队列且不会发生溢出
int Q[MaxSize]; cout<<vertex[v]; visited[v]=1; Q[++rear]=v;
while (front!=rear) {
v=Q[++front];
for (j=0; j<vertexNum; j++)
if (arc[v][j]==1 && visited[j]==0 ) {
cout<<vertex[j]; visited[j]=1; Q[++rear]=j;
}
}
}
邻接矩阵上的其他操作:
增加一个点:
template <class T>
void MGraph<T>::InsertVex(int num,T name) {
if ( num<0|| num>vertexNum) throw "位置";
int row, col, numv;
numv = vertexNum-1;
vertexNum++;
for(int i=numv;i>=num;i--) vertex[i++]=vertex[i];
vertex[num]=name;
for(row=numv;row>=0;row--) {所有行上num列之后的列后移,增加一列,
for(col=numv;col>=num;col--) arc[row][col+1]=arc[row][col];
arc[row][num]=0;
}
for(row=numv;row>=num;row--)
for(col=0;col<=numv+1;col++) arc[row+1][col]=arc[row][col];
for(col=0;col<vertexNum;col++) arc[num][col]=0;
}
删除一个顶点
template <class T> void MGraph<T>::DeleteVex(int pos){
if ( pos<0|| pos>MaxSize) throw "位置";
int row, col;
int numv=vertexNum;
for(int i=pos;i<numv;i++) vertex[i]=vertex[i+1];
vertexNum--;
for(row=0;row<numv;row++) { //删除一列
for(col=pos;col<numv;col++) arc[row][col]=arc[row][col+1];
}
for(row=pos;row<numv;row++)
for(col=0;col<numv;col++)
arc[row][col]=arc[row+1][col];
}
}
插入一条边
tmplate <class T>
void MGraph<T>::InsertArc(int i, int j)
{
if ( i>MaxSize|| j>MaxSize) throw "位置";
arc[i][j]=1;
arc[j][i]=1;
}
删除一条边
template <class T>
void MGraph<T>::DeleteArc(int i, int j)
{
if ( i>MaxSize|| j>MaxSize) throw "位置";
arc[i][j]=arc[j][i]=0;
}
图的连通性
有向图的连通子图的求解过程
⑴ 从某顶点出发进行深度优先遍历,并按其所有邻接点都访问完(即出栈)的顺序将顶点排列起来。
⑵ 从最后完成访问的顶点出发,沿着以该顶点为头的弧作逆向的深度优先遍历。若不能访问到所有顶点,则从余下的顶点中最后访问的那个顶点出发,继续作逆向的深度优先遍历,直至有向图中所有顶点都被访问到为止。
普里姆(Prim)算法——基本思想:
设G=(V, E)是具有n个顶点的连通网,
T=(U, TE)是G的最小生成树,
T的初始状态为U={u0}(u0∈V),TE={ },
重复执行下述操作:
在所有u∈U,v∈V-U的边中找一条代价最小的边(u, v)并入集合TE,同时v并入U,直至U=V。
最小生成树
Prim算法——伪代码:
初始化两个辅助数组lowcost(=arc[0][i])和adjvex(=0)(0是始点);
输出顶点u0,将顶点u0加入集合U中;
重复执行下列操作n-1次
3.1 在lowcost中选取最短边(lowcost[k]),取对应的顶点序号k;
3.2 输出顶点k和对应的权值;
3.3 将顶点k加入集合U中(lowcost[k]=0);
3.4 调整数组lowcost和adjvex;
Void prime(MGraph G){
for(int i=1;i<G.vertexNu;i++){
lowcost[i]=G.arc[0][i]; adjvex[i]=0;
}
lowcost[0]=0;
for(i=1;i<G.vertexNum;i+++){
k=MinEdge(lowcost,G.vertexNum)
cout<<K<<adjvex[k]<<lowcost[k];
lowcost[k]=0;
for(j=1;j<G.vertexNum;j++)
if((G.arc[k][j]<lowcost[j]){
lowcost[j]=G.arc[k][j];
arcvex[j]=k;
}
}
}
克鲁斯卡尔(Kruskal)算法 ——基本思想:
1.设无向连通网为G=(V, E),令G的最小生成树为T=(U, TE),其初态为U=V,TE={ },
2.然后,按照边的权值由小到大的顺序,考察G的边集E中的各条边。
(1)若被考察的边的两个顶点属于T的两个不同的连通分量,则将此边作为最小生成树的边加入到T中,同时把两个连通分量连接为一个连通分量;
(2)若被考察边的两个顶点属于同一个连通分量,则舍去此边,以免造成回路,
3.如此下去,当T中的连通分量个数为1时,此连通分量便为G的一棵最小生成树。
克鲁斯卡尔(Kruskal)算法 ——伪代码:
初始化:U=V; TE={ };
循环直到T中的连通分量个数为1
2.1 在E中寻找最短边(u,v);
2.2 如果顶点u、v位于T的两个不同连通分量,则
2.2.1 将边(u,v)并入TE;
2.2.2 将这两个连通分量合为一个;
2.3 在E中标记边(u,v),使得(u,v)不参加后续最短边的选取;
关键问题:
1.图的存储结构
采用边集数组存储图。
2.如何判断一条边所依附的两个顶点在同一个连通分两中
定义Parent[i]数组,辅助完成连通分量的处理。数组分量的值表示顶点i的双亲节点(初值为-1;)
当一条边(u,v)的两个顶点的根结不同时,这两个结点属于不同的连通分量(利用parent 数组查找一棵树的根节点。当一个结点n的parent==-1,树的根节点即为n)
3. 如何将一条边所依附的两个顶点合并到同一个连通分量中
要进行联通分量的合并 ,其中一个顶点所在的树的根节点为vex1,另一个顶点所在的树的根节点为vex2,则:parent[vex2]=vex1;
for (k=0;k<arcNum;k++) {
begin=edge[k].from; end=edge[k].to;
int m,n;
m=Find(parent,begin); n=Find(parent,end);
if(m!=n) {
cout<<begin<<","<<end<<","<<edge[k].weight<<endl;
parent[n]=m; count++;
if(count==vertexNum-1) break;
}
}
return 0;
}
int Find(int *parent, int node)
{
int f;
f=node;
while(parent[f]>-1)
f=parent[f];
return f;
}
—
最短路径
Dijkstra算法和Floyed算法
最短路径:
在非网图中,最短路径是指两顶点之间经历的边数最少的路径。
在网图中,最短路径是指两顶点之间经历的边上权值之和最短的路径。
最短路径问题:
单源点到其他顶点的最短路径:
Dijkstra方法,O(n2)按路径长度递增
任意一对顶点之间的最短路径:
Floyed方法,O(n3)
Dijkstra算法:按路径长度递增
1.设置一个集合S存放已经找到最短路径的顶点,S的初始状态只包含源点v,
2.对vi∈V-S,假设从源点v到vi的有向边为最短路径(从v到其余顶点的最短路径的初值)。
3.以后每求得一条最短路径v, …, vk,就将vk加入集合S中,并将路径v, …, vk , vi与原来的假设相比较,取路径长度较小者为最短路径。
4.重复上述过程,直到集合V中全部顶点加入到集合S中。
路径长度最短的最短路径(即第一条最短路)的特点:
在这条路径上,必定只含一条边,并且这条边上的权值最小。
下一条路径长度次短的最短路径的特点:
(1)直接从源点到该点(只含一条边);
(2)从源点经过顶点v1(第一条最短路径所依附的顶点),再到达该顶点(由两条边组成)。
再下一条路径长度次短的最短路径的特点:
(1)直接从源点到该点(只含一条边);
(2)从源点经过顶点v1,再到达该顶点(由两条边组成);
(3)从源点经过顶点v2,再到达该顶点(两条条边);
(4)从源点经过顶点v1、v2,再到达该顶点(多条边)。
其余最短路径的特点:
(1)直接从源点到该点(只含一条边);
(2)从源点经过已求得最短路径的顶点(集合S中的顶点),再到达该顶点。
Dijkstra算法的主要步骤:
(1) g为用邻接矩阵表示的带权图。
S←{v0} , dist[i]= g.arcs[v0][vi],path[i]=“v0vi”或“”;
将v0到其余顶点的路径长度初始化为权值;
(2) 选择vk,使得
dist[vk]=min(dist[i] | vi∈V-S)
vk为目前求得的下一条从v0出发的最短路径的终点。
将vk加入到S中
(3) 修改从v0出发到集合V-S上任一顶点vi的最短路径的长度。如果
dist[k]+ g.arcs[k][i]<dist[i]
则将dist[i]修改为
dist[k]+ g.arcs[k][i]
path[i]=path[k]+”vi”
(4) 重复(2)、(3) n-1次,即可按最短路径长度的递增顺序,逐个求出v0到图中其它每个顶点的最短路径。
const int MAX=1000;
void Dijkstra(MGraph g, int v){
for ( i =0; i<g.vexnum ; i++){
dist[i]=g.arcs[v][i];
if ( dist[i]!= MAX)
path [i]=g.vertex[v]+g.vertex[i];
else
path[i]=“”;
}
S[0]=g.vertex[v];
num=1;
While (num<g.vextexNum){
k=0;
for(i=0;i<G.vertexNum;i++)
if((dist[i]<dist[k]) k=i
cout<<dist[k]<<path[k];
s[num++]=G.vertex[k];
for(i=0;i<G.vertexNum;i++)
if(dist[k]+g.arc[k][i]<dist[i] {
dist[i]=dist[k]+g.arc[k][i];
path[i]=path[k]+g.vertex[i];
}
}
}
Floyed算法——基本思想:
设图g用邻接矩阵法表示, 求图g中任意一对顶点vi、 vj间的最短路径。
(-1) 将vi到vj 的最短的路径长度初始化为(vi,vj), 然后进行如下n次比较和修正:
(0) 在vi、vj间加入顶点v0,比较(vi, v0, vj)和(vi, vj)的路径的长度,取其中较短的路径作为vi到vj的且中间顶点号不大于0的最短路径。
(1) 在vi、vj间加入顶点v1,
得(vi, …,v1)和(v1, …,vj),其中:
(vi, …, v1)是vi到v1 的且中间顶点号不大于0的最短路径,
(v1, …, vj) 是v1到vj 的且中间顶点号不大于0的最短路径,
这两条路径在上一步中已求出。
将(vi, …, v1, …, vj)与上一步已求出的且vi到vj 中间顶点号不大于0的最短路径比较,取其中较短的路径作为vi到vj 的且中间顶点号不大于1的最短路径。
(2)在vi、vj间加入顶点v2,得
(vi, …, v2)和(v2, …, vj), 其中:
(vi, …, v2)是vi到v2 的且中间顶点号不大于1的最短路径,
(v2, …, vj) 是v2到vj 的且中间顶点号不大于1的最短路径,
这两条路径在上一步中已求出。
将(vi, …, v2, …, vj)与上一步已求出的且vi到vj 中间顶点号不大于1的最短路径比较, 取其中较短的路径作为vi到vj 的且中间顶点号不大于2的最短路径。
void Floyd(MGraph G)
{
for (i=0; i<G.vertexNum; i++)
for (j=0; j<G.vertexNum; j++)
{
dist[i][j]=G.arc[i][j];
if (dist[i][j]!=∞)
path[i][j]=G.vertex[i]+G.vertex[j];
else path[i][j]="";
}
for (k=0; k<G.vertexNum; k++)
for (i=0; i<G.vertexNum; i++)
for (j=0; j<G.vertexNum; j++)
if (dist[i][k]+dist[k][j]<dist[i][j]) {
dist[i][j]=dist[i][k]+dist[k][j];
path[i][j]=path[i][k]+path[k][j];
}
}
AOV网与拓扑排序
拓扑排序:
基本思想:
⑴ 从AOV网中选择一个没有前驱的顶点并且输出;
⑵ 从AOV网中删去该顶点,并且删去所有以该顶点为尾的弧;
⑶ 重复上述两步,直到全部顶点都被输出,或AOV网中不存在没有前驱的顶点。
AOV-网的特性如下:
若vi为vj的先行活动,vj为vk的先行活动,则vi必为vk的先行活动,即先行关系具有可传递性。
显然,在AOV-网中不能存在回路,否则回路中的活动就会互为前驱,从而无法执行。
AOV-网的拓扑序列不是唯一的。
基于邻接表的拓扑排序基本思想:
(1)找G中无前驱的顶点查找indegree [i]为零的顶点vi;
(2)修改邻接于顶点i的顶点的入度(删除以i为起点的所有弧)
对链在顶点i后面的所有邻接顶点k,将对应的indegree[k] 减1。
为了避免重复检测入度为零的顶点,可以再设置一个辅助栈,若某一顶点的入度减为0,则将它入栈。每当输出某一入度为0的顶点时,便将它从栈中删除。
void TOpSort(){
int top=-1, count=0;
for(int i=0;i<vertexnum;i++)
if(adjlist[i].in==0) s[++top]=i;
while(top!=-1){
j=s[top--]; cout <<adjlist[j].vertext; count++;
p=adjlist[j].firstedge;
while(p!=NULL){
k=p->adjvex; adjlist[k].in--;
if(adjlist[k].in==0) s[top++]=k;
p=p->next;
}
}
If (count<vertexNum) cout<<“有回路”;
}
AOE网与关键路径
定义:
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,边上的权值表示活动的持续时间,称这样的有向图叫做边表示活动的网,简称AOE网。
特点:
AOE网中没有入边的顶点称为始点(或源点),没有出边的顶点称为终点(或汇点)。
AOE网的性质:
⑴ 只有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始;
⑵ 只有在进入某顶点的各活动都结束,该顶点所代表的事件才能发生。
解决方法:
首先计算以下与关键活动有关的量:
⑴ 事件的最早发生时间ve[k]
q.push(0);//源点时间入队
for(j=0;j<vertexnum;j++) { //初始化每个事件最早发生时间
ve[j]=0; visit[j]=0; }
visit[0]=1;
while(!q.empty()) {
i=q.front(); //利用标准模板库中的队列实现
q.pop();
for(j=0;j<vertexnum;j++){//计算i的邻接点的ve
if(adjlist[i][j]!=9999 && ve[i]+adjlist[i][j]>ve[j] ){
ve[j]=ve[i]+adjlist[i][j];
if(!visit[j]) //如果j没有被访问过,顶点j入队
q.push(j);
visit[j]=1;
}
}
}
⑵ 事件的最迟发生时间vl[k]
q.push(vertexnum-1);
for(j=0;j<vertexnum;j++) {
vl[j]=ve[vertexnum-1]; visit[j]=0; }
while(!q.empty()) {
i=q.front();
q.pop();
for(j=0;j<vertexnum;j++) {
if(adjlist[j][i]!=9999 && vl[i]-adjlist[j][i]<vl[j] ){
vl[j]=vl[i]-adjlist[j][i];
if(!visit[j])
q.push(j);
visit[j]=1;
}
}
}
⑶ 活动的最早开始时间e[i]
for(i=0;i<e;i++)
{
edge[i].e=ve[edge[i].from];
}
⑷ 活动的最晚开始时间l[i]
for(i=0;i<e;i++)
{
edge[i].e=ve[edge[i].from];
edge[i].l=vl[edge[i].to]-adjlist[edge[i].from][edge[i].to];
if(edge[i].e==edge[i].l)
cout<<edge[i].from<<" "<<edge[i].to<<endl;
}
最后计算各个活动的时间余量 l[k] - e[k],时间余量为0者即为关键活动。