[algorithm] 1MB의 RAM으로 백만 개의 8 진수 숫자 정렬

RAM이 1MB이고 다른 로컬 저장소가없는 컴퓨터가 있습니다. TCP 연결을 통해 백만 개의 8 자리 10 진수를 받아들이고 정렬 한 다음 정렬 된 목록을 다른 TCP 연결을 통해 보내야합니다.

숫자 목록에 중복이 포함될 수 있으므로 삭제해서는 안됩니다. 코드는 ROM에 배치되므로 1MB에서 코드 크기를 뺄 필요가 없습니다. 이미 이더넷 포트를 구동하고 TCP / IP 연결을 처리하는 코드가 있으며 코드를 통해 데이터를 읽고 쓰는 데 필요한 1KB 버퍼를 포함하여 상태 데이터에 2KB가 필요합니다. 이 문제에 대한 해결책이 있습니까?

질문과 답변의 출처 :

slashdot.org

cleaton.net



답변

여기까지 언급되지 않은 다소 교묘 한 속임수가 있습니다. 데이터를 저장할 수있는 추가 방법이 없다고 가정하지만 이는 사실이 아닙니다.

문제를 해결하는 한 가지 방법은 다음과 같은 끔찍한 일을하는 것입니다. 어떤 상황에서도 다른 사람이 시도해서는 안됩니다. 네트워크 트래픽을 사용하여 데이터를 저장하십시오. 그리고 아니요, NAS를 의미하지 않습니다.

다음과 같은 방법으로 몇 바이트의 RAM만으로 숫자를 정렬 할 수 있습니다.

  • 먼저 두 변수를 가지고 : COUNTERVALUE.
  • 먼저 모든 레지스터를 0;
  • 정수를받을 때마다 I증분 COUNTER하고로 설정 VALUE하십시오 max(VALUE, I).
  • 그런 다음 데이터가 설정된 ICMP 에코 요청 패킷을 I라우터로 보냅니다 . 지우고 I반복하십시오.
  • 반환 된 ICMP 패킷을받을 때마다 정수를 추출하여 다른 에코 요청으로 다시 보냅니다. 이것은 정수를 포함하여 앞뒤로 많은 ICMP 요청을 생성합니다.

COUNTER도달하면 1000000ICMP 요청의 연속 스트림에 모든 값이 저장되고 VALUE이제 최대 정수가 포함됩니다. 일부를 선택하십시오 threshold T >> 1000000. COUNTER0으로 설정하십시오 . ICMP 패킷을 수신 할 때마다 COUNTER포함 된 정수를 증가 시키고 I다른 에코 요청으로 다시 보내십시오 I=VALUE.이 경우 정렬 된 정수의 대상으로 전송 하지 않습니다 . 에 의해 COUNTER=T감소한 후 0으로 재설정 하고 반복하십시오. 한번VALUE1COUNTERVALUE 0에 도달하면 모든 정수를 가장 큰 것부터 가장 작은 것까지 순서대로 전송해야하며, 두 개의 영구 변수 (및 임시 값에 필요한 소량)에 약 47 비트의 RAM 만 사용했습니다.

나는 이것이 끔찍하다는 것을 알고 있으며, 모든 종류의 실질적인 문제가있을 수 있다는 것을 알고 있지만, 그것은 그것이 당신에게 웃음을 주거나 적어도 당신을 놀라게 할 것이라고 생각했습니다.


답변

다음은 작동하는 C ++ 코드입니다. 은 문제를 해결하는 입니다.

메모리 제약 조건이 충족되었다는 증거 :

편집자 : 이 게시물이나 블로그에서 작성자가 제공 한 최대 메모리 요구 사항에 대한 증거는 없습니다. 값을 인코딩하는 데 필요한 비트 수는 이전에 인코딩 된 값에 따라 다르므로 이러한 증거는 사소하지 않을 수 있습니다. 저자는 경험적으로 우연히 발견 할 수있는 가장 큰 인코딩 된 크기는 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 바이트가 남습니다.

내 Xeon 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!')

알고리즘에 대한 자세한 설명은 다음 일련의 게시물에서 찾을 수 있습니다.


답변

참조하십시오 최초의 정답 또는 산술 인코딩 나중에 답을 .아래는 재미 있지만 100 % 방탄 솔루션은 아닙니다.

이것은 매우 흥미로운 작업이며 여기에 또 다른 해결책이 있습니다. 누군가가 결과가 유용하다고 생각하기를 바랍니다.

