问题描述
我需要你的专业知识:
我即将在 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
您如何看待第二种解决方案 - 它可能已经知道并正在使用中吗? 考虑 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 列表可能更快。