关于图的知识点总结

图

图的定义

图是由一个顶点集V和一个弧集R构成的数据结构Graph=(V,R),而R={VR},其中VR=<v,w>(v称之为弧尾,w称之为弧头),由于弧是有方向的,因此由顶点集和弧集构成的图叫有向图
若<v,w>在弧集中一定有<w,v>在弧集中,则成v和w之间存在一条边,由顶点集和边集构成的图叫无向图
网:弧上带有权值的,称之为有向网,边上带有权值称之为无向网
如果图G=(V,{VR})和图G1=(V1,{VR}1),由G1的点集和边集都包含在G的点集和边集中,则称G1为G的子图
完全图:假设图中有n个顶点和e条边,则含有e=n(n-1)/2条边的无向图称之为完全图(任意两个顶点之间都有一条边),含有n(n-1) 条弧的有向图称之为有向完全图
稀疏图:若边或者弧的个数小于nlog2(n)nlog_2(n)nlog2​(n)则称之为稀疏图
若v和w直接存在一条边,则v和w互为邻接点。
对于无向图:和顶点v相关联 的边的个数称之为节点的度:表示TD(顶点)表示顶点的度
对于有向图:入度:以该节点作为弧头的弧个数称之为该节点的入度 OD(顶点)
初度:以该节点作为弧尾的弧的个数称之为该节点的出度
ID(顶点)

TD=ID+OD TD=ID+OD TD=ID+OD
从顶点u到顶点w的边的个数称为路径长度
简单路径:首尾顶点不相同,路径中不存在重复顶点的称之为简单路径
简单回路:指序列中第一个顶点和最后一个顶点相同的简单路径
连通图:图G中任意两个顶点之间都有路径相通,则称此图为连通图
连通分量:若无向图为非连通图,则图中的各个极大连通子图称之为此图的连通分量(就是把一块上面能连的都连在一起了)
强连通图:对于有向图,任意两个顶点之间都存在有向路径称之为有向图的强连通图
强连通分量:如果有向图为非强连通图,则各个强连通子图为其强连通分量
生成树:假设以后个n个顶点和e条边,其中n个顶点和n-1条边构成的连通子图,称此极小连通子图为生成树。

几种特殊的图及其相关性质

二部图

设无向图G=<V,E>G=<V,E>G=<V,E>,若能将VVV划分为V1V_1V1​V2V_2V2​,是的GGG中每条边的两个端点都其中一个属于V1V_1V1​,另外一个属于V2V_2V2​,则程GGG为二部图记为<V1,V2,E><V_1,V_2,E><V1​,V2​,E>,称V1V_1V1​V2V_2V2​为互补顶点集。如果V1V_1V1​中每个顶点又都和V2V_2V2​中每个顶点相邻则称之为完全二部图。

比较特殊的情况:n阶零图也是二部图

判断二部图的方法:当且仅当GGG中没有奇数圈的时候可以这样做

匹配:任意两条边都不相邻的边子集
极大匹配:添加任意一条边后都不再是匹配的匹配
最大匹配:变数最多的匹配
匹配数:最大匹配中的变数
极大匹配不一定是最大匹配,最大匹配一定是极大匹配
M匹配的饱和点:M中有边与v相关联
V1和V2中有一个里面每一点都是匹配,那么称之为完备匹配
M中的每一点都是饱和点,那么这个匹配就是完美匹配
Hall定理:设在二部图G=<V1,V2,E>,|V1|<=|V2|,G中存在完备匹配当且仅当V1中任意k个顶点至少和V2中k个顶点相邻

欧拉图和哈密顿图
欧拉哈密顿
回路经过所有边只有一次,回到原点经过所有点一次,回到原点
通路经过所有边一次,不回原点经过所有点一次,不回原点
图有回路有回路
半图有通路有通路
判断图无向图没有奇度项,有向图每个入度等于出度阶数n大于等于3,任意两个不相邻的顶点度数之和大于等于n
判断半图无向图只有两个奇度顶点,有向图有两个顶点初入度不相同,一个入比出多一,一个出比入多一无向图任意两个不相邻顶点的度数之和不小于n-1,其中n为阶数