1 단계 : 초기 데이터 구조, 대략적인 압축 방식, 기본 결과

간단한 계산을 해보자. 10 ^ 6 8 자리 10 진수를 저장할 수있는 1M (1048576 바이트)의 RAM이 처음에있다. [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를 준비했지만 여전히 강조해야 할 세부 사항이 있습니다.

  1. 메모리가 제한되어 있으므로 숫자를 미리 정렬하고 압축 된 버킷 (동적 크기)을 임시 저장소로 사용하는 것이 좋습니다.
  2. 미리 정렬 된 데이터로 더 나은 압축 계수를 달성하는 것이 더 쉬우므로 각 버킷에 정적 버퍼가 있습니다 (버퍼의 숫자는 LZMA 전에 정렬되어야 함)
  3. 각 버킷에는 특정 범위가 있으므로 각 버킷에 대해 최종 정렬을 개별적으로 수행 할 수 있습니다.
  4. 버킷 크기를 올바르게 설정할 수 있으므로 저장된 데이터의 압축을 풀고 각 버킷에 대해 최종 정렬을 수행하기에 충분한 메모리가 있습니다.

인 메모리 정렬

첨부 코드는 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

편집하다

결론:

  1. 자연 을 속이려하지 마십시오
  2. 더 적은 메모리 공간으로 더 간단한 압축 사용
  3. 몇 가지 추가 단서가 실제로 필요합니다. 일반적인 방탄 솔루션은 실현 가능하지 않은 것 같습니다.

2 단계 : 압축 향상, 최종 결론

이전 섹션에서 이미 언급했듯이 적절한 압축 기술을 사용할 수 있습니다. 간단하고 더 나은 (가능한 경우) 접근 방식을 위해 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;
}

이 접근 방식은 다음과 같습니다.

  1. 많은 메모리를 소비하지 않습니다
  2. 스트림과 함께 작동
  3. 그렇게 나쁘지 않은 결과를 제공합니다

전체 코드는 here , BinaryInput 및 BinaryOutput 구현은 여기 에서 찾을 수 있습니다.

최종 결론

마지막 결론은 없습니다 🙂 때로는 한 수준 위로 올라가 메타 수준의 관점 에서 작업을 검토하는 것이 좋습니다 .

이 작업에 시간을 보내는 것이 재미있었습니다. BTW, 아래에 흥미로운 답변이 많이 있습니다. 당신의 관심과 행복한 코딩에 감사드립니다.


답변

해결책은 1MB와 100 만 바이트의 차이로 인해 가능합니다. 8093729.5에 대해 중복이 허용 된 백만 개의 8 자리 숫자를 선택하고 중요하지 않은 순서로 약 2 개의 방법이 있으므로, 백만 바이트의 RAM이있는 머신에는 모든 가능성을 나타내는 데 충분한 상태가 없습니다. 그러나 1M (TCP / IP의 경우 2k 미만)은 1022 * 1024 * 8 = 8372224 비트이므로 솔루션이 가능합니다.

1 부, 초기 솔루션

이 방법은 1M보다 조금 더 필요합니다. 나중에 1M에 맞도록 수정하겠습니다.

7 비트 숫자의 하위 목록 시퀀스로 0에서 99999999 범위의 간단한 정렬 된 숫자 목록을 저장합니다. 첫 번째 서브리스트는 0에서 127까지의 숫자를 보유하고, 두 번째 서브리스트는 128에서 255까지의 숫자를 보유합니다. 100000000/128은 정확히 781250이므로 781250의 이러한 서브리스트가 필요합니다.

각 하위 목록은 2 비트 하위 목록 헤더와 하위 목록 본문으로 구성됩니다. 서브리스트 본문은 서브리스트 항목 당 7 비트를 차지합니다. 하위 목록은 모두 함께 연결되며 형식을 사용하면 한 하위 목록이 끝나고 다음이 시작되는 위치를 알 수 있습니다. 완전히 채워진 목록에 필요한 총 스토리지는 2 * 781250 + 7 * 1000000 = 8562500 비트이며 이는 약 1.021MB입니다.

4 가지 가능한 서브리스트 헤더 값은 다음과 같습니다.

00 빈 서브리스트, 다음은 아무것도 없습니다.

01 싱글 톤, 서브리스트에는 단 하나의 엔트리 만이 있고 다음 7 비트는 그것을 유지합니다.

