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


726

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

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

질문과 답변의 출처 :

slashdot.org

cleaton.net


45
Ehm, 백만배 8 자리 10 진수 (최소 27 비트 정수 이진)> 1MB 램
Mr47

15
1M RAM은 2 ^ 20 바이트를 의미합니까? 그리고이 아키텍처에서 바이트에 몇 비트가 있습니까? 그리고 "백만 8 자리 10 진수"의 "백만"은 SI 백만 (10 ^ 6)입니까? 8 자리 10 진수, 자연수 <10 ^ 8, 소수점을 제외하고 10 진수로 8 자리를 취하는 유리수 등은 무엇입니까?

13
백만 8 자리 숫자 또는 백만 8 비트 숫자?
패트릭 화이트

13
"Dr Dobb 's Journal"(1998-2001 년 사이)의 기사를 상기시켜 주었다. 저자는 전화 번호를 읽을 때 삽입 정렬을 사용하여 전화 번호를 정렬하는 방식을 사용했다. 알고리즘이 더 빠를 수 있습니다 ...
Adrien Plisson 21:29에

103
아직 언급하지 않은 또 다른 솔루션이 있습니다. 2MB RAM이있는 하드웨어를 구입하십시오. 훨씬 비싸지 않아야하며 문제를 훨씬 쉽게 해결할 수 있습니다.
Daniel Wagner

답변:


716

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

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

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


27
기본적으로 네트워크 대기 시간을 활용하고 라우터를 일종의 질문으로 바꾸고 있습니까?
Eric R.

335
이 솔루션은 상자 밖에있는 것이 아닙니다. D : 집에서 그 상자를 잊어 버린 것 같다
블라디슬라프 Zorov

28
훌륭한 답변 ...이 답변은 솔루션이 얼마나 다양한 문제에 직면 할 수 있는지를 폭로하기 때문에이 답변을 좋아합니다.
StackOverflowed

33
ICMP는 신뢰할 수 없습니다.
sleeplessnerd

13
@MDMarra : 맨 위에서 "문제를 해결하는 한 가지 방법은 다음과 같은 끔찍한 일을하는 것입니다. 어떤 상황에서도 누군가가 시도해서는 안됩니다." 내가 이것을 말한 이유가있었습니다.
Joe Fitzsimons

423

다음은 작동하는 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!')

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


8
@ preshing yes 우리는 이것에 대한 자세한 설명을 원합니다.
T Suds

25
8 자리 숫자는 약 26.6 비트의 정보를 갖고 백만은 19.9 비트라는 것이 핵심 관찰이라고 생각합니다. 목록을 델타 압축하면 (인접한 값의 차이를 저장) 차이 범위는 0 (0 비트)에서 99999999 (26.6 비트) 사이이지만 모든 쌍 사이에 최대 델타를 가질 수는 없습니다 . 최악의 경우 실제로는 백만 개의 고르게 분포 된 값이어야하며, 델타 당 (26.6-19.9) 또는 델타 당 약 6.7 비트가 필요합니다. 백만 비트의 6.7 비트를 저장하면 1M에 쉽게 맞습니다. 델타 압축에는 연속 병합 정렬이 필요하므로 거의 무료로 얻을 수 있습니다.
Ben Jackson

4
달콤한 솔루션. 당신은 설명 preshing.com/20121025/…에
davec

9
@ BenJackson : 수학 어딘가에 오류가 있습니다. 저장하는 데 8.094 x 10 ^ 6 비트 (메가 바이트 미만의 머리)를 사용하는 2.265 x 10 ^ 2436455 고유 한 가능한 출력 (순차 10 ^ 6 8 자리 정수 세트)이 있습니다. 이 정보 이론적 한계를 넘어서는 영리한 체계는 손실없이 압축 할 수 없습니다. 당신의 설명은 당신이 훨씬 적은 공간이 필요하다는 것을 암시하며, 따라서 잘못되었습니다. 실제로, 위의 솔루션에서 "원형"은 필요한 정보를 보유 할만큼 충분히 크므로 preshing이이를 고려한 것으로 보이지만 누락되었습니다.
Joe Fitzsimons

5
@JoeFitzsimons : 재귀를 해결하지 못했습니다 (0..m에서 n 개의 고유 정렬 세트는 (n+m)!/(n!m!)). 그렇습니다. 아마도 b 비트의 델타가 저장하는 데 b 비트가 걸리는 것으로 추정됩니다. 분명히 0의 델타는 저장하는 데 0 비트가 걸리지 않습니다.
Ben Jackson

371

참조하십시오 최초의 정답 또는 산술 인코딩 나중에 답을 .아래는 재미 있지만 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, 아래에 흥미로운 답변이 많이 있습니다. 당신의 관심과 행복한 코딩에 감사드립니다.


17
Inkscape 사용했습니다 . 그건 그렇고 훌륭한 도구입니다. 이 다이어그램 소스 를 예로 사용할 수 있습니다 .
Renat Gilmanov

21
확실히 LZMA는이 경우에 너무 많은 메모리가 필요합니까? 알고리즘 으로서는 메모리에서 효율적이지 않고 저장되거나 전송되어야하는 데이터의 양을 최소화하는 것을 의미합니다.
Mjiig

67
이것은 말도 안됩니다 ... 임의의 27 비트 정수를 가져 와서 정렬하고 원하는 LZMA에 관계없이 7zip, xz로 압축하십시오. 결과는 1MB를 초과합니다. 맨 위의 전제는 순차적 숫자의 압축입니다. 0 비트로 델타 인코딩하면 숫자가됩니다 (예 : 1000000 (예 : 4 바이트)). 순차적 및 중복 (간격 없음)을 사용하면 숫자 1000000 및 1000000 비트 = 128KB이며 중복 숫자는 0이고 다음으로 표시하려면 1입니다. 작은 간격이라도 임의의 간격이 있으면 LZMA는 어리 석습니다. 이를 위해 설계되지 않았습니다.
alecco 님