图的存储和表示

邻接矩阵的存储表示
无向图

定义:矩阵的元素定义为Mij=1M_{ij}=1Mij​=1 ,(i,j)(i,j)(i,j)之间存在一条边,为0则不存在一条边
性质:
一旦完成之后矩阵的第i行代表与第i个元素相关的信息
(无向图的矩阵表示法是一个沿着主对角线的对称矩阵)
没一行上所有元素之和得到的就是这一行表示的顶点的度的数
矩阵中1的个数是边的个数的二倍(握手定理的体现)

有向图:

定义:第一个坐标代表(行数)代表弧尾(出发点),第二个坐标(列数)代表弧头(目标点)
性质:

  1. 其中1的个数等于图中弧的个数
  2. 列上之和带代表入度,行上之和代表出度
typedef struct 
{
	char vexs[MAXVEX];
	int arc[MAXVEX][MAXVEX];//邻接矩阵
	int numVertexes,numEdges;
}MGragh;
typedef struct 
{
	char vexs[MAXVEX];
	int arc[MAXVEX][MAXVEX];
	int numVertexes,numEdges;
}MGragh;
void CreateMGraph(MGragh &M)//创建一个无向图3俄
{
	int i,j,k,w;
	std::cout<<"请输入顶点数和边数"<<std::endl;
	std::cin>>M.numVertexes>>M.numEdges;
	for(int i=0;i<M.numVertexes;i++)
		{
			std::cin>>M.vexs[i];//输入每个节点的数据
		}
	for(int i=0;i<M.numVertexes;i++)
		for(int j=0;j<M.numVertexes;j++)
			{
				M.arc[i][j]=INFINITY;
			}
	for(k=0;k<M.numEdges;k++)
	{	//输入节点的
		cout<<"请输出边起始点的下标,和权重"<<endl;
		cin>>i>>j>>w;
		//这里生成对是无向图,因此最后得到的是一个对称矩阵
		M.arc[i][j]=w;
		M.arc[j][i]=w;
	}
}
邻接表存储
无向图:

每个节点两个部分,一部分节点的信息,另外一部分为一个指针,指针指向一个链表,指向链表中存储着这个节点的邻接节点

有向图:

每个节点两个部分,一部分节点的信息,另外一部分为一个指针,指针指向一个链表,指向链表中存储着以这个节点为弧尾的弧的信息)。
便于统计这个节点的出度,但是要是统计入度就需要遍历整个链表
另外一种方法:建立两个邻接表,一个邻接标一个逆邻接标,逆邻接标用于中的链表中跟着的是以这个节点作为弧头的信息。

typedef struct EdgeNode
{
	int adjvex;//存储该顶点的下标
	int weight;//权
	struct EdgeNode* next;//链表
}EdgeNode;

typedef struct VertexNode
{
	char data;
	EdgeNode *firstedge;
}VertexNode,AdjList[MAXVEX];
typedef struct
{
	AdjList adjlist;
	int numVertexes,numEdges;//图中的顶点数和边数
}GraphAdjList;
void CreateALGraph(GraphAdjList &G)
{
	int i,j,k;
	EdgeNode *e;
	cout<<"请输入顶点个数和边数\n";
	cin>>G.numVertexes>>G.numEdges;
	for(int i=0;i<G.numVertexes;i++)
	{	//输入每个节点的数据
		cin>>G.adjlist[i].data;
		G.adjlist[i].firstedge=NULL;
	}
	for(k=0;k<G.numVertexes;k++)
	{
		cout<<"请输入弧的起点的终点序号"<<endl;
		cin>>i>>j;
		//在临接表中创建
		e=(EdgeNode*)malloc(sizeof(EdgeNode));
		e->adjvex=j;
		e->next=G.adjlist[i].firstedge;
		G.adjlist[i].firstedge=e;
		//在逆临接表中创建
		e=(EdgeNode*)malloc(sizeof(EdgeNode));
		e->adjvex=i;
		e->next=G.adjlist[j].firstedge;
		G.adjlist[j].firstedge=e;
	}
}