10 하위 목록에는 최소한 2 개의 고유 숫자가 있습니다. 마지막 항목이 첫 번째 항목보다 작거나 같은 것을 제외하고 항목은 감소하지 않는 순서로 저장됩니다. 이를 통해 서브리스트의 끝을 식별 할 수 있습니다. 예를 들어 숫자 2,4,6은 (4,6,2)로 저장됩니다. 숫자 2,2,3,4,4는 (2,3,4,4,2)로 저장됩니다.

11 서브리스트는 단일 숫자의 반복을 두 번 이상 보유합니다. 다음 7 비트는 숫자를 나타냅니다. 그런 다음 값이 1 인 7 비트 항목이 0 개 이상이고 값이 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 비트 항목

Part 2, 1M으로 짜다

위의 솔루션을 1M로 짜려면 압축 목록 형식을 좀 더 압축해야합니다. 하위 목록 유형 중 하나를 제거하여 가능한 3 개의 다른 하위 목록 헤더 값이 있습니다. 그런 다음 “00”, “01”및 “1”을 하위 목록 헤더 값으로 사용하고 몇 비트를 저장할 수 있습니다. 하위 목록 유형은 다음과 같습니다.

빈 하위 목록, 다음은 아무것도 없습니다.

B 싱글 톤의 경우, 서브리스트에 단 하나의 항목 만 있고 다음 7 비트가이를 보유합니다.

C 하위 목록에는 최소 2 개의 고유 숫자가 있습니다. 마지막 항목이 첫 번째 항목보다 작거나 같은 것을 제외하고 항목은 감소하지 않는 순서로 저장됩니다. 이를 통해 서브리스트의 끝을 식별 할 수 있습니다. 예를 들어 숫자 2,4,6은 (4,6,2)로 저장됩니다. 숫자 2,2,3,4,4는 (2,3,4,4,2)로 저장됩니다.

D 서브리스트는 단일 숫자의 두 번 이상의 반복으로 구성됩니다.

내 3 개의 서브리스트 헤더 값은 “A”, “B”및 “C”가되므로 D- 타입 서브리스트를 나타내는 방법이 필요합니다.

“C [17] [101] [58]”과 같이 C- 타입 서브리스트 헤더 뒤에 3 개의 항목이 있다고 가정합니다. 세 번째 항목은 두 번째 항목보다 작지만 첫 번째 항목보다 많기 때문에 위에서 설명한대로 유효한 C 유형 하위 목록에 속할 수 없습니다. 이 유형의 구성을 사용하여 D 유형 하위 목록을 나타낼 수 있습니다. 비트 용어로, “C {00 ?????} {1 ??????} {01 ?????}”는 불가능한 C- 타입 서브리스트입니다. 이것을 사용하여 단일 숫자의 3 회 이상의 반복으로 구성된 하위 목록을 나타냅니다. 처음 2 개의 7 비트 단어는 숫자 (아래 “N”비트)를 인코딩하고 0 개 이상의 {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 비트를위한 충분한 공간이 있지만이 패턴은 그것이 나타내는 하위 목록보다 길기 때문에 조금 더 복잡합니다. 끝에있는 5 개의 물음표는 패턴의 일부가 아닌 것으로 간주 될 수 있으므로 “C {0NNNNNN} {11N ????} 10″을 패턴으로 사용하고 반복되는 숫자는 “N에 저장됩니다. “에스. 2 비트가 너무 깁니다.

이 패턴에서 2 비트를 빌려서 사용하지 않은 4 비트에서 갚아야합니다. 읽을 때 “C {0NNNNNN} {11N00AB} 10″이 발생하면 “N”에있는 숫자의 인스턴스 2 개를 출력하고 끝에 A와 B로 “10”을 덮어 쓰고 읽기 포인터를 2만큼 되감습니다. 비트. 각 컴팩트 목록이 한 번만 걷기 때문에이 알고리즘에 대한 파괴적인 읽기가 가능합니다.

단일 숫자의 두 번 반복되는 서브리스트를 작성할 때 “C {0NNNNNN} 11N00″을 쓰고 빌린 비트 카운터를 2로 설정하십시오. 빌린 비트 카운터가 0이 아닌 모든 쓰기에서 기록 된 각 비트에 대해 감소합니다. 카운터가 0에 도달하면 “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 하위 목록을 각각 78125 개의 하위 목록으로 구성된 10 개의 하위 목록 그룹으로 나누겠습니다. 각 그룹에는 고유 한 독립적 인 서브리스트 헤더 매핑이 있습니다. 그룹에 문자 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

매핑 변경 중 하위 목록 그룹의 최악의 임시 확장은 4k 미만에서 78125/3 = 26042 비트입니다. 완전히 채워진 컴팩트 목록에 4k와 1037764 바이트를 허용하면 메모리 맵의 “Z”에 대해 8764-4096 = 4668 바이트가 남습니다.

10 개의 하위 목록 헤더 매핑 테이블, 30 개의 하위 목록 헤더 발생 횟수 및 필요한 다른 카운터, 포인터 및 작은 버퍼 및 함수 호출 반환 주소의 스택 공간 및 지역 변수.

3 부, 얼마나 오래 걸립니까?

빈 컴팩트 목록의 경우 1 비트 목록 헤더가 빈 하위 목록에 사용되고 목록의 시작 크기는 781250 비트입니다. 최악의 경우 목록은 추가 된 각 숫자에 대해 8 비트로 증가하므로 32 비트 숫자 각각이 목록 버퍼의 맨 위에 배치 된 다음 정렬 및 병합하려면 32 + 8 = 40 비트의 여유 공간이 필요합니다. 최악의 경우, 서브리스트 헤더 맵핑을 변경하면 2 * 781250 + 7 * 항목-781250/3 비트의 공간 사용량이 발생합니다.

목록에 최소 800000 개의 숫자가 있으면 5 번의 병합 후마다 서브리스트 헤더 매핑을 변경하는 정책으로 최악의 경우 약 30M의 간단한 목록 읽기 및 쓰기 작업이 필요합니다.

출처:

http://nick.cleaton.net/ramsortsol.html


답변

길마 노프의 대답은 그 가정에서 매우 잘못되었습니다. 그것은 백만 연속 정수 의 무의미한 측정을 기반으로 추측을 시작합니다 . 그것은 격차가 없음을 의미합니다. 이러한 임의의 차이는 작지만 실제로는 나쁜 아이디어입니다.

직접 해보십시오. 원하는 LZMA에 관계없이 백만 개의 임의 27 비트 정수를 가져 와서 정렬하고 7-Zip , xz 로 압축하십시오 . 결과는 1.5MB를 초과합니다. 맨 위의 전제는 순차적 숫자의 압축입니다. 심지어 델타 인코딩 그입니다 이상 1.1 MB . 그리고 이것이 압축을 위해 100MB 이상의 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는 큰 실수이다)

