我有一台具有1 MB RAM且没有其他本地存储的计算机。我必须使用它来通过TCP连接接受一百万个8位十进制数字,对它们进行排序,然后通过另一个TCP连接将排序后的列表发送出去。
数字列表可能包含重复项,我不能丢弃。该代码将放置在ROM中,因此我不必从1 MB中减去代码的大小。我已经有了驱动以太网端口和处理TCP / IP连接的代码,它的状态数据需要2 KB,包括一个1 KB的缓冲区,代码将通过该缓冲区读写数据。有解决这个问题的方法吗?
问题和答案的来源:
我有一台具有1 MB RAM且没有其他本地存储的计算机。我必须使用它来通过TCP连接接受一百万个8位十进制数字,对它们进行排序,然后通过另一个TCP连接将排序后的列表发送出去。
数字列表可能包含重复项,我不能丢弃。该代码将放置在ROM中,因此我不必从1 MB中减去代码的大小。我已经有了驱动以太网端口和处理TCP / IP连接的代码,它的状态数据需要2 KB,包括一个1 KB的缓冲区,代码将通过该缓冲区读写数据。有解决这个问题的方法吗?
问题和答案的来源:
Answers:
到目前为止,这里还没有提到一个相当狡猾的把戏。我们假设您没有额外的存储数据的方法,但是严格来说并非如此。
解决问题的一种方法是执行以下可怕的事情,任何人在任何情况下都不应尝试以下事情:使用网络流量存储数据。不,我不是说NAS。
您可以通过以下方式仅用几个字节的RAM对数字进行排序:
COUNTER
和VALUE
。0
;I
,递增COUNTER
并设置VALUE
为max(VALUE, I)
;I
到路由器。擦除I
并重复。一旦COUNTER
到达1000000
,您就将所有值存储在不断的ICMP请求流中,并且VALUE
现在包含最大整数。选择一些threshold T >> 1000000
。设置COUNTER
为零。每次收到ICMP数据包时,都将递增COUNTER
并I
在另一个回显请求中将包含的整数发送出去,除非I=VALUE
,在这种情况下,将其传输到已排序整数的目的地。一旦COUNTER=T
,递减VALUE
的1
,复位COUNTER
至零和重复。一旦VALUE
达到零,您应该按从最大到最小的顺序将所有整数传输到目的地,并且仅将约47位RAM用于两个持久变量(以及临时值所需的任何数量)。
我知道这很可怕,而且我知道可能存在各种各样的实际问题,但是我认为这可能会让你们中的一些人大笑或至少使您恐惧。
满足内存约束的证明:
编辑器:在本文或他的博客中,没有提供作者提供的最大内存要求的证据。由于对一个值进行编码所需的位数取决于先前编码的值,因此这种证明可能并非无关紧要。作者注意到,根据经验1011732
,他可能偶然发现的最大编码大小为,并1013000
任意选择了缓冲区大小。
typedef unsigned int u32;
namespace WorkArea
{
static const u32 circularSize = 253250;
u32 circular[circularSize] = { 0 }; // consumes 1013000 bytes
static const u32 stageSize = 8000;
u32 stage[stageSize]; // consumes 32000 bytes
...
这两个阵列一起占用1045000字节的存储空间。剩下的1048576-1045000-2×1024 = 1528个字节可用于剩余变量和堆栈空间。
它在我的至强W3520上运行约23秒。假设程序名称为,您可以使用以下Python脚本验证程序是否正常运行sort1mb.exe
。
from subprocess import *
import random
sequence = [random.randint(0, 99999999) for i in xrange(1000000)]
sorter = Popen('sort1mb.exe', stdin=PIPE, stdout=PIPE)
for value in sequence:
sorter.stdin.write('%08d\n' % value)
sorter.stdin.close()
result = [int(line) for line in sorter.stdout]
print('OK!' if result == sorted(sequence) else 'Error!')
有关该算法的详细说明,请参见以下系列文章:
(n+m)!/(n!m!)
),所以你一定是对的。大概是我的估计,b位的增量需要b位来存储-显然0的增量就不需要0位来存储。
请参阅第一个正确答案或以后的算术编码答案。您可能会在下面找到一些有趣的东西,但并不是100%的防弹解决方案。
这是一个非常有趣的任务,这是另一个解决方案。我希望有人会觉得结果有用(或至少很有趣)。
阶段1:初始数据结构,粗略压缩方法,基本结果
让我们做一些简单的数学运算:我们最初有1M(1048576字节)RAM用于存储10 ^ 6 8位十进制数字。[0; 99999999]。因此,要存储一个数字,需要27位(假设将使用无符号数字)。因此,要存储原始流,需要约3.5M的RAM。有人已经说过这似乎不可行,但是我要说的是,只要输入“足够好”就可以解决任务。基本上,该想法是使用0.29或更高的压缩因子压缩输入数据,并以适当的方式进行排序。
让我们首先解决压缩问题。已经有一些相关的测试:
http://www.theeggeadventure.com/wikimedia/index.php/Java_Data_Compression
“我进行了一项测试,使用各种压缩形式压缩一百万个连续整数。结果如下:”
None 4000027
Deflate 2006803
Filtered 1391833
BZip2 427067
Lzma 255040
看起来LZMA(Lempel-Ziv-Markov链算法)是继续的好选择。我已经准备了一个简单的PoC,但仍有一些细节需要强调:
请注意,附加代码是POC,不能用作最终解决方案,它只是演示了使用几个较小的缓冲区以某种最佳方式(可能是压缩的)存储预排序数字的想法。不建议将LZMA作为最终解决方案。它用作向此PoC引入压缩的最快方法。
请参阅下面的PoC代码(请注意,这只是一个演示,需要使用LZMA-Java进行编译):
public class MemorySortDemo {
static final int NUM_COUNT = 1000000;
static final int NUM_MAX = 100000000;
static final int BUCKETS = 5;
static final int DICT_SIZE = 16 * 1024; // LZMA dictionary size
static final int BUCKET_SIZE = 1024;
static final int BUFFER_SIZE = 10 * 1024;
static final int BUCKET_RANGE = NUM_MAX / BUCKETS;
static class Producer {
private Random random = new Random();
public int produce() { return random.nextInt(NUM_MAX); }
}
static class Bucket {
public int size, pointer;
public int[] buffer = new int[BUFFER_SIZE];
public ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
public DataOutputStream tempDataOut = new DataOutputStream(tempOut);
public ByteArrayOutputStream compressedOut = new ByteArrayOutputStream();
public void submitBuffer() throws IOException {
Arrays.sort(buffer, 0, pointer);
for (int j = 0; j < pointer; j++) {
tempDataOut.writeInt(buffer[j]);
size++;
}
pointer = 0;
}
public void write(int value) throws IOException {
if (isBufferFull()) {
submitBuffer();
}
buffer[pointer++] = value;
}
public boolean isBufferFull() {
return pointer == BUFFER_SIZE;
}
public byte[] compressData() throws IOException {
tempDataOut.close();
return compress(tempOut.toByteArray());
}
private byte[] compress(byte[] input) throws IOException {
final BufferedInputStream in = new BufferedInputStream(new ByteArrayInputStream(input));
final DataOutputStream out = new DataOutputStream(new BufferedOutputStream(compressedOut));
final Encoder encoder = new Encoder();
encoder.setEndMarkerMode(true);
encoder.setNumFastBytes(0x20);
encoder.setDictionarySize(DICT_SIZE);
encoder.setMatchFinder(Encoder.EMatchFinderTypeBT4);
ByteArrayOutputStream encoderPrperties = new ByteArrayOutputStream();
encoder.writeCoderProperties(encoderPrperties);
encoderPrperties.flush();
encoderPrperties.close();
encoder.code(in, out, -1, -1, null);
out.flush();
out.close();
in.close();
return encoderPrperties.toByteArray();
}
public int[] decompress(byte[] properties) throws IOException {
InputStream in = new ByteArrayInputStream(compressedOut.toByteArray());
ByteArrayOutputStream data = new ByteArrayOutputStream(10 * 1024);
BufferedOutputStream out = new BufferedOutputStream(data);
Decoder decoder = new Decoder();
decoder.setDecoderProperties(properties);
decoder.code(in, out, 4 * size);
out.flush();
out.close();
in.close();
DataInputStream input = new DataInputStream(new ByteArrayInputStream(data.toByteArray()));
int[] array = new int[size];
for (int k = 0; k < size; k++) {
array[k] = input.readInt();
}
return array;
}
}
static class Sorter {
private Bucket[] bucket = new Bucket[BUCKETS];
public void doSort(Producer p, Consumer c) throws IOException {
for (int i = 0; i < bucket.length; i++) { // allocate buckets
bucket[i] = new Bucket();
}
for(int i=0; i< NUM_COUNT; i++) { // produce some data
int value = p.produce();
int bucketId = value/BUCKET_RANGE;
bucket[bucketId].write(value);
c.register(value);
}
for (int i = 0; i < bucket.length; i++) { // submit non-empty buffers
bucket[i].submitBuffer();
}
byte[] compressProperties = null;
for (int i = 0; i < bucket.length; i++) { // compress the data
compressProperties = bucket[i].compressData();
}
printStatistics();
for (int i = 0; i < bucket.length; i++) { // decode & sort buckets one by one
int[] array = bucket[i].decompress(compressProperties);
Arrays.sort(array);
for(int v : array) {
c.consume(v);
}
}
c.finalCheck();
}
public void printStatistics() {
int size = 0;
int sizeCompressed = 0;
for (int i = 0; i < BUCKETS; i++) {
int bucketSize = 4*bucket[i].size;
size += bucketSize;
sizeCompressed += bucket[i].compressedOut.size();
System.out.println(" bucket[" + i
+ "] contains: " + bucket[i].size
+ " numbers, compressed size: " + bucket[i].compressedOut.size()
+ String.format(" compression factor: %.2f", ((double)bucket[i].compressedOut.size())/bucketSize));
}
System.out.println(String.format("Data size: %.2fM",(double)size/(1014*1024))
+ String.format(" compressed %.2fM",(double)sizeCompressed/(1014*1024))
+ String.format(" compression factor %.2f",(double)sizeCompressed/size));
}
}
static class Consumer {
private Set<Integer> values = new HashSet<>();
int v = -1;
public void consume(int value) {
if(v < 0) v = value;
if(v > value) {
throw new IllegalArgumentException("Current value is greater than previous: " + v + " > " + value);
}else{
v = value;
values.remove(value);
}
}
public void register(int value) {
values.add(value);
}
public void finalCheck() {
System.out.println(values.size() > 0 ? "NOT OK: " + values.size() : "OK!");
}
}
public static void main(String[] args) throws IOException {
Producer p = new Producer();
Consumer c = new Consumer();
Sorter sorter = new Sorter();
sorter.doSort(p, c);
}
}
使用随机数会产生以下结果:
bucket[0] contains: 200357 numbers, compressed size: 353679 compression factor: 0.44
bucket[1] contains: 199465 numbers, compressed size: 352127 compression factor: 0.44
bucket[2] contains: 199682 numbers, compressed size: 352464 compression factor: 0.44
bucket[3] contains: 199949 numbers, compressed size: 352947 compression factor: 0.44
bucket[4] contains: 200547 numbers, compressed size: 353914 compression factor: 0.44
Data size: 3.85M compressed 1.70M compression factor 0.44
对于一个简单的升序(使用一个存储桶),它将产生:
bucket[0] contains: 1000000 numbers, compressed size: 256700 compression factor: 0.06
Data size: 3.85M compressed 0.25M compression factor 0.06
编辑
结论:
第二阶段:增强压缩效果,最终结论
如前一节所述,可以使用任何合适的压缩技术。因此,让我们摆脱LZMA的束缚,转而采用更简单,更好(如果可能)的方法。有很多好的解决方案,包括算术编码,基数树等。
无论如何,简单但有用的编码方案将比另一个外部库更具说明性,并提供了一些漂亮的算法。实际的解决方案非常简单:由于存在带有部分排序数据的存储桶,因此可以使用增量代替数字。
随机输入测试显示出更好的结果:
bucket[0] contains: 10103 numbers, compressed size: 13683 compression factor: 0.34
bucket[1] contains: 9885 numbers, compressed size: 13479 compression factor: 0.34
...
bucket[98] contains: 10026 numbers, compressed size: 13612 compression factor: 0.34
bucket[99] contains: 10058 numbers, compressed size: 13701 compression factor: 0.34
Data size: 3.85M compressed 1.31M compression factor 0.34
样例代码
public static void encode(int[] buffer, int length, BinaryOut output) {
short size = (short)(length & 0x7FFF);
output.write(size);
output.write(buffer[0]);
for(int i=1; i< size; i++) {
int next = buffer[i] - buffer[i-1];
int bits = getBinarySize(next);
int len = bits;
if(bits > 24) {
output.write(3, 2);
len = bits - 24;
}else if(bits > 16) {
output.write(2, 2);
len = bits-16;
}else if(bits > 8) {
output.write(1, 2);
len = bits - 8;
}else{
output.write(0, 2);
}
if (len > 0) {
if ((len % 2) > 0) {
len = len / 2;
output.write(len, 2);
output.write(false);
} else {
len = len / 2 - 1;
output.write(len, 2);
}
output.write(next, bits);
}
}
}
public static short decode(BinaryIn input, int[] buffer, int offset) {
short length = input.readShort();
int value = input.readInt();
buffer[offset] = value;
for (int i = 1; i < length; i++) {
int flag = input.readInt(2);
int bits;
int next = 0;
switch (flag) {
case 0:
bits = 2 * input.readInt(2) + 2;
next = input.readInt(bits);
break;
case 1:
bits = 8 + 2 * input.readInt(2) +2;
next = input.readInt(bits);
break;
case 2:
bits = 16 + 2 * input.readInt(2) +2;
next = input.readInt(bits);
break;
case 3:
bits = 24 + 2 * input.readInt(2) +2;
next = input.readInt(bits);
break;
}
buffer[offset + i] = buffer[offset + i - 1] + next;
}
return length;
}
请注意,这种方法:
完整的代码可以在这里找到,BinaryInput和BinaryOutput实现可以在这里找到
定论
没有最终结论:)有时候,将自己升级到一个级别并从元级别的角度审查任务确实是个好主意。
花一些时间来完成这项任务很有趣。顺便说一句,下面有很多有趣的答案。感谢您的关注和满意。
仅由于1兆字节和1百万字节之间的差异,才可能找到解决方案。8093729.5的幂大约有2种,可以选择100万个允许重复且顺序不重要的8位数字,因此,只有100万字节RAM的计算机没有足够的状态来表示所有可能性。但是1M(对于TCP / IP,小于2k)是1022 * 1024 * 8 = 8372224位,因此可以解决。
第1部分,初始解决方案
这种方法所需的资源略高于1M,我将对其进行改进以使其在以后适应1M。
我将数字的紧凑排序列表存储在0到99999999之间,作为7位数字子列表的序列。第一个子列表保存从0到127的数字,第二个子列表保存从128到255的数字,依此类推。100000000/128恰好是781250,因此需要781250这样的子列表。
每个子列表由一个2位子列表标头和一个子列表正文组成。子列表主体每个子列表条目占用7位。子列表全部串联在一起,并且格式可以告诉一个子列表在哪里结束,下一个开始。完全填充的列表所需的总存储量为2 * 781250 + 7 * 1000000 = 8562500位,大约为1.021 M字节。
4个可能的子列表标头值为:
00空的子列表,什么也没有。
01 Singleton,子列表中只有一个条目,接下来的7位保存该条目。
10子列表至少包含2个不同的数字。条目以非降序存储,但最后一个条目小于或等于第一个条目。这样可以确定子列表的末尾。例如,数字2,4,6将存储为(4,6,2)。数字2,2,3,4,4将存储为(2,3,4,4,2)。
11子列表包含一个数字的2个或更多重复。接下来的7位给出数字。然后是零个或多个值为1的7位条目,然后是值为0的7位条目。子列表主体的长度决定了重复次数。例如,数字12,12将存储为(12,0),数字12,12,12将存储为(12,1,0),12,12,12,12将存储为(12,1 ,1,0)等。
我从一个空列表开始,读入一堆数字并将其存储为32位整数,将新数字排序到位(可能使用堆排序),然后将它们合并为新的紧凑排序列表。重复直到没有更多的数字可读取,然后再次遍历压缩列表以生成输出。
下面的行表示列表合并操作开始之前的内存。“ O”是保存排序的32位整数的区域。“ X”是保存旧压缩列表的区域。“ =”符号是紧凑列表的扩展空间,“ O”中的每个整数7位。“ Z”是其他随机开销。
ZZZOOOOOOOOOOOOOOOOOOOOOOOOOO==========XXXXXXXXXXXXXXXXXXXXXXXXXX
合并例程从最左边的“ O”和最左边的“ X”开始读取,并从最左边的“ =”开始写入。在合并所有新的整数之前,写入指针不会捕获紧凑列表读取指针,因为两个指针对于旧子压缩列表中的每个子列表前进2位,对于每个条目前进7位,并且有足够的额外空间来容纳压缩列表读取指针。新数字的7位输入。
第2部分,塞入1M
要将上述解决方案压缩为1M,我需要使压缩列表格式更紧凑。我将摆脱一种子列表类型,以便只有3种可能的子列表标头值。然后,我可以使用“ 00”,“ 01”和“ 1”作为子列表标题值并保存一些位。子列表类型为:
一个空的子列表,什么也没有。
B Singleton,子列表中只有一个条目,接下来的7位保存该条目。
C子列表至少包含2个不同的数字。条目以非降序存储,但最后一个条目小于或等于第一个条目。这样可以确定子列表的末尾。例如,数字2,4,6将存储为(4,6,2)。数字2,2,3,4,4将存储为(2,3,4,4,2)。
D子列表由2个或多个重复的单个数字组成。
我的3个子列表标题值为“ A”,“ B”和“ C”,因此我需要一种表示D型子列表的方法。
假设我有C型子列表标头,后跟3个条目,例如“ C [17] [101] [58]”。如上所述,这不能成为有效C类型子列表的一部分,因为第三个条目小于第二个条目,但大于第一个条目。我可以使用这种类型的构造来表示D型子列表。用位表示,在任何我有“ C {00 ?????} {1 ??????} {01 ?????}”的地方,都是不可能的C类型子列表。我将使用它来表示一个由3个或更多重复的单个数字组成的子列表。前两个7位字对数字进行编码(下面的“ N”位),后跟零个或多个{0100001}字,后跟一个{0100000}字。
For example, 3 repetitions: "C{00NNNNN}{1NN0000}{0100000}", 4 repetitions: "C{00NNNNN}{1NN0000}{0100001}{0100000}", and so on.
剩下的列表只包含一个数字的2个重复。我将用另一个不可能的C型子列表模式来代表它们:“ C {0 ??????} {11 ??????} {10 ?????}”。前两个字中的数字的7位有足够的空间,但是此模式比它表示的子列表更长,这使事情变得更加复杂。可以将结尾处的五个问号视为模式的一部分,因此我将“ C {0NNNNNN} {11N ????} 10”作为我的模式,并将要重复的数字存储在“ N”中的。那太长了2位。
在这种模式下,我将不得不借用2个比特并从4个未使用的比特中偿还它们。读取时,遇到“ C {0NNNNNN} {11N00AB} 10”时,输出2个以“ N”表示的数字实例,在末尾用位A和B覆盖“ 10”,然后将读取指针后退2位。此算法可以进行破坏性读取,因为每个紧凑列表仅被遍历一次。
当写一个由2个重复组成的子列表时,写“ C {0NNNNNN} 11N00”并将借位计数器设置为2。在每次写时借位计数器为非零值时,写的每一位都会递减,当计数器为零时,将写入“ 10”。因此,接下来写入的2位将进入插槽A和B,然后将“ 10”放到末尾。
使用“ 00”,“ 01”和“ 1”表示的3个子列表标题值,我可以将“ 1”分配给最受欢迎的子列表类型。我需要一个小表将子列表标题值映射到子列表类型,并且每个子列表类型都需要一个出现计数器,以便知道最佳的子列表标题映射是什么。
当所有子列表类型都同样受欢迎时,最坏的情况就是最小化完全填充的紧凑列表的表示。在那种情况下,我为每3个子列表标题保存1位,因此列表大小为2 * 781250 + 7 * 1000000-781250/3 = 8302083.3位。向上舍入到32位字边界,即8302112位或1037764字节。
1M减去TCP / IP状态的2k,缓冲区为1022 * 1024 = 1046528字节,剩下8764字节可用来玩。
但是,更改子列表标头映射的过程呢?在下面的内存映射中,“ Z”是随机开销,“ =”是可用空间,“ X”是压缩列表。
ZZZ=====XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
从最左边的“ X”开始阅读,并从最左边的“ =”开始书写,然后向右工作。完成后,压缩列表将短一些,并且将位于错误的内存末尾:
ZZZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=======
因此,我需要将其右移:
ZZZ=======XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
在头映射更改过程中,多达1/3的子列表头将从1位更改为2位。在最坏的情况下,这些都将排在列表的首位,因此在开始之前,我至少需要781250/3位可用存储空间,这使我回到了精简版以前版本的内存要求: (
为了解决这个问题,我将781250个子列表分为10个子列表,每个子列表包含78125个子列表。每个组都有自己的独立子列表标头映射。将字母A到J用于组:
ZZZ=====AAAAAABBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
在子列表标头映射更改期间,每个子列表组都会缩小或保持不变:
ZZZ=====AAAAAABBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAA=====BBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABB=====CCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCC======DDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDD======EEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEE======FFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFF======GGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGG=======HHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHH=======IJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHI=======JJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ=======
ZZZ=======AAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
在映射更改期间,子列表组的临时扩展的最坏情况是78125/3 = 26042位,小于4k。如果我允许4k加上1037764字节来填充一个完全填充的压缩列表,那么内存映射中的“ Z”将剩下8764-4096 = 4668字节。
对于10个子列表标头映射表,30个子列表标头出现计数以及我需要的其他一些计数器,指针和小缓冲区,以及我不注意使用的空间(例如,函数调用返回地址的堆栈空间和局部变量。
第三部分,运行需要多长时间?
如果列表为空,则1位列表头将用于空子列表,列表的起始大小为781250位。在最坏的情况下,列表中每个增加的数字都会增长8位,因此,要将32位数字中的每个数字都放在列表缓冲区的顶部,然后对其进行排序和合并,则需要32 + 8 = 40位可用空间。在最坏的情况下,更改子列表标头映射会导致2 * 781250 + 7 *条目781250/3位的空间使用。
如果在列表中至少有80万个数字之后,每五次合并后更改子列表标头映射的策略,最坏的情况将涉及总共约30M的紧凑列表读写活动。
资源:
3 * 3 * 3 = 27 < 32
。你把它们结合起来combined_subheader = subheader1 + 3 * subheader2 + 9 * subheader3
。
吉尔马诺夫的答案在其假设中是非常错误的。它开始基于一百万个连续整数的无意义度量进行推测。这意味着没有差距。那些随机的间隙,无论多么小,确实使它成为一个糟糕的主意。
自己尝试。获得1百万个随机的27位整数,对其进行排序,并使用7-Zip,xz 压缩,无论您要使用哪种LZMA。结果超过1.5 MB。最重要的前提是压缩序号。即使是增量编码,也超过1.1 MB。没关系,这正在使用超过100 MB的RAM进行压缩。因此,即使压缩的整数也无法解决问题,并且不必担心运行时RAM的使用情况。
让我感到难过的是,人们如何赞成漂亮的图形和合理化。
#include <stdint.h>
#include <stdlib.h>
#include <time.h>
int32_t ints[1000000]; // Random 27-bit integers
int cmpi32(const void *a, const void *b) {
return ( *(int32_t *)a - *(int32_t *)b );
}
int main() {
int32_t *pi = ints; // Pointer to input ints (REPLACE W/ read from net)
// Fill pseudo-random integers of 27 bits
srand(time(NULL));
for (int i = 0; i < 1000000; i++)
ints[i] = rand() & ((1<<27) - 1); // Random 32 bits masked to 27 bits
qsort(ints, 1000000, sizeof (ints[0]), cmpi32); // Sort 1000000 int32s
// Now delta encode, optional, store differences to previous int
for (int i = 1, prev = ints[0]; i < 1000000; i++) {
ints[i] -= prev;
prev += ints[i];
}
FILE *f = fopen("ints.bin", "w");
fwrite(ints, 4, 1000000, f);
fclose(f);
exit(0);
}
现在用LZMA压缩ints.bin ...
$ xz -f --keep ints.bin # 100 MB RAM
$ 7z a ints.bin.7z ints.bin # 130 MB RAM
$ ls -lh ints.bin*
3.8M ints.bin
1.1M ints.bin.7z
1.2M ints.bin.xz
我认为,从组合学的角度考虑这一问题的一种方法是:有多少种可能的排序数字顺序组合?如果我们给组合0,0,0,.....,0代号0,0,0,0,...,1代号1,而99999999、99999999,... 99999999代号N,什么是N?换句话说,结果空间有多大?
好吧,考虑这一点的一种方法是注意到这是在N x M网格中找到单调路径数的问题的一个双射,其中N = 1,000,000,M = 100,000,000。换句话说,如果您有一个1,000,000宽,100,000,000高的网格,那么从左下角到右上角的最短路径有多少条?当然,最短的路径只要求您向右或向上移动(如果要向下或向左移动,则将撤消先前已完成的进度)。若要查看这是我们的数字排序问题的双射,请注意以下几点:
您可以将路径中的任何水平支腿想象成订购中的数字,其中支腿的Y位置代表值。
因此,如果路径只是简单地一直向右移动到最后,然后一直跳到顶部,这相当于排序0,0,0,...,0。相反,如果它从一路跳到顶部开始,然后向右移动1,000,000次,相当于99999999,99999999,...,99999999。该路径向右移动一次,然后向上移动一次,然后向右移动一次,然后上升一次,以此类推,直到最后(然后必须一直跳到顶部),等于0、1、2、3,...,999999。
幸运的是,对于我们来说,这个问题已经解决了,这样的网格具有(N + M)个选择(M)个路径:
(1,000,000 + 100,000,000)选择(100,000,000)〜= 2.27 * 10 ^ 2436455
N因此等于2.27 * 10 ^ 2436455,因此代码0表示0,0,0,...,0,代码2.27 * 10 ^ 2436455,并且一些更改表示99999999,99999999,...,99999999。
为了存储从0到2.27 * 10 ^ 2436455的所有数字,您需要lg2(2.27 * 10 ^ 2436455)= 8.0937 * 10 ^ 6位。
1兆字节= 8388608位> 8093700位
因此,看来我们至少实际上有足够的空间来存储结果!当然,现在有趣的是,随着数字的流进来,正在进行排序。不确定是否有最佳的解决方案,我们还剩下294908位。我想象一种有趣的技术是在每个点上假设这是整个排序,找到该排序的代码,然后在您收到一个返回新数字并更新先前代码的代码时。手波手波。
我的建议在很大程度上归功于Dan的解决方案
首先,我认为解决方案必须处理所有可能的输入列表。我认为,流行的答案并未做出这种假设(IMO是一个巨大的错误)。
众所周知,没有任何形式的无损压缩会减小所有输入的大小。
所有流行的答案都假定他们将能够有效地施加压缩以为其留出更多空间。实际上,有大量的额外空间,它们足以以未压缩的形式保存部分完成的列表的一部分,并允许他们执行排序操作。这只是一个错误的假设。
对于这样的解决方案,任何知道如何进行压缩的人都将能够设计出一些对于该方案无法很好压缩的输入数据,并且“解决方案”很可能会因为空间不足而中断。
相反,我采用数学方法。我们可能的输出是长度为LEN的所有列表,这些列表由范围为0..MAX的元素组成。此处LEN为1,000,000,而我们的MAX为100,000,000。
对于任意LEN和MAX,编码此状态所需的位数为:
Log2(MAX多选LEN)
因此,对于我们的数字,一旦完成接收和排序,我们将至少需要Log2(100,000,000 MC 1,000,000)位,以便可以唯一区分所有可能的输出的方式存储结果。
这是〜= 988kb。因此,我们实际上有足够的空间来保存结果。从这个角度来看,这是可能的。
[现在已经有了更好的例子,删除了毫无意义的闲逛...]
最佳答案在这里。
另一个好的答案在这里,基本上使用插入排序作为将列表扩展一个元素的功能(缓冲几个元素和预排序,以允许一次插入多个,节省了一些时间)。也使用了很好的紧凑状态编码,七位增量的存储桶
假设此任务是可能的。在输出之前,将在内存中显示百万个排序的数字。有多少种不同的表示形式?由于可能存在重复的数字,因此我们不能使用nCr(选择),但是有一个称为multichoose的操作适用于多集。
因此,从理论上讲,如果您能提出一个合理(足够)的数字排序列表表示,则是有可能的。例如,疯狂的表示可能需要10MB的查询表或数千行代码。
但是,如果“ 1M RAM”表示一百万个字节,则显然没有足够的空间。从理论上说,增加5%的内存使之成为可能,这一事实向我表明,表示必须非常高效,并且可能并非理智。
(我原来的答案是错误的,对不起,数学不好,请参阅下面的中断。)
这个怎么样?
前27位存储您所看到的最低数字,然后将差值存储到所看到的下一个数字,其编码如下:5位存储用于存储差异的位数,然后存储差异。使用00000表示您再次看到该数字。
之所以可行,是因为插入的数字越多,数字之间的平均差异就越小,因此当您添加更多的数字时,使用较少的位来存储差异。我相信这称为增量列表。
我能想到的最坏情况是所有数字均等分布(按100),例如,假设第一个数字为0:
000000000000000000000000000 00111 1100100
^^^^^^^^^^^^^
a million times
27 + 1,000,000 * (5+7) bits = ~ 427k
Reddit进行救援!
如果您只需要对它们进行分类,那么此问题将很容易。存储您所看到的数字需要122k(100万比特)(如果看到0,则显示第0位;如果看到2300,则存储第2300位,依此类推。
您读取数字,将其存储在位字段中,然后在保持计数的同时将位移出。
但是,您必须记住您已经看过多少次。上面的子列表答案启发了我提出这个方案:
而不是使用一位,而是使用2或27位:
我认为这行得通:如果没有重复项,则您的列表为244k。在最坏的情况下,您看到每个数字两次(如果一次看到三个数字,则会缩短列表的其余部分),这意味着您看到了50,000次以上,并且看到了950,000个项目0或1次。
50,000 * 27 + 950,000 * 2 = 396.7k。
如果使用以下编码,则可以进行进一步的改进:
0表示您没有看到数字10表示您曾经看到它11是保持计数的方式
平均而言,这将导致280.7k的存储量。
编辑:我星期天早上的数学是错误的。
最坏的情况是我们两次看到500,000个数字,因此数学公式变为:
500,000 * 27 + 500,000 * 2 = 1.77M
备用编码会导致平均存储
500,000 * 27 + 500,000 = 1.70M
:(
您正在使用哪种计算机?它可能没有任何其他“正常”本地存储,但是例如,它具有视频RAM吗?1兆像素x每个像素32位(例如)非常接近您所需的数据输入大小。
(我主要是在旧Acorn RISC PC的内存中询问,如果您选择低分辨率或低色深的屏幕模式,它可能会“借用” VRAM来扩展可用的系统RAM!)。这在只有几MB正常RAM的计算机上非常有用。
在10 ^ 8的范围内有10 ^ 6个值,因此平均每100个代码点有一个值。存储从第N个点到第(N + 1)个的距离。重复值的跳跃为0。这意味着该跳跃平均需要不到7位的存储空间,因此其中的一百万将很高兴地适合我们的800万存储位。
需要将这些跳过部分编码为比特流,例如通过霍夫曼编码。插入是通过遍历位流并在新值之后重写来进行的。通过迭代并写出隐含值来输出。出于实用性考虑,它可能想作为10 ^ 4个列表来完成,每个列表覆盖10 ^ 4个代码点(平均100个值)。
可以通过在跳过的长度上假设一个泊松分布(均值=方差= 100)来先验地建立好的随机数据霍夫曼树,但是真实的统计信息可以保留在输入上,并用于生成最佳树来处理病理病例。
我认为解决方案是结合视频编码中的技术,即离散余弦变换。在数字视频中,不是将视频的亮度或颜色的变化记录为常规值(例如110112115116),而是从最后一个值中减去(类似于游程长度编码)。110 112 115 116变为110 2 31。值2 3 1比原始值需要更少的位。
因此,可以说我们在输入值到达套接字时创建了一个列表。我们在每个元素中存储的不是值,而是值在它之前的偏移量。我们按照进行排序,因此偏移量只会是正数。但是偏移量可以是8个十进制数字宽,适合3个字节。每个元素不能为3个字节,因此我们需要打包它们。我们可以将每个字节的高位用作“连续位”,指示下一个字节是数字的一部分,并且每个字节的低7位需要组合。零对重复项有效。
随着列表的填满,数字应该变得更接近,这意味着平均只有1个字节用于确定到下一个值的距离。7位值和1位偏移量(如果方便),但是可能存在一个最佳点,即“连续”值需要少于8位。
无论如何,我做了一些实验。我使用随机数生成器,可以将一百万个排序的8位十进制数字放入大约1279000字节中。每个数字之间的平均间隔始终为99 ...
public class Test {
public static void main(String[] args) throws IOException {
// 1 million values
int[] values = new int[1000000];
// create random values up to 8 digits lrong
Random random = new Random();
for (int x=0;x<values.length;x++) {
values[x] = random.nextInt(100000000);
}
Arrays.sort(values);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int av = 0;
writeCompact(baos, values[0]); // first value
for (int x=1;x<values.length;x++) {
int v = values[x] - values[x-1]; // difference
av += v;
System.out.println(values[x] + " diff " + v);
writeCompact(baos, v);
}
System.out.println("Average offset " + (av/values.length));
System.out.println("Fits in " + baos.toByteArray().length);
}
public static void writeCompact(OutputStream os, long value) throws IOException {
do {
int b = (int) value & 0x7f;
value = (value & 0x7fffffffffffffffl) >> 7;
os.write(value == 0 ? b : (b | 0x80));
} while (value != 0);
}
}
Google的(不好的)方法,来自HN线程。存储RLE样式的计数。
您的初始数据结构为'99999999:0'(全零,没有看到任何数字),然后假设您看到数字3,866,344,因此您的数据结构将变为'3866343:0,1:1,96133654:0'可以看到数字总是在零位数和“ 1”位数之间交替,因此您可以假设奇数代表0位,偶数代表1位。这变成(3866343,1,96133654)
他们的问题似乎并未涵盖重复项,但假设他们对重复项使用“ 0:1”。
大问题#1:1M整数的插入会花费年龄。
大问题2:像所有普通增量编码解决方案一样,某些发行版无法用这种方式覆盖。例如,距离为0:99的1m个整数(例如,每个+99)。现在想一想,但随机距离在0:99范围内。(注意:99999999/1000000 = 99.99)
Google的方法既不值得(缓慢)又不正确。但是为了辩护,他们的问题可能有所不同。
为了表示排序后的数组,可以只存储第一个元素以及相邻元素之间的差。通过这种方式,我们可以对10 ^ 6个元素进行编码,这些元素最多可以累加10 ^ 8个。让我们把这种d。要编码D的元素,可以使用霍夫曼码。霍夫曼代码的字典可以随时创建,并且每次将新项插入已排序的数组(插入排序)时,都会更新该数组。请注意,当字典由于新项而更改时,应更新整个数组以匹配新的编码。
如果我们具有相等数量的每个唯一元素,则用于编码D的每个元素的平均位数将最大化。说元素D1,D2,...,牛顿在d各出现˚F倍。在那种情况下(在最坏的情况下,输入序列中既有0又有10 ^ 8)
sum(1 <= i <= N) F。di = 10 ^ 8
哪里
sum(1 <= i <= N) F = 10 ^ 6或F = 10 ^ 6 / N,归一化频率为p = F / 10 ^ = 1 / N
平均位数为-log2(1 / P)= log2(N)。在这种情况下,我们应该找到使N最大化的情况。如果我们有从0开始的连续数字di,或者di = i -1,则会发生这种情况,因此
10 ^ 8 = sum(1 <= i <= N) F。di = sum(1 <= i <= N)(10 ^ 6 / N)(i-1)=(10 ^ 6 / N)N(N -1)/ 2
即
N <=201。在这种情况下,平均位数为log2(201)= 7.6511,这意味着每个输入元素大约需要1个字节来保存排序后的数组。请注意,这并不意味着D通常不能包含201个以上的元素。它只是撒播如果D的元素均匀分布,则其唯一值不能超过201。
这是针对此类问题的通用解决方案:
采取的方法如下。该算法在32位字的单个缓冲区上运行。它循环执行以下过程:
我们从一个缓冲区填充了最后一次迭代的压缩数据开始。缓冲区看起来像这样
|compressed sorted|empty|
计算此缓冲区可以压缩和未压缩的最大数字量。将缓冲区分为两部分,从压缩数据的空间开始,到未压缩的数据结束。缓冲区看起来像
|compressed sorted|empty|empty|
用要排序的数字填充未压缩的部分。缓冲区看起来像
|compressed sorted|empty|uncompressed unsorted|
用就地排序对新数字进行排序。缓冲区看起来像
|compressed sorted|empty|uncompressed sorted|
在压缩部分将上次迭代中已压缩的数据右对齐。此时,缓冲区已分区
|empty|compressed sorted|uncompressed sorted|
在压缩部分执行流式解压缩-重新压缩,将未压缩部分中的排序数据合并。随着新压缩段的增长,旧的压缩段将被消耗。缓冲区看起来像
|compressed sorted|empty|
执行此过程,直到所有数字都已排序。
当然,这种算法仅在有可能在实际知道实际要压缩的内容之前计算出新排序缓冲区的最终压缩大小时才起作用。紧接着,压缩算法必须足够好以解决实际问题。
所使用的方法分三个步骤。首先,该算法将始终存储排序的序列,因此我们可以纯粹存储连续条目之间的差异。每个差异在[0,99999999]范围内。
然后将这些差异编码为一元比特流。此流中的1表示“向累加器加1,A 0表示“将累加器作为条目输入并重置”。因此,差N将由N 1和一个0表示。
所有差异的总和将接近算法支持的最大值,所有差异的计数将接近算法中插入的值的数量。这意味着我们希望流最后包含最大值1和计数0。这使我们能够计算出流中0和1的预期概率。即,0 count/(count+maxval)
的概率为,1的概率为maxval/(count+maxval)
。
我们使用这些概率在此比特流上定义算术编码模型。此算术代码将在最佳空间中精确编码此数量的1和0。我们可以计算出任何中间比特流13759这个模型的空间:bits = encoded * log2(1 + amount / maxval) + maxval * log2(1 + maxval / amount)
。要计算算法所需的总空间,请将其设置encoded
为数量。
为了不需要大量的迭代,可以将少量开销添加到缓冲区。这将确保算法至少在适合此开销的数量上运行,因为到目前为止,算法的最大时间成本是每个周期的算术编码压缩和解压缩。
紧接着,还需要一些开销来存储簿记数据并处理算术编码算法的定点近似中的细微误差,但总的来说,即使有一个额外的缓冲区可以容纳1MiB,该算法也可以容纳1MiB的空间。 8000个数字,总共1043916字节的空间。
除了减少算法的(较小)开销外,理论上不可能获得较小的结果。为了仅包含最终结果的熵,将需要1011717字节。如果减去为效率而增加的额外缓冲区,此算法将使用1011916字节来存储最终结果+开销。
排序是这里的第二个问题。就像其他所说的那样,仅存储整数非常困难,并且不能在所有输入上使用,因为每个数字需要27位。
我对此的看法是:仅存储连续(排序)整数之间的差异,因为它们之间的差异很可能很小。然后使用一种压缩方案,例如每个输入数字具有2个附加位,以对该数字存储在多少位上进行编码。就像是:
00 -> 5 bits
01 -> 11 bits
10 -> 19 bits
11 -> 27 bits
在给定的内存限制内应该可以存储相当数量的可能的输入列表。如何选择压缩方案以使其在最大输入数量上工作的数学知识超出了我的范围。
我希望您也许能够利用您输入的特定领域知识来基于此找到足够好的整数压缩方案。
哦,然后,当您接收数据时,您对该排序后的列表进行插入排序。
现在针对实际解决方案,仅使用1MB RAM即可覆盖8位范围内所有可能的输入情况。注意:正在进行中,明天将继续。使用排序的整数的增量的算术编码,最坏的情况是1M排序的整数每个条目的成本约为7位(因为99999999/1000000为99,而log2(99)几乎为7位)。
但是,您需要将1m个整数排序以获得7或8位!较短的序列将具有较大的增量,因此每个元素的位数更多。
我正在尝试尽可能多地压缩(几乎)就地压缩。接近25万个整数的第一批最多最多需要9位。因此结果大约需要275KB。重复几次剩余的可用内存。然后对这些压缩的块进行解压缩就地压缩。这是很难的,但是可能的。我认为。
合并的列表将越来越接近每个整数目标7位。但是我不知道合并循环需要进行多少次迭代。也许3。
但是算术编码实现的不精确性可能使其无法实现。如果根本不可能解决这个问题,那将是非常严格的。
有志愿者吗?
您只需要按顺序存储数字之间的差异,并使用编码来压缩这些序列号。我们有2 ^ 23位。我们将其分为6位块,并让最后一位指示该数字是否扩展到另外6位(5位加上扩展块)。
因此,000010是1,000100是2。000001100000是128。现在,我们考虑最差的强制转换,以表示最大为10,000,000的数字序列的差异。可以有大于2 ^ 5的10,000,000 / 2 ^ 5差异,大于2 ^ 10的10,000,000 / 2 ^ 10差异和大于2 ^ 15的10,000,000 / 2 ^ 15差异等。
因此,我们添加了代表序列所需的位数。我们有1,000,000 * 6 +汇总(10,000,000 / 2 ^ 5)* 6 +汇总(10,000,000 / 2 ^ 10)* 6 +汇总(10,000,000 / 2 ^ 15)* 6 +汇总(10,000,000 / 2 ^ 20)* 4 = 7935479。
2 ^ 24 =8388608。由于8388608> 7935479,我们应该轻松拥有足够的内存。插入新数字时,我们可能还需要一点内存来存储位置的总和。然后,我们遍历序列,找到在哪里插入我们的新数字,如有必要,减小下一个差异,然后将所有内容右移。
如果我们对这些数字一无所知,则受到以下约束的限制:
如果这些假设成立,那么您将无法执行任务,因为您将至少需要26,575,425位存储空间(3,321,929字节)。
您能告诉我们有关您数据的哪些信息?
技巧是将算法状态表示为整数多集,并将其表示为“增量计数器” =“ +”和“输出计数器” =“!”的压缩流。字符。例如,集合{0,3,3,4}将表示为“!+++ !! +!”,后跟任意数量的“ +”字符。要修改多集,您可以流出字符,一次仅保持恒定的解压缩量,然后在以压缩形式流回字符之前进行适当的更改。
细节
我们知道最后一组中恰好有10 ^ 6个数字,因此最多有10 ^ 6个“!” 字符。我们也知道我们的范围的大小为10 ^ 8,这意味着最多有10 ^ 8个“ +”字符。我们可以在10 ^ 8“ +”中排列10 ^ 6“!”的方式有(10^8 + 10^6) choose 10^6
,因此指定某种特定的排列需要〜0.965 MiB`的数据。那会很合身。
我们可以将每个角色视为独立角色,而不会超出我们的配额。“ +”字符比“!”多100倍 字符,如果我们忘记它们是从属的,则简化为每个字符100:1的赔率是“ +”。100:101的奇数对应于每个字符约0.08位,总计约0.965 MiB(在这种情况下,忽略依赖项仅花费约12位!)。
存储具有已知先验概率的独立字符的最简单技术是霍夫曼编码。请注意,我们需要一棵不切实际的大树(用于10个字符的块的霍夫曼树每块的平均成本约为2.4位,总计〜2.9 Mib。用于20个字符的块的霍夫曼树每块的平均成本大约3位,总共约1.8 MiB。我们可能需要一个大约100个大小的块,这意味着树中的节点数超过了所有现有计算机设备可以存储的数量。 )。但是,根据问题,ROM在技术上是“免费的”,利用树中规则性的实际解决方案看起来基本相同。
伪码
我们有1 MB-3 KB RAM = 2 ^ 23-3 * 2 ^ 13位= 8388608-24576 = 8364032位可用。
我们在10 ^ 8的范围内给了10 ^ 6个数字。这给出了〜100 <2 ^ 7 = 128的平均差距
首先,让我们考虑一下所有间隙均小于128时,等间距数字的简单问题。这很容易。只需存储第一个数字和7位间隙:
(27位)+ 10 ^ 6 7位间隙数= 7000027位
请注意,重复数字的间隔为0。
但是,如果我们的差距大于127,该怎么办?
好的,假设直接表示小于127的间隙大小,但是间隙大小127之后是用于实际间隙长度的连续8位编码:
10xxxxxx xxxxxxxx = 127 .. 16,383
110xxxxx xxxxxxxx xxxxxxxx = 16384 .. 2,097,151
等等
请注意,此数字表示形式描述了其自己的长度,因此我们知道下一个间隔数字何时开始。
如果间隙小于127,这仍然需要7000027位。
最多可以有(10 ^ 8)/(2 ^ 7)= 781250 23位间隙数,这需要额外的16 * 781,250 = 12,500,000位,这太多了。我们需要更加紧凑和缓慢增加的差距代表。
平均间隙大小为100,因此,如果将它们重新排序为[100,99,101,98,102,...,2,198,1,199,0,200,201,202,...]并为此索引使用密集的二进制斐波那契基本编码,没有成对的零(例如11011 = 8 + 5 + 2 + 1 = 16),其数字以'00'分隔,那么我认为我们可以使间隙表示足够短,但是需要更多分析。
在接收流时,请执行以下步骤。
第一设置一些合理的块大小
伪代码的想法:
接收流时,请继续前4步。最后一步将是失败,如果超出内存,或者一旦开始收集范围的所有数据,则开始输出结果,方法是开始对范围进行排序并按顺序吐出结果,然后对需要解压缩的结果进行解压缩,然后对它们进行排序你去他们那里。