图的遍历

深度优先遍历

连通图的深度优先遍历

从图中某个顶点v0出发,访问此顶点,然后依次从他的邻接顶点出发,访问所有和他连通的点
深度优先遍历不是唯一的
每次访问前先判断这个点是否已经被访问过(利用visitied[]数组,刚开始全部置为0,遍历后置为1),如果一个节点的所有指向的节点都已经被访问过,这时候回溯,看看前面一个访问的节点能不能通向某个没有被访问过的节点,如果有这样的节点就接着访问,如果没有就继续回溯。

大话数据结构代码:

bool visited[MAXVEX];
//对连通图的遍历
void DFS(GraphAdjList GL,int i)
{
	EdgeNode *p;
	visited[i]=true;
	cout<<GL.adjlist[i].data<<" ";
	while(p)
	{
		if(!visited[p->adjvex])//没有被访问过则从这里开始进行深度优先遍历
			DFS(GL, p->adjvex);
		//访问结束之后退回到原来的这一层,在其中寻找没有被访问过的节点
		p=p->next;
	}
}
//对非连通图进行遍历
void DFSTraverse(GraphAdjList GL)
{
	int i;
	for(int i=0;i<GL.numVertexes;i++)
	{//在进行遍历开始把所有的节点都设置为未访问过的节点
		visited[i]=false;
	}
	for(int i=0;i<GL.numVertexes;i++)
		if(!visited[i])
			DFS(GL,i);
}

课本做法,用来看指向的节点是否有邻接节点,如果没有节点,那么返回-1

int FirstAdjVex(ALGraph G,int v)
{
  ArcNode *p=G.vertices[v].firstarc;
  if(p)
    return p->next;
  else 
    return -1;
}
int NextAdj(AlGraph G,int v,int w)
{
  ArcNode *p=G.vertices[v].firstarc;
  if(p)
  {
    while(visited[p->next]/*没有被访问过*/&&p->next!=w/*可以通向其他地方*/)
      p=p->next;
    return p->next;
  }
  return -1;
}
非连通图的遍历

从每一个顶点出发,去深度优先遍历,然后跳到下一个顶点,找到一个没有被访问的,以这个顶点作为起始点,开始进行深度优先遍历,如果这个节点被遍历过了,那么跳到下一个顶点

对于有n个节点的连通图的深度优先遍历生成树含有n-1条边
把图没有在生成树上的边加上去,这些边称之为回边,可以用于判断回边

广度优先遍历

按照路径长度的由近及远进行遍历(类似于树的层序遍历)
那个邻接点被先遍历,他的邻接点要比其他的节点的邻接点先遍历,因此需要借助队列来辅助进行遍历

bool visited[MAXVEX];
void BFSTraverse(GraphAdjList GL)
{
	int i;
	EdgeNode *p;
	//把记录节点是否访问过的数组全部记为没有访问过
	for(int i=0;i<GL.numVertexes;i++)
		{
			visited[i]=false;
		}
	queue<int> Q;
	for(i=0;i<GL.numVertexes;i++)
	{
		if(!visited[i])
		{
			visited[i]=true;
			cout<<GL.adjlist[i].data;
			Q.push(i);
			while(!Q.empty())
			{
				i=Q.front();
				Q.pop();
				p=GL.adjlist[i].firstedge;
				while(p)//只要p不是空表,那么进来遍历吧
				{
					if(!visited[p->adjvex])//如果没有被访问过
					{
						visited[p->adjvex]=true;
						cout<<GL.adjlist[p->adjvex].data<<" ";
					}
					p=p->next;//指向下一个节点
				}
			}
		}
	}
}

连通网的最小生成树

在一个含有n个节点的图中挑选n-1条边,使之这个能连通。
在e条带权的边中选取n-1条边(不构成回路),使权值之和最小,称之为最小生成树

普里姆算法(重点掌握)

任意一个顶点作为树的根,以后往生成树上添加一个新的顶点w,两个之间的权值在所有与w相连的顶点之间权值最小,知道上面有n个顶点

