【问题标题】:Representation of a simple undirected graph简单无向图的表示
【发布时间】:2021-06-13 04:00:15
【问题描述】:

我需要你的专业知识:

我即将在 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 位)的向量。在 0 上初始化每个条目。如果顶点 3 和顶点 5 之间存在边,则将 pow(2,5) 添加到属于顶点 3 且对称的向量的条目中。因此,当 3 和 5 之间有一条边时,在顶点 5 的位置处的顶点 3 的条目中有一个 1。将我的图插入这个数据结构后,我认为应该能够在恒定时间内访问邻域二元运算:3 和 5 是否相连?是的,如果 v[3] ^ pow(2,5) == 0。当顶点数超过 8 个时,每个顶点都需要在向量中获得多个条目,我需要为访问正确的位置。

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

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

我很高兴收到有关此想法的一些反馈。我可能会走得很远 - 在这种情况下请善待:D

【问题讨论】:

  • 只是想说你可以实现一个邻接列表,这样你就可以回答以下形式的查询:u 和 v 是否在 O(1) 中连接。例如,对于每个顶点 u,您可以拥有 u 的所有邻居的哈希集。

标签: c++ data-structures graph discrete-mathematics


【解决方案1】:

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

VerticeA -> VerticeB   and   VerticeB -> VerticeA

你最终会填满矩阵的 40%,其余的都是空的。


边缘

我能想到的最佳方法是使用 2D 布尔向量

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

查找将花费 O(1) 时间,std::vector&lt;bool&gt; 通过为每个布尔值使用单个位来节省空间。您最终将使用 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&lt;int&gt; 中访问时间:

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

这将占用内存等于:

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

另一方面,如果 id 值不形成连续的整数范围,则使用std::unordered_map&lt;int, int&gt; 可能是一个更好的主意,这平均会为您提供 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&lt;int, int&gt; 使用的内存量将为:

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

所有在一起,对于边缘

Type Memory Access time
std::vector&lt;std::vector&lt;bool&gt;&gt; 125 kB O(1)

对于顶点

Type Memory Access time
std::vector&lt;int&gt; 3.9 kB O(1)
std::unordered_map&lt;int, int&gt; 7.8 kB O(1) on avg.

【讨论】:

  • 感谢您的回答!我不知道 std::vector 的优化存储——这很好。看起来很有希望 - 我想我会采用这种方法:)
【解决方案2】:

如果你使用位矩阵,那么内存使用量是 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 个顶点与 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,以及何时溢出到虚拟内存中。

换句话说,具有高连通性的位矩阵应该比连通性低得多或顶点数多得多的边缘列表要快,边缘列表可能会更快。

【讨论】:

  • 对于位矩阵,如果 a>b 使用交换 a&b 的相同技巧,您还可以节省(几乎)一半的内存。找到正确的位有点复杂,但这可能是值得的,因为在缓存中找到该位的机会几乎翻了一番(特别是如果经常写入边缘,尽管这里不是这种情况)。两者都非常容易实现并衡量哪个最快。
  • 很好的答案!在此基础上,我将能够明智地选择 - 实际上我认为我会使用位矩阵,因为具有大量顶点的密集图在我的案例中并不是特别感兴趣
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-23
  • 2018-07-14
  • 2011-06-14
  • 2016-05-22
  • 1970-01-01
  • 2012-11-09
相关资源
最近更新 更多