基础算法合集-图论

发布于:2025-07-01 ⋅ 阅读:(21) ⋅ 点赞:(0)

 本文将介绍数据结构图论部分中常见的算法

单源最短路径问题(用来计算一个点到其他所有顶点的最短路径)
Dijkstra(n*n)
1. 初始化: 先找出从源点V0到各终点Vk的直达路径(V0,Vk), 即通过一条弧到达的路径
2. 选择: 从这些路径中找出一条长度最短的路径(V0,u)
3. 更新: 然后对其余各条路径进行适当的调整
   若在图中存在弧(u,Vk),  且(Vo,u,Vk)<(Vo,Vk), 则以路径(Vo,u,Vk) 代替(Vo,Vk)
4. 把V分成两组:
   (1) S: 已求出最短路径的顶点的集合
   (2) T=V-S: 尚未确定最短路径的顶点集合
算法步骤:
1.初始时令S={V0}, T={其余顶点}
2.T中顶点对应的距离值用辅助数组D存放
   D[i] 初值: 若<V0,Vi>存在, 则为其权值; 否则为无穷  注: 从源点V0开始
3.从T中选取一个其距离值最小的顶点Vj, 加入S
4.对T中顶点的距离值进行修改: 若加进Vj 作中间顶点, 从V0到Vi的距离值比不加Vj的路径要短, 则修改此距离值
重复上述步骤, 直到S=V为止
代码实现:
辅助数组: 
-- 数组bool S[n]: 记录相应顶点是否已被确定最短距离
-- 数组D[n]: 记录源点到相应顶点路径长度
-- 数组Path[n]: 记录相应顶点的前驱顶点
1. 从D(T集合中)找最小值v   2. 将最小值对应的顶点, 加入S   3.更新D数组, Path数组
注:找最小值的时候要从D数组中为false的点中找,  并且不能为源点

#include<iostream>
using namespace std;
#define MVNum 20
#define MAXNUM 32767
typedef char VerTexType;  //顶点类型
typedef int ArcType;      //弧类型 
typedef struct{
	VerTexType vexs[MVNum];   //vexs代表顶点,arcs代表邻接矩阵
	ArcType arcs[MVNum][MVNum];
	int vexnum,arcnum;  //vexnum表示图中顶点的数量,arcnum表示图中边的数量
}AMGraph;
int LocateVex(AMGraph G,VerTexType v){
	for(int i=0;i<G.vexnum;i++){
		if(v==G.vexs[i]){
			return i;
		}
	} 
	return -1;
}
void CreateUDG(AMGraph &G){
	cin>>G.vexnum>>G.arcnum;
	for(int i=0;i<G.vexnum;i++){
		cin>>G.vexs[i];
	}
	for(int i=0;i<G.vexnum;i++){
		for(int j=0;j<G.vexnum;j++){
			G.arcs[i][j]=MAXNUM;
		}
	}
	VerTexType v1,v2;
	ArcType w;
	for(int k=0;k<G.arcnum;k++){
		cin>>v1>>v2>>w;
		int i=LocateVex(G,v1);
		int j=LocateVex(G,v2);
		G.arcs[i][j]=w;
	}
}
bool S[MVNum];
ArcType D[MVNum];
int Path[MVNum];
void ShortestPath_Dijkstra(AMGraph G,VerTexType u){
	int k=LocateVex(G,u);
	for(int i=0;i<G.vexnum;i++){
		S[i]=false;
		D[i]=G.arcs[k][i];
		if(D[i]<MAXNUM){
			Path[i]=k;
		}else{
			Path[i]=-1;
		}
	}
	S[k]=true;
	int min,v;
	for(int i=1;i<G.vexnum;i++){
		min=MAXNUM;
		for(int j=0;j<G.vexnum;j++){
			if(j!=k){
				if(!S[j]&&D[j]<min){
				  min=D[j];
				  v=j;
				}
			}
		}
		S[v]=true;
		for(int j=0;j<G.vexnum;j++){
			if(!S[j]&&D[v]+G.arcs[v][j]<D[j]){
				D[j]=D[v]+G.arcs[v][j];
				Path[j]=v; 
			}
		}
	}
	
}
int main(){
	AMGraph G;
	CreateUDG(G);
	char u;
	cin>>u;
	ShortestPath_Dijkstra(G,u);
	for(int i=0;i<G.vexnum;i++){
		cout<<D[i]<<" ";
	}
	return 0;
}

所有顶点间的最短路径
方法一:每次以一个顶点源点, 重复执行Dijkstra算法n 次
方法二:floyd 算法(n*n*n)
以上两种方法的时间复杂度是一样的
1. 初始时设置一个n阶方阵, 令其对角线元素为0, 若存在弧<Vi,Vj>, 则对应元素为权值; 否则为无穷
2. 逐步试着在原直接路径中增加中间节点, 若加入中间节点后路径变短, 则修改之; 否则, 维持原值. 所有顶点试探完毕, 算法结束
代码实现:
path 数组初始时为-1, 存储的是前驱节点
邻接矩阵, 对角线存储的是0, 有权值就存权值, 没有权值就存储无穷

#include<iostream>
using namespace std;
#define MVNum 20
#define MAXNUM 32767
typedef char VerTexType;  //顶点类型
typedef int ArcType;      //弧类型 
typedef struct{
	VerTexType vexs[MVNum];   //vexs代表顶点,arcs代表邻接矩阵
	ArcType arcs[MVNum][MVNum];
	int vexnum,arcnum;  //vexnum表示图中顶点的数量,arcnum表示图中边的数量
}AMGraph;
int LocateVex(AMGraph G,VerTexType v){
	for(int i=0;i<G.vexnum;i++){
		if(v==G.vexs[i]){
			return i;
		}
	} 
	return -1;
}
void CreateUDG(AMGraph &G){
	cin>>G.vexnum>>G.arcnum;
	for(int i=0;i<G.vexnum;i++){
		cin>>G.vexs[i];
	}
	for(int i=0;i<G.vexnum;i++){
		for(int j=0;j<G.vexnum;j++){
			if(i!=j){
			   G.arcs[i][j]=MAXNUM;
			}else{
			   G.arcs[i][j]=0;
			}
		}
	}
	VerTexType v1,v2;
	ArcType w;
	for(int k=0;k<G.arcnum;k++){
		cin>>v1>>v2>>w;
		int i=LocateVex(G,v1);
		int j=LocateVex(G,v2);
		G.arcs[i][j]=w;
	}
}
ArcType D[MVNum][MVNum];
int P[MVNum][MVNum];
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];
		if(D[i][j]<MAXNUM&&i!=j){
			P[i][j]=i;
		}else{
			P[i][j]=-1;
		}	 
	 }
   }
   for(int k=0;k<G.vexnum;k++){  //我咧个n次尝试 
   	 for(int i=0;i<G.vexnum;i++){
   	    for(int j=0;j<G.vexnum;j++){
   	   	    if(D[i][j]>D[i][k]+D[k][j]){
   	   	   	   D[i][j]=D[i][k]+D[k][j];
   	   	   	   P[i][j]=P[k][j];  //从上一个Path二维表里面找,永远存储的是距离终点最近的前驱顶点(因为k是按顺序进行尝试的) 
		    }
	    }	
	 }
   }
}
void Print(int n){
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			if(D[i][j]<MAXNUM){
			  printf("%4d(%2d)",D[i][j],P[i][j]);
	     	}else{
	     	  printf("%4c(%2d)",'#',P[i][j]);
			 }
		}
		printf("\n");
	}
} 
int main(){
	AMGraph G;
	CreateUDG(G);
	int n=G.vexnum;
	ShortestPath_Floyd(G);
	Print(n);
	return 0;
}

 总结来说第一种算法就是先找最小值, 第二种算法就是n次尝试

关键路径问题:拓扑排序
把工程计划表示为边表示活动的网络, 即AOE网, 用顶点表示事件, 弧表示活动, 弧的权表示活动持续时间
事件(顶点)表示在它之前的活动已经完成,在它之后的活动可以开始
源点:入度为0的顶点   汇点:出度为0的顶点
关键路径----路径长度最长的路径
路径长度----路径上各活动持续时间之和
ve(vj)----表示事件vj的最早发生时间
vl(vj)----表示事件vj的最迟发生时间
e(i)----表示活动ai的最早开始时间   l(i)----表示活动ai的最迟开始时间
l(i)-e(i)----表示完成活动ai的时间余量
关键活动----关键路径上的活动,即l(i)==e(i)(即l(i)-e(i)==0) 的活动
如何找到l(i) == e(i) 的关键路径?
    设活动ai用弧<j,k>表示,其持续时间记为:Wj,k
    则有:(1) e(i)==ve(j)           -> 直接查弧尾的最早发生时间
             (2) l(i) = vl(k) - Wj,k   -> 直接查弧头的最迟发生时间- 权值w
如何求ve(j) 和 vl(j) ?
事件j的最早发生时间?
(1) 从ve(1) = 0 开始向前递推
     ve(j) = Max{ve(i)+Wi,j},<i,j>->T, 2=<j<=n
     其中T是所有以j为头的弧的集合
事件i的最晚发生时间?
(2) 从vl(n)==ve(n) 开始向后向后递推
     vl(i)=Min{vl(j)-Wi,j}, <i,j>->S,1<=i<=n-1
     其中S是所有以i为尾的弧的集合
1.第一个是从源点往右算  2. 第二个是从汇点往左算 

生成树:所有定点均由边连接在一起, 但不存在回路的图
    - 一个图可以有许多棵不同的生成树
    - 所有生成树具有以下共同特点
1. 生成树的顶点个数与图的顶点个数相同
2. 生成树 是图的极小连通子图,去掉一条边则非联通
3. 一个有n个顶点的连通图的生成树有n-1 条边
4. 在生成树中再加一条边必然形成回路
5. 生成树中任意两个顶点间的路径是唯一的
最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那颗生成树称为该网的最小生成树,也叫最小代价生成树
已上两种定义简单来说就是:1.生成树就是连通但是内部没有环,有n-1条边.  2. 最小生成树就是权值最小的生成树
MST性质:
在生成树的构造过程中,图中n个顶点分属两个集合:
     - 已落在生成树上的顶点集:  U
     - 尚未落在生成树上的顶点集:  V-U
接下来则应在所有连通U中顶点和V-U中顶点的边种选取权值最小的边(作为生成树的一条边,前提是不能有回路)
Prim 算法
设N={V,E} 是连通网, TE是N上最小生成树中边的集合        {a,b} a 代表点, b代表边
1. 初始令U={u0},{u0->V},TE={ }
2. 在所有u->U, v->V-U的边(u,v)->E中,找一条代价最小的边(u0,v0). 注: 就是权值最小的边
3. 将(u0,v0) 这条边并入集合 TE, 同时 v0这个点并入U
4. 重复上述操作直至U=V为止, 则T=(V,TE) 为N 的最小生成树  (意思就是把所有的点都选进集合U, 所以前面写的是U=V)

通俗解释:在U集合中选一个点和在U-V集合中选一个点, 使得这个权值的最小, 然后把弧上的顶点加入到集合U中, 直到U=T集合,得到的连通图就是最小生成树

#include<stdio.h>
#define max 32767
typedef struct
{
	char *date;
	int **array;
	int nodenumber,edgenumber;
} Graph;
struct close   //保存节点的权值和字符
{
	char date;
	int wed;
};
int LocateNode(Graph L,char ch)
{
	for(int i=0; i<L.nodenumber; i++)
		if(L.date[i]==ch) return i;
	return -1;
}
void CreatUDG(Graph &L)
{
	scanf("%d %d",&L.nodenumber,&L.edgenumber);
	//输入图的节点数和边数
	getchar();
	L.date=new char[L.nodenumber];   //节点数组
	L.array=new int*[L.nodenumber];  //邻接矩阵
	for(int i=0; i<L.nodenumber; i++)
	{
		L.date[i]=getchar();
		getchar();
		L.array[i]=new int[L.nodenumber];
	}
	for(int i=0; i<L.nodenumber; i++)
		for(int j=0; j<L.nodenumber; j++)
			L.array[i][j]=max;
	for(int k=0; k<L.edgenumber; k++)
	{
		char ch1,ch2;
		int w;
		scanf("%c %c %d",&ch1,&ch2,&w);
		getchar();
		int i=LocateNode(L,ch1);
		int j=LocateNode(L,ch2);
		L.array[i][j]=w;
		L.array[j][i]=w;
	}
}
int min(struct close *clos,int n)
{
	int k;
	for(int i=1; i<n; i++)
		if(clos[k].wed==0||(clos[i].wed!=0&&clos[i].wed<clos[k].wed)) k=i;
	return k;
}
void Prim(Graph L,char v)
{
	struct close clos[L.nodenumber];
	int k=LocateNode(L,v);
	for(int i=0; i<L.nodenumber; i++)
	{
		clos[i].date=v; 	//类似于一种前驱节点的感觉
		clos[i].wed=L.array[k][i];
	}//起始节点与其他节点建立联系
	clos[k].wed=0; //将起始节点的权值设置为0,表示起始节点已经被选中
	for(int i=1; i<L.nodenumber; i++)
	{
		k=min(clos,L.nodenumber);
		char u0=clos[k].date;   //存储的是k的前驱节点
		char v0=L.date[k];      //索引k下的节点字符
		printf("(%c,%c)\n",u0,v0);
		clos[k].wed=0;          //将选中节点的权值设置为0,表示已经加入到最小生成树
		for(int j=0; j<L.nodenumber; j++)  //这个循环遍历图中的所有节点。变量 j 表示当前正在检查的节点的索引
			//如果当前被选中的节点(索引为k),到其他(未被选入最小生成树)节点的权值比原来更小,那么
			if(L.array[k][j]<clos[j].wed)  
			{
				clos[j].date=L.date[k];  //更新前驱
                // 更新 clos 数组中节点 j 的字符为当前选中节点的字符
				clos[j].wed=L.array[k][j]; //更新权值
				//更新 clos 数组中节点 j 的权值为当前选中节点与节点 j 之间的边的权重
			}
	}
}
int main(){
	Graph L;
	CreatUDG(L);
	Prim(L,L.date[0]);  //传到Prim函数集合U中起始节点U0
	return 0;
}

 Kruskal算法
1.设 连通网 N=(V,E), 令最小生成树初始化状态为只有n个顶点而无边的非联通图 T={V,{ }}, 每个顶点自成一个联通分量
2. 在E中选取代价最小的边, 若该边依附的顶点落在 T中不同的连通分量上(即: 不能成环), 则将此边加入到T中; 否则, 舍去此边, 选取下一条代价最小的边
3.依次类推, 直至T中所有顶点都在同一连通分量上为止
注:通俗来讲就是, 直接把所有顶点都先加入到生成树T中, 然后从所有的边中一次找最短, 次短的边....进行连通,(但是不能成环)

注:最小生成树可能有很多个,同时Kruskal算法在寻找最小边的时候, 可以使用小顶堆{priority_queue<int,vector<int>,greater<int>> pq}, s所以Kruskal算法的时间复杂度就是堆排序的时间复杂度
两种算法比较:
算法名         Prim算法                     Kruskal算法
算法思想      选择点                         选择边
时间复杂度   O(n^2)(n为顶点数)     O(eloge)(e为边数)
适应范围      (稠密图)(点多的)          稀疏图(边多的)

#include<algorithm>
#include<iostream>
using namesapce std;
#define MVNum 20    //定点数的最大值
#define MANum 20    //边数的最大值
typedef char VerTexType;
typedef int ArcType;
typedef struct{
	VerTexType vexs[MVNum];
	int vexnum,arcnum;
}AMGraph;
typedef struct{
	VerTexType Head; //边的起始点
	VerTexType Tail; //边的终止点
	ArcType lowcost; //边上的权值
}Edge; //边的类型
Edge edges[MANum] //存储边信息的辅助数组
//辅助数组 VexSet,记录每个顶点所在联通分量编号
int VexSet[MVNum];
//确定顶点v在G中位置, 即顶点数组的下标
int LocateVex(AMGraph G,VerTexType v){
	for(int i=0;i<G.vexnum;i++){
		if(v==G.vexs[i]){
			return i;
		}
	}
	return -1;
}
void CreateAMGraph(AMGraph &G){
	cin>>G.vexnum>>G.arcnum;
	for (int i = 0; i <G.vexnum; i+)
	{
		cin>>G.vexs[i];
	}
	VerTexType v1,v2;
	ArcType w;
	for(int k=0;k<G.arcnum;k++){
		cin>>v1>>v2>>w;
        edges[k].Head=v1;
        edges[k].Tail=v2;
        edges[k].lowcost=w;
	}
}
//按边上的权值Lowcost进行排序
bool cmp(Edge a,Edge b){
    if(a.lowcost<b.lowcost){
    	return true;
    }
}
void MiniSpanTree_Kruskal(AMGraph G){
	 //按边上的权值Lowcost进行排序
     sort(edges,edges+G.arcnum,cmp);
     //初始化,每个顶点都是一个独立的连通分量
     for(int i=0;i<G.vexnum;i++){
     	VexSet[i]=i;
     }
     //反复执行,获取G.vexnum-1边
     for(int i=0;i<G.arcnum;i++){
     	int v1=LocateVex(G.edges[i].Head); //边的起始顶点存储位置的序号,那个顶点    通俗讲:就是找顶点存储在数组的那个位置
     	int v2=LocateVex(G.edges[i].Tail); //边的终止顶点存储位置的序号,那个顶点
     	int vs1=VexSet[v1];  //边的起始顶点所在联通分量的编号
     	int vs1=VexSet[V2];  //边的终止顶点所在连通分量的编号
     	if(vs1!=vs2){
     		printf("(%c,%c)\n",edges[i].Head,edges[i].Tail);
     		//同时将起始顶点所在联通分量和终止顶点所在的连通分量进行(合并)统一编号,变为起始顶点所在连通分量
     		//经过多次统一编号之后, 每个连通分量中就会包含多个顶点
     		for(int j=0;j<G.vexnum;j++){
     			if(VexSet[j]==vs2){
     				VexSet[j]=vs1;
     			}
     		}
     	}
     }
}
int main(){
	AMGraph G;
	CreateAMGraph(G);
	MiniSpanTree_Kruskal(G)
    return 0;
}

 感谢大家的点赞和关注,你们的支持是我创作的动力!


网站公告

今日签到

点亮在社区的每一天
去签到