30
실제로 작동하지 않습니다. 시뮬레이션을 실행했으며 압축 된 데이터가 1MB (약 1.5MB)를 초과하는 동안 여전히 100MB 이상의 RAM을 사용하여 데이터를 압축합니다. 따라서 압축 정수조차도 런타임 RAM 사용량을 언급하지 않는 문제에 맞지 않습니다. 현상금을 수여하는 것은 stackoverflow에서 가장 큰 오류입니다.
좋아하는 Onwuemene

10
이 답변은 많은 프로그래머들이 입증 된 코드가 아닌 반짝이는 아이디어를 좋아하기 때문에 많이 찬성되었습니다. 이 아이디어가 효과가 있다면, 실제로 그것을 할 수있는 것이 있다는 단정적 인 주장이 아니라 실제 압축 알고리즘이 선택되고 입증 된 것을 보게 될 것입니다. .
Olathe

185

해결책은 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


15
더 나은 솔루션이 가능하다고 생각하지 않습니다 (압축 할 수없는 값으로 작업 해야하는 경우). 그러나 이것은 조금 개선 될 수 있습니다. 1 비트와 2 비트 표현간에 하위 목록 헤더를 변경할 필요는 없습니다. 대신 산술 코딩을 사용 하여 알고리즘을 단순화하고 헤더 당 최악의 비트 수를 1.67에서 1.58로 줄입니다. 또한 메모리에서 간단한 목록을 이동할 필요가 없습니다. 대신 순환 버퍼 를 사용 하고 포인터 만 변경하십시오.
Evgeny Kluev

5
마지막으로 인터뷰 질문 이었습니까?
mlvljr

2
다른 가능한 개선 사항은 128 요소 하위 목록 대신 100 요소 하위 목록을 사용하는 것입니다 (하위 목록의 수가 데이터 세트의 요소 수와 같을 때 가장 간결한 표현을 얻기 때문에). 서브리스트의 각 값은 산술 코딩 (각 값에 대해 동일한 주파수 1/100)으로 인코딩됩니다. 이것은 서브리스트 헤더의 압축보다 훨씬 적은 약 10000 비트를 절약 할 수 있습니다.
Evgeny Kluev

C의 경우 "마지막 항목이 첫 번째 항목보다 작거나 같은 것을 제외하고는 항목이 감소하지 않는 순서로 저장됩니다"라고 말합니다. 그렇다면 2,2,2,3,5를 어떻게 인코딩하겠습니까? {2,2,3,5,2}는 2,2처럼 보일 것입니다
Rollie

1
복잡한 스위칭 전환없이 서브 헤더 당 동일한 압축비 1.67 비트로 서브리스트 헤더 인코딩의 간단한 솔루션이 가능합니다. 연속 된 3 개의 서브 헤더를 모두 결합 할 수 있으며, 이는 5 비트로 쉽게 인코딩 될 수 있습니다 3 * 3 * 3 = 27 < 32. 당신은 그들을 결합합니다 combined_subheader = subheader1 + 3 * subheader2 + 9 * subheader3.
hynekcer

57

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

직접 해보십시오. 원하는 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

7
사전 기반 압축과 관련된 모든 알고리즘은 지체되지 않습니다. 몇 가지 사용자 정의 알고리즘을 코딩 했으며 자체 해시 테이블을 배치하기 위해 꽤 많은 메모리를 사용합니다. 가장 가까운 솔루션은 가변 비트 길이로 델타 인코딩하고 원하지 않는 TCP 패킷을 수신 거부하는 것입니다. 피어는 재전송하지만 여전히 엉망입니다.
bestsss 2008 년

@bestsss 예! 마지막 진행중인 답변을 확인하십시오. 내가 생각하는 수도 가능하다.
alecco

3
죄송하지만 실제로 질문대답 하지 않는 것 같습니다 .
n611x007

@naxa 예, 답변 : 원래 질문의 매개 변수 내에서 수행 할 수 없습니다. 숫자 분포가 엔트로피가 매우 낮은 경우에만 수행 할 수 있습니다.
alecco

1
이 모든 대답은 표준 압축 루틴이 1MB 미만의 데이터를 압축하는 데 어려움이 있다는 것입니다. 1MB 미만을 요구하도록 데이터를 압축 할 수있는 인코딩 체계가있을 수도 있고 아닐 수도 있지만,이 답변은 데이터를 많이 압축하는 인코딩 체계가 없다는 것을 증명하지는 않습니다.
Itsme2003

41

나는 이것에 대해 생각할 수있는 한 가지 방법이 조합 론적 관점이라고 생각합니다. 정렬 된 숫자 순서의 가능한 조합은 몇 개입니까? 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 비트가 남아 있다는 것입니다. 흥미로운 점은 각 시점에서 그것이 전체 주문이라고 가정하고 해당 주문에 대한 코드를 찾은 다음 새로운 번호를 받고 이전 코드를 업데이트 할 때 흥미로운 기술이라고 생각합니다. 손 파 손 파.


이것은 정말 많은 손을 흔들며입니다. 한편으로는 이론적으로 이것이 해결책이지만, 우리는 여전히 크지 만 여전히 유한 한 상태 머신을 작성할 수 있기 때문입니다. 반면에, 큰 상태 머신에 대한 명령어 포인터의 크기는 1 메가 바이트 이상일 수 있으며,이를 스타터가 아닌 것으로 만듭니다. 주어진 문제를 실제로 해결하기 위해서는 이것보다 약간 더 많은 생각이 필요합니다. 모든 상태뿐만 아니라 주어진 다음 입력 번호에서 수행 할 작업을 계산하는 데 필요한 모든 전이 상태도 나타내야합니다.
Daniel Wagner