#include <iostream>
#define MAXVEX 100
#define INFINITY 65535//用来代指∞
using namespace std;
typedef struct 
{
	char vexs[MAXVEX];
	int arc[MAXVEX][MAXVEX];
	int numVertexes,numEdges;
}MGragh;
void CreateMGraph(MGragh &M)//创建一个无向图3俄
{
	int i,j,k,w;
	std::cout<<"请输入顶点数和边数"<<std::endl;
	std::cin>>M.numVertexes>>M.numEdges;
	for(int i=0;i<M.numVertexes;i++)
		{
			std::cin>>M.vexs[i];//输入每个节点的数据
		}
	for(int i=0;i<M.numVertexes;i++)
		for(int j=0;j<M.numVertexes;j++)
			{
				M.arc[i][j]=INFINITY;
			}
	for(k=0;k<M.numEdges;k++)
		{	//输入节点的
			cout<<"请输出边起始点的下标,和权重"<<endl;
			cin>>i>>j>>w;
			M.arc[i][j]=w;
			M.arc[j][i]=w;
		}	
}
/*********基于邻接矩阵实现普里姆算法*****************/
void MiniSpanTree_Prim(MGragh G)
{
	int min,i,j,k;
	int adjvex[MAXVEX];//用于保存相关顶点下标
	int lowcost[MAXVEX];//保存相关顶点间的权值
	lowcost[0]=0;//把第一个值设置为0,也就是从第一个顶点开始生成最小生成树
	adjvex[0]=0;//初始化第一个下标为0
	
	
/*******************进行初始化工作******************/
	for(i=1;i<G.numVertexes;i++)
	{
		lowcost[i]=G.arc[0][i];//保存和0节点想邻接的所有节点的值
		adjvex[i]=0;//初始化都为v0的下标,先放在这里,我认为目前这里表示这里是和0节点相连
	}
	
	
/****************构造最小生成树*****************/
	for(i=1;i<G.numVertexes;i++)//对于每一个节点进行搜索
	{
		min=INFINITY;//把最小值初始化为无穷
		j=1;//用来循环所有节点的下标
		k=0;//用来记录和当前节点路径最短的点之间的距离
		while(j<G.numVertexes)//循环所有节点,找到最小值
		{
			if(lowcost[j]!=0&&lowcost[j]<min)//如果这个没有加入最小生成树,并且这个这个节点的权值比现在的最小值要小
			{
				min=lowcost[j];//改变最小值
				k=j;//改变将要加入最小生成树的节点位置
			}
			j++;
		}
		cout<<adjvex[k]<<","<<k<<endl;//打印当前边中权值最小的边
		lowcost[k]=0;//将当前点权值设置为0,表示这个点的任务已经完成
		for(j=1;j<G.numVertexes;j++)//循环所有顶点,也就是查找和k相连的所有边的权值
		{
			if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j])
			{//若下标为k顶点个遍权值小于此前这些顶点,未被加入生成树的权值
			//!!!!突然明白书上那张表的意思了!!!!和上面的这个进行比较,如果比上面对应的边要小,那么铁定在这一次里面就一定不会用到这条边了!!!!
				lowcost[j]=G.arc[k][j];
				adjvex[j]=k;
			}
		}
	}
}


int main() {
	MGragh G;
	CreateMGraph(G);
	MiniSpanTree_Prim(G);
	return 0;
}

克鲁斯卡尔算法

选取其中权值最小的边加入(贪心算法的思想)并且每次加入时确认目前的生成树当中没有形成回路