무손실 압축 형태는 모든 입력의 크기를 줄이지 않는 것으로 알려져 있습니다.

모든 인기있는 답변은 추가 공간을 확보 할 수있을만큼 효과적인 압축을 적용 할 수 있다고 가정합니다. 실제로, 부분적으로 완성 된 목록의 일부를 압축되지 않은 형태로 유지하고 정렬 작업을 수행 할 수있을 정도로 큰 추가 공간 청크. 이것은 단지 나쁜 가정입니다.

이러한 솔루션의 경우 압축 방식에 대한 지식이있는 사람은이 구성표에 대해 잘 압축되지 않는 일부 입력 데이터를 설계 할 수 있으며 공간 부족으로 인해 “솔루션”이 중단 될 가능성이 높습니다.

대신 나는 수학적 접근법을 취합니다. 가능한 출력은 0..MAX 범위의 요소로 구성된 길이 LEN의 모든 목록입니다. 여기서 LEN은 1,000,000이고 MAX는 100,000,000입니다.

임의의 LEN 및 MAX의 경우이 상태를 인코딩하는 데 필요한 비트 수는 다음과 같습니다.

Log2 (MAX 멀티 초이스 LEN)

따라서 숫자의 경우 수신 및 정렬이 완료되면 가능한 모든 출력을 고유하게 구별 할 수있는 방식으로 결과를 저장하려면 최소한 Log2 (100,000,000 MC 1,000,000) 비트가 필요합니다.

~ = 988kb 입니다. 따라서 실제로 결과를 저장할 수있는 충분한 공간이 있습니다. 이 관점에서 가능합니다.

[더 나은 예제가 존재하기 때문에 무의식적으로 삭제되었습니다 …]

가장 좋은 대답 은 여기 입니다.

또 다른 좋은 대답 은 여기에 있으며 기본적으로 삽입 정렬을 하나의 요소로 확장하는 함수로 사용합니다 (한 번에 여러 요소를 삽입 할 수 있도록 몇 가지 요소와 사전 정렬 버퍼링, 약간의 시간 절약). 7 비트 델타의 버킷 인 멋진 컴팩트 상태 인코딩도 사용합니다.