4
다른 답변은 손을 흔드는 것에 대해 더 미묘하다고 생각합니다. 이제 결과 공간의 크기를 알았으므로 필요한 공간이 얼마나되는지 알고 있습니다. 다른 답변은 가능한 모든 최종 답변을 8093700 비트보다 작은 것으로 저장할 수 없습니다. 압축 (최종 상태) 이렇게하면 기껏해야 수 있습니다 가끔 (더 압축 알고리즘은 모든 입력을 압축 할 수없는) 공간을 줄일 수 있지만, 항상 전체 공간을 필요로 몇 가지 답이있을 것이다.
프란시스코 라이언 톨마 스키 I

어쨌든 몇몇 다른 답변은 어쨌든 어려운 하한을 언급했습니다 (예 : 원래 질문자 답변의 두 번째 문장).이 답변이 gestalt에 무엇을 추가하고 있는지 잘 모르겠습니다.
Daniel Wagner

원시 스트림을 저장하기 위해 3.5M을 참조하고 있습니까? (그렇지 않으면 사과하고이 응답을 무시하십시오). 그렇다면, 그것은 완전히 무관 한 하한입니다. 내 하한은 결과가 차지하는 공간의 양이며, 하한은 입력을 저장해야 할 경우 입력이 차지하는 공간의 양입니다. 문제는 TCP 연결에서 들어오는 스트림으로 표시됩니다 실제로 필요한지 여부가 확실하지 않은 경우 한 번에 하나의 숫자를 읽고 상태를 업데이트하므로 3.5M이 필요하지 않습니다. 어쨌든 3.5는이 계산과 직교합니다.
프란시스코 라이언 톨마 스키 I

"질문과 대답은 원래 2 번부터 8093729.5까지 2 개의 다른 방법으로 허용됩니다. 중복은 허용 된 100 만 개의 8 자리 숫자를 선택하고 중요하지 않은 순서로 정렬합니다." 내가 말하는 바에 대해 더 명확하게하는 방법을 모른다. 나는 나의 마지막 주석에서이 문장에 대해 구체적으로 언급했다.
Daniel Wagner

20

여기에 내 제안은 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 비트 델타의 버킷 인 멋진 컴팩트 상태 인코딩도 사용합니다.


다음 날에 항상 자신의 답변을 다시 읽는 것이 즐겁습니다. 따라서 가장 좋은 답변은 틀렸지 만 받아 들여지는 것은 stackoverflow.com/a/12978097/1763801 입니다. 기본적으로 삽입 정렬을 함수 LEN-1을 가져오고 LEN을 반환하는 함수로 사용합니다. 작은 세트를 미리 정렬하면 한 번에 모두 삽입하여 효율성을 높일 수 있다는 사실을 활용하십시오. 상태 표현은 내 손으로 제안하는 것보다 훨씬 작고 직관적입니다 (7 비트 숫자 버킷). 내 comp geo 생각은
멍청

1
나는 당신의 산술이 약간 떨어져 있다고 생각합니다. 나는 lg2 (100999999! / (99999999! * 1000000!)) = 1011718.55를 얻음
NovaDenizen

그렇습니다. 그것은 965가 아니라 988kb였습니다. 나는 1024 대 1000의 관점에서 느슨했습니다. 우리는 여전히 약 35kb를 가지고 놀았습니다. 답변에 수학 계산에 대한 링크를 추가했습니다.
davec

18

이 작업이 가능하다고 가정하십시오. 출력 직전에 백만 개의 정렬 된 숫자가 인 메모리로 표시됩니다. 그러한 표현에는 몇 가지가 있습니까? 반복되는 숫자가있을 수 있으므로 nCr (선택)을 사용할 수는 없지만 multisets에서 작동하는 multichoose라는 연산이 있습니다 .

  • 있다 2.2e2436455 범위 0..99,999,999에 만 개 번호를 선택하는 방법.
  • 가능한 모든 조합 또는 1,011,717 바이트를 나타내 려면 8,093,730 비트가 필요 합니다.

정렬 된 숫자 목록을 깔끔하게 (충분히) 표현할 수 있다면 이론적으로 가능할 수 있습니다. 예를 들어, 미친 표현에는 10MB 조회 테이블 또는 수천 줄의 코드가 필요할 수 있습니다.

그러나 "1M RAM"이 백만 바이트를 의미하는 경우 공간이 충분하지 않은 것이 분명합니다. 5 % 더 많은 메모리가 이론적으로 가능하다는 사실은 표현이 매우 효율적이어야하고 제정신이 아닐 수 있음을 나타냅니다.


백만 개의 숫자 (2.2e2436455)를 선택하는 방법의 수는 (256 ^ (1024 * 988))에 가깝습니다 (2.0e2436445). Ergo, 1M에서 약 32KB의 메모리를 제거하면 문제를 해결할 수 없습니다. 또한 최소 3KB의 메모리가 예약되어 있습니다.
johnwbyrd

물론 이것은 데이터가 완전히 무작위라고 가정합니다. 우리가 아는 한, 나는 단지 :)
Thorarin

이 수의 가능한 상태를 나타내는 일반적인 방법은 로그베이스 2를 가져 와서이를 나타내는 데 필요한 비트 수를보고하는 것입니다.
NovaDenizen

@ Thorarin, yup, 나는 일부 입력에서만 작동하는 "솔루션"에 아무런 의미가 없습니다.
Dan

12

(원래의 대답은 잘못되었습니다. 나쁜 수학에 대해 죄송합니다. 아래 나누기를 참조하십시오.)

이건 어때요?

처음 27 비트는 가장 낮은 숫자를 저장 한 후 다음 숫자와의 차이를 다음과 같이 인코딩합니다. 차이를 저장하는 데 사용되는 비트 수를 저장하는 5 비트, 그 다음 차이. 00000을 사용하여 해당 번호를 다시 보았다는 것을 나타냅니다.

숫자가 더 많이 삽입 될수록 숫자 사이의 평균 차이가 줄어들 기 때문에 더 많은 숫자를 추가 할 때 더 적은 비트를 사용하여 차이를 저장하기 때문에 작동합니다. 이것이 델타 목록이라고 믿습니다.

내가 생각할 수있는 최악의 경우는 모든 숫자가 균등 간격 (100)입니다. 예를 들어 0이 첫 번째 숫자라고 가정합니다.