#include <iostream>
#include<algorithm>
#define MAXVEX 100
#define INFINITY 65535//用来代指∞
#define MAXEDGE (100*99/2)
using namespace std;
typedef struct
{
	int begin;
	int end;
	int weight;
}Edge;
typedef struct 
{
	char vexs[MAXVEX];
	int arc[MAXVEX][MAXVEX];
	int numVertexes,numEdges;
}MGragh;
void CreateMGraph(MGragh &M)//创建一个无向图3俄
{
	int i,j,k,w;
	std::cout<<"请输入顶点数和边数"<<std::endl;
	std::cin>>M.numVertexes>>M.numEdges;
	for(int i=0;i<M.numVertexes;i++)
		{
			std::cin>>M.vexs[i];//输入每个节点的数据
		}
	for(int i=0;i<M.numVertexes;i++)
		for(int j=0;j<M.numVertexes;j++)
			{
				M.arc[i][j]=INFINITY;
			}
	for(k=0;k<M.numEdges;k++)
		{	//输入节点的
			cout<<"请输出边起始点的下标,和权重"<<endl;
			cin>>i>>j>>w;
			M.arc[i][j]=w;
			M.arc[j][i]=w;
		}
	
}
int Find(int *parent,int f)
{
	while(parent[f]>0)
		{
			f=parent[f];
		}
	return f;
}
void MiniSpanTree_Kruskal(MGragh G)
{
	int i,n,m;
	Edge edges[MAXEDGE];
	int parent[MAXVEX];//这个数组用来判断是否形成了回路
	//创建边节点数组,并将其按照权值的从小到大进行排序
	int k=0;
	for(int i=0;i<G.numVertexes;i++)
		for(int j=i+1;j<G.numVertexes;j++)
	{
		if(G.arc[i][j]!=INFINITY)
		{
			edges[k].begin=i;
			edges[k].end=j;
			edges[k].weight=G.arc[i][j];
			k++;
		}
	}
	for(int i=0;i<k;i++)
		for(int j=i+1;j<k;j++)
	{
		if(edges[j].weight>edges[j+1].weight)
		{
			Edge t;
			t=edges[j];
			edges[j]=edges[j+1];
			edges[j+1]=t;
		}
	}
	for(i=0;i<G.numVertexes;i++)
	{
		parent[i]=0;
	}
	for(i=0;i<G.numVertexes;i++)//循环每一条边
	{
		n=Find(parent,edges[i].begin);
		m=Find(parent, edges[i].end);
		if(n!=m)//不想等的情况下说明没有生成环路
		{
			parent[n]=m;//将节点的下标放入其中表示这个节点已经加入了最小生成树
			cout<<"("<<edges[i].begin<<","<<edges[i].end<<")"<<" "<<edges[i].weight<<endl;
		}
	}
}

关节点和连通分量

关节点的定义:如果去掉某一节点和它相关联的边,将一个图的连通分量分割为两个或者两个以上的连通分量,则这个点称之为关节点
重连通图:没有一个关节点的图称之为重连通图

由深度优先生成树得到的关节点的特性:

  1. 若生成树的根有两个或者两个以上的分量,那这个根节点一定是关节点
  2. 若某个非叶子节点,其某棵子树的其他节点均没有指向祖先节点的回边,则v为关节点

补充深度优先生成树:
深度优先生成树的画法就是按照节点在深度优先的过程当做某个树的先序遍历的过程画出来的树(该向下时向下,该回溯时要回溯),在画的时候记得把图中存在但是生成树当中没有用上的边补上。

拓扑排序

方法:

  1. 选择有向图中一个没有前驱的顶点并输出

  2. 删除和该节点相关的节点

  3. 重复上面两步骤

代码

void TopSort(MGragh M)
{
    int i,j,k,w;
    int count[MAXVEX];
    for(i=0;i<MAXVEX;i++)
    {
        count[i]=0;
    }
    for(i=0;i<M.numVertexes;i++)
    {
        for(j=0;j<M.numVertexes;j++)
        {
            if(M.arc[i][j]!=0)
            {
                count[i]++;
            }
        }
    }
    for(i=0;i<M.numVertexes;i++)
    {
        if(count[i]==0)
        {
            cout<<M.vexs[i]<<" ";
        }
    }
}

关键路径

关键活动:最早开始时间和最迟开始时间相同的称之为关键活动
关键活动组成的路径称为关键路径
从前向后求出最早开始时间(遇到交叉时选大的)
从后向前求最晚开始时间(遇到交叉时选小的)
顶点的最迟发生时间和最早发生时间中:顶点的最早发生时间就是以这个节点作为弧尾的弧的最早发生时间
顶点的最迟发生时间时:没有交叉时就是以该顶点为弧尾最迟发生时间(交叉的情况下就选小的那个)

