图由顶点和边组成,每条边的两端都必须是图的两个顶点。记号G(V,E)表示图G的顶点集为V,边集为E。
一般来说,图可以分为有向图和无向图。
顶点的度是指和该顶点相连的边的条数。对有向图来说,顶点的出边条数称为该顶点的出度,顶点的入边条数称为该顶点的入度。
顶点和边都可以有一定属性,而量化的属性称为权值,顶点的权值和边的权值分别称为点权和边权。
一般来说图的存储方式有两种:邻接矩阵和邻接表。
当顶点数目不大的时候(不超过1000),使用邻接矩阵存储图。
设图G(V,E)的顶点坐标为0,1,····,N-1,那么就可以令二维数组G[N][N]的两维分别表示图的顶点标号,即如果G[i][j]为1,则说明顶点i和顶点j之间有边;如果G[i][j]为0,说明i和j之间不存在边。这个二维数组G[][]被称为邻接矩阵。另外如果边存在权值,则可以令G[i][j]存放边权
在一些顶点数目较大(在1000以上)的情况下,一般都需要使用邻接表而非邻接矩阵来存储图。
设图G(V,E)的顶点坐标为0,1,····,N-1,如果把同一个顶点的所有出边放在一个列表中,那么N个顶点就会有N个列表,这N个列表被称为G的邻接表,即为Adj[N],其中Adj[i]存放顶点i的所有出边组成的列表。
使用vector来实现邻接表更为简单。开一个vector数组Adj[N],其中N为顶点个数,这样每个Adj[i]就都是一个变长数组vector,使得存储空间只与图的边数有关。
如果邻接表只存放每条边的终点编号,而不存放边权,那么vector中的元素类型可以直接定义为int型:
vector<int> Adj[N];
如果想添加一条从1号顶点到3号的有向边:
Adj[1].push_back(3);
如果同时存放终点编号和边权,那么可以建立结构体Node,用来存放每条边的终点编号和边权:
struct Node{
int v;//边的终点编号
int w;//边权
};
这样邻接表中的元素类型就是Node型:
vector<Node> Adj[N];
如果想添加一条从1号顶点到3号的边权为4的有向边:
Node temp;
temp.v = 3;
temp.w = 4;
Adj[1].push_back(temp);
更快的做法是定义结构体构造函数:
struct Node{
int v;
int w;
Node(int _v,int _w): v(_v),w(_w){} //构造函数
};
这样就可以不定义临时变量来实现加边操作:
Adj[1].push_back(Node(3,4));
图的遍历是指对图中所有顶点按一定顺序进行访问,遍历方法一般有两种:深度优先搜索(DFS)和广度优先搜索(BFS)。
使用DFS来遍历图,沿着一条路径直到无法继续前进,才退回到路径上离当前顶点最近的还存在未访问分支顶点的岔道口,并前往访问那些未访问分支顶点,直到遍历完整个图。
先介绍两个概念:
连通分量和强连通分量均称为连通块。
DFS遍历图的基本思路:将经过的顶点设置已访问,在下次递归碰到这个顶点的时候就不再去处理,直到整个图中所有顶点都被标记为已访问。
DFS伪代码如下,如果已知给定的图是一个连通图,则只用一次DFS就可以完成遍历:
DFS(u){//访问顶点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所在的连通块
}
#include
using namespace std;
const int MAXV = 1000;
const int INF = 0x3fffffff;
int n,G[MAXV][MAXV];//n为顶点个数,G为图
bool vis[MAXV] = {false};//标记是否访问
void DFS(int u,int depth){ //u为当前访问的顶点标号,depth为深度
vis[u] = true;//标记u已被访问
//如果需要对u进行一些操作,可以在这里进行
//下面对所有从u出发能到达的分支顶点v进行枚举
for(int v= 0;v<n;v++){//对每个顶点v
if(vis[v]== false && G[u][v] !=INF){
DFS(v,depth+1);//访问v,深度+1
}
}
}
void DFSTrave(){//遍历图G
for(int u =0;u<n;u++){
if(vis[u]==false){
DFS(u,1);//访问u和u所在的连通块,1表示初始为1层
}
}
}
#include
using namespace std;
const int MAXV = 1000;
const int INF = 0x3fffffff;
struct Node{
int v;//终点顶点
int w;//权值
Node(int _v,int _w): v(_v),w(_w) {}
};
vector<Node> Adj[MAXV];//邻接表
int n;//顶点个数
bool vis[MAXV] = {false};
void DFS(int u,int depth){
vis[u] = true;
//如果要对u进行一些操作,可以在此处进行
for(int i = 0;i<Adj[u].size();i++){
int v = Adj[u][i].v;
if(vis[v]==false){
DFS(v,depth+1);
}
}
}
void DFSTrave(){
for(int u =0;u<n;u++){
if(vis[u] == false){
DFS(u,1);
}
}
}
使用BFS需要一个队列,通过反复取队首顶点,然后将该顶点可达的所有未曾加入过队列的顶点全部入队,直到队列为空时遍历结束。
如果要遍历整个图,则需要对所有连通块分别进行遍历。基本思路是:建立一个队列,并把初始顶点加入队列,此后每次都取出队首顶点进行访问,并把从该顶点出发可以到达的未曾加入过队列的顶点全部加入队列,直到队列为空。
下面是伪代码,如果图连通,只需要一次BFS即可完成遍历:
BFS(u){//遍历u所在的连通块
queue q;//定义队列q
将u入队;
inq[u] = true;
while(q非空){
取出q的队首元素u进行访问;
for(从u出发的所有可达顶点v)
if(inq[v] == false){
将v入队;
inq[v] = true;
}
}
}
BFSTrave(G){//遍历图G
for(G的所有顶点u)
if(inq[u] == false){
BFS(u);
}
}
1.邻接矩阵版:
#include
using namespace std;
const int MAXV = 1000;
const int INF = 0x3fffffff;
int n,G[MAXV][MAXV];
bool inq[MAXV] = {false};
void BFS(int u){//遍历u所在的连通块
queue<int> q;//定义队列q
q.push(u);
inq[u] = true;
while(!q.empty()){
int u = q.front();
q.pop();
for(int v = 0;v<n;v++){
if(inq[v] == false && G[u][v] != INF){
q.push(v);
inq[v] = true;
}
}
}
}
void BFSTrave(){
for(int u = 0;u<n;u++){
if(inq[u] == false){
BFS(u);
}
}
}
#include
using namespace std;
const int MAXV = 1000;
const int INF = 0x3fffffff;
struct Node{
int v;
int layer;//顶点层号
};
vector<Node> Adj[MAXV];
int n;
void BFS(int s){//s为起始顶点编号
queue<Node> q;
Node start;//起始顶点
start.v = s;//起始顶点编号
start.layer = 0;//起始顶点层号为0
q.push(start);
inq[start.v] = true;
while(!q.empty()){
Node topNode = q.front();
q.pop();
int u = topNode.v;
for(int i =0;i<Adj[u].size(),i++){
Node next = Adj[u][i];
next.layer = topNode.layer+1;//next层号等于当前顶点层号+1
if(inq[next.v] == false){
q.push(next);
inq[next.v] = true;
}
}
}
}
void BFSTrave(){
for(int u = 0;u<n;u++){
if(inq[u]==false){
BFS(u);
}
}
}