我有一个非常大的矩阵(100M行乘100M列),它们有很多重复的值,彼此相邻。例如:
8 8 8 8 8 8 8 8 8 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 8 8 8 8 8 8 8 8 8 8 8 8
8 8 3 3 3 3 3 3 3 3 3 3 3
我想要一个数据结构/算法来尽可能紧凑地存储像这样的基质。例如,上面的矩阵应该只占用O(1)空间(即使矩阵被任意拉大),因为只有一定数量的矩形区域,每个区域只有一个值。
重复发生在行和列之间,因此逐行压缩矩阵的简单方法不够好。 (这将需要至少O(num_rows)空间来存储任何矩阵。)
矩阵的表示也需要逐行访问,这样我就可以对列向量进行矩阵乘法。
答案 0 :(得分:13)
您可以将矩阵存储为quadtree,其中的叶子包含单个值。将其视为价值的二维“运行”。
答案 1 :(得分:10)
现在我的首选方法。
好的,正如我在之前的答案行中提到的那样,矩阵A中每列中的相同条目将在矩阵AB中相乘得到相同的结果。如果我们能够维持这种关系,那么理论上我们可以显着加快计算速度(分析器是你的朋友)。
在这个方法中,我们维护矩阵的行*列结构。
每行都用任何方法压缩,可以足够快地解压缩,不会过多地影响乘法速度。 RLE可能就足够了。
我们现在有一个压缩行列表。
我们使用熵编码方法(如Shannon-Fano,Huffman或算术编码),但我们不用此压缩行中的数据,我们用它来压缩行集。 我们用它来编码行的相对频率。即我们对待行的方式与标准熵编码处理字符/字节的方式相同。
在此示例中,RLE压缩 a 行,而Huffman压缩整行的 set 。
因此,例如,给定以下矩阵(前缀为行号,霍夫曼用于解释)
0 | 8 8 8 8 8 8 8 8 8 8 8 8 8 |
1 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
2 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
3 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
4 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
5 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
6 | 8 8 8 8 8 8 8 8 8 8 8 8 8 |
7 | 8 8 3 3 3 3 3 3 3 3 3 3 3 |
运行长度编码
0 | 8{13} |
1 | 8{1} 4{1} 8{2} 1{5} 8{4} |
2 | 8{1} 4{1} 8{2} 1{5} 8{4} |
3 | 8{1} 4{1} 8{2} 1{5} 8{4} |
4 | 8{1} 4{1} 8{2} 1{5} 8{4} |
5 | 8{1} 4{1} 8{2} 1{5} 8{4} |
6 | 8{13} |
7 | 8{2} 3{11} |
因此,0和6出现两次,1 - 5出现5次。 7只一次。
频率表
A: 5 (1-5) | 8{1} 4{1} 8{2} 1{5} 8{4} |
B: 2 (0,6) | 8{13} |
C: 1 7 | 8{2} 3{11} |
霍夫曼树
0|1
/ \
A 0|1
/ \
B C
因此,在这种情况下,需要一位(对于每一行)编码行1-5,并使用2位来编码行0,6和7。
(如果运行时间超过几个字节,那么就在你进行RLE时建立的哈希上对freq进行计数。)
存储Huffman树,唯一字符串和行编码位流。
关于Huffman的好处是它具有唯一的前缀属性,所以你总能知道你何时完成。因此,给定位串10000001011
,您可以从存储的唯一字符串和树重建矩阵A.编码的比特流告诉您行出现的顺序。
您可能希望研究自适应霍夫曼编码或其算术对应物。
看到A中具有相同列条目的行与AB相对于矢量B的相同结果相乘,您可以缓存结果并使用它而不是再次计算(如果可以的话,最好避免100M * 100M乘法)
链接到更多信息:
Arithmetic Coding + Statistical Modeling = Data Compression
比较
<强>未压缩强>
0 1 2 3 4 5 6 7
=================================
0 | 3 3 3 3 3 3 3 3 |
|-------+ +-------|
1 | 4 4 | 3 3 3 3 | 4 4 |
| +-----------+---+ |
2 | 4 4 | 5 5 5 | 1 | 4 4 |
| | | | |
3 | 4 4 | 5 5 5 | 1 | 4 4 |
|---+---| | | |
4 | 5 | 0 | 5 5 5 | 1 | 4 4 |
| | +---+-------+---+-------|
5 | 5 | 0 0 | 2 2 2 2 2 |
| | | |
6 | 5 | 0 0 | 2 2 2 2 2 |
| | +-------------------|
7 | 5 | 0 0 0 0 0 0 0 |
=================================
= 64字节
<强>四叉树强>
0 1 2 3 4 5 6 7
=================================
0 | 3 | 3 | | | 3 | 3 |
|---+---| 3 | 3 |---+---|
1 | 4 | 4 | | | 4 | 4 |
|-------+-------|-------+-------|
2 | | | 5 | 1 | |
| 4 | 5 |---+---| 4 |
3 | | | 5 | 1 | |
|---------------+---------------|
4 | 5 | 0 | 5 | 5 | 5 | 1 | 4 | 4 |
|---+---|---+---|---+---|---+---|
5 | 5 | 0 | 0 | 2 | 2 | 2 | 2 | 2 |
|-------+-------|-------+-------|
6 | 5 | 0 | 0 | 2 | 2 | 2 | 2 | 2 |
|---+---+---+---|---+---+---+---|
7 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
=================================
0 +- 0 +- 0 -> 3
| +- 1 -> 3
| +- 2 -> 4
| +- 3 -> 4
+- 1 -> 3
+- 2 -> 4
+- 3 -> 5
1 +- 0 -> 3
+- 1 +- 0 -> 3
| +- 1 -> 3
| +- 2 -> 4
| +- 3 -> 4
+- 2 +- 0 -> 5
| +- 1 -> 1
| +- 2 -> 5
| +- 3 -> 1
+- 3 -> 4
2 +- 0 +- 0 -> 5
| +- 1 -> 0
| +- 2 -> 5
| +- 3 -> 0
+- 1 +- 0 -> 5
| +- 1 -> 5
| +- 2 -> 0
| +- 3 -> 2
+- 2 +- 0 -> 5
| +- 1 -> 0
| +- 2 -> 5
| +- 3 -> 0
+- 3 +- 0 -> 0
+- 1 -> 2
+- 2 -> 0
+- 3 -> 0
3 +- 0 +- 0 -> 5
| +- 1 -> 1
| +- 2 -> 2
| +- 3 -> 2
+- 1 +- 0 -> 4
| +- 1 -> 4
| +- 2 -> 2
| +- 3 -> 2
+- 2 +- 0 -> 2
| +- 1 -> 2
| +- 2 -> 0
| +- 3 -> 0
+- 3 +- 0 -> 2
+- 1 -> 2
+- 2 -> 0
+- 3 -> 0
((1*4) + 3) + ((2*4) + 2) + (4 * 8) = 49 leaf nodes
49 * (2 + 1) = 147 (2 * 8 bit indexer, 1 byte data)
+ 14 inner nodes -> 2 * 14 bytes (2 * 8 bit indexers)
= 175 Bytes
区域哈希
0 1 2 3 4 5 6 7
=================================
0 | 3 3 3 3 3 3 3 3 |
|-------+---------------+-------|
1 | 4 4 | 3 3 3 3 | 4 4 |
| +-----------+---+ |
2 | 4 4 | 5 5 5 | 1 | 4 4 |
| | | | |
3 | 4 4 | 5 5 5 | 1 | 4 4 |
|---+---| | | |
4 | 5 | 0 | 5 5 5 | 1 | 4 4 |
| + - +---+-------+---+-------|
5 | 5 | 0 0 | 2 2 2 2 2 |
| | | |
6 | 5 | 0 0 | 2 2 2 2 2 |
| +-------+-------------------|
7 | 5 | 0 0 0 0 0 0 0 |
=================================
0: (4,1; 4,1), (5,1; 6,2), (7,1; 7,7) | 3
1: (2,5; 4,5) | 1
2: (5,3; 6,7) | 1
3: (0,0; 0,7), (1,2; 1,5) | 2
4: (1,0; 3,1), (1,6; 4,7) | 2
5: (2,2; 4,4), (4,0; 7,0) | 2
区域:(3 + 1 + 1 + 2 + 2 + 2)* 5 = 55字节{4字节矩形,1字节数据)
{查找表是一个排序数组,所以它不需要额外的存储空间}。
霍夫曼编码RLE
0 | 3 {8} | 1
1 | 4 {2} | 3 {4} | 4 {2} | 2
2,3 | 4 {2} | 5 {3} | 1 {1} | 4 {2} | 4
4 | 5 {1} | 0 {1} | 5 {3} | 1 {1} | 4 {2} | 5
5,6 | 5 {1} | 0 {2} | 2 {5} | 3
7 | 5 {1} | 0 {7} | 2
RLE Data: (1 + 3+ 4 + 5 + 3 + 2) * 2 = 36
Bit Stream: 20 bits packed into 3 bytes = 3
Huffman Tree: 10 nodes * 3 = 30
= 69 Bytes
一个巨大的RLE流
3{8};4{2};3{4};4{4};5{3};1{1};4{4};5{3};1{1};4{2};5{1};0{1};
5{3};1{1};4{2};5{1};0{2};2{5};5{1};0{2};2{5};5{1};0{7}
= 2 * 23 = 46 Bytes
使用公共前缀折叠编码的一个巨型RLE流
3{8};
4{2};3{4};
4{4};5{3};1{1};
4{4};5{3};
1{1};4{2};5{1};0{1};5{3};
1{1};4{2};5{1};0{2};2{5};
5{1};0{2};2{5};
5{1};0{7}
0 + 0 -> 3{8};4{2};3{4};
+ 1 -> 4{4};5{3};1{1};
1 + 0 -> 4{2};5{1} + 0 -> 0{1};5{3};1{1};
| + 1 -> 0{2}
|
+ 1 -> 2{5};5{1} + 0 -> 0{2};
+ 1 -> 0{7}
3{8};4{2};3{4} | 00
4{4};5{3};1{1} | 01
4{4};5{3};1{1} | 01
4{2};5{1};0{1};5{3};1{1} | 100
4{2};5{1};0{2} | 101
2{5};5{1};0{2} | 110
2{5};5{1};0{7} | 111
Bit stream: 000101100101110111
RLE Data: 16 * 2 = 32
Tree: : 5 * 2 = 10
Bit stream: 18 bits in 3 bytes = 3
= 45 bytes
答案 2 :(得分:4)
如果您的数据非常正常,您可能会受益于以结构化格式存储它;例如您的示例矩阵可能存储为以下“填充矩形”指令列表:
(0,0)-(13,7) = 8
(4,1)-(8,5) = 1
(然后要查找特定单元格的值,您将向后遍历列表,直到找到包含该单元格的矩形)
答案 3 :(得分:3)
正如Ira Baxter所说, 您可以将矩阵存储为四叉树,叶子包含单个值。
最简单的方法是使四叉树的每个节点覆盖一个区域2 ^ n x 2 ^ n, 每个非叶节点指向其4个大小为2 ^(n-1)x 2 ^(n-1)的子节点。
使用自适应四叉树可以获得稍微更好的压缩,允许不规则的细分。 然后每个非叶节点存储切点(B,G)并指向其4个子节点。 例如,如果某些非叶节点覆盖了从左上角的(A,F)到右下角的(C,H)的区域, 然后它的4个孩子覆盖区域 (A,F)至(B-1,G-1) (A,G)至(B-1,H) (B,F)至(C,G-1) (B,G)至(C,H)。
您将尝试为每个非叶节点选择(B,G)切割点,使其与数据中的某些实际划分对齐。
例如,假设你有一个矩阵,中间有一个小方块,里面有九个,其他地方都是零。
使用简单的两个四叉树幂,最终将得到至少21个节点:5个非叶节点,4个9叶节点和12个零叶节点。 (如果居中的小方块不是精确地距离左边缘和顶边缘的两个幂的距离,而不是本身的精确二次幂),你将得到更多的节点。 / p>
使用自适应四叉树,如果你足够聪明,可以在该方块的左上角选择根节点的切点,那么对于根的右下角的孩子,你可以在下方选择一个切点在方形的右角,你可以在9个节点中表示整个矩阵:2个非叶子节点,1个叶子节点用于9个节点,6个叶子节点用于零。
答案 4 :(得分:2)
你知道......间隔树吗?
间隔树是一种有效存储间隔的方法,然后查询它们。概括是Range Tree,可以适应任何维度。
在这里,您可以有效地描述矩形并为其附加值。当然,矩形可以重叠,这就是使它有效的原因。
0,0-n,n --> 8
4,4-7,7 --> 1
8,8-8,n --> 3
然后在查询某个特定位置的值时,会返回一个包含多个矩形的列表,需要确定最里面的一个:这是此位置的值。
答案 5 :(得分:1)
最简单的方法是在一个维度上使用游程编码,而不用担心其他维度。
(如果数据集不是那么庞大,将其解释为图像并使用标准的无损图像压缩方法也会非常简单 - 但是因为你必须努力使算法适用于稀疏矩阵,它不会那么简单。)
另一个简单的方法是尝试矩形泛光填充 - 从右上角的像素开始,然后将其增加到最大的矩形(宽度优先);然后将所有这些像素标记为“完成”并拍摄右上角剩余的像素,重复直到完成。 (您可能希望将这些矩形存储在某种BSP或四叉树中。)
一种非常有效的技术 - 不是最优的,但可能足够好 - 是使用二进制空间分区树,其中“空间”不是在空间上测量,而是由变化的数量来衡量。你会递归切割,以便你在左右两侧(或者顶部和底部 - 你想要保持正方形)有相同数量的变化,并且随着你的尺寸变小,所以你会削减尽可能多的尽可能改变。最终,你最终会切割出彼此分开的两个矩形,每个矩形都具有相同的数字;然后停下来(在X和y中用RLE编码会很快告诉你变化点在哪里。)
答案 6 :(得分:1)
对于尺寸为100M x 100M的矩阵,您对O(1)空间的描述令人困惑。当你有一个有限矩阵,那么你的大小是一个常数(除非生成矩阵的程序不会改变它)。因此,即使你将它与标量相乘,存储所需的空间量也是一个常数。绝对是读取和写入矩阵的时间不会是O(1)。
我可以想到稀疏矩阵可以减少存储这种矩阵所需的空间量。您可以将此稀疏矩阵写入文件并将其存储为tar.gz,这将进一步压缩数据。
我确实有一个问题,100M中的M表示什么?这是指兆字节/百万?如果是,则此矩阵大小将为100 x 10 ^ 6 x 100 x 10 ^ 6字节= 10 ^ 16/10 ^ 6 MB = 10 ^ 10/10 ^ 6 TB = 10 ^ 4 TB !!!你在用什么样的机器?
答案 7 :(得分:1)
我不确定为什么这个问题是社区维基,但事实如此。
我将依赖于您拥有线性代数应用程序的假设,并且您的矩阵具有矩形冗余类型。如果是这样,那么你可以比四叉树做得更好,比将矩阵切割成矩形更清晰(这通常是正确的想法)。
设M为你的矩阵,让v为你想要乘以M的向量,然后让 A是特殊矩阵
A = [1 -1 0 0 0]
[0 1 -1 0 0]
[0 0 1 -1 0]
[0 0 0 1 -1]
[0 0 0 0 1]
你还需要A的逆矩阵,我称之为B:
B = [1 1 1 1 1]
[0 1 1 1 1]
[0 0 1 1 1]
[0 0 0 1 1]
[0 0 0 0 1]
将矢量v乘以A是快速而简单的:您只需获取v的连续元素对的差异。将矢量v乘以B乘以快速且简单:Bv的条目是v的元素的部分和那你想用等式
Mv = B AMA B v
矩阵AMA是稀疏的:在中间,每个条目是M个4个条目的交替和,形成2 x 2平方。你必须在M的一个矩形的一角,这个交替的和是非零的。由于AMA是稀疏的,您可以将其非零条目存储在关联数组中,并使用稀疏矩阵乘法将其应用于向量。
答案 8 :(得分:0)
我对您显示的矩阵没有具体的答案。在有限元分析(FEA)中,您有包含冗余数据的矩阵。在我的研究生项目中实施FEA包时,我使用了天际线存储方法。
一些链接:
答案 9 :(得分:0)
首先要尝试的是现有的库和解决方案。让自定义格式最终处理您想要的所有操作需要做很多工作。稀疏矩阵是一个老问题,因此请务必阅读现有内容。
假设您找不到合适的内容,我建议使用基于行的格式。不要试图用超紧凑的表示来过于花哨,最终需要为代码中的每个小操作和错误进行大量处理。而是尝试分别压缩每一行。你知道你将不得不扫描每一行的矩阵向量乘法,让你自己很轻松。
我将从运行长度编码开始,看看它是如何工作的。一旦它工作,尝试添加一些技巧,如引用前一行的部分。因此,行可能被编码为:126个零,8个,直接从上面的行复制的1000个条目,32个零。对你的例子来说,这似乎非常有效。
答案 10 :(得分:0)
上述许多解决方案都很好。
如果您正在使用文件,请考虑面向文件 压缩工具,如压缩,bzip,zip,bzip2和朋友。 它们工作得很好,特别是如果数据包含冗余 ASCII字符。使用外部压缩工具消除了 代码中的问题和挑战,并将压缩 二进制和ASCII数据。
在您的示例中,您显示一个字符数字。 数字0-9可以用较小的四位表示 编码模式。您可以使用其他位 一个字节作为计数。四位为您提供额外的代码 逃避额外...但有一个谨慎到达 回到旧的Y2K错误,其中使用了两个字符 一年。来自一组的字节编码会给出 255年和相同的两个字节将跨越所有书面 历史,然后一些。
答案 11 :(得分:0)
您可能需要查看GIF format及其压缩算法。只需将您的矩阵视为位图......
答案 12 :(得分:0)
让我检查一下我的假设,如果没有其他原因,而不是指导我对这个问题的思考:
可以尝试各种聪明的方案来检测矩形或自相似性等,但这最终会在进行乘法时损害性能。我提出了两个相对简单的解决方案。
我将不得不向后退一步,所以请耐心等待我。
如果数据主要偏向于水平重复,则以下情况可能会很好。
将矩阵展平为数组(这实际上就是它存储在内存中的方式)。 E.g。
A
| w0 w1 w2 |
| x0 x1 x2 |
| y0 y1 y2 |
| z0 z1 z2 |
变为
A’
| w0 w1 w2 x0 x1 x2 y0 y1 y2 z0 z1 z2 |
我们可以使用任何索引[i,j] = i * j.
因此,当我们进行乘法运算时,我们迭代“矩阵”数组A',其中k = [0..m * n-1]并使用(k mod n)索引到向量B中,并使用向量AB转换为向量AB (k div n)。 “div”是整数除法。
所以,例如,A[10] = z1
。 10 mod 3 = 1
和10 div 3 = 3 A[3,1] = z1.
现在,进行压缩。 我们进行磨机运行长度编码(RLE)的正常运行,但是对A'而不是A.对于扁平阵列,将会有更长的重复序列,因此压缩效果更好。然后在对运行进行编码之后,我们执行另一个过程,我们提取公共子串。我们可以做一种字典压缩形式,或者将运行数据处理成某种形式的空间优化图形,如基数树/后缀树或自己创建的合并顶部和尾部的设备。该图应该包含数据中所有唯一字符串的表示。您可以选择任意数量的方法将流分解为字符串:匹配前缀,长度或其他内容(最适合您的图形)但是在运行边界上执行,而不是字节,否则您的解码将变得更加复杂。当我们解压缩流时,图形成为状态机。
我将使用位流和Patricia trie作为示例,因为它最简单,但您可以使用其他内容(每个状态更多位更改更好合并等等。通过{{查找论文3}})。
为了压缩运行数据,我们针对图形构建了一个哈希表。该表将字符串映射到位序列。您可以通过遍历图形并将每个左分支编码为0并将右分支编码为1(任意选择)来完成此操作。
处理运行数据并构建一个位字符串,直到在哈希表中得到匹配,输出位并清除字符串(这些位不在字节边界上,所以你可能需要缓冲直到你得到一个足够长的序列来写出来)。冲洗并重复,直到您处理完整的运行数据流。您存储图形和位流。比特流对字符串进行编码,而不是字节。
如果您反转该过程,使用位流来遍历图形,直到到达叶子/终端节点,您将返回原始运行数据,您可以动态解码该数据以生成您乘以的整数流得到AB的向量B.每次运行耗尽时,都会读取下一位并查找其对应的字符串。我们不关心我们没有随机访问A,因为我们只需要它在B中(B可以是范围/间隔压缩但不需要)。
因此,即使RLE偏向水平运行,我们仍然可以获得良好的垂直压缩,因为常见字符串只存储一次。
我将在单独的答案中解释另一种方法,因为它太长了,但是由于矩阵A中的重复行与AB中的相同结果相乘,该方法实际上可以加速计算。 / p>
答案 13 :(得分:0)
你需要一个压缩算法尝试 RLE (运行长度编码),当数据是 高度冗余。