1.图的基本概念
2.图的存储及基本操作
邻接矩阵
邻接表
十字链表、邻接多重表
图的基本操作
图的存储结构及相关算法
邻接矩阵适合存储稠密图、邻接表适合存储稀疏图
3.图的相关算法
3.1最小生成树(prim和kruskal)
prim
最小生成树针对的是一个连通图,当图中边各不相同时或者有相同的边但是这些边都被加入到生成树中时,此刻最小生成树是唯一的,其余时候不好说
---------------------------------------------------------------
prim算法思路:从图中任意取一个顶点把它当成一棵树,然后从与这棵树相接的边中选取一条最短边,并将这条边及其所连接的顶点也并入这棵树中,得到一棵具有两个顶点的树,以此类推知道所有顶点被并入到一棵树中,此算法是针对图中顶点进行操作的
存储结构:邻接矩阵
所需要的工具:lowcost数组:记录某个节点到最小生成树的权值
set数组:记录某个节点是否已经被添加至最小生成树中去
时间复杂度:O(n^2)
算法通过顶点处理实现,所以适用于稠密图求最小生成树
void prim(MGraph *g,int v)
{
int lowcost[maxsize],set[maxsize];//lowcost数组记录当前生成树距某一顶点的最小值,set数组记录已被并入生成树中的顶点
int i,j=0,k,min=0,mini,sum=0;//sum用来累加权值,mini用来记录下一次并入生成树的顶点的编号,min用来记录下一条并入生成树的边的权值
for(i=0;i<g->n;i++)//对两个数组初始化
{
lowcost[i]=g->edges[v][i];
set[i]=0;
}
set[v]=1;
while(j<g->n-1)//以及并入一个顶点,还要并入n-1个顶点,循环n-1次
{
min=INF;//INF是一个宏常量
for(k=0;k<g->n;k++)//寻找剩余未并入顶点中里当前生成树最短的边
{
if(min>lowcost[k]&&set[k]==0)
{
min=lowcost[k];
mini=k;
}
}
set[mini]=1;
sum+=min;//累加选出的最小边的权值
for(k=0;k<g->n;k++)//更新lowcost数组,因为可能由于新并入了一个顶点,使得树"变大了"一些,可能导致那些未并入的顶点以与这个顶点邻接的方式到达这棵树的距离更近
{
if(set[k]==0&&lowcost[k]>g->edges[mini][k])//极容易易错点
{
lowcost[k]=g->edges[mini][k];
}
}
j++;//不要忘记自增,否则就用for循环实现
}
}
kruskal算法
kruskal算法思路:每次找出候选边中权值最小的边,将该边并入到生成树中,重复此步骤直到所有边被检测完毕,算法中需要用到并查集相关知识(类似于树的双亲存储结构)
存储结构:邻接矩阵
所需要的工具:一个包含图中各边信息的Road结构型数组
getroot函数辅助求出某个顶点的双亲
某种排序手段
并查集:用于存储每个顶点的双亲
时间复杂度:取决于排序算法的选择,一般大于常量级
算法通过边的处理实现,所以适用于稀疏图求最小生成树
typedef struct//一个记录边的信息(由哪两个顶点组成,权值多少)的结构体类型
{
int a,b;
int dis;
}Road;
Road road[maxsize];//边数组
int shuangqing[maxsize];//存储每个顶点双亲的数组
int getroot(int v)//用以找某个顶点的根节点
{
while(v!=shuangqing[v])
{
v=shuangqing[v];
}
return v;
}
void kruskal(MGraph &g)
{
void sort(int *)//对边按照权值排序,排序的方法直接关乎
//kruckal算法的时间复杂度
int i,j,a,b,sum=0;//a,b用来接收构成某条边的两个顶点的根是谁
for(i=0;i<g->n;i++)//开始时将每个节点当成单独的一棵树
//双亲就是他自己
{
shuangqing[i]=i;
}
sort(road);
for(j=0;j<g->e;j++)
{
a=getroot(road[j].a,shuangqing);//分别获取构成当前边的两个顶点的根是谁,若根相同证明可以并入,否则证明二者以及被并入同一棵树下
b=getroot(road[j].b,shuangqing);
if(a!=b)
{
shuangqing[a]=b;//合并这两棵树为一棵(也就是让一棵树的根结点成为另一棵树的孩子)
sum=sum+road[j].dis;//累加权值
}
}
}
总结:prim和kruskal针对的都是无向图
3.2最短路径(迪杰斯特拉和弗洛伊德)
迪杰斯特拉算法
迪杰斯特拉算法(单源最短路径)
算法思想:设有两个顶点集合S和T,S中存放已经找到最短路径的顶点,T中存放图中剩余顶点,初始状态时S中只包括源点V0,然后不断的从集合T中选取到顶点V0路径长度最短的顶点Vu并入S中,每并入一个新的顶点Vu,都要修改顶点V0到集合T中顶点的最短路径长度值,直到T中顶点被全部并入S中去
存储结构:邻接矩阵
所需工具:dist一维数组:存储其余顶点到v的最短距离
set一维数组:某节点是否已经找到了到达v的最短路径
path数组:记录某节点的最短路径序列
print函数打印最短路径
时间复杂度:O(n^2)
void dijiestla(MGraph *g,int v)//主要算法
{
int dist[maxsize],set[maxsize];//dist数组用于记录剩余未被并入顶点距离顶点v的最短路径长度,set数组用于标记已被并入的顶点
int path[maxsize];//path数组用于记录从v到i需要经过的中间顶点
for(i=0;i<g->n;i++)
{
dist[i]=g->edges[v][i];
set[i]=0;
if(dist[i]<TNF)//TNF是一个宏常量,代表一个比图中任意边长度还长的值
{
path[i]=v;
}
else
{
path[i]=-1;
}
}
path[v]=-1;
set[v]=1;
//初始化结束,关键操作开始
for(i=0;i<g->n-1;i++)
{
min=INF;
if(dist[i]<min&&set[i]==0)
{
mini=i;//mini用于记录下一个并入S中的顶点的编号
min=dist[i];
}
set[mini]=1;
for(j=0;j<g->n;j++)
{
if(set[j]==0&&dist[mini]+g->edges[mini][j]<dist[j])//易错点
{
dist[j]=dist[minj]+g->edges[minj][j];
path[j]=minj;
}
}
}
}
void print(int path[],int v)//输出从源点到v的最短路径上经过的顶点
{
int stack[maxsize],top=-1;
while(path[v]!=-1)
{
stack[++top]=v;
v=path[v];
}
stack[++top]=v;//不要忘记把源点也入栈了
while(top!=-1)
{
printf("%d ",stack[top--]);
}
}
大致流程:开始时只有v在集合S中,我们求的是其余各顶点到它的最短路径,那么刚开始的dist数组就被初始化为邻接矩阵中每个点到它的距离,我们想要找到下一个到v距离最短的顶点,那么肯定在与它邻接的顶点当中啊,于是我们找到了,将它放入集合S,想要在找下一个到v距离最短的顶点,但此时原来的dist数组未必还能继续使用,原因则是,比如在刚开始时dist[4]=∞,而因为有了一个新的顶点加入,可能会导致4顶点到v顶点的距离不再是∞了,我们可以先走到新顶点,再从新顶点走到4,所以接下来要干的就是在找到下一个并入S集合中的顶点后,更新dist数组为找寻下一个顶点做准备,当然dist数组中某个值可以被更新的前提是T中某个顶点经过新并入的顶点在到达v的距离值要小于它以前的方式到达v的距离值,然后,如果某个dist值被更新了,因顺势为其path数组附上值,意思就是,某个顶点到达v的最短路径中要经过"它"
print函数的解释:迪杰斯特拉算法结束后,path数组中保存的其实就是一棵以双亲存储的树,通过此树可以输出源点到任何顶点的最短路径上经过的顶点,但是path数组它是以一种告诉你想要找的顶点的最短路径上前一个顶点是谁的方式存储的(相当于倒着告诉你这条路径怎么走),所以用栈在将此路径倒过来输出就是了
弗洛伊德算法
弗洛伊德算法(多源最短路径)
算法思想:将任意两顶点之间的最短距离通过不断的以某个顶点作为中间点的方式筛选出来
存储结构:邻接矩阵
所需工具:A二维数组:存储任意两顶点间的最短距离
path二维数组:存储两顶点最短路径的序列
时间复杂度:O(n^3)
void floyd(MGraph*g)
{
int A[maxsize][maxsize],path[maxsize][maxsize];
int i,j,k;
for(i=0;i<g->n;i++)
{
for(j=0;j<g->n;j++)
{
A[i][j]=g->edges[i][j];
path[i][j]=-1
}
}
for(i=0;i<g->n;i++)
{
for(j=0;j<g->n;j++)
{
for(k=0;k<g->n;k++)
{
if(A[j][k]>A[j][i]+A[i][k])//若满足则证明从j到k的路径中间经过i是较好的选择
{
A[j][k]=A[j][i]+A[i][k];
path[j][k]=i;
}
}
}
}
}
void print(int path[][],int v1,int v2,int A[][])//打印任意两点间路径
{
if(A[v1][v2]==INF)
{
提示两点间无路径;
}
else
{
if(path[v1][v2]==-1)
{
直接输出路径v1→v2
}
else
{
mid=path[v1][v2];
print(path,v1,mid,A);//处理前半段路径
print(path,mid,v2,A);//处理后半段路径
}
}
对弗洛伊德算法的理解:任意两点i和j之间的最短距离如果在思考后可以发现,它是一段路径,这段路径有好多节,比如1到5的最短路径为 1-3-4-5,那么我们是不是应该用 1-3之间的最短路径+ 3-4之间的最短路径+ 4-5之间的最短路径,而弗洛伊德算法就是用来把这一节节最短路径求出来最终相加得到的当然是目标两点的最短路径
3.3拓扑排序(AOV网)
AOV网:一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的,但有些子工程没有先决条件,可以安排在任何时间开始。为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网
拓扑排序:就是将上述AOV网中表达出来的活动的先后次序表达出来,拓扑排序成功的条件是有向无环图,拓扑排序结束后图中无入度为0的顶点
算法思想:1)从有向无环图中选择一个入度为0的顶点输出
2)出栈1)中顶点,并删除从该顶点出发的所有边(将其邻接顶点入度-1)
3)重复上两步直至图为空或者不存在入度为0的顶点,后一种情况说明有向图中必然存在环
时间复杂度:O(n+e)
typedef struct
{
char data;
int count;//记录顶点的入度
Arcnode *firstarc;
}VNode;
void Topsort(AGraph *G)
{
int stack[maxsize],top=-1,i,j,k,n;
Arcnode *p=NULL;
for(i=0;i<G->n;i++)
{
if(G->adjlist[i].count==0)//将所有入度为0的顶点入栈
{
stack[++top]=i;
}
}
while(top!=-1)
{
j=stack[top--];
++n;//记录拓扑排序输出的顶点数,作为能否拓扑排序成功的检测条件
printf("%d ",j);
p=G->adjlist[j].firstarc;
while(p!=NULL)
{
k=p->adjvex;
--(G->adjlist[k].count);
if(G->adjlist[k].count==0)
{
stack[++top]=k;
}
p=p->next;
}
}
if(n==G->n)
{
return 1;
}
else
{
return 0;
}
}
---------------------------------------------
逆拓扑排序,其实就是拓扑排序序列倒过来,拓扑排序每次入栈一个入度为0的顶点,逆拓扑排序则每次入栈一个出度为0的顶点,拓扑排序用到的是邻接表,逆拓扑排序则用逆邻接表存储,因为每次出栈顶点后需要将其余顶点的出度-1
特别注意:当图是有向无环图时,还可以采用DFS进行拓扑排序,因为从某一点开始进行DFS算法时,最先退出算法的顶点即为出度为0的顶点(退出算法指的是退出系统栈,证明此顶点的邻接点已被处理完(出度为0)),而拓扑排序序列中的最后一个顶点即为出度为0的顶点,因此按照DFS出栈顺序记录下的序列为逆拓扑序列
3.4关键路径(AOE网)
代码
/*
ele:活动最早时间
lte:活动最晚时间
elv:事件最早时间
ltv:事件最晚时间
*/
#include<stdio.h>
#include<stdlib.h>
#define maxsize 20
typedef struct node
{
int dex;
int power;//储存边的权值
struct node *next;
}Arcnode;
typedef struct Vnode
{
int data;
int count;//记录结点入度,结构体定义处赋值无效,不能写成int count=0,不会默认为0的
Arcnode *firstarc;
}Vnode;
typedef struct
{
Vnode adglist[maxsize];
int n, e;
}AGraph;
int top = -1;//top需设置为全局变量,因为它会在拓扑排序函数和关键路径函数都用到
int main()
{
int TopSort(AGraph *, int[], int *);//拓扑排序函数,此函数目的是完成事件最早发生时间以及逆拓扑排序的存储
void KeyPath(AGraph *, int *,int * ,int []);//关键路径核心算法代码
void CreatePicture(AGraph *&);//创建AOE网的函数
AGraph *G = NULL;
CreatePicture(G);
int stack[maxsize];
int *etv = (int *)malloc(sizeof(int)*G->n);//事件早发生时间数组
int *ltv = (int *)malloc(sizeof(int)*G->n);//事件最迟发生时间数组
KeyPath(G, etv,ltv,stack);
}
int TopSort(AGraph * G, int stack[], int *etv)
{
int stack1[maxsize],top1=-1;//用于存储拓扑排序
int s,k,count=0;
Arcnode *e;
for (int i = 0; i < G->n; i++)//首先将所有入度为0的结点入栈
{
if (G->adglist[i].count == 0)
stack1[++top1] = i;
}
for (int j = 0; j < G->n; j++)//初始化事件最早数组,因为后续计算其它事件最早发生时间时需要基于前一个事件的最早发生时间计算
etv[j] = 0;
while (top1!=-1)//拓扑排序的常规代码入口
{
s = stack1[top1--];
count++;//标志着拓扑排序是否成功的标志
stack[++top] = s;//拓扑排序算法的改进处,主要是为一会儿在KeyPath计算时间最迟发生时间时用,用于存储逆拓扑序列
for (e = G->adglist[s].firstarc; e; e = e->next)//依次处理当前结点的各个邻接点
{
k = e->dex;
if (!(--G->adglist[k].count))//首先先看它入度-1后能否加入拓扑排序的栈中
stack1[++top1] = k;
if (etv[s] + e->power > etv[k])//另一个改进处,同时在这里算得每一个邻接结点(事件)的最早发生时间,50和51行为次行服务
etv[k] = etv[s] + e->power;//就是刚出栈的结点(事件)的最早发生时间+当前结点的权值(也就是他们两之间那条线上的活动时间)
//这一句体现了算事件最早发生事件中要选大的那个值的思想,仔细体会
//记住:此时处理的所有结点均是s的邻接点,所以当前所有结点的etv都是基于s的etv算的
}
}
if (count == G->n)//返回是否拓扑排序成功
return 1;
else
return 0;
}
void KeyPath(AGraph *G, int *etv,int *ltv, int stack[])
{
int ee,le;//活动最早时间和活动最晚时间
int s,k,o;
Arcnode *p = NULL,*q=NULL;
if (TopSort(G, stack, etv))//若条件成立,则此时已经拥有了etv与逆拓扑排序序列
{
for (int i = 0; i < G->n; i++)//初始化事件最晚数组,因为事件最晚事件是从后往前算的,并且一会儿处理的第一个结点也确实是逆拓扑第一个结点(拓扑的最后一个节点)
ltv[i] = etv[G->n - 1]; //最后一个事件的etv=ltv
while (top!=-1)
{
s = stack[top--];
for (p = G->adglist[s].firstarc; p; p = p->next)//从后向前算事件最晚发生事件
{
k = p->dex;
if (ltv[k] - p->power < ltv[s])//这一句体现了算事件最晚发生事件中要选小的那个值的思想,仔细体会
ltv[s] = ltv[k] - p->power;
}
}
for (int m = 0; m < G->n; m++)
{
q = G->adglist[m].firstarc;
while (q)
{
o = q->dex;
ee = etv[m];
le = ltv[o] - q->power;
if (ee == le)
{
printf("%d→%d length=%d\n", m, o, q->power);
}
q = q->next;
}
}
}
}
void CreatePicture(AGraph * &G)
{
int n, e;
int start, fina, powwerr;//一条边的起始点以及权值
G = (AGraph *)malloc(sizeof(AGraph));
printf("请输入要创建的图的顶点数、边数:\n");
scanf("%d%d", &n, &e);
for (int i = 0; i < n; i++)//不要忘记,当初写的时候因为没写这一句折磨两小时
{
G->adglist[i].count = 0;
}
G->n = n;
G->e = e;
for (int i = 0; i < n; i++)
G->adglist[i].firstarc = NULL;
printf("请输入边及权值:\n");
for (int i = 0; i < e; i++)
{
Arcnode *newnode = (Arcnode *)malloc(sizeof(Arcnode));
scanf("%d%d%d", &start, &fina, &powwerr);
G->adglist[fina].count++;//不要忘记
newnode->dex = fina;
newnode->power = powwerr;
newnode->next = G->adglist[start].firstarc;
G->adglist[start].firstarc = newnode;
}
}
//stack1与stack的思想:我们将拓扑排序算法改进了,使得它既能整出来拓扑排序序列也可以用出栈一个结点时再将它入栈到stack
//中的方法最终将逆拓扑排序序列保存在stack中(stack栈顶结点为逆拓扑第一个结点)
1.连通分量:连通图的极大连通子图称为连通分量
2.强连通分量:强连通图的极大强连通子图称为强连通分量
3.极大连通子图对应于连通分量,极大强连通子图对应于强连通分量
4.无向图的极小连通子图对应于其生成树,没有有向图的极小强连通子图之说
5.连通图的极大连通子图为其本身;非连通图的极大连通子图有多个
6.强连通图的极大强连通子图为强连通分量,非强连通图的极大非强连通子图叫做非强连通分量
DFS空间与时间复杂度
空间复杂度:主要是递归工作栈所耗费为O(V),V是顶点数
时间复杂度:主要是对顶点及顶点的邻接边的访问的时间总和,若采用邻接表则为O(V+E),采用邻接矩阵则为O(V*2),注意:对DFS及BFS的时间复杂度分析不同于普通算法对时间复杂度的分析,不能用考虑最内层操作的执行次数来确定其时间复杂度,只需牢记:DFS和BFS都需要对所有顶点及边进行一次遍历即可
BFS时间与空间复杂度
空间复杂度:主要是额外的队列所耗费的空间为O(V),V是顶点数
时间复杂度:同DFS