000000000000000000000000000 00111 1100100
                            ^^^^^^^^^^^^^
                            a million times

27 + 1,000,000 * (5+7) bits = ~ 427k

구조에 레딧!

정렬 만하면이 문제는 쉬울 것입니다. 본 숫자를 저장하려면 122k (1 백만 비트)가 필요합니다 (0이 보이면 0 비트, 2300이 보이면 2300 비트 등).

숫자를 읽고 비트 필드에 저장 한 다음 카운트를 유지하면서 비트를 이동시킵니다.

그러나, 당신은 당신이 본 것을 기억해야합니다. 위의 하위 목록 답변에서이 구성표를 생각해 냈습니다.

하나의 비트를 사용하는 대신 2 또는 27 비트를 사용하십시오.

  • 00은 숫자가 보이지 않았다는 의미입니다.
  • 01은 한 번 본 것을 의미합니다
  • 1은 당신이 그것을 보았 음을 의미하며, 다음 26 비트는 몇 번의 카운트입니다.

나는 이것이 효과가 있다고 생각한다 : 중복이 없다면, 당신은 244k 목록을 가지고있다. 최악의 경우 각 숫자가 두 번 표시됩니다 (하나의 숫자가 세 번 표시되면 나머지 목록이 줄어 듭니다). 이는 50,000 번을 두 번 이상 보았으며 950,000 개의 항목을 0 번 또는 1 번 봤음을 의미합니다.

50,000 * 27 + 950,000 * 2 = 396.7k.

다음 인코딩을 사용하면 추가로 개선 할 수 있습니다.

0은 숫자 10을 보지 않았다는 것을 의미합니다. 10은 한 번 보았 음을 의미합니다.

평균적으로 280.7k의 스토리지가 생성됩니다.

편집 : 내 일요일 아침 수학이 잘못되었습니다.

최악의 경우 500,000 개의 숫자가 두 번 표시되므로 수학은 다음과 같습니다.

500,000 * 27 + 500,000 * 2 = 1.77M

대체 인코딩으로 인해 평균 스토리지 용량이

500,000 * 27 + 500,000 = 1.70M

