1.定义
图由顶点(Vertex)和边(Edge)组成,每条边的两端都必须是图的两个顶点,记号G(V,E)表示图G的顶点集为V,边集为E。
图可以分为有向图和无向图,有向图的所有边都有方向,即确定了顶点到顶点的一个指向;无向图的所有边都是双向的,即无向边所连接的两个顶点可以互相到达。
顶点的度是指和该顶点相连的边的条数,对有向图来说,顶点的出边条数称为该顶点的出度,顶点的入边条数称为该顶点的入度。
顶点和边都可以有一定属性,而量化的属性称为权值,顶点的权值和边的权值分别称为点权和边权。
2.存储
图的存储方式常用的有两种:邻接矩阵和邻接表。
1.邻接矩阵
设图G(V,E)的顶点标号为0,1,…N-1,那么可以令二维数组G[N]
[N]的两维分别表示图的顶点标号,即如果G[i][i]为1,则说明顶点i和顶点j之间有边;如果G[i][j]为0,则说明顶点i和顶点j之间不存在边,而这个二维数组G[][]则被称为邻接矩阵。如果存在边权,则可以令G[i][j]存放边权,对不存在的边可以设边权为0、-1或者一个很大的数。无向图与对应的邻接矩阵如下图所示。
2.邻接表
设图G(V,E)的顶点编号为0,1,…N-1,每个顶点都可能有若干条出边,如果把同一个顶点的所有出边放在一个列表中,那么N个顶点就会有N个列表(没有出边,则对应空表)。这N个列表被称为图G的邻接表,记为Adj[N],其中Adj[N]存放顶点i的所有出边组成的列表,这样Adj[0],Adj[1],…,Adj[N-1]就分别都是一个列表。由于列表可以用链表实现。可以使用vector来实现链表,vector有变长数组之称,因此可以开一个vector数组Adj[N],其中N为顶点个数,这样每个Adj[i]就都是一个变长数组vector,使得存储空间只与图的边数有关。
如果邻接表只存放每条边的终点编号,而不存放边权,则vector中的元素可以直接定义为int型,如下所示:
vector<int> Adj[N];
如果需要同时存放边的终点编号后和边权,那么可以建立结构体Node,用来存放每条边的终点编号和边权,代码如下:
struct Node{
int v; //边的终点编号
int w; //边权
}
//这样vector邻接表中的元素类型就是Node型的
vector<Node> Adj[N];
图的遍历是指对图的所有顶点按一定顺序进行访问,遍历方法一般有两种:深度优先搜索(DFS)和广度优先搜索(BFS)。
3.图的深度优先搜索
深度优先搜索以“深度”作为第一个关键词,每次都是沿着路径到不能再前进时才退回到最近的岔道口,以一个有向图为例,深度优先遍历搜索路径为:V0、V1、V3、V4、V5、V2。
DFS具体实现需要理解的概念:
(1)连通分量:在无向图中,如果两个顶点之间可以互相到达(可以是通过一定路径间接到达),那么称为这两个顶点连通。如果图G(V,E)的任意两个顶点都连通,则称图G为连通图;否则称图G为非连通图,且称其中的极大连通子图为连通分量。
(2)强连通分量:在有向图中,如果两个顶点可以各自通过一条有向路径到达另一个顶点,就称这两个顶点强连通。如果图G(V,E)的任意两个顶点都强连通,则称图G为强联通图;否则称图G为非强连通图,且称其中的极大强连通子图为强连通分量。
连通分量和强连通分量均称为连通块。
DFS遍历图的基本思路就是将经过的顶点设置为已访问,在下次递归碰到这个顶点就不再去处理,直到整个图的顶点都标记为已访问。如果已知给定的图是一个连通图,则只需要一次DFS就能完成遍历。
伪代码如下:
DFS(u){ //访问顶点
vis[u] = true; //设置u已被访问
for(从u出发能到达的所有顶点v) //枚举从u出发可以到达的所有顶点v
{
if(vis[v] == false){ //如果v未被访问
DFS(v); //递归访问v
}
}
}
DFSTrave(G){ //遍历图G
for(G的所有顶点u){ //对G的所有顶点u
if(vis[u] == false){ //如果u未被访问
DFS(u); //访问u所在的连通块
}
}
}
邻接矩阵版DFS
constint MAXV = 1000; //最大顶点数
constint INF = 1000000000; // 设INF为一个很大的数
int n, G[MAXV][MAXV]; //n为顶点数,MAXV为最大顶点数
bool vis[MAXV] = {false}; //如果顶点i已被访问,则vis[i]== true.初值为false
void DFS(int u, int depth){ // u为当前访问的顶点标号,depth为深度
vis[u] = true; //设置u已被访问
//如果需要对u进行一些操作,可以在这里进行
//下面对所有从u出发能到达的分支顶点进行枚举
for(int v = 0; v<n; v++){
if(vis[v] == false && G[u][v] != INF){ //如果v未被访问,且u可到达v
DFS(v, depth+1); //访问,深度加1
}
}
}
void DFSTrave() { //遍历图G
for(int u=0; u<n; u++) { //对每个顶点v
if(vis[u] == false){//如果u未被访问
DFS(u, 1); //访问u和u所在的连通块,1表示初始为第一层
}
}
}
邻接表版DFS
vector<int>Adj[MAXV] ; //图G的邻接表
int n; //n为顶点数,MAXV为最大的顶点数
bool vis[MAXV] = {false}; //如果顶点i已被访问,则vis[i]==true, 初值为false
void DFS(int u, int depth){ //u为当前访问的顶点标号,depth为深度
vis[u] = true; //设置u已被访问
for(int i = 0; i<Adj[u].size(); i++){ //对从u出发可以到达的所有顶点v
int v = Adj[u][i];
if(vis[v] == false){ // 如果v未被访问
DFS(v, depth+1); //访问v,depth+1
}
}
}
void DFSTrave() { //遍历图G
for(int u=0; u<n; u++) { //对每个顶点v
if(vis[u] == false){//如果u未被访问
DFS(u, 1); //访问u和u所在的连通块,1表示初始为第一层
}
}
}
4.图的广度优先搜索
广度优先搜索以“广度”作为关键词,每次以扩散的方式向外访问顶点,和树的遍历一样,使用BFS遍历图需要使用一个队列,通过反复取出队首顶点,将该顶点可到达的未曾加入过队列的盯电脑全部入队,直到队列为空时遍历结束。下图的遍历顺序为;V0、V1、V2、V3、V4、V5。
BFS的具体实现:
和DFS一样,上面的例子是对单个连通块进行的遍历操作。如果要遍历整个图,则需要对所有连通块进行遍历 。使用BFS遍历图的基本思想是建立一个队列,并把初始顶点加入队列,此后每次都取出队首顶点进行访问,并把从该顶点出发可以到达的未曾加入过队列的顶点。伪代码如下:
BFS(u){ //遍历u所在的连通块
queue q; //定义队列q
将u入队;
inq[u] = true; //设置u已被加入过队列
while(q非空){ //只要队列非空
取出q的队首元素u进行访问;
for(从u出发可达到的所有顶点){ //枚举从u能直接到达的顶点v
if(inq[v] == false){ // 如果v未曾加入过队列
将v入队;
inq[v] = true; //设置v已被加入过队列
}
}
}
BFSTrave(G){ //遍历图G
for(G的所有顶点u) { //枚举G的所有顶点u
if( inq[u] == false){
BFS(u);
}
}
}
邻接矩阵BFS:
int n, G[MAXV][MAXV];
bool inq[MAXV] = {false};
void BFS(int u){
queue<int> q;
q.push(u); //将初始点u入队
inq[u] = true; //设置u已被加入过队列
while(!q.empty()){ //队列非空
int u = q.front(); //取出队首元素
q.pop(); //将队首元素出对
for(int v=0; v<n; v++){ //如果v的邻接点v未曾加入过队列
if(inq[v] == false && G[u][v] != INF){
q.push(v); //将v入队
inq[v] = true; //标记v为已被加入过队列
}
}
}
}
void BSFTrave(){//遍历图
for(int u=0; u<n; u++){ //枚举所有顶点
if(inq[u] == false ){ //如果u未曾加入过队列
BFS(q); //遍历u所在的连通块
}
}
}
邻接表BFS:
vector<int> Adj[MAXV];
int n;
bool inq[MAXV] = {false};
void inq[MAXV] = {false};
void BFS (int u){// 遍历单个连通块
queue<int> q;
q.push(u);
inq[u] = true;
while(!q.empty()){ //队列非空
int u = q.front(); //取出队首元素
q.pop(); //将队首元素出队
for(int i=0; i<Adj[u].size; i++){ //枚举从u出发能到达的所有顶点
int v = Adj[u][v];
if(inq[v] == false){ //如果v未曾加入过队列
q.push(v); //将v入队
inq[v] = true; //标记v为已被加入过队列
}
}
}
}
void BSFTrave(){//遍历图
for(int u=0; u<n; u++){ //枚举所有顶点
if(inq[u] == false ){ //如果u未曾加入过队列
BFS(q); //遍历u所在的连通块
}
}
}