关于图的简单介绍
-
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。在图中的数据元素,我们称之为顶点(Vertex),顶点集合有穷非空。在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
-
图按照边的有无方向分为无向图和有向图。无向图由顶点和边组成,有向图由顶点和弧构成。弧有弧尾和弧头之分,带箭头一端为弧头。
-
图按照边或弧的多少分稀疏图和稠密图。如果图中的任意两个顶点之间都存在边叫做完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图。
-
图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度。有向图顶点分为入度和出度。
-
图上的边或弧带有权则称为网。
-
图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复的叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称为强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称为强连通分量。
-
无向图中连通且n个顶点n-1条边称为生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。
使用邻接矩阵来实现图
- 本质上就是一个二维矩阵。给这个二维矩阵的元素保存bool类型的值。
- 对于一个无向图来说,如上图所示,节点i和节点j之间如果是向连接的,那么在其对应的邻接矩阵中的元素G[i][j] = true
。如果节点i和节点j之间如果没有连接,那么在其对应的邻接矩阵中的元素G[i][j] = false
。 - 我们来举一个例子。无向图和有向图。
- 我们先不过多讨论利用临接矩阵所表示的图的性质。先来看一下代码实现
//利用邻接矩阵实现稠密图
class DenseGraph
{
private:
int node, side; //node是图的结点 side是图上的边
bool directed; //true代表有向图,false代表无向图
vector<vector<bool>> g; //二维数组代表邻接矩阵
public:
DenseGraph(int node, bool directed)
{
this->node = node;
this->side = 0;
this->directed = directed;
for (int i = 0; i < node; i++)
{
g.push_back(vector<bool>(node, false));
}
}
~DenseGraph() //由于没有开辟新的空间,因此虚构函数可不写
{
}
int NodeSize()
{
return this->node;
}
int SideSize()
{
return this->side;
}
bool HasSide(int v, int m)
{
assert(v >= 0 && v < node);
assert(m >= 0 && m < node);
return g[v][m];
}
void AddSide(int v,int m)
{
assert(v >= 0 && v < node);
assert(m >= 0 && m < node);
if (HasSide(v, m) == true)
return;
g[v][m] = true;
if (this->directed == false) // 如果为无向图
g[m][v] = true;
this->side += 1; //边的数量加1
}
};
利用临接表来实现图
- 我们可以看清楚的看出,相对于邻接矩阵来说。他只需要维护一个数组就好了,对于上图的无向图。我们可以看出,对于节点0,与他连接的只有节点1.对于节点1,与他连接的有节点0、2、3.下面的都是类似的。
- 对于上图的有向图来说,我们可以看出,节点0和节点1之间的关系使节点0指向节点1的,但是节点1不指向节点0的。因此在节点1指向的相邻元素只有节点2。 - 那么我们用邻接表实现的图的代码如下
//使用临接表实现稀疏图
class SparseGraph {
private:
int node, side; //node表示结点,side表示边
bool directed; //true为有向图,false为无向图
vector<vector<int>> g;
public:
SparseGraph(int node, bool directed)
{
this->node = node;
this->side = 0;
this->directed = directed;
for (int i = 0; i < node; i++)
{
g.push_back(vector<int>());
}
}
~SparseGraph()
{
}
int NodeSize()
{
return this->node;
}
int SideSize()
{
return this->side;
}
bool HasSide(int v, int m)
{
assert(v >= 0 && v < node);
assert(m >= 0 && m < node);
for (int i = 0; i < g[v].size(); i++)
{
if (g[v][i] == m)
return true;
}
return false;
}
void AddSide(int v, int m)
{
assert(v >= 0 && v < node);
assert(m >= 0 && m < node);
if (HasSide(v, m) == true)
return;
g[v].push_back(m);
if (v != m && this->directed == false)
g[m].push_back(v);
this->side += 1;
}
};
图的遍历
- 对于图的遍历,我心在说两个方法。深度优先遍历和广度优先遍历。
- 深度优先遍历:就是顺着一个节点一直往下找,一直找到头为止。找到头后再回溯。广度优先遍历:把这个节点相连的所有节点都拿到,然后在将所拿到的每一个节点的相邻节点都拿到,使用队列进行操作。
- 同时呢,为了方便起见,我封装了一个类,用来遍历二维数组或者一维数组中一行的所有元素。
//深度优先
template<typename Graph>
class Path {
private:
Graph &graph;
int s; //某一个点
bool* visited;
int* from; //他是从那个点来的
void dfs(int v)
{
visited[v] = true;
typename Graph::adjIterator adj(graph, v);
for(int i = adj.begin();!adj.end();i = adj.next())
if (!visited[i])
{
from[i] = v;
dfs(i);
}
}
public:
Path(Graph &graph, int s) :graph(graph) {
assert(s >= 0 && s < graph.NodeSize());
visited = new bool[graph.NodeSize()];
from = new int[graph.NodeSize()];
for (int i = 0; i < graph.NodeSize(); ++i)
{
visited[i] = false;
from[i] = -1;
}
this->s = s;
//寻找路由
dfs(s);
}
~Path()
{
delete[] visited;
delete[] from;
}
bool hasPath(int w)
{
assert(w >= 0 && w < graph.NodeSize());
return visited[w];
}
void path(int w, vector<int> &vec)
{
stack<int> s;
int p = w;
while (p != -1)
{
s.push(p);
p = from[p];
}
vec.clear();
while (!s.empty())
{
vec.push_back(s.top());
s.pop();
}
}
void showPath(int w)
{
vector<int> vec;
path(w, vec);
for (int i = 0; i < vec.size(); ++i)
{
cout << vec[i];
if (i == vec.size() - 1)
cout << endl;
else
cout << "→";
}
}
};
//广度优先
template<typename Graph>
class ShortPath {
private:
Graph &graph;
int s;
bool* visited;
int* from;
int* ord;
public:
ShortPath(Graph &graph, int s) :graph(graph) {
assert(s >= 0 && s < graph.NodeSize());
visited = new bool[graph.NodeSize()];
from = new int[graph.NodeSize()];
ord = new int[graph.NodeSize()];
for (int i = 0; i < graph.NodeSize(); ++i)
{
visited[i] = false;
from[i] = -1;
ord[i] = -1;
}
this->s = s;
queue<int> q;
q.push(s);
visited[s] = true;
ord[s] = 0;
while (!q.empty())
{
int w = q.front();
q.pop();
typename Graph::adjIterator adj(graph, w);
for (int i = adj.begin(); !adj.end(); i = adj.next())
{
if (!visited[i])
{
q.push(i);
visited[i] = true;
from[i] = w;
ord[i] = ord[w] + 1;
}
}
}
}
~ShortPath()
{
delete[] visited;
delete[] from;
delete[] ord;
}
bool hasPath(int w)
{
assert(w >= 0 && w < graph.NodeSize());
return visited[w];
}
void path(int w, vector<int> &vec)
{
stack<int> s;
int p = w;
while (p != -1)
{
s.push(p);
p = from[p];
}
vec.clear();
while (!s.empty())
{
vec.push_back(s.top());
s.pop();
}
}
void showPath(int w)
{
vector<int> vec;
path(w, vec);
for (int i = 0; i < vec.size(); ++i)
{
cout << vec[i];
if (i == vec.size() - 1)
cout << endl;
else
cout << "→";
}
}
int length(int w)
{
assert(w >= 0 && w < graph.NodeSize());
return ord[w];
}
};
图的连通分量
- 连通分量就是用来表示一个图中有有多少个分组。
- 比如上图中,我们说这个图的连通分量就是3。就是有三个分组,他们没有连在一起。
template<typename Graph>
class Comment {
private:
Graph &graph;
bool* visited;
int* id; // 检测两个结点之间是否连接
int ccount;
void dfs(int i)
{
visited[i] = true;
id[i] = ccount;
typename Graph::adjIterator adj(graph, i);
for (int w = adj.begin(); !adj.end(); w = adj.next())
{
if (visited[w] == false)
dfs(w);
}
}
public:
Comment(Graph graph) :graph(graph) {
visited = new bool[graph.NodeSize()];
id = new int[graph.NodeSize()];
ccount = 0;
for (int i = 0; i < graph.NodeSize(); ++i)
{
visited[i] = false;
id[i] = -1;
}
for (int i = 0; i < graph.NodeSize(); i++)
{
if (visited[i] == false)
{
dfs(i);
ccount++;
}
}
}
~Comment()
{
delete[] visited;
}
int Count()
{
return ccount;
}
bool isConnection(int v, int m)
{
assert(v >= 0 && v < graph.NodeSize());
assert(m >= 0 && m < graph.NodeSize());
return id[v] == id[m];
}
};