图(Graph)是离散数学中的一种重要数据结构,用于描述对象(称为顶点,或节点)之间的关系(称为边)。图可以表示各种事物之间的连接关系,比如网络中的路由器、社交网络中的用户、城市之间的道路等。
,其中
是顶点数。
在理解图的结构和操作时,有一些与图密切相关的概念有助于更好地分析和处理图的算法和应用。
路径是在图中从一个顶点到另一个顶点的行进序列,它由一系列边和顶点组成。根据图的类型和要求,路径可以分为几类:
图的连通性描述了图中顶点与顶点之间的可达性。
图中一个顶点的度表示与该顶点连接的边的数量。
子图是从原始图中选取部分顶点和边构成的图。常见的子图类型包括:
生成树是无向图的一个子图,包含图中的所有顶点,且是一个树形结构(无环、连通)。
生成树:
最小生成树:
二分图是一种特殊的图,可以将顶点集合分为两个不相交的子集,且所有边都连接两个不同子集中的顶点,而子集中没有内部连接。
对于有向无环图(DAG),拓扑排序是一种将顶点按依赖关系线性排序的算法,通常用于解决调度、依赖管理等问题。
如果是无向图的话,那么邻接矩阵就是沿着对角线对称的。
如果是无向图的话,那么就可以通过压缩只存储一半。 除了需要一个存储权值的邻接矩阵我们还需要一个vector来存储顶点,如果涉及到邻接矩阵,那么就会涉及到下标,所以我们应该还需要一个顶点映射下标的map。
// 顶点 weight 有向图/无向图
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:
private:
vector<V> _vertexs; //顶点集合
map<V, int> _indexMap; //顶点对应下标的关系(顶点---->下标)
vector<vector<W>> _matrix; //邻接矩阵
};
}
初始化
Graph(const V* a, size_t n)
{
// 开辟n个空间
_vertexs.resize(n);
for (size_t i = 0;i < n;i++)
{
_vertexs[i] = a[i];
//顶点----->下标
_indexMap[a[i]] = i;
}
_matrix.resize(n);
//权值用MAX_W初始化
for (size_t i = 0;i < n;i++) _matrix[i].resize(n, MAX_W);
}
获取顶点的下标
size_t GetVertexIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it != _indexMap.end()) return it->second;
else
{
// 抛异常出去
throw invalid_argument("顶点不存在");
// 过编译器逻辑
return -1;
}
}
通过map的接口find找到v对应的下标,如果it为end(),说明没有这个顶点,直接抛异常,如果存在,则返回对应的下标 添加边
// 原点 目标点 权值
void AddEdge(const V& src, const V& dst, const W& w)
{
// 获取原点下标
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
_matrix[srci][dsti] = w;
// 无向图添加两个权值
if (Direction == false) _matrix[dsti][srci] = w;
}
找到原点和目标点对应的下标,如果是有向图的话,只需要添加原点到目标点的边,如果是无向图的话,就需要反向添加一下目标点到原点的边了。
邻接表是图的一种存储表示方法,常用于存储稀疏图(即边数相对较少的图)。它通过每个顶点对应一个链表或数组来记录与其相邻的顶点。邻接表的空间复杂度相对较小,适合存储大规模图。
对于无向图来说不存在入度和出度的概念,所以只需要一个邻接表表示,下标的邻接表就是用来表示边的集合。
拿第一个点为例,这里使用一个结构体来维护的,这个结构体中有指向一下下一个边集的指针,还有一个权值和目标点的下标。 我们说形象一点:
类似于哈希桶的那个做法
template<class W>
struct Edge
{
int _dsti; //目标点的下标
W _w; //权值
Edge<W>* _next;
Edge(int dsti,const W& w)
:_dsti(dsti)
,_w(w)
,_next(nullptr)
{}
};
// 顶点 weight 有向图/无向图
template<class V, class W,bool Direction = false>
class Graph
{
typedef Edge<W> Edge;
public:
private:
vector<V> _vertexs; //顶点集合
map<V, int> _indexMap; //顶点对应下标的关系(顶点---->下标)
vector<Edge*> _tables; //邻接表
};
初始化
Graph(const V* a, size_t n)
{
// 开辟n个空间
_vertexs.resize(n);
for (size_t i = 0;i < n;i++)
{
_vertexs[i] = a[i];
//顶点----->下标
_indexMap[a[i]] = i;
}
_tables.resize(n,nullptr);//给tables开辟n个空间(n个顶点)
}
获取顶点对应下标
size_t GetVertexIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it != _indexMap.end()) return it->second;
else
{
// 抛异常出去
throw invalid_argument("顶点不存在");
// 过编译器逻辑
return -1;
}
}
添加边
void AddEdge(const V& src, const V& dst, const W& w)
{
// 获取原点下标
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
//1 --> 2
//原点到目标的权值
Edge* eg = new Edge(dsti, w);
//这里进行头插
eg->_next = _tables[srci];
_tables[srci] = eg;
//无向图进行特殊处理
//2 --> 1
if (Direction == false)
{
//反向指向
Edge* eg = new Edge(srci, w);
eg->_next = _tables[dsti];
_tables[dsti] = eg;
}
}
这个和邻接矩阵就不一样,邻接表中添加边应该先创建一个对应边的结构体,然后将这个结构体头插到原点对应下标的边集的位置上,如果是无向图的话,需要反向添加一条目标点到原点的边。注意:头插之后需要改变头的位置
通过本篇文章的介绍,我们初步了解了图的基本概念、图的表示方法(如邻接矩阵和邻接表)、以及图中的各种基本性质。图论作为计算机科学和数学中的一个重要分支,其应用范围广泛,从网络设计到路径规划,都有着广泛的应用场景。
在接下来的学习中,我们可以进一步探讨更高级的图算法,如最短路径算法(Dijkstra、Bellman-Ford 等)、图的遍历算法(深度优先搜索、广度优先搜索)、以及图的连通性和最小生成树等高级主题。这些内容将为解决更复杂的实际问题提供坚实的理论基础。
图论的世界广阔而有趣,期待你能在之后的学习和实践中不断挖掘其奥秘!