: (


1
두 번째 숫자는 500000이기 때문에 아니요.
jfernand

11은 최대 64 회 (다음 6 비트 사용)를 보았 음을 의미하고 11000000은 다른 32 비트를 사용하여 보았던 횟수를 저장하는 것과 같은 중간 정도를 추가 할 수 있습니다.
τεκ

10
"백만 비트"번호는 어디서 얻었습니까? 당신은 2300 번째 비트가 2300이 보이는지를 나타냅니다. (실제로 2301st를 의미한다고 생각합니다.) 어떤 비트가 99,999,999가 표시되었는지 (최대 8 자리 숫자)를 나타 냅니까? 아마도 1 억 비트 일 것입니다.
user94559

당신은 당신의 백만과 1 억을 거꾸로 얻었다. 값이 발생할 수있는 대부분의 시간은 1 백만이며 값의 발생 횟수를 나타내는 데 20 비트 만 필요합니다. 마찬가지로 각 가능한 값에 대해 하나씩 100,000,000 비트 필드 (100 만 아님)가 필요합니다.
Tim R.

Uh, 27 + 1000000 * (5 + 7) = 12000027 비트 = 1.43M, 427K가 아닙니다.
Daniel Wagner

10

가능한 모든 입력에서이 문제에 대한 한 가지 해결책이 있습니다. 사기.

  1. TCP를 통해 m 값을 읽습니다. 여기서 m은 메모리에서 정렬 될 수있는 최대 값에 가깝습니다 (아마도 n / 4).
  2. 250,000 개 정도의 숫자를 정렬하여 출력하십시오.
  3. 다른 3/4 동안 반복하십시오.
  4. 수신자가 처리 할 때 수신 한 4 개의 숫자 목록을 병합합니다. (단 하나의 목록을 사용하는 것보다 훨씬 느리지 않습니다.)

7

나는 Radix Tree를 시도 할 것 입니다. 데이터를 트리에 저장할 수 있으면 순서대로 순회하여 데이터를 전송할 수 있습니다.

나는 당신이 이것을 1MB에 넣을 수 있는지 확신 할 수는 없지만 시도해 볼 가치가 있다고 생각합니다.


7

어떤 종류의 컴퓨터를 사용하고 있습니까? 다른 "일반"로컬 저장소는 없지만 비디오 RAM이 있습니까? 픽셀 당 1 메가 픽셀 x 32 비트 (예 : 32 비트)는 필요한 데이터 입력 크기와 거의 비슷합니다.

( 저 해상도 또는 저 색상 화면 모드를 선택한 경우 VRAM을 '빌려'사용 가능한 시스템 RAM을 확장 할 수 있는 오래된 Acorn RISC PC 의 메모리에 주로 요청 합니다!). 이것은 몇 MB의 일반 RAM 만있는 머신에서 오히려 유용했습니다.


1
의견이 있으십니까? - 난 그냥 질문의 명백한 제약 조건을 (스트레칭하기 위해 노력하고있어, 즉 속임수 창조적 ;-)
DNA

해커 뉴스의 관련 스레드가 한 번 구글 인터뷰 질문이라고 언급했기 때문에 컴퓨터가 전혀 없을 수도 있습니다.
mlvljr

1
예-질문을 편집하기 전에 면접 질문임을 표시했습니다.
DNA

6

기수 트리는 "접두사 압축"을 이용하기 때문에 기수 트리 표현이이 문제를 처리하는 데 가깝습니다. 그러나 1 바이트의 단일 노드를 나타낼 수있는 기수 트리 표현을 생각하기는 어렵습니다. 아마도 2가 한계에 달할 것입니다.

그러나 데이터가 표현되는 방식에 관계없이 일단 정렬되면 데이터를 접두사 압축 형식으로 저장할 수 있습니다. 여기서 10, 11 및 12는 001b, 001b, 001b로 표시됩니다. 이전 번호에서. 아마도 10101b는 5 씩 증가하고, 1101001b는 9 씩 증가 할 것입니다.


6

10 ^ 8 범위의 10 ^ 6 값이 있으므로 평균적으로 100 코드 포인트 당 하나의 값이 있습니다. N 번째 점에서 (N + 1) 번째까지의 거리를 저장하십시오. 중복 값은 0으로 건너 뜁니다. 즉, 건너 뛰기에 평균 7 비트 미만의 저장 공간이 필요하므로 8 백만 비트의 저장 공간에 백만 개가 행복하게 맞습니다.

이러한 스킵은 비트 스트림으로 인코딩되어야합니다 (예 : 허프만 인코딩). 삽입은 비트 스트림을 반복하고 새로운 값 뒤에 다시 쓰는 것입니다. 내재 된 값을 반복해서 작성하여 출력합니다. 실제로는 10 ^ 4 개의 코드 포인트 (및 평균 100 개의 값)를 포함하는 10 ^ 4 개의 목록으로 수행하려고합니다.

건너 뛰기의 길이에 대한 포아송 분포 (평균 = 분산 = 100)를 가정하여 랜덤 데이터에 대한 적절한 허프만 트리를 사전에 구축 할 수 있지만 실제 통계를 입력에 유지하고 처리 할 최적의 트리를 생성하는 데 사용할 수 있습니다 병리학 적 사례.


5

RAM이 1M이고 다른 로컬 저장소가없는 컴퓨터가 있습니다.

속이는 또 다른 방법 : 로컬이 아닌 (네트워크로 연결된) 스토리지를 대신 사용할 수 있고 (질문이 이것을 배제하지는 않음) 간단한 디스크 기반 병합을 사용하거나 인 메모리를 정렬하기에 충분한 RAM을 사용할 수있는 네트워크 서비스를 호출 할 수 있습니다 이미 주어진 (정말 독창적 인) 솔루션을 필요로하지 않고 1M 숫자 만 수락하면됩니다.

이것은 부정 행위 일 수도 있지만 실제 문제에 대한 해결책을 찾고 있는지, 규칙을 구부릴 수있는 퍼즐을 찾고 있는지 확실하지 않습니다 ... 후자의 경우 간단한 치트가 복잡한 것보다 더 나은 결과를 얻을 수 있습니다 그러나 "정품"솔루션 (다른 사람들이 지적했듯이 압축 가능한 입력에만 작동 할 수 있음).


5

해결책은 비디오 인코딩 기술, 즉 이산 코사인 변환 기술을 결합하는 것입니다. 디지털 비디오에서, 비디오의 밝기 또는 색상의 변화를 110 112 115 116과 같은 규칙적인 값으로 기록하는 대신, 각각은 마지막 (실행 길이 인코딩과 유사)에서 뺀다. 110 112 115 116은 110 2 3 1이됩니다. 값 2 3 1은 원본보다 적은 비트를 필요로합니다.

소켓에 도착하면 입력 값의 목록을 만듭니다. 우리는 각 요소에 값을 저장하지 않고 그 앞에있는 요소의 오프셋을 저장합니다. 우리는 가면서 정렬하므로 오프셋은 양수입니다. 그러나 오프셋은 너비가 8 자리 일 수 있으며 이는 3 바이트에 해당합니다. 각 요소는 3 바이트가 될 수 없으므로이를 패킹해야합니다. 각 바이트의 최상위 비트를 "연속 비트"로 사용할 수 있습니다. 이는 다음 바이트가 숫자의 일부이며 각 바이트의 하위 7 비트가 결합되어야 함을 나타냅니다. 중복에는 0이 유효합니다.

목록이 채워질수록 숫자는 서로 더 가까워 져야합니다. 즉, 다음 값까지의 거리를 결정하는 데 평균 1 바이트 만 사용됩니다. 편리한 경우 7 비트의 값과 1 비트의 오프셋이 있지만 "계속"값에 8 비트 미만이 필요한 스위트 스폿이있을 수 있습니다.

어쨌든, 나는 약간의 실험을했다. 난수 생성기를 사용하고 백만 개의 정렬 된 8 자리 10 진수를 약 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);
    }
}

4

우리는 모든 숫자를 얻기 전에 네트워킹 스택을 가지고 숫자를 정렬 된 순서대로 보낼 수 있습니다. 1M의 데이터를 보내면 TCP / IP는 1500 바이트 패킷으로 나누고 대상에 순서대로 스트리밍합니다. 각 패킷에는 시퀀스 번호가 부여됩니다.

우리는 이것을 손으로 할 수 있습니다. RAM을 채우기 직전에 우리는 우리가 가진 것을 정렬하고 목록을 대상에 보낼 수 있지만 각 숫자 주위에 순서대로 구멍을 남길 수 있습니다. 그런 다음 시퀀스의 구멍을 사용하여 같은 방법으로 숫자의 두 번째 1/2를 처리하십시오.

맨 끝에있는 네트워킹 스택은 결과 데이터 스트림을 애플리케이션에 전달하기 전에 순서대로 순서대로 어셈블합니다.

네트워크를 사용하여 병합 정렬을 수행하고 있습니다. 이것은 완전한 해킹이지만 이전에 나열된 다른 네트워킹 해킹에서 영감을 얻었습니다.


4

HN 스레드에서 Google 의 (나쁜) 접근 방식. RLE 스타일 카운트를 저장하십시오.

초기 데이터 구조는 '99999999 : 0'(모두 0, 숫자가 표시되지 않음)이며 숫자 3,866,344를 표시하므로 데이터 구조가 '3866343 : 0,1 : 1,96133654 : 0'이됩니다. 숫자가 항상 0 비트 수와 '1'비트 수 사이에서 번갈아 나타나는 것을 볼 수 있으므로 홀수는 0 비트와 짝수는 1 비트를 나타냅니다. 이것은 (3866343,1,96133654)가됩니다

