简单无向图的表示

问题描述

我需要你的专业知识:

我即将在 C++ 中实现一个图形类并考虑正确的表示。图形简单且无向。顶点数量目前最多可达 1000,但未来可能会更高。边数高达 200k 甚至更高。每个顶点都有一个颜色 (int) 和一个 id (int)。边传输的信息并不比连接顶点多。

我存储图形,只需要访问 x 和 y 是否连接 - 这我经常需要。 初始化后,我永远不会删除或添加新的顶点或边(N = 顶点数,M = 从开始给出的边数)!

我已经可以使用的一种表示:

一个邻接列表推出到一个长列表中。伴随这种表示的是一个数组,每个顶点的起始索引。存储 O(2M) 并检查 x 和 y 之间的边缘是否平均为 O(n/m)

我想到的一个表示:

这个想法是,不是将邻接列表推出到一个数组中,而是用邻接矩阵来做。那么存储 O(N^2) 吗?是的,但我想将边存储在一位中,除了一个字节。(实际上是对称的 2 位) 示例:假设 N=8,然后创建一个长度为 8(64 位)的 vector。将每个条目初始化为 0。如果顶点 3 和顶点 5 之间存在边,则将 pow(2,5) 添加到属于顶点 3 且对称的我的向量的条目中。因此,当 3 和 5 之间有一条边时,顶点 5 位置的顶点 3 的条目中有一个 1。将我的图形插入此数据结构后,我认为应该能够在恒定时间内通过一个二元运算:3 和 5 是否相连?是的,如果 v[3] ^ pow(2,5) == 0。当顶点数多于 8 时,那么每个顶点都需要在向量中获得多个条目,我需要执行一个模和一个除法运算访问正确的位置。

您如何看待第二种解决方案 - 它可能已经知道并正在使用中吗? 考虑 O(1) 的访问我错了吗? 没有真正的性能改进是否需要付出很多努力?

将两种表示加载到一个大列表中的原因是我被告知缓存改进。

我很高兴收到关于这个想法的一些反馈。我可能会离开 - 在这种情况下请善待:D

解决方法

具有 200,000 条边的 1000x1000 矩阵将非常稀疏。由于图是无向图,矩阵中的边将被写入两次:

VerticeA -> VerticeB   and   VerticeB -> VerticeA

您最终将填满矩阵的 40%,其余为空。


边缘

我能想到的最好方法是使用布尔值的二维向量

std::vector<std::vector<bool>> matrix(1000,std::vector<bool>(1000,false));

查找将花费 O(1) 时间,std::vector<bool> 为每个布尔值使用一个位来节省空间。您最终将使用 1Mbit 或 ~128kB (125 kB) 的内存。

存储不一定是 bool 值数组,但库实现可能会优化存储,以便每个值都存储在一个位中。

这将允许您检查这样的边缘:

if( matrix[3][5] )
{
    // vertice 3 and 5 are connected
}
else
{
    // vertice 3 and 5 are not connected
}

顶点

如果顶点的 id 值形成一个连续的整数范围(例如 0,1,2,3,...,999),那么您可以将颜色信息存储在具有 O( 1) 访问时间:

std::vector<int> colors(1000);

这将消耗的内存等于:

1000 * sizeof(int) = 4000 B ~ 4 kB (3.9 kB)

另一方面,如果 id 值没有形成连续的整数范围,使用 std::vector<int> 可能是一个更好的主意,它平均会给你 O(1) 的查找时间。>

std::unordered_map<int,int> map;

所以例如存储和查找顶点 4 的颜色:

map[4] = 5;            // assign color 5 to vertice 4
std::cout << map[4];   // prints 5

std::unordered_map<int,int> 使用的内存量为:

1000 * 2 * sizeof(int) = 8000 B ~ 8 kB (7.81 kB)

一起,对于

类型 内存 访问时间
std::vector<std::vector<bool>> 125 KB O(1)

对于顶点

类型 内存 访问时间
std::vector<int> 3.9 KB O(1)
std::unordered_map<int,int> 7.8 KB 平均 O(1)。
,

如果你使用位矩阵,那么内存使用量是 O(V^2),所以 ~1Mb 位或 128KB,其中略少于一半是重复的。

如果您将边缘数组 O(E) 和另一个索引数组放入从顶点到其第一条边缘的边缘中,则使用 200K*sizeof(int) 或 800KB,这要多得多,它的一半也是重复的(AB 和 BA 是相同的),在这里实际上可以保存。如果您知道(或可以从中模板化)顶点的数量可以存储在 uint16_t 中,那么同样可以再次保存。

为了节省一半,您只需检查哪个顶点的编号较小并检查其边缘。

要确定何时停止查找,请使用下一个顶点的索引。

因此,对于您的数字,使用位矩阵很好,甚至很好。

第一个问题出现在 (V^2)/8 > (E*4) 时,尽管 Edge 算法中的二分搜索仍然比稍微检查慢得多。如果我们设置 E = V * 200(1000 个顶点 vs 200K 边),就会发生这种情况

V*V/8 > V*200*4
V/8 > 200*4
V > 200*4*8 = 6400

这将是 5120000 ~ 5MB 现在很容易放入 L3 缓存。如果连通性(此处为每个顶点的平均连接数)高于 200,则更好。

检查边缘也将花费 lg2(connectivity)*K(mispredicts) 这变得相当陡峭。检查位矩阵将是 O(1)。

您需要在位矩阵显着破坏 L3 而边缘列表仍然适合 L3 时以及何时溢出到虚拟内存中时进行测量。

换言之,如果连通性高,位矩阵应该击败具有更低连通性或更多顶点数量的 Edge 列表,Edge 列表可能更快。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...