最短路径

从某个源点出发到其他节点的最短路径

依照最短路径长度递增的次序来求得最短路径

狄杰斯特拉算法

  1. 在从原点出发的弧中选取权值最小的弧,即为第一条最短路径,其中最小值即为最短路径的长度
  2. 修改其它各顶点的Dijst[k]值,假设求的最短路径的顶点为u,若Dijst[u]=G
#include <iostream>
#define MAXVEX 9
#define INFINITY 65535
typedef int  Pathmatrix[MAXVEX];//用于存储最短路径下标的数组
typedef int ShortPathTable[MAXVEX];//用于存储到个点最短路径的权值和
using namespace std;
/*******************先来创建一波邻接矩阵表示的图*********************/
typedef struct 
{
	char vexs[MAXVEX];
	int arc[MAXVEX][MAXVEX];
	int numVertexes,numEdges;
}MGragh;
void CreateMGraph(MGragh &M)//创建一个无向图3俄
{
	int i,j,k,w;
	std::cout<<"请输入顶点数和边数"<<std::endl;
	std::cin>>M.numVertexes>>M.numEdges;
	for(int i=0;i<M.numVertexes;i++)
		{
			std::cin>>M.vexs[i];//输入每个节点的数据
		}
	for(int i=0;i<M.numVertexes;i++)
		for(int j=0;j<M.numVertexes;j++)
			{
				M.arc[i][j]=INFINITY;
			}
	for(k=0;k<M.numEdges;k++)
		{	//输入节点的
			cout<<"请依次输出边起始点的下标,和权重"<<endl;
			cin>>i>>j>>w;
			M.arc[i][j]=w;
			M.arc[j][i]=w;
		}
}
/*****************************************************/

//狄杰斯特拉算法:求有向图G的顶点v0顶点到其余顶点v最短路径p[v]及带权长度D[v]
//p[v]的值为前驱顶点到下标,D[v]表示v0到v的最短路径长度之和
void ShortestPath_Dijkstra(MGragh G,int v0,Pathmatrix *p,ShortPathTable *D)
{
	int v,w,k,min;
	int final[MAXVEX];//final[w]=1表示求出v0到vw的最短路径
	for(v=0;v<G.numVertexes;v++)
	{
		if(v==v0)
			continue;
		final[v]=0;//所有的路径都初始化为未知路径的状态
		(*D)[v]=G.arc[v0][v];//将与v0相连顶点加上权值
		(*p)[v]=0;//初始化路径数组为0
	}
	(*D)[v0]=0;//自己到自己的距离设置为0
	final[v0]=1;//不需要求路径,因此直接设置为已经知道路径的状态
/********************开始主循环,也就是在这里开始求出到每个顶点的最短路径****************/
	for(v=1;v<G.numVertexes;v++)
	{
		min=INFINITY;//先把最短距离设置为无穷大,过一会再进行更新运算
		for(w=0;w<G.numVertexes;w++)
		{
			if(!final[w]&&(*D)[w]<min)//寻找离v0最接近的顶点
			{
				k=w;//k是用来记录取到最短路径的长度节点的下标
				min=(*D)[w];//去更新最小值
			}
		}
		final[k]=1;//将离他最近的点设置为已经知道路径并且进行标记
		for(w=0;w<G.numVertexes;w++)//对于当前的最短路径进行修正
		{
			//如果经过v顶点的路径比现在这条路径的长度短的话
			if(!final[w]&&(min+G.arc[k][w]<(*D)[w]))//说明找到了更短的路径
			{
				//找到与当前点进行连接的其它点之间的路径进行比较,如果比较小,那么就换掉
				(*D)[w]-=min+G.arc[k][w];
				(*p)[w]=k;
			}
		}
	}
}

两点之间最短路径问题

从某个原点到其它各点到最短路径
依照弗洛伊德算法,弗洛伊德算法的基本思想:从vi到vj的所有可能路径之间选出一条最短路径

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