그들의 문제는 중복을 다루지 않는 것처럼 보이지만 중복에 "0 : 1"을 사용한다고 가정 해 봅시다.

큰 문제 # 1 : 1M 정수를 삽입하는 데는 시간이 걸립니다 .

큰 문제 # 2 : 모든 일반 델타 인코딩 솔루션과 마찬가지로 일부 배포판은이 방법으로 처리 할 수 ​​없습니다. 예를 들어, 거리가 0:99 인 1m 정수 (예 : 각각 +99). 이제 0:99 범위 에서 임의의 거리 로 동일하게 생각하십시오 . (참고 : 99999999/1000000 = 99.99)

Google의 접근 방식은 합당하지 않고 느립니다. 그러나 그들의 방어에있어 그들의 문제는 약간 다를 수있다.


3

정렬 된 배열을 나타 내기 위해 첫 번째 요소와 인접한 요소 간의 차이를 저장할 수 있습니다. 이런 식으로 우리는 최대 10 ^ 8까지 합칠 수있는 10 ^ 6 요소를 인코딩하는 것에 관심이 있습니다. 이것을 D 라고합시다 . D 의 요소를 인코딩하려면 허프만 코드를 사용할 수 있습니다 . 허프만 코드의 사전은 이동 중에 생성 할 수 있으며 정렬 된 배열에 새 항목을 삽입 할 때마다 배열이 업데이트됩니다 (삽입 정렬). 새 항목으로 인해 사전이 변경되면 전체 배열을 새 인코딩과 일치하도록 업데이트해야합니다.

각 고유 요소의 수가 같은 경우 D의 각 요소를 인코딩하기위한 평균 비트 수가 최대화됩니다. D의 요소 d1 , d2 , ..., dN 이 각각 F 번 나타납니다 . 이 경우 (최악의 경우 입력 시퀀스에 0과 10 ^ 8이 모두 있음)

SUM (1 <= I <= N ) F . = 10 ^ 8

어디

