图
6.1图的基本概念
一、图的定义
由顶点的非空有穷集合与顶点之间的关系(边或弧)的集合构成的结构,通常表示为:
G = (V , E)
其中,V为顶点集合(非空有穷集合),E为关系(边或弧)集合
关于一条边或弧的表示:(vi,vj)或<vi , vj>
二、图的分类
无向图:对于(vi , vj)从属于E,必有(vj, vi) 从属于E,并且偶对中顶点的前后顺序无关。
有向图:顶点的有序偶对
网(络):与边有关的数据称为权,边上带权的图成为网络
三、名词术语
1. 顶点的度
依附于顶点vi的边的数目,记为TD(vi)
对于有向图而言,由:
顶点的出度:以顶点vi为出发点的边的数目,记为OD(vi)
顶点的入度:以顶点vi为终止边的数目,记为ID(vi)
TD(vi) = OD(vi) + ID(vi)
结论一:对于具有n个顶点,e条边的图,有: e/2 = n条边TD的加和
结论二:具有n个顶点的无向图最多有n(n-1)/2条边
结论三:具有n个顶点的有向图最多有n(n-1)条边
边的数目达到最大的图称为完全图,边的数目达到或接近最大的图称为稠密图,否则,称为稀疏图
2. 路径和路径的长度
顶点v(x)到v(y)之间有路径P(v(x),v(y))的充分必要条件为:
存在顶点序列 v(x) , v(i1) , v(i2) , … , v(im) , v(y),并且序列中相邻两个顶点构成的顶点偶对分别为图中的一条边
出发点与终止点相同的路径称为回路或环;顶点序列中顶点不重复出现的路径称为简单路径。不带权的图的路径长度是指路径上所经过的边的数目,带权图的路径长度是指路径上经过的边上的权值之和。
3. 子图
4. 图的连通
1.无向图(Digraph)的连通
无向图中顶点v(i) 到v(j) 有路径,则称顶点v(i) 与v(j) 是连通的。若无向图中任意两个顶点都连通, 则称该无向图是连通的(称为连通图)
连通分量:无向图中的极大连通子图
2.有向图的连通
若有向图中顶点v(i) 到v(j) 有路径,并且顶点v(j)到v(i) 也有路径,则称顶点v(i) 与v(j) 是连通的。
若有向图中任意两个顶点都连通,则称该有向图是强连通的。
强连通分量:有向图中的极大连通子图
生成树
包含具有n个顶点的连通图G的全部n个顶点,仅包含其n-1条边的极小连通子图称为G的一个生成树
性质:
1.包含具有n个顶点的图:连通且仅有n-1条边
当且仅当 无回路且仅有n-1条边
当且仅当 无回路且连通
当且仅当 是一棵树
2.如果n个顶点的图中只要少于n-1条边,图将不连通
3.如果n个顶点的图中只要有多于n-1条边,图将有环(回路)
4.一般情况下,生成树不唯一
本章不讨论的图:
- 带自身回环的图
- 多重图
只讨论简单图
6.2 图的存储方法
对于一个图,需要存储的信息应该包括:
- 所有顶点的数据信息
- 顶点之间关系(边或弧)的信息
- 权的信息(对于网络)
一. 邻接矩阵的存储方法
核心思想:采用两个矩阵储存一个图
-
定义一个一维数组VERTEX[0…n-1]存放图中所有顶点的数据信息(若顶点信息为0,1,2,3, … ,此数组可略)。(称为顶点数组)
-
定义一个二维数组A[0…n-1,0…n-1]存放图中所有顶点关系的信息(该数组被称为邻接矩阵)
不带权的图:赋值为:1/0
带权的图:赋值为:权值/∞
特点:
-
无向图的邻接矩阵一定是一个对称矩阵
-
不带权的有向图的邻接矩阵一般是稀疏矩阵
(在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵为稀疏矩阵;与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵。定义非零元素的总数比上矩阵所有元素的总数为矩阵的稠密度)
-
无向图的邻接矩阵的第i行(或第i列)非0或非∞的元素的个数称为第i个顶点的度数
-
有向图的邻接矩阵的第i行非0或非∞元素的个数称为第i个顶点的出度;第i列非0或非∞元素的个数称为第i个顶点的入度
-
空间复杂度:O(n^2)
(稀疏矩阵)三元储存法
三元组( i, j, value )
三元组表示适合存储稀疏矩阵(稀疏图),针对图来说,是一种按边存储的方式,又称为边集数组,特别适合于图的按边访问应用。当然,若按顶点来访问图将不是很方便。
二. 邻接表存储方法
核心思想:建立n个线性链表存储该图
-
每一个链表前面设置一个头节点,用来存放一个顶点的数据信息,称之为顶点结点。
其构造为 vertex link
其中,vertex域存放某个顶点的数据信息;link域存放某个链表中第一个结点的地址。
n个头节点之间为一数组结构
-
第i个链表中的每一个链接点(称之为边结点)表示以第i个顶点为出发点的一条边,边结点的构造为 adjvex weight next
其中,next域 为 指针域;
weight域 为 权值域 (若图不带权,则无此域)
adjvex域 存放以第i个顶点为出发点的一条边的另一端点在头节点数组中的位置
特点:
- 无向图的第i个链表中边结点的个数是第i个顶点的度数
- 有向图的第i个链表中边结点的个数是第i个顶点的出度
- 无向图边结点的个数一定为偶数,边结点个数为奇数的图一定是有向图
关于逆邻接表
第i个链表中的每一个链结点(称之为边结点)表示以第i个顶点为终止点的一条边;
C语言描述
邻接矩阵:
#define MaxV//最大顶点个数
//定义边类型
typedef struct edge{
int weight;
//...
}Edge;
Vertype Vertex[MaxV];//顶点信息数组
Edge G[MaxV][MaxV];//邻接矩阵
邻接表:
#define MaxV//最大顶点个数
//定义边结点类型
typedef struct edge{
int adjvex;
int weight;
struct edge *next;
}ELink;
//定义顶点结点类型
typedef struct ver{
vertype vertex;
ELink *link;
}VLink;
VLink G[MaxV];//建立邻接表
边集数组-稀疏图:
#define MaxV //<最大顶点个数>
#define MaxE //<最大边数>
//定义边类型
typedef struct edge{
int v1;
int v2;
int weight;
}Edge;
Vertype Vertex[MaxV];//顶点信息数组
Edge G[MaxE];//边集数组(三元组)
图的基本操作
createGraph() //创建一个图
destoryGraph() //删除一个图
insertVex(v) //在图中插入一个顶点v
deleteVex(v) //在图中删除一个顶点v
insertEdge(v,w) //在图中插入一条边<v,w>
deleteEdge(v,w) //在图中删除一条边<v,w>
traverseGraph() //遍历一个图
创建一个邻接表存储的图算法:
//例子
/*若有如下输入:
8
0 2 4 … -1
1 3 6 8 … -1
…
第一行为图的顶点个数,从第二行开始第一个数为顶点序号,第二个数字开始为该顶点的邻接顶点,每行以-1结束,则创建一个邻接表存储的图算法如下:
*/
#define MaxV 256
typedef struct edge{
int adj;
int wei;
struct edge *next;
}Elink;
typedef struct ver{
ELink *link;
}Vlink;
VLink G[MaxV];
void createGraph(VLink graph[])
{
int i,n,v1,v2;
scanf(“%d”,&n);
for(i=0; i<n; i++)
{
scanf(“%d %d”,&v1,&v2);
while(v2 != -1)
{
graph[v1].link=insertEdge(graph[v1].link, v2);
graph[v2].link=insertEdge(graph[v2].link, v1);
//邻接矩阵:graph[v1][v2]= graph[v2][v1]= 1;
scanf(“%d”,&v2);
}
}
}
//在链表尾插入一个节点
Elink *insertEdge(ELink *head, int avex)
{
ELink *e,*p;
e =(ELink *)malloc(sizeof(ELink));
e->adj= avex; e->wei=1; e->next = NULL;
if(head == NULL)
{
head=e;
return head;
}
for(p=head; p->next != NULL; p=p->next)
;
p->next = e;
return head;
}
6.3 图的遍历
以无向图为例:
从图中某个指定的顶点出发, 按照某一原则对图中所有顶点都访问一次, 得到一个由图中所有顶点组成的序列, 这一过程称为图的遍历 。
利用图的遍历:
- 确定图中满足条件的顶点
- 求解图的连通性问题
- 判断图中是否存在回路
一. 深度优先遍历
原则:
从图中某个指定的顶点v出发,先访问顶点v,然后从顶点v未被访问过的一个邻接点出发,继续进行深度优先遍历,直到图中与v相通的所有顶点都被访问(完成一个连通分量的遍历);
若此时图中还有未被访问过的顶点, 则从另一个未被访问过的顶点出发重复上述过程,直到遍历全图(完成所有连通分量的遍历)。
递归过程
类似二叉树的前序遍历
如何确定顶点不被重复访问
为了标记某一时刻图中哪些顶点是否被访问,定义一维数组visited[0…n-1], 有
visited[i] = 1 表示对应的顶点已经被访问
0 表示对应的顶点还未被访问
算法分析
采用邻接矩阵存储该图:O(n^2)
采用邻接表存储该图:O(n+e)
树深度优先遍历算法:
void DFStree(TNodeptr t)
{
int i;
if(t!=NULL)
{
VISIT(t); /* 访问t指向结点 */
for(i=0;i<MAXD; i++)
if(t->next[i] != NULL)
DFStree(t->next[i]);
}
}
图深度优先遍历算法:
int Visited[N]={0}; //标识顶点是否被访问过,N为顶点数
void travelDFS(VLink G[ ], int n)
{
int i;
for(i=0; i<n; i++)
Visited[i] = 0;
for(i=0; i<n; i++)
if( !Visited[i] ) DFS(G, i);//完成一个连通分量的遍历
}
void DFS(VLink G[ ], int v)
{
ELink *p;
Visited[v] = 1; //标识某顶点被访问过
VISIT(G, v); //访问某顶点
for(p = G[v].link; p !=NULL; p=p->next)
if( !Visited[p->adjvex] )
DFS(G, p->adjvex);
}
算法分析:
如果图中具有n个顶点、e条边,则
- 若采用邻接表存储该图,由于邻接表中有2e个或e个边结点,因而扫描边结点的时间为O(e);而所有顶点都递归访问一次,所以,算法的时间复杂度为O(n+e)。
- 若采用邻接矩阵存储该图,则查找每一个顶点所依附的所有边的时间复杂度为O(n),因而算法的时间复杂度为O(n^2)。
二. 广度优先遍历
原则:
从图中某个指定的顶点v出发,先访问顶点v,然后依次访问顶点v的各个未被访问过的邻接点,然后又从这些邻接点出发, 按照同样的规则访问它们的那些未被访问过的邻接点,如此下去,直到图中与v 相通的所有顶点都被访问(完成一个连通分量的遍历);
若此时图中还有未被访问过的顶点, 则从另一个未被访问过的顶点出发重复上述过程, 直到遍历全图(完成所有连通分量的遍历)。
类似于树的层次遍历
如何确定顶点不被重复访问
为了标记某一时刻图中哪些顶点是否被访问,定义一维数组visited[0…n-1], 有
visited[i] = 1 表示对应的顶点已经被访问
0 表示对应的顶点还未被访问
算法分析
采用邻接矩阵存储该图:O(n^2)
采用邻接表存储该图:O(n+e)
树广度优先遍历算法:
void BFStree(TNodeptr t)
{
TNodeptr p;
int i;
if(t!=NULL)
{
enQueue(t);
while(!isEmpty())
{
p = deQueue();
VISIT(p);
for(i = 0; i < MAXD ; i++)
if( p->next[i] != NULL)
enQueue(p);
}
}
}
图广度优先遍历算法:
int Visited[N]={0}; //标识顶点是否被访问守,N为顶点数
void travelBFS(VLink G[ ], int n)
{
int i;
for(i=0; i<n; i++)
Visited[i] = 0;
for(i=0; i<n; i++)
if( !Visited[i] ) BFS(G, i);//完成一个连通分量的遍历
}
void BFS(VLink G[ ], int v)
{
ELink *p;
Visited[v] = 1; //标识某顶点已入队
enQueue(Q, v);
while(!emptyQ(Q))
{
v = deQueue(Q); //取出队头元素
VISIT(G, v); //访问当前顶点
for(p=G[v].link; p!=NULL; p=p->next ) //访问该顶点的每个邻接顶点
if( !Visited[p->adjvex] )
{
Visited[p->adjvex] = 1; //标识某顶点入队
enQueue(G, p->adjvex);
}
}
}
DFS与BFS
对比这两个图的遍历算法,其实它们在时间复杂度上是一样的,不同之处仅仅在于对顶点的访问的顺序不同。
具体用哪个取决于具体问题。通常:
DFS更适合目标比较明确,以找目标为主要目的的情况
BFS更适合在不断扩大遍历范围时找到相对最优解的情况
问题:独立路径计算–数据结构设计
本问题的实质:给定起点(如图中A),对图进行遍历,并在遍历图的过程中找到到达终点(如图中B)的所有情况。
前面介绍的DFS和BFS算法都是从源点出发对邻接顶点的遍历。而问题是本文中两个点间可能有多个边(如图所示)。
算法策略是对DFS算法(或BFS)进行改进,在原来按邻接顶点进行遍历,改为按邻接顶点的边进行遍历(即从一个顶点出发遍历其邻接顶点时,按邻接顶点的边进行深度遍历,即只有当某顶点的所有邻接顶点的所有边都遍历完才结束该结点的遍历)。
采用邻接表来存储图,邻接表设计如下:
#define MAXSIZE 512
struct edge{//边结点结构
int eno; //边序号
int adjvex; //邻接顶点
int weight; //边的权重(可为距离或时间),本文中为1
struct edge *next;
};
struct ver {//顶点结构,邻接表下标即为顶点序号
struct edge *link;
} ;
struct ver G[MAXSIZE]; //由邻接表构成的图
char Visted[MAXSIZE] = {0}; //标识相应顶点是否被访问
int paths[MAXSIZE]; //独立路径
6.4 最小生成树
一. 什么是最小生成树
包含着连通图的全部n个顶点,仅包含其(n-1)条边的极小连通子图。
带权连通图中,总的权值最小的带权生成树为最小生成树。最小生成树也称最小代价生成树,或最小花费生成树
构造最小生成树的基本原则
- 只能利用图中的边来构造最小生成树
- 只能使用、且只能使用图中的n-1条边来连接图中的n个顶点
- 不能使用图中产生回路的边
二. 求最小生成树
普里姆(Prim)算法
设G=(V, GE)为具有n个顶点的带权连通图;T=(U, TE)为生成的最小生成树,
初始时, TE = 空 , U = { v } , v 属于 V
依次在G中选择一条一个顶点仅在V中,另一个顶点在U中,并且权值最小的边加入集合TE,同时将该边仅在 V 中的那个顶点加入集合U。重复上述过程(n–1)次,使得 U = V , 此时T为G的最小生成树。
Prim算法数据结构说明
-
int weights[MAXVER] [MAXVER];
当图G中存在边 ( i , j ) ,则weights[i][j]为其权值,否则为一个INFINITY
-
int edges[MAXVER];
存入生成的最小生成树的边,如 :( i , edges[i] ) 为最小生成树的一条边,
应有n-1条边 -
int minweight[MAXVER];
存放未确定为生成树的顶点至已确定的生成树上顶点的边权重
minweight[i]表示顶点i至生成树上顶点的边权重, minweight[i] = 0 表示顶点i已确定为最小生成树顶点
Prim算法
#define MAXVER 512
#define INFINITY 32767
void Prim(int weights[][MAXVER] , int n , int src , int edges[])
{//weight为权重数组,n为顶点个数,src为最小树第一个顶点,edge为最小生成树边
int minweight[MAXVER];//存放未确定为生成树的顶点至已确定的生成树上顶点的边权重
int min;
int i , j , k;
for(i = 0 ; i < n ; i++)//初始化相关数组
{
minweight[i] = weight[src][i]//将src顶点与之有边的权值存入数组
edges[i] = src;//初始时所有顶点的前序顶点设为src,(src , i)
}
minweight[src] = 0;//将第一个顶点src加入生成树
for(i = 1 ; i < n ; i++)//找到最小生成树的(n-1)条边
{
min = INFINITY;
for(j = 0 , k = 0 ; j < n ; j++)//在数组中找到最小值,其下标为k
{
if(minweight[j] != 0 && minweight[j] < min)//在数组中找到最小值,其下标为k
{
min = minweight[j];
k = j;
}
}
minweight[k] = 0;//找到最小树的一个顶点
for(j = 0 ; j < n ; j++)//找到一个顶点后进行一次数据的更新(集中在新加入最小生成树的这个k上)
{
if(minweight[j] != 0 && weights[k][j] < minweight[j])
{
minweight[j] = weights[k][j]; //将小于当前权值的边(k,j)权值加入数组中
edges[j] = k; //将边(j,k)信息存入边数组中
}
}
}
}
问题:北航网络中心铺设电缆
设计考虑:
- 可用邻接矩阵存储网络图
数据结构:
struct edge {
int id;
int wei;
};
struct edge graph[MAXVER] [MAXVER];//邻接矩阵
int edges[MAXVER]={0};//生成树数组
根据输入值对<id,v1,v2,wei>构造图:
graph[v1] [v2].id = id; graph[v1] [v2].weight = wei;
graph[v2] [v1].id = id; graph[v2] [v1].weight = wei; - 调用Prim算法得到最小生成树,存放在edges数组中
- 根据生成树数组edges可得到生成树边序号 为graph[i][edges[i]].id的边,其权重为: graph[i][edges[i]].wei
- 最小生成树按边序号进行排序输出.
克鲁斯卡尔(Kruskal)方法
基本思想:
设G=(V , GE);T=(U , TE)
初始时, TE = 空 , U = V
从G中选择一条当前未选择过的、且边上的权值最小的边加入TE,若加入TE后使
得T未产生回路,则本次选择有效,如使得T产生回路,则本次选择无效,放弃本次选择的边。重复上述选择过程直到TE中包含了G的n-1条边,此时的T为G的最小生成树。
判断
-
任意连通图中,假设没有相同权值的边存在,则权值最小的边一定是其最小生成树中的边。(√)
-
任意连通图中,假设没有相同权值的边存在,则权值最大的边一定不是其最小生成树中的边。(×)
-
任意连通图中,假设没有相同权值的边存在,则与同一顶点相连的权值最小的边一定是其最小生成树中的边。(√)
-
采用克鲁斯卡尔算法求最小生成树的过程中,判断一条待加入的边是否形成回路,只需要判断该边的两个顶点是否都已经加入到集合U中。(×)
(连接两个连通分量时反例?)
Kruskal算法:
#include <stdio.h>
#define MAXE 100
#define MAXV 100
typedef struct{
int vex1; //边的起始顶点
int vex2; //边的终止顶点
int weight; //边的权值
}Edge;
void kruskal(Edge E[],int n,int e)
{
int i,j,m1,m2,sn1,sn2,k,sum=0;
int vset[n+1]; //辅助数组(标记所属的连通分量)!!!
for(i=1;i<=n;i++) //初始化辅助数组
vset[i]=i;
k=1;//表示当前构造最小生成树的第k条边,初值为1
j=0;//E中边的下标,初值为0
while(k<e)//生成的边数小于e时继续循环
{
m1 = E[j].vex1;
m2 = E[j].vex2;//取一条边的两个邻接点
sn1 = vset[m1];
sn2 = vset[m2];//分别得到两个顶点所属的集合编号
if(sn1 != sn2)//两顶点分属于不同的集合,该边是最小生成树的一条边
{//防止出现闭合回路
printf("V%d-V%d=%d\n",m1,m2,E[j].weight);
//这里可以再加上一个记录的过程,记录下最小生成树
sum += E[j].weight;//总权值加和
k++; //生成边数增加
if(k>=n) break;
for(i=1;i<=n;i++) //两个集合统一编号
if(vset[i]==sn2) //集合编号为sn2的改为sn1
vset[i]=sn1;
}
j++; //扫描下一条边
}
printf("最小权值之和=%d\n",sum);
}
int fun(Edge arr[],int low,int high)//快排部分的代码
{
int key = arr[low].weight;
Edge lowx = arr[low];
while(low < high)
{
while(low < high && arr[high].weight >= key)
high--;
if(low < high)
arr[low++] = arr[high];
while(low < high && arr[low].weight <= key)
low++;
if(low < high)
arr[high--] = arr[low];
}
arr[low]=lowx;
return low;
}
void quick_sort(Edge arr[],int start,int end)
{
int pos;
if(start<end)
{
pos=fun(arr,start,end);
quick_sort(arr,start,pos-1);
quick_sort(arr,pos+1,end);
}
}
int main()
{
Edge E[MAXE];
int nume,numn;
//freopen("1.txt","r",stdin);//文件输入
printf("输入顶数和边数:\n");
scanf("%d %d",&numn,&nume);
for(int i=0;i<nume;i++)
scanf("%d %d %d",&E[i].vex1,&E[i].vex2,&E[i].weight);
quick_sort(E,0,nume-1);
kruskal(E,numn,nume);
return 0;
}
Prim算法和Kruskal算法
本质上它们是一种贪婪算法(greedy algorithm)
对比两个算法,Kruskal算法主要是针对边展开,边数少时效率会非常高,所以对稀疏图有很大的优势;Prim算法对于稠密图,即边数非常多的情况会好一些。
Prim:存成邻接矩阵
Kruskal:按边存
6.5最短路径问题(Dijkstra算法)- 单原点问题
一. 路径长度的定义
- 不带权的图:路径上所经过的边的数目
- 带权的图:路径上所经过的边上的权值之和
二. 问题的提出
设出发顶点为v(通常称为源点)
- 单源点最短路径
- 每对顶点之间的最短路径
- 求图中第一短、第二短、… 的最短路径
- …
三. 解决问题所需要确定的数据结构
1. 图的存储
以0~n-1 分别代表n个顶点,采用邻接矩阵存储该图,有
A[i] [j] = Wij 当顶点vi 到顶点vj 有边,且权为Wij
∞ 当顶点vi 到顶点vj无边时
0 当vi=vj 时
四. Dijkstra算法(用自然语言表达)
本质上它也是一种贪婪算法(greedy algorithm)
设:
v0为源顶点,
Weights为顶点间权重数组(邻接矩阵),
Sweight为v0到相应顶点最小权重数组,
Spath为最短路径数组,
wfound表示某顶点是否已确定最短路径(0未确定,1已确定),
有如下定义:
int Weigths[VNUM] [VNUM];
int Sweight[VNUM];
int Spath[VNUM] = {0};
int wfound[VNUM];
-
初始化数组Sweight,使得Sweight[i] = Weigths[v0] [i]
-
初始化Sweight[v0] = 0
Spath[i] = v0
wfound[v0] = 1
-
查找与v0间权重最小且没有确定最短路径的顶点v,即在Sweight数组中查找权重最小且没有确定最短路径的顶点
-
标记v为已找到最短路径的顶点
-
对于图G中每个“从顶点v0到其最短路径还未找到,且存在边(v,w),如果从v0通过v到w的路径权值小于它当前的权值“,则更新w的权值为:v的权值+边(v,w)的权值,即:Sweight[w] = Sweight[v]+Weights[v] [w]
-
重复上述过程的第3至第5步n–1 次
注:最短路径数组Spath含义为:
Spath[v]表示顶点v在最短路径上的直接前驱顶点。
假设某最短路径由顶点v0 , v1 , v2 , v3组成,则有:
v2 = Spath[v3]
v1 = Spath[v2]
v0 = Spath[v1]
Dijkstra算法:
Dijkstra(int v0)
{
int i, j, v, minweight;
char wfound[VNUM] = { 0 };
//用于标记从v0到相应顶点是否找到最短路径,0未找到,1找到
for(i = 0; i < VNUM; i ++)
{
Sweight[i] = Weights[v0][i];
Spath[i] = v0;
}
//初始化数组Sweight和Spath
Sweight [v0] = 0;//关于v0的初始化
wfound [v0] = 1; //关于v0的初始化
for(i = 0; i < VNUM - 1; i ++)//迭代VNUM-1次(到其余(VNUM-1)个顶点的最短路径)
{
minweight = INFINITY;
for(j = 0 ; j < VNUM ; j ++)//找到未标记的最小权重值顶点v
if( !wfound[j] && ( Sweight[j] < minweight) )
{
v = j;
minweight = Sweight[v];
}
wfound[v] = 1; //标记该顶点为已找到最短路径
for(j = 0 ; j < VNUM ; j ++)//找到未标记顶点且其权值大于v的权值+(v,j)的权值,更新其权值
{
if( !wfound[j] && (minweight + Weights[v][j] < Sweight[j] ))
{
Sweight[j] = minweight + Weights[v][j];
Spath[j] = v; //记录前驱顶点
}
}
}
}
最小代价生成树(与源点无关)和最短(路径)生成树(与源点有关)不相同!!!
Dijkstra算法的局限性–对于带有负权值的图
Dijkstra算法要求已经确定的最短路径在后续算法执行时不会被修改。即路径长度要单调递增。
解决方法:re-weighting,每条路径增加2?仍然不解决问题!
如果存在负回路,则最短路径可以任意负下去!
直接求s-t的简单最短路径是难解问题!
Bellman-Ford algorithm(无负回路)
问题6.1:北京地铁乘坐线路查询(略,有时间再看)
6.5 a 最短路径问题(Floyd算法)- 多源点问题
一. 问题的提出
计算图中所有顶点间最短路径
二. 方法一
遍历图中每个顶点,为其调用Dijkstra算法,算法复杂度为O(n^3)。该方法适用于稀疏图。
三. 方法二
Floyd算法,算法复杂度亦为O(n^3),但算法非常简洁。该方法适用于稠密图。
Floyd算法(对有向图):
#include <Stdio.h>
#define inf 9999999
void InitNet(int, int [][10]);
void NetWeight(int, int [][10]);
void Floyd(int, int [][10]);
int main() {
int n, m, i, j, e[10][10];
printf("请输入顶点个数和边的条数:\n");
scanf("%d %d", &n, &m);
InitNet(n, e);/* 初始化网 */
NetWeight(m, e);/*读入边的权值*/
Floyd(n, e);/*Floyd 核心代码*/
//输出最终结果
for (i = 1; i <= n; i++) {
for (j = 1; j <= n; j++) printf("%10d", e[i][j]);
printf("\n");
}
return 0;
}
/* 初始化网 */
void InitNet(int n, int e[10][10]) {
int i, j;
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
if (i == j) e[i][j] = 0;
else e[i][j] = inf;
}
/*读入边的权值*/
void NetWeight(int m, int e[10][10]) {
int t1, t2, t3;
printf("请输入边的权值:\n");
for (int i = 1; i <= m; i++) {
scanf("%d %d %d", &t1, &t2, &t3);
e[t1][t2] = t3;
}
}
/*Floyd 核心代码*/
void Floyd(int n, int e[10][10]) {
int k, j, i;
for (k = 1; k <= n; k++)
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
if (e[i][j] > e[i][k] + e[k][j] && e[i][k] < inf && e[k][j] < inf)
{//算法改进:禁止正无穷的权值的与其他权值相加
e[i][j] = e[i][k] + e[k][j];
}
}
6.6 AOV网与拓扑排序
一. 什么是AOV网
e.g. 一个建筑工程的施工流程
e.g. 计算机专业专业课程教学流程安排
AOV网的定义
以顶点表示活动,以有向边表示活动之间的优先关系的有向图称为顶点表示活动的网(Activity On Vertex Network),简称AOV网。
在AOV网中,若顶点i到顶点j之间有路径,则称顶点i为顶点j的前驱,顶点j为顶点i的后继;若顶点i到顶点j之间为一条有向边,则称顶点i为顶点j的直接前驱,顶点j为顶点i的直接后继。
检测工程能否正常进行,首先要判断对应的AOV网中是否存在回路,达到该目的最有效的方法之一是对AOV网构造其顶点的拓扑序列,即对AOV网进行拓扑排序 。(离散数学:由某个集合上的一个偏序得到该集合上的一个全序的操作称为拓扑排序)
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1 , v2 , … , vn ,满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必在顶点vj之前,则称这样的顶点序列为一个拓扑序列。构造拓扑序列的过程就是拓扑排序。
二. 拓扑排序
构造AOV网的一个顶点序列,使得该顶点序列满足下列条件:
-
若在AOV网中,顶点 i 优先于顶点 j ,则在该序列中顶点 i 仍然优先于顶点 j ;
-
若在AOV网中,顶点 i 与顶点 j 之间不存在优先关系,则在该序列中建立它们的优先关系,即顶点 i 优先于顶点 j ,或者顶点 j 优先于顶点 i ;
-
若能构造出这样的拓扑序列,则拓扑序列包含AOV网的全部顶点,说明AOV网中没有回路。
若构造不出这样的序列,说明AOV网中存在回路。
三. 拓扑排序的方法
-
从AOV网中任意选择一个没有前驱的顶点(入度为0)
-
从AOV网中去掉该顶点以及以该顶点为出发点的所有边
-
重复上述过程,直到AOV网中的所有顶点都被去掉(说明AOV网中无回路)
或者AOV网中还有顶点,但不存在入度为0 的顶点(说明AOV网中存在回路)
拓扑序列不一定唯一
用自然语言描述的算法:
- 首先建立一个入度为0的顶点栈,将网中所有入度为0的顶点分别进栈。
- 当堆栈不空时,反复执行以下动作:
- 从顶点栈中退出一个顶点,并输出它;
- 从AOV网中删去该顶点以及以它发出的所有边,并分别将这些边的终点的入度减1;
- 若此时边的终点的入度为0,则将该终点进栈;
- 若输出的顶点个数少于AOV网中的顶点个数,则报告网中存在回路,否则,说明该网中不存在回路。
自学该算法的C语言描述
除了进行拓扑排序,还可以采用什么方法判断一个有向图是否存在回路?
进行深度优先遍历。若从某个顶点v出发,遍历结束前出现了从顶点u到顶点v的回边,则可以断定图中包含顶点v到顶点u的回路。
6.7 AOE网与关键路径
更关心
- 每个活动持续多少时间?
- 完成整个工程至少需要多少时间?
- 哪些活动是关键活动?
一. AOE网的定义
AOE(Activity On Edge)网为一个带权的有向、无环图,其中,以顶点表示事件,有向边表示活动,边上的权值表示活动持续的时间。
正常情况下(网中无回路),AOE网中只有一个入度为0的顶点,称之为源点;有一个出度为0的顶点,称之为终点。
AOE网的特点:
- 只有在某个顶点所代表的事件发生以后,该顶点引发的活动才能开始。
- 进入某事件的所有边代表的活动都已完成,该顶点代表的事件才能发生。
二. AOE网的储存方法
采用邻接矩阵储存方法
三. 关键路径
1. 关键路径的定义
从源点到终点的路径中具有最大长度的路径为关键路径;关键路径上的活动称为关键活动。
2. 关键路径的特点
- 关键路径的长度(路径上的边的权值之和)为完成整个工程所需要的最短时间。
- 关键路径的长度变化(即任意关键活动的权值变化)将影响整个工程的进度,而其他非关键活动在一定范围内的变化不会影响工期。
求关键活动的思路:
e[i] :活动 ai 的最早开始时间;
l[i] : 活动 ai 的最晚开始时间;
l[i] – e[i] :缓冲时间/松弛时间/时间余量
若 l[i] – e[i] = 0 ,则说明活动 ai 为一个关键活动。
ee[k] :事件 k 的最早发生时间
le[k] :事件 k 的最晚发生时间
结论:
事件k的最早发生时间 ee[k] → 活动的ai最早开始时间e[i]
事件k的最晚发生时间 le[k] → 活动的ai最晚开始时间l[i]
求 e[i] = l[i] (ai 为关键活动)
四. 求关键路径
1. 计算事件k的最早发生时间ee[k]
事件k的最早发生时间决定了由事件k出发的所有活动的最早开始时间;
该时间是指从源点到顶点(事件)k的最大路径长度。
计算方法:
ee[0] = 0
ee[k] = MAX { ee[j] + <j,k>的权} (其中 <j , k> 从属于P(k) )
P(k) :k的前驱事件
2. 计算事件k的最晚发生时间le[k]
所谓事件k的最晚发生时间是指不影响整个工期的前提下事件k必须发生的最晚时间,它必须保证从事件k发出的所有活动的终点事件(k的后继事件)的最迟发生时间。
计算方法(从后向前反推计算)
le[n-1] = ee[n-1]
le[k] = MIN { le[j] – <k,j>的权 }(其中 <j , k> 从属于S(k) )
S(k) :k的后继事件
在 k →活动i→ j 中:
3. 计算活动 i 的最早开始时间e[i]
所谓活动 i 的最早开始时间实际上是事件 k 发生的最早时间,即只有事件 k 发生,活动 i 才能开始。
计算方法:
e[i] = ee[k]
4. 计算活动 i 的最晚开始时间l[i]
所谓活动i的最晚开始时间是指不推迟整个工期的前提下活动i开始的最晚时间。
计算方法:
l[i] = le[j] - <k,j>的权
5. 求出关键活动与关键路径
计算方法:
l[i] = e[i] ,则a[i]是关键活动
自学关键活动和关键路径的计算算法的C实现。
6.8 网络流量问题
一. 相关概念
设给定边容量为c(v,w)的有向图G=(V,E)。
(容量可以表示通过一个管道的水、电、交通、网络等最大流量)
有两个顶点,一个是 s ,称为源点(source),一个是t称为汇点(sink)。
对于任一条边 (v,w),最多有“流”的 c(v,w) 个单位(容量)可以通过。
在既不是源点s又不是汇点t的任一顶点v,总的进入流必须等于总的发出的流。每条边上的流满足下面两个条件:
- 通过边的流不能大于边的容量(容量约束)
- 到达顶点v的流的总和与从v流出的总和相同,其中v不是源点或汇点。(流守恒)
最大流问题:确定从s到t可以通过的最大流量。
二. 最大流算法原理
算法设有3个图(原图G、流图Gf、残余图Gr),在其上分阶段进行。
Gf表示在算法的任意阶段已经达到的流,算法终止时其包含最大流;
Gr称为残余图(residual graph),它表示每条边还能再添加上多少流(即还残余多少流),对于Gr中每条边(称为残余边,residual edge)可以从其容量中减去当前流来计算其残余流。
- 初始时Gf所有边都没有流(流为0),Gr与G相同;
- 每个阶段,先从Gr中找一条从s到t的路径(称为增长路径augmenting path);
- 将该路径上最小边的流量作为整个路径的流(权),并将路径加至流图Gf中;
- 将该权值路径从Gr中减去,若某条边权值为0,则从Gr中除去;
- 将具有该权的反向路径加到Gr中;(好好理解这一步的必要性!!!)
- 重新执行步骤2,直到Gr中无从s到t的路径;
- 将Gf中顶点t的每条入边流值相加得到最大流。