黑河市网站建设_网站建设公司_腾讯云_seo优化
2025/12/21 14:07:52 网站建设 项目流程

1. 图(Graph)的基本概念

一个图 ( G = (V, E) ) 包含:

  • 节点 / 顶点(Vertex):( V )
  • 边(Edge):( E )

分为:

  • 无向图
  • 有向图
  • 带权图(权重通常为距离、概率等)

2. 邻接矩阵(Adjacency Matrix)


2.1 定义

邻接矩阵 (A) 对于有 (n) 个顶点的图是一个 (n\times n) 的二维数组。常见变体:

  • 无向无权图
    (A[i][j] = 1) 表示存在边,(0) 表示无边。对称矩阵:(A[i][j] = A[j][i])。
  • 有向无权图
    (A[i][j] = 1) 表示从 (i) 指向 (j) 的有向边(不一定对称)。
  • 带权图(有向或无向):
    (A[i][j] = w_{ij})(例如边长、代价、相似度)。无边处通常用特殊值表示(0、INF、或 NaN,取决于语义)。
  • 自环:如果允许自环,则对角线 (A[i][i]) 可为 1 或权值。
  • 稀疏矩阵 vs 密集矩阵:邻接矩阵天然是密集表示;当 (m \ll n^2)(稀疏图)时它并非空间友好。

无边的表示(实务)

  • 带权最常用:A[i][j] = +Inf(用 std::numeric_limits<double>::infinity()),这样在最短路算法中方便跳过;也可用一个布尔 mask 表示存在性与否。
  • 无权场景下可用 0/1,但当 0 可能是合法权重时需小心。

2.2 图示例

举例扩展:带权有向图的邻接矩阵示例(设无边用 INF):

   0   1   2   3
0  0  2.5 INF INF
1 INF 0  1.0  INF
2 INF INF 0  3.2
3 INF INF INF 0

该矩阵直接可用于 Floyd–Warshall(全源最短路),矩阵自底向上更新 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])


2.3 内存复杂度

空间复杂度:(O(n^2))。

  • 若使用 double 存权值:内存约 8 * n^2 bytes。
    例如 (n=10000) 时:8 * 1e8 = 800MB(仅矩阵)。
  • 若使用 bool 或位图(bitset):可以压缩到约 n^2 / 8 bytes——仍会很快变大。

工程建议

  • (n \lesssim 10^4) 且图稠密:可用密集邻接矩阵。
  • (n) 很大且稀疏(如 SLAM 的变量图通常 (m = O(n))):不要用邻接矩阵,应使用邻接表或稀疏矩阵(CSR/CSC)。

2.4 典型操作复杂度

  • 检查边(i,j):O(1) —— 直接访问 A[i][j]
  • 遍历 i 的所有邻居:O(n) —— 扫一整行。稀疏情况下这是低效的(很多是0/INF)。
  • 插入/删除边:O(1) —— 直接写入/置零。
  • 增添节点:需要重新分配并复制整个 (n\times n) 矩阵:O(n^2) —— 非常昂贵。
  • 矩阵乘法 / 幂:用于计数长度为 k 的路径(A^k)等,可用线性代数库(复杂度 (O(n^3)) 或更好并行实现)。

2.5 优点


2.6 缺点

  • 空间没处可逃:稀疏图会浪费大量内存。
  • 遍历邻居代价高:即使实际度很小,也要扫描整个行。
  • 动态调整困难:添加/删除大量节点/重编号代价高。
  • 缓存局部性:虽然矩阵按行存储在内存中,连续访问行很快,但若算法需要随机访问多行/列会出现缓存不友好问题。

2.7 C++ 示例

1) 固定大小的静态数组

适合小图、最高性能需求,但不灵活:

const int N = 100;
std::array<std::array<int, N>, N> adj{}; // 或静态 int adj[N][N]adj[u][v] = 1;

