最小生成树、最短路径等图问题解法


最小生成树

 在 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 的顶点数百分比 */
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值