一、 graph_traits 的完整展开
graph_traits<Graph>= 图对算法暴露的"最小公共接口"
算法只认识 traits,不认识 adjacency_list。
1 graph_traits 是什么?
cpp
template <class Graph>
struct graph_traits;
针对每一种图类型(如 adjacency_list),Boost 特化 了 graph_traits。
2 graph_traits 提供了什么?
以常用配置为例:
cpp
using Graph = boost::adjacency_list<
boost::vecS,
boost::vecS,
boost::directedS,
VertexProperty,
EdgeProperty
>;
2.1 核心类型(算法最关心的)
cpp
using Traits = boost::graph_traits<Graph>;
| 类型 | 含义 |
|---|---|
Traits::vertex_descriptor |
顶点句柄 |
Traits::edge_descriptor |
边句柄 |
Traits::vertices_size_type |
顶点数类型 |
Traits::edges_size_type |
边数类型 |
Traits::degree_size_type |
度数类型 |
2.2 iterator 类型(算法遍历依赖)
cpp
Traits::vertex_iterator
Traits::edge_iterator
Traits::out_edge_iterator
Traits::adjacency_iterator
算法写法:
cpp
Traits::vertex_iterator vi, vi_end;
for (tie(vi, vi_end) = vertices(g); vi != vi_end; ++vi)
算法完全不管 iterator 背后是 vector 还是 list
2.3 图方向能力(编译期)
cpp
Traits::directed_category
Traits::edge_parallel_category
Traits::traversal_category
例如:
cpp
using Cat = Traits::directed_category;
static_assert(std::is_same_v<Cat, boost::directed_tag>);
算法在编译期就能裁剪路径
3 descriptor 本质是什么
当使用时:
cpp
VertexListS = vecS
OutEdgeListS = vecS
实际类型
cpp
vertex_descriptor = std::size_t
edge_descriptor = detail::edge_desc_impl
顶点 = 索引
边 = (source, index into out-edge vector)
这就是为什么 vecS 性能最好
4 graph_traits 的核心思想
Traits 把"图的实现细节"全部屏蔽掉,只留下算法需要的抽象能力。
二、 adjacency_list 的内存布局示意
下面用最常用的配置来讲:
cpp
adjacency_list<
vecS, // OutEdgeList
vecS, // VertexList
directedS,
VertexProp,
EdgeProp
>
1 整体内存结构
text
Graph
│
├── vector<Vertex> vertices_
│ │
│ ├── Vertex 0
│ │ ├── VertexProperty
│ │ └── vector<Edge> out_edges
│ │ ├── Edge 0
│ │ │ ├── target vertex
│ │ │ └── EdgeProperty
│ │ └── Edge 1
│ │
│ ├── Vertex 1
│ │ └── vector<Edge> out_edges
│ │
│ └── Vertex N
没有"Edge 对象池"
边是嵌在源顶点里的
2 Vertex 的真实结构(简化版)
cpp
struct Vertex {
VertexProperty property;
std::vector<Edge> out_edges;
};
3 Edge 的真实结构(简化版)
cpp
struct Edge {
std::size_t target; // 目标顶点 index
EdgeProperty property;
};
edge_descriptor 并不等于 Edge *
它是:
text
(vertex_index, edge_index)
4 edge_descriptor 如何定位一条边?
text
edge_descriptor e:
e.source = u
e.idx = k
访问流程:
cpp
auto& edge = vertices_[u].out_edges[k];
O(1)
5 为什么 reverse_edge 必须显式存?
因为内存结构是:
text
u.out_edges → v
v.out_edges → u (如果存在)
Boost 不假设对称性
所以:
cpp
edge_reverse_t : edge_descriptor
必须由使用者维护。
6 adjacency_list 的三种常见布局对比
| VertexList | OutEdgeList | 内存特征 |
|---|---|---|
| vecS | vecS | 最紧凑、cache-friendly |
| vecS | listS | 出边插删稳定 |
| listS | listS | 高度动态,但慢 |
SLAM / 流 / 最短路 → vecS + vecS
7 adjacency_list vs adjacency_matrix
| adjacency_list | adjacency_matrix | |
|---|---|---|
| 内存 | O(V + E) | O(V²) |
| 稀疏图 | ✔ | ✘ |
| 遍历邻接 | 快 | 快 |
| 动态修改 | ✔ | ✘ |
8 把 traits + 内存模型连起来
text
算法
│
│ uses
▼
graph_traits
│
│ returns descriptor
▼
adjacency_list 内部结构
Traits 是"接口",adjacency_list 是"实现"
九、总结
Boost.Graph = traits 定义能力,property_map 承载数据,adjacency_list 只是其中一种实现。
三、 自己实现一个「最小 Graph」
目标:不使用
adjacency_list,但能直接喂给 BFS / DFS / Dijkstra
1 Boost.Graph 对 Graph 的"最低要求"
Boost.Graph 并不要求继承任何基类,只要求满足:
三个维度的"概念(Concept)"
- graph_traits
- 自由函数接口
- property_map(可选)
2 一个最小可用 Graph 的功能目录
以 最小有向图 + BFS 为例:
必须提供:
| 能力 | 函数 |
|---|---|
| 顶点遍历 | vertices(g) |
| 邻接遍历 | out_edges(v, g) |
| 边终点 | target(e, g) |
| 顶点数 | num_vertices(g) |
| traits | graph_traits<Graph> |
3 Step 1:定义 Graph 内部结构(最小)
cpp
struct SimpleGraph {
using Vertex = std::size_t;
std::vector<std::vector<Vertex>> adj; // 邻接表
};
4 Step 2:graph_traits 特化(核心)
cpp
namespace boost {
template <>
struct graph_traits<SimpleGraph> {
using vertex_descriptor = std::size_t;
using edge_descriptor = std::pair<std::size_t, std::size_t>;
using vertex_iterator = counting_iterator<std::size_t>;
using out_edge_iterator = counting_iterator<std::size_t>;
using directed_category = directed_tag;
using edge_parallel_category = allow_parallel_edge_tag;
using traversal_category = incidence_graph_tag;
};
}
注意 :
Boost.Graph 的算法只看 traits
5 Step 3:实现自由函数接口
顶点遍历
cpp
std::pair<
boost::counting_iterator<std::size_t>,
boost::counting_iterator<std::size_t>
>
vertices(const SimpleGraph& g) {
return {0, g.adj.size()};
}
出边遍历
cpp
std::pair<
boost::counting_iterator<std::size_t>,
boost::counting_iterator<std::size_t>
>
out_edges(std::size_t v, const SimpleGraph& g) {
return {0, g.adj[v].size()};
}
target()
cpp
std::size_t target(
std::size_t edge_idx,
const SimpleGraph& g,
std::size_t v
) {
return g.adj[v][edge_idx];
}
6 现在可以直接跑 BFS
cpp
SimpleGraph g;
g.adj = {{1,2}, {2}, {}};
boost::breadth_first_search(g, 0,
boost::visitor(boost::make_bfs_visitor(
boost::null_visitor()
))
);
完全不依赖 adjacency_list