注意std::vector<std::vector<int>> 会额外分配很多小块,导致性能下降;优先使用一维连续数组模拟二维(vector<T> data(n*n); data[i*n + j])以获得更好缓存。

2) 动态大小

struct AdjMatrix {
size_t n;
std::vector<double> data; // 使用 double 表示权值(无边用 INF)static constexpr double INF = std::numeric_limits<double>::infinity();AdjMatrix(size_t n_) : n(n_), data(n_*n_, INF) {for(size_t i=0;i<n;i++) data[i*n + i] = 0.0;}inline double& at(size_t i, size_t j) { return data[i*n + j]; }inline bool hasEdge(size_t i,size_t j) { return data[i*n + j] != INF; }std::vector<size_t> neighbors(size_t i) {std::vector<size_t> nb;for(size_t j=0;j<n;j++) if(data[i*n + j] != INF && i!=j) nb.push_back(j);return nb;}};

优势:一维连续存储,友好缓存;方便序列化与 GPU 拷贝。

注意vector<bool> 不建议用于位图(语义/效率问题),若需要位图请用 std::vector<uint64_t>boost::dynamic_bitset

3) 使用位矩阵(节省空间、加速位运算)

当图是无权且只需存在性测试,可以用位矩阵(每行为 bitset),支持快速交并运算(例如计算公共邻居,Jaccard 相似度):

#include <bitset> // 若 n 编译时已知std::vector<std::vector<uint64_t>> bit_rows; // 行压缩为 uint64_t 单元// 测试公共邻居数uint64_t count_common(size_t i, size_t j) {uint64_t sum = 0;for(k...) sum += __popcnt64(rowi[k] & rowj[k]);return sum;}

位运算非常适合 GPU/向量化加速与大量相似度计算(例如 loop closure 相似度矩阵)。

4) 用线性代数库(如 Eigen)

带权图且需要做谱分解 / 特征值运算时,使用 Eigen / Armadillo:

#include <Eigen/Dense>Eigen::MatrixXd A(n, n);// fill AEigen::SelfAdjointEigenSolver<Eigen::MatrixXd> solver(A);auto evals = solver.eigenvalues();

注意:Eigen 的 MatrixXd 是 row-major 或 col-major 可配置;与你内存布局要匹配以免额外拷贝。


附:邻接矩阵的常见优化与变体

  1. 只存上(三角)或下三角(对称无向图):节省一半空间,但访问需做索引变换。
  2. 压缩行(CSR / CSC):虽然这本质是稀疏矩阵格式,若邻接矩阵非常稀疏,可以把邻接矩阵视作稀疏矩阵存储,利用 row_ptr, col_idx, val 来节省空间并加快遍历邻居(与邻接表类似但更便于数值运算)。
  3. 位图 + 权值分离:存在性用位图,权值另存(仅保存实际边)。对查询速度与存储权衡有利。
  4. Block-sparse / Tiled:把大矩阵分成块,适合 GPU / 分布式场景(按块分配可加速并行乘法与存取)。

在 SLAM / 图优化 中的实践提示


常见误区与陷阱

  • vector<vector<T>> 表示邻接矩阵会产生大量小内存分配,影响性能;优先用 vector<T> 一维数组映射 2D 索引。
  • vector<bool> 作为位图时语义/性能会受损(vector<bool> 为特殊化实现),建议用 std::bitset(编译期大小)或 boost::dynamic_bitset,或自己用 vector<uint64_t>
  • 混合使用多处 Eigen 版本或旧 Eigen 可能导致 CUDA 编译失败(你之前遇到的问题),务必保证 include 顺序与版本一致。
  • 若算法频繁增删节点,应避免邻接矩阵。

实用算法示例(邻接矩阵场景优先)

  1. Floyd–Warshall(全源最短路):直接在邻接矩阵上三重循环 O(n^3)。
  2. 路径计数(A^k):矩阵幂可以求长度恰为 k 的路径数量,可用快速幂或并行矩阵乘法。
  3. 谱聚类 / 特征值分解:从邻接矩阵构造度矩阵 (D) 与拉普拉斯 (L = D - A),然后求前 k 个特征向量。
  4. 快速公共邻居 / 相似度:若用位矩阵,公共邻居数可通过位与 + popcount 迅速计算;适用于 loop closure 提名。