합 (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 = 합 (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 개 이상의 고유 값을 가질 수 없도록 파종 합니다.


1
그 번호를 잊어 버린 것 같아요.
bestsss

중복 숫자의 경우 인접한 숫자의 차이는 0입니다. 문제를 일으키지 않습니다. 허프만 코드에는 0이 아닌 값이 필요하지 않습니다.
Mohsen Nosratinia

3

TCP의 재전송 동작을 악용합니다.

  1. TCP 구성 요소가 큰 수신 창을 작성하게하십시오.
  2. ACK를 보내지 않고 일정량의 패킷을 수신합니다.
    • 패스 (prefix)로 압축 된 데이터 구조를 생성하여 패스 처리
    • 더 이상 필요하지 않은 마지막 패킷에 대한 중복 ack 전송 / 재전송 시간 초과 대기
    • 고토 2
  3. 모든 패킷이 수락되었습니다

이것은 버킷 또는 여러 패스의 이점이 있다고 가정합니다.

배치 / 버킷을 정렬하고 병합하여 가능합니다. -> 기수

이 기술을 사용하여 처음 80 %를 승인하고 정렬 한 다음 마지막 20 %를 읽고 마지막 20 %에 가장 낮은 숫자의 처음 20 %에 해당하는 숫자가 포함되어 있지 않은지 확인하십시오. 그런 다음 20 % 가장 낮은 숫자를 보내고 메모리에서 제거하고 나머지 20 %를 새 숫자로 수락하고 병합하십시오. **


3

이러한 종류의 문제에 대한 일반적인 해결책은 다음과 같습니다 .

일반 절차

취해진 접근법은 다음과 같습니다. 이 알고리즘은 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]입니다.

이러한 차이는 단항 비트 스트림으로 인코딩됩니다. 이 스트림에서 A 1은 "누산기에 1을 추가하고 A 0은"누산기를 항목으로 방출하고 재설정 "을 의미하므로 차이 N은 N 1과 1로 표시됩니다.

모든 차이의 합은 알고리즘이 지원하는 최대 값에 도달하고 모든 차이의 개수는 알고리즘에 삽입 된 값의 양에 접근합니다. 이것은 결국 스트림이 최대 값 1과 카운트 0을 포함 할 것으로 예상한다는 것을 의미합니다. 이를 통해 스트림에서 0과 1의 예상 확률을 계산할 수 있습니다. 즉, 0 count/(count+maxval)의 확률은 1이고 1의 확률은이다 maxval/(count+maxval).

이 확률을 사용하여이 비트 스트림에 대한 산술 코딩 모델을 정의합니다. 이 산술 코드는 최적의 공간에서 정확히 1과 0의 양을 인코딩합니다. 중간 비트 스트림에 대해이 모델에서 사용되는 공간을 다음과 같이 계산할 수 있습니다 bits = encoded * log2(1 + amount / maxval) + maxval * log2(1 + maxval / amount). 알고리즘에 필요한 총 공간을 계산하려면 encodedamount와 동일하게 설정하십시오 .

말도 안되는 반복을 필요로하지 않기 위해 작은 오버 헤드가 버퍼에 추가 될 수 있습니다. 이는 알고리즘의 최대 시간 비용이 각 사이클마다 산술 코딩 압축 및 압축 해제이므로 알고리즘이이 오버 헤드에 적합한 수만큼 작동하도록합니다.

다음으로, 부기 데이터를 저장하고 산술 코딩 알고리즘의 고정 소수점 근사치에서 약간의 부정확성을 처리하기 위해 약간의 오버 헤드가 필요하지만, 전체적으로 알고리즘은 포함 할 수있는 여분의 버퍼로도 1MiB의 공간에 적합 할 수 있습니다 8000 개의 숫자, 총 1043916 바이트의 공간

최적

알고리즘의 (작은) 오버 헤드를 줄이는 것 외에는 더 작은 결과를 얻는 것이 이론적으로 불가능합니다. 최종 결과의 엔트로피를 포함하려면 1011717 바이트가 필요합니다. 효율성을 위해 추가 된 추가 버퍼를 빼면이 알고리즘은 1011916 바이트를 사용하여 최종 결과 + 오버 헤드를 저장했습니다.


2

입력 스트림을 몇 번 수신 할 수 있다면 훨씬 쉬워 질 것입니다.

그런 다음 십진수 값을 셀 수 있습니다. 계산 된 값을 사용하면 출력 스트림을 쉽게 만들 수 있습니다. 값을 세어 압축합니다. 입력 스트림에 무엇이 있는지에 달려 있습니다.


1

입력 스트림을 몇 번 수신 할 수 있다면 훨씬 쉽습니다 (아이디어, 아이디어 및 시간 성능 문제에 대한 정보 없음). 그런 다음 십진수 값을 셀 수 있습니다. 계산 된 값을 사용하면 출력 스트림을 쉽게 만들 수 있습니다. 값을 세어 압축합니다. 입력 스트림에 무엇이 있는지에 달려 있습니다.


1

정렬은 이차적 인 문제입니다. 다른 말했듯이, 정수를 저장하는 것은 어렵고 모든 입력 에서 작동하지 않습니다 . 숫자 당 27 비트가 필요하기 때문입니다.

이것에 대한 나의 취지는 : 연속적 인 (정렬 된) 정수 사이의 차이 만 저장하는 것이 가장 작기 때문에 저장하십시오. 그런 다음 숫자가 저장된 비트 수를 인코딩하기 위해 입력 번호 당 2 개의 추가 비트와 같은 압축 체계를 사용하십시오. 다음과 같은 것 :

00 -> 5 bits
01 -> 11 bits
10 -> 19 bits
11 -> 27 bits

주어진 메모리 제약 내에서 가능한 많은 입력 목록을 저장할 수 있어야합니다. 최대 입력 수에서 작동하도록 압축 구성표를 선택하는 방법에 대한 수학은 저를 초월합니다.

입력대한 도메인 별 지식 을 활용하여이를 기반으로 충분한 정수 압축 방식 을 찾을 수 있기를 바랍니다 .

그런 다음 데이터를 받으면 해당 정렬 목록에서 삽입 정렬을 수행합니다.


1

이제 실제 솔루션을 목표로 1MB의 RAM만으로 8 자리 범위의 모든 입력 사례를 처리합니다. 참고 : 진행중인 작업, 내일은 계속됩니다. 정렬 된 정수의 델타의 산술 코딩을 사용하면 1M 정렬 된 정수의 최악의 경우 항목 당 약 7 비트가 필요합니다 (99999999/1000000은 99이고 log2 (99)는 거의 7 비트이므로).

그러나 7 또는 8 비트에 도달하려면 1m 정수가 필요합니다! 더 짧은 계열은 더 큰 델타를 가지므로 요소 당 더 많은 비트를 갖습니다.

나는 가능한 한 많이 가져 와서 (거의) 압축을 시도하고 있습니다. 250K int에 가까운 첫 번째 배치에는 각각 약 9 비트가 필요합니다. 따라서 결과는 약 275KB가 소요됩니다. 남은 여유 메모리를 몇 번 반복하십시오. 그런 다음 압축 된 청크를 압축 해제하여 그 자리에서 압축 해제하십시오. 이것은 매우 어렵지만 가능합니다. 내 생각에

병합 된 목록은 정수 대상 당 7 비트에 점점 더 가까워집니다. 그러나 병합 루프에 얼마나 많은 반복이 필요한지 알 수 없습니다. 아마도 3.

그러나 산술 코딩 구현의 부정확성으로 인해 불가능할 수 있습니다. 이 문제가 전혀 발생하지 않으면 매우 빡빡 할 것입니다.

자원 봉사자가 있습니까?


산술 코딩이 가능합니다. 각 연속 델타가 음의 이항 분포에서 도출됨을 알 수 있습니다.
혼잡

1

숫자 사이의 차이를 순서대로 저장하고 인코딩을 사용하여 이러한 순서 번호를 압축하면됩니다. 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이므로 메모리가 충분해야합니다. 새로운 숫자를 삽입 할 때의 위치 합계를 저장하려면 약간의 메모리가 필요할 것입니다. 그런 다음 순서를 살펴보고 새 숫자를 삽입 할 위치를 찾고 필요한 경우 다음 차이를 줄인 후 모든 것을 올바르게 바꿉니다.


나는 믿고 내 분석 여기 이 제도는 일을하지 않는 것을 (우리가 5 비트 아닌 다른 크기를 선택할 수없는 경우에도)을 보여줍니다.
Daniel Wagner

@Daniel Wagner-덩어리 당 일정한 수의 비트를 사용할 필요가 없으며 덩어리 당 정수 비트를 사용할 필요도 없습니다.
군중

@crowding 구체적인 제안이 있으시면 듣고 싶습니다. =)
Daniel Wagner

@crowding 공간 산술 코딩에 걸리는 양에 대한 계산을 수행하십시오. 조금 울어 그렇다면 더 열심히 생각하십시오.
Daniel Wagner

더 알아보기. 오른쪽 중간 표현에서 기호의 완전한 조건부 분포 (Francisco는 Strilanc와 마찬가지로 가장 단순한 중간 표현을 가짐)는 계산하기 쉽습니다. 따라서 인코딩 모델은 문자 그대로 완벽 할 수 있으며 엔트로피 한계의 1 비트 내에있을 수 있습니다. 유한 정밀도 산술은 몇 비트를 추가 할 수 있습니다.
혼잡

1

우리가 그 숫자에 대해 아무것도 모른다면, 우리는 다음과 같은 제약으로 제한됩니다 :

  • 정렬하기 전에 모든 숫자를로드해야합니다.
  • 숫자 세트는 압축 할 수 없습니다.

이러한 가정이 유지되면 최소한 26,575,425 비트의 스토리지 (3,321,929 바이트)가 필요하므로 작업을 수행 할 방법이 없습니다.

귀하의 데이터에 대해 무엇을 말씀해 주시겠습니까?


1
당신은 그들을 읽고 가서 당신이 그들을 정렬합니다. 이론적으로 100M 구별 상자에 1M 구별 할 수없는 항목을 저장하려면 lg2 (100999999! / (99999999! * 1000000!)) 비트가 필요하며, 이는 1MB의 96.4 %에 해당합니다.
NovaDenizen

1

트릭은 "증가 카운터"= "+"및 "출력 카운터"= "!"의 압축 스트림으로 정수 다중 세트 인 알고리즘 상태를 나타내는 것입니다. 문자. 예를 들어, 세트 {0,3,3,4}는 "! +++ !! +!"로 표시되고 뒤에 "+"문자가 표시됩니다. 다중 세트를 수정하려면 한 번에 일정한 양의 압축을 해제 한 상태로 문자를 스트리밍하고 압축 된 형식으로 다시 스트리밍하기 전에 변경을 수행하십시오.

세부

최종 세트에는 정확히 10 ^ 6 개의 숫자가 있으므로 최대 10 ^ 6 개의 "!" 문자. 또한 범위의 크기는 10 ^ 8이며 최대 10 ^ 8 개의 "+"문자가 있음을 알고 있습니다. 10 ^ 8 "+"중 10 ^ 6 "!"를 배열 할 수있는 방법의 수는 (10^8 + 10^6) choose 10^6이므로 특정 배열을 지정하려면 ~ 0.965MiB` 의 데이터가 필요합니다. 꼭 맞습니다.

할당량을 초과하지 않고 각 캐릭터를 독립적으로 취급 할 수 있습니다. "!"보다 "+"문자가 정확히 100 배 더 많습니다. 문자를 사용하면 각 문자가 "+"인 100 : 1 확률로 단순화됩니다. 100 : 101의 승률은 문자 당 ~ 0.08 비트에 해당하며 , ~ 0.965MiB 와 거의 동일한 총계입니다 (종속성을 무시하면 ~ 12 비트 의 비용 만 발생 함) ,이 경우를!).

알려진 확률로 독립 문자를 저장하는 가장 간단한 기술은 허프만 코딩 입니다. 비현실적으로 큰 나무가 필요합니다 (10 문자 블록의 허프만 트리의 평균 비용은 약 2.4 비트이며 총 ~ 2.9 Mib입니다. 20 문자 블록의 허프만 트리의 평균 비용은 블록 당입니다 약 3 비트, 총 ~ 1.8MiB입니다. 우리는 아마도 백 개 정도의 크기의 블록이 필요할 것입니다. 이는 기존의 모든 컴퓨터 장비가 저장할 수있는 것보다 트리에 더 많은 노드를 의미합니다. ). 그러나 ROM은 문제에 따라 기술적으로 "무료"이며 트리의 규칙 성을 이용하는 실제 솔루션은 본질적으로 동일하게 보입니다.

의사 코드

  • ROM에 충분히 큰 허프만 트리 (또는 유사한 블록 단위 압축 데이터)가 저장되어 있어야합니다.
  • 10 ^ 8 "+"문자의 압축 문자열로 시작하십시오.
  • 숫자 N을 삽입하려면 N "+"문자가 지날 때까지 압축 된 문자열을 스트리밍 한 다음 "!"를 삽입하십시오. 오버 / 언더런을 피하기 위해 일정한 양의 버퍼링 된 블록을 유지하면서 재 압축 된 스트링을 이전 스트링으로 다시 스트리밍하십시오.
  • [입력, 스트림 압축 풀기> 삽입> 압축] 백만 번 반복 한 다음 압축을 풀고 출력

1
지금까지 이것은 실제로 문제에 대한 답을 얻는 유일한 답변입니다! 나는 산술 코딩이 코드북을 저장하고 심볼 경계에 대해 걱정할 필요가 없기 때문에 허프만 코딩보다 더 적합하다고 생각합니다. 의존성을 설명 할 수도 있습니다.
혼잡

입력 정수는 정렬되지 않습니다. 먼저 정렬해야합니다.
alecco

1
@alecco 알고리즘은 진행에 따라 정렬합니다. 그들은 분류되지 않은 상태로 저장되지 않습니다.
Craig Gidney

1

1MB-3KB 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, ...]로 재정렬하고이를 색인화하면 숫자가 '00'으로 구분 된 0 쌍 (예 : 11011 = 8 + 5 + 2 + 1 = 16)이없는 밀도가 높은 이진 피보나치 기본 인코딩을 사용하면 간격 표현을 충분히 짧게 유지할 수 있다고 생각합니다. 더 많은 분석.


0

스트림을받는 동안 다음 단계를 수행하십시오.

첫 번째 합리적인 청크 크기 설정

의사 코드 아이디어 :

  1. 첫 번째 단계는 모든 복제본을 찾아서 개수가 포함 된 사전에 붙이고 제거하는 것입니다.
  2. 세 번째 단계는 알고리즘 단계의 순서로 존재하는 숫자를 배치하고 n, n + 1 ..., n + 2, 2n, 2n + 1과 같은 첫 번째 숫자와 단계를 사용하여 카운터 특수 사전에 배치하는 것입니다. 2n + 2 ...
  3. 반복 횟수가 적은 나머지 1000 개 또는 10000 개와 같이 적당한 수의 범위를 청크로 압축하기 시작합니다.
  4. 숫자가 발견되면 해당 범위를 압축 해제하고 범위에 추가 한 후 잠시 더 오래 압축 해제하십시오.
  5. 그렇지 않으면 그 숫자를 바이트에 추가하십시오 [chunkSize]

스트림을받는 동안 처음 4 단계를 계속하십시오. 마지막 단계는 메모리를 초과하거나 범위를 정렬하기 시작하여 모든 데이터가 수집되고 결과를 순서대로 내뿜고 압축을 풀어야 할 때 순서대로 압축을 풀면 결과를 출력하기 시작하면 실패하는 것입니다. 당신은 그들에게 도착합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.