基础算法合集-图论

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

单源最短路径问题(用来计算一个点到其他所有顶点的最短路径)

Dijkstra(n*n)

  1. 初始化: 先找出从源点V0到各终点Vk的直达路径(V0,Vk), 即通过一条弧到达的路径

  2. 选择: 从这些路径中找出一条长度最短的路径(V0,u)

  3. 更新: 然后对其余各条路径进行适当的调整

若在图中存在弧(u,Vk), 且(Vo,u,Vk)<(Vo,Vk), 则以路径(Vo,u,Vk) 代替(Vo,Vk)

  1. 把V分成两组:

(1) S: 已求出最短路径的顶点的集合

(2) T=V-S: 尚未确定最短路径的顶点集合

算法步骤:

1.初始时令S={V0}, T={其余顶点}

2.T中顶点对应的距离值用辅助数组D存放

Di 初值: 若<V0,Vi>存在, 则为其权值; 否则为无穷 注: 从源点V0开始

3.从T中选取一个其距离值最小的顶点Vj, 加入S

4.对T中顶点的距离值进行修改: 若加进Vj 作中间顶点, 从V0到Vi的距离值比不加Vj的路径要短, 则修改此距离值

重复上述步骤, 直到S=V为止

代码实现:

辅助数组:

-- 数组bool Sn: 记录相应顶点是否已被确定最短距离

-- 数组Dn: 记录源点到相应顶点路径长度

-- 数组Pathn: 记录相应顶点的前驱顶点

  1. 从D(T集合中)找最小值v 2. 将最小值对应的顶点, 加入S 3.更新D数组, Path数组

注:找最小值的时候要从D数组中为false的点中找, 并且不能为源点

cpp 复制代码
#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, 有权值就存权值, 没有权值就存储无穷

cpp 复制代码
#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集合,得到的连通图就是最小生成树

cpp 复制代码
#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,{ }}, 每个顶点自成一个联通分量

  1. 在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为边数)

适应范围 (稠密图)(点多的) 稀疏图(边多的)

cpp 复制代码
#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;
}

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

相关推荐
阿正的梦工坊3 分钟前
【Rust】03-所有权、移动与复制
开发语言·算法·rust
一切皆是因缘际会13 分钟前
因果推理人工智能
大数据·数据结构·人工智能
好评笔记16 分钟前
深度学习面试八股—— GRU(Gated Recurrent Unit)
人工智能·rnn·深度学习·算法·机器学习·gru·校招
搞科研的小刘选手21 分钟前
【智能计算方向专题研讨会】第三届智能计算与数据分析国际学术会议(ICDA 2026)
大数据·算法·机器学习·数据挖掘·数据分析·可视化·计算
量化君也27 分钟前
桥水基金全天候策略拆解,构建中国ETF躺平版策略
大数据·人工智能·python·算法·金融·业界资讯
蓦然回首却已人去楼空35 分钟前
画图专用文档
算法
洛水水37 分钟前
【力扣100题】78.在排序数组中查找元素的第一个和最后一个位置
数据结构·算法·leetcode
江屿风39 分钟前
C++图论基础拓扑排序算法流食般投喂
开发语言·c++·笔记·算法·排序算法
海棠AI实验室1 小时前
AI 时代文献综述:从检索到成稿的 RAG 五步法
windows·算法·自动化·llm·rag
H178535090961 小时前
SolidWorks_基于草图的实体特征14_扫描扭转与控制
前端·人工智能·算法·3d建模·solidworks