3. 邻接表(Adjacency List)


3.1 定义

邻接表为每个顶点维护一个容器(vectorlistdeque、甚至链表索引数组),存储该顶点的所有邻居(以及可选的边属性,如权重、时间戳、信息矩阵等)。

常见表示:

注意:邻接表按顶点维护“出边列表”(directed)或“邻居列表”(undirected,存双向或半存储)。


3.2 图示例

同样图 0–1–2–3:

  • vector<vector<int>> adj 表示:
adj[0] = {1}
adj[1] = {0,2}
adj[2] = {1,3}
adj[3] = {2}
  • 带权例子(vector<vector<pair<int,double>>>):
adj[1] = { {0,0.8}, {2,1.2} }
  • Forward-star(数组表示)示意:
head[0] = 0; head[1]=1; head[2]=3; head[3]=5; head[4]=6 (head[n]=m)
to = [1, 0,2, 1,3, 2]
offset method: neighbors of u are to[offset[u] .. offset[u+1]-1]

3.3 内存复杂度

空间复杂度:(O(n + m)),更精确:

  • 使用 vector<vector<int>>:节点数组 n + 所有邻节点存储 2m(无向双向存储)元素 + 每个内层 vector 的元数据(约 3 pointers 每个);
  • 使用 CSR/offset:offset 长度 n+1to 长度 m(或 2m),极佳压缩比;

工程量化示例:

  • (n=10^6, m=5\times10^6)(稠密度低):

    • vector<vector<int>> 可能因每个 vector 分配开销显著;
    • CSR 存储 offset + to 仅需 ~ 8*(n+1) + 4*m bytes(假设 4 字节 int),非常节省。

3.4 操作复杂度

操作邻接表(vector 列表)
判断边 (i,j)O(k)(k = deg(i)),若用 hash 集合则 O(1) 平均
遍历邻居O(k)
插入边平均 O(1) (push_back,若内存扩容则摊销)
删除边O(k)(若不知道位置),可用 swap-erase 达到 O(1)(但破坏顺序)
增加节点O(1)(adj.push_back({})
静态构建(一次性填充)O(m)(如果预分配)

细节:


3.5 优点

  • 节省空间:对稀疏图最优。
  • 遍历邻居高效:遍历速度与实际邻居数成正比。
  • 易动态修改:添加节点/边、增删动态方便。
  • 适配多种边属性:可直接在邻居项中加权值、时间戳、covariance 等结构体字段,便于 SLAM 中记录多模态信息(例如 loop confidence、information matrix)。
  • 与稀疏线性代数相配:生成稀疏矩阵(CSR)容易,用于稀疏优化。

3.6 缺点


3.7 C++ 示例与工程实现变体

下面给出多个工程可用的数据结构实现,以及并发 / 高性能 / 序列化 / 转换代码片段。

A. 最简单(开发与教学用)

int n = 100;
std::vector<std::vector<int>> adj(n);// add undirected edgevoid add_edge(int u, int v) {adj[u].push_back(v);adj[v].push_back(u);}// iteratefor (int v : adj[u]) {// ...}

问题:每个 push_back 可能会引起 reallocation,建议 reserve


B. 带权、带属性(常用 SLAM)

struct Edge {
int to;
double weight;
uint64_t timestamp;
// optional: information matrix index or small fixed-size array
};
std::vector<std::vector<Edge>> adj;

适合在每条边上存储 covariance/information/score,直接用于 loop-closure 筛选和后端因子构造。


C. 前置分配 + swap-erase 删除(高性能)

// reserve neighbor lists
adj[u].reserve(expected_deg[u]);
// O(1) remove (unordered)
void remove_edge_unordered(int u, int v) {
auto &list = adj[u];
for (size_t i = 0; i < list.size(); ++i) {
if (list[i] == v) {
list[i] = list.back();
list.pop_back();
break;
}
}
}

优点:删除 O(1),代价是破坏邻居顺序(通常可接受)。


D. 使用 unordered_set 实现 O(1) 存在性检查

std::vector<robin_hood::unordered_flat_set<int>> adj_set; // 或 std::unordered_setbool has_edge(int u, int v) {return adj_set[u].find(v) != adj_set[u].end();}void add_edge(int u, int v) {adj_set[u].insert(v);adj_set[v].insert(u);}

权衡:更高内存开销,但查询快。适合需要频繁查边(而非遍历邻居)的场景(如动态约束冲突检测)。


E. CSR / Offset 表示(静态图或批量构建首选)

构建步骤:

  1. 统计每个节点度 deg[u]
  2. offset[0]=0; offset[i+1]=offset[i]+deg[i]
  3. 填充 to[offset[u] .. offset[u+1]-1]

代码片段(构建):

std::vector<int> deg(n,0);for(auto &e: edges) {deg[e.u]++; deg[e.v]++;}std::vector<int> offset(n+1);for(int i=0;i<n;i++) offset[i+1]=offset[i]+deg[i];std::vector<int> to(offset.back());std::vector<int> cur = offset;for(auto &e: edges) {to[cur[e.u]++] = e.v;to[cur[e.v]++] = e.u;}

优点:内存紧凑、遍历邻居更快(连续内存)、方便并行和 GPU 拷贝。缺点:不支持高效随机增删。


F. Forward-star(链式数组,节省指针)

常见于竞赛/嵌入式:

const int MAXM = ...;
int head[N], to[MAXM], next[MAXM], cnt=0;
void add_edge(int u,int v) {
to[++cnt] = v; next[cnt] = head[u]; head[u] = cnt;
}

优点:低开销,适合静态或构建一次后多查询场景。缺点:删除复杂。


G. 并发场景(多线程读/写)

  • 读多写少:读无需锁,写时对单个 adj[u] 使用 mutex 或原子操作。
  • 高并发写入:使用分段锁(per-vertex mutex)或 lock-free structures(复杂)。
  • 构建阶段并行:用线程局部 buffer 收集边,最后合并到主结构(避免锁竞争)。

示例(per-vertex mutex):

std::vector<std::mutex> vertex_mutex(n);void thread_safe_add_edge(int u,int v) {std::scoped_lock lock(vertex_mutex[u], vertex_mutex[v]);adj[u].push_back(v);adj[v].push_back(u);}

H. GPU / 大规模并行处理

  • 把邻接表转成 CSR (offset + to arrays),上传到 GPU;在 kernel 中用 offset[u]..offset[u+1] 范围遍历;
  • 位图表示(bitset)也适合 GPU 用于快速并行相似度计算(popcount)。
  • 注意 host->device 的内存对齐与数据类型大小(use 32-bit indices if possible)。

I. 序列化 / 存储(磁盘 / 网络)


J. 转换:邻接矩阵 ↔ 邻接表

  • 矩阵 -> 邻接表:对每行扫描 O(n^2);稀疏时代价高。
  • 邻接表 -> CSR:O(n+m) 通过前述 degree->offset 构建步骤。

并发/性能优化建议

  1. 预分配(reserve):如果能估计度数,请 reserve 每个 vector,避免多次 reallocation。
  2. 使用连续内存:如果追求最高性能,优先使用 CSR 或 flat_vector(自实现一维数组 + offset)而不是 vector<vector<T>>
  3. 内存对齐与类型:顶点索引用 uint32_t 优于 uint64_t(节省空间),除非 n>4e9。
  4. 定期压缩/整理:对动态图,周期性合并/compact 邻接列表以减少碎片。
  5. 访问模式友好:尽量按顶点顺序访问以提高 cache hit(对 BFS/DFS 有益)。
  6. 使用自定义 allocator:避免小块频繁分配的开销(你之前感兴趣的 static memory pool 很适合这里)。

在 SLAM / 图优化 / 路径规划 中的具体应用建议

  • SLAM 因子图:因是稀疏且动态(随着关键帧增长),邻接表或 CSR(周期性 rebuild)为佳。每个邻接项存 factor id /信息矩阵索引 -> 在构造线性化矩阵时可直接映射到稀疏 Hessian。
  • 回环候选管理:把回环置信度、检测时间、关键点数存到边属性,快速筛选用 adj[u] 遍历并用阈值过滤。
  • A*:邻接表加权图 + 优先队列实现,遍历邻居为 O(k) 最佳。
  • 增量优化:支持快速插入/删除边(swap-erase),并在后端维护稀疏矩阵增量更新。

推荐的工程级模板类

这是一个工程级的邻接表类草案(支持带权、reserve、swap-erase、CSR 导出):

template<typename EdgeAttr = int>class AdjacencyList {public:using Edge = std::pair<int, EdgeAttr>;AdjacencyList(size_t n=0) { resize(n); }void resize(size_t n) {adj.resize(n);}size_t size() const { return adj.size(); }void reserve_node(size_t u, size_t deg) {adj[u].reserve(deg);}void add_edge(int u, int v, const EdgeAttr& attr = EdgeAttr()) {adj[u].emplace_back(v, attr);}// undirected conveniencevoid add_undirected(int u,int v,const EdgeAttr& attr=EdgeAttr()){add_edge(u,v,attr); add_edge(v,u,attr);}// unordered remove (O(1))bool remove_edge_unordered(int u, int v) {auto &list = adj[u];for(size_t i=0;i<list.size();++i) {if(list[i].first==v) {list[i] = list.back();list.pop_back();return true;}}return false;}// export CSRvoid to_csr(std::vector<int>& offset, std::vector<int>& to) const {int n = (int)adj.size();offset.assign(n+1,0);for(int i=0;i<n;++i) offset[i+1] = offset[i] + (int)adj[i].size();to.resize(offset.back());std::vector<int> cur = offset;for(int i=0;i<n;++i){for(auto &e: adj[i]) to[cur[i]++] = e.first;}}const std::vector<Edge>& neighbors(int u) const { return adj[u]; }private:std::vector<std::vector<Edge>> adj;};

4. 邻接矩阵 vs 邻接表对比总结

项目邻接矩阵邻接表
内存O(n²)O(n + m)
图类型稠密图稀疏图
查边✔ O(1)✘ O(k)
遍历邻居O(n)O(k)
增删节点❌ 难✔ 简单
适用算法Floyd, DPBFS/DFS, Dijkstra, 图优化

5. 在 SLAM 与图优化中的应用

邻接表 —— 因子图最常用的数据结构

GTSAM、Ceres、SLAM 后端优化中:

  • 图是稀疏的
  • 每个节点仅连接少量因子

使用邻接表表示 Factor Graph

示例(gtsam):

x0 → (imu, odom)
x1 → (imu, loop)
x2 → (odom)
...

每个变量只连接少量因子,所以邻接表最适合。


邻接矩阵 —— 回环检测与特征匹配中常见

例如:

  • BoW 词典相似度矩阵(ORB-SLAM)
  • GNSS / WiFi RTI 指纹图
  • GPU 加速 pairwise 关系计算

6. 什么时候用哪一种

场景推荐结构
SLAM 图优化(Factor Graph)邻接表
稠密图(社交网络、全连接图)邻接矩阵
BFS / DFS邻接表
Floyd 全源最短路邻接矩阵
Dijkstra 稠密图邻接矩阵
Dijkstra 稀疏图邻接表 + 堆
GPU 并行计算邻接矩阵
路径规划(地图网格)邻接表(隐式图)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询