Сортування 1 мільйона 8-десяткових цифр з 1 Мб оперативної пам’яті


726

У мене є комп’ютер з 1 Мб оперативної пам’яті і немає іншого локального сховища. Я повинен використовувати його, щоб прийняти 1 мільйон 8-значних десяткових чисел через з'єднання TCP, сортувати їх, а потім відправити відсортований список через інше з'єднання TCP.

Список номерів може містити дублікати, які я не повинен відкидати. Код буде розміщено в ПЗУ, тому мені не потрібно віднімати розмір коду від 1 Мб. У мене вже є код для керування портом Ethernet та обробки TCP / IP-з'єднань, і для його стану потрібно 2 Кб, включаючи буфер 1 Кб, за допомогою якого код буде читати і записувати дані. Чи є рішення цієї проблеми?

Джерела запитань та відповідей:

slashdot.org

cleaton.net


45
Ем, 8-значний десятковий номер у мільйон разів (мінімум 27-бітове ціле двійкове число)> 1 Мб оперативної пам’яті
Mr47

15
1М оперативної пам’яті означає 2 ^ 20 байт? А скільки бітів у байті цієї архітектури? І чи є "мільйон" в "1 мільйон 8-значних десяткових чисел" мільйон SI (10 ^ 6)? Що таке 8-значне десяткове число, натуральне число <10 ^ 8, раціональне число, десяткове подання якого займає 8 цифр, виключаючи десяткову точку, чи щось інше?

13
1 мільйон 8 десяткових цифр чи 1 мільйон 8-бітних чисел?
Патрік Уайт

13
це нагадує мені статтю в "Журналі доктора Добба" (десь між 1998-2001 рр.), де автор використовував сортування вставок для сортування номерів телефонів під час їх читання: це був перший раз, коли я зрозумів, що іноді повільніше алгоритм може бути швидшим ...
Адрієн Пліссон

103
Ще одне рішення, про яке ніхто ще не згадував: придбайте обладнання з 2 Мб оперативної пам’яті. Це не повинно бути набагато дорожчим, і це зробить проблему набагато, набагато простіше вирішити.
Даніель Вагнер

Відповіді:


716

Є один досить підлий трюк, про який досі не згадували. Ми припускаємо, що у вас немає додаткового способу зберігання даних, але це не зовсім суто.

Один із способів вирішити вашу проблему - зробити таке жахливе, чого ніхто не повинен намагатися ні за яких обставин: Використовуйте мережевий трафік для зберігання даних. І ні, я не маю на увазі NAS.

Ви можете сортувати номери лише з кількома байтами оперативної пам’яті наступним чином:

  • Спочатку візьміть 2 змінні: COUNTER і VALUE.
  • Спочатку встановіть усі регістри на 0;
  • Кожен раз, коли ви отримуєте ціле число I, збільшення COUNTERта набірVALUE значення max(VALUE, I);
  • Потім надішліть ICMP-пакет запиту ехо з набором даних I на маршрутизатор . Стерти Iі повторити.
  • Кожен раз, коли ви отримуєте повернений пакет ICMP, ви просто витягаєте ціле число і знову відправляєте його знову в іншому запиті ехо. Це створює величезну кількість запитів ICMP, що прямують назад і вперед, містять цілі числа.

Як тільки ви COUNTERдосягнете 1000000, у вас є всі значення, що зберігаються в неперервному потоці запитів ICMP, і VALUEтепер містить максимальне ціле число. Виберіть кілька threshold T >> 1000000. Встановити COUNTERнуль. Кожен раз, коли ви отримуєте пакет ICMP, збільшуйте COUNTERта відправляйте вміщене ціле число Iназад в інший запит ехо, якщо I=VALUE, у разі, не передавайте його до місця призначення для відсортованих цілих чисел. Один раз COUNTER=T, зменшення VALUEна 1, скиньте COUNTERна нуль і повторіть. РазVALUE ви досягнете нуля, ви повинні передати всі цілі числа для того, щоб від найбільшого до найменшого до пункту призначення, і використали лише близько 47 біт оперативної пам’яті для двох постійних змінних (і будь-яку невелику кількість вам потрібно для тимчасових значень).

Я знаю, що це жахливо, і я знаю, що можуть виникати всілякі практичні питання, але я думав, що це може когось із вас посміяти або принаймні жахнути.


27
Ви в основному використовуєте мережеві затримки і перетворюєте свій маршрутизатор на якусь чергу?
Ерік Р.

335
Це рішення не просто поза коробкою; здається, забув свій ящик вдома: D
Владислав Зоров

28
Чудова відповідь ... Я люблю ці відповіді, тому що вони справді розкривають, наскільки різноманітним може бути рішення проблеми
StackOverflowed

33
ICMP не є надійним.
безсонник

13
@MDMarra: Ви помітите прямо вгорі, я кажу: "Один із способів вирішити вашу проблему - зробити наступну жахливу справу, яку ніхто не повинен намагатися ні за яких обставин". Була причина, що я це сказав.
Джо Фіцсімонс

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 байт для залишкових змінних та простору стека.

Він працює приблизно за 23 секунди на моєму Xeon W3520. Ви можете перевірити, що програма працює, використовуючи наступний скрипт 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
@ натискаючи так, ми дуже хочемо детального пояснення цього.
T Suds

25
Я думаю, що ключове зауваження полягає в тому, що 8-розрядне число містить близько 26,6 біт інформації, а один мільйон - 19,9 біт. Якщо ви дельту стискаєте список (зберігаєте відмінності суміжних значень), різниці становлять від 0 (0 біт) до 99999999 (26,6 біт), але ви не можете мати максимальну дельту між кожною парою. Найгіршим випадком насправді повинен бути один мільйон рівномірно розподілених значень, вимагаючи дельти (26,6-19,9) або приблизно 6,7 біт на дельту. Збереження одного мільйона значень 6,7 біт легко вписується в 1М. Стиснення Delta вимагає постійного сортування злиття, так що ви майже отримуєте це безкоштовно.
Бен Джексон

4
солодкий розчин. Вам слід відвідати його блог для пояснення preshing.com/20121025/…
davec

9
@BenJackson: Десь у вашій математиці є помилка. Є 2.265 x 10 ^ 2436455 унікальних можливих виходів (упорядковані набори 10 ^ 6 8-розрядних цілих чисел), для зберігання яких потрібно 8,094 х 10 ^ 6 бітів (тобто волосся під мегабайт). Жодна розумна схема не може стиснутись за межі цієї теоретичної межі інформації без втрат. З вашого пояснення випливає, що вам потрібно набагато менше місця, а значить, неправильно. Дійсно, "циркулярний" у наведеному вище рішенні достатньо великий, щоб вмістити потрібну інформацію, тому, здається, попереднє врахування врахувало це, але ви цього не вистачаєте.
Джо Фіцсімонс

5
@JoeFitzsimons: Я не розробив рекурсію (унікальні відсортовані набори n чисел від 0..м є (n+m)!/(n!m!)), тож ви повинні мати рацію. Ймовірно, це моє підрахунок, що дельта бітів містить три біти для зберігання - явно дельти 0 не приймають 0 біт для зберігання.
Бен Джексон

371

Будь ласка, дивіться першу правильну відповідь або пізню відповідь з арифметичним кодуванням . Нижче ви можете розважитись, але не на 100% безпроблемне рішення.

Це досить цікаве завдання, і ось ще одне рішення. Я сподіваюся, що хтось знайде результат корисним (або принаймні цікавим).

Етап 1: Початкова структура даних, приблизний метод стиснення, основні результати

Зробимо просту математику: у нас є 1М (1048576 байт) оперативної пам’яті спочатку для зберігання 10 ^ 6 8-значних десяткових чисел. [0; 99999999]. Отже, для зберігання одного числа потрібно 27 біт (з припущенням, що будуть використані непідписані числа). Таким чином, для зберігання необмеженого потоку буде потрібно 3,5 Мб оперативної пам’яті. Хтось уже сказав, що це здається нездійсненним, але я б сказав, що завдання можна вирішити, якщо введення "достатньо добре". В основному ідея полягає в тому, щоб стиснути вхідні дані з коефіцієнтом стиснення 0,29 або вище і сортувати належним чином.

Давайте спочатку вирішимо питання стиснення. Вже є деякі відповідні тести:

http://www.theeggeadventure.com/wikimedia/index.php/Java_Data_Compression

"Я провів тест на стиснення одного мільйона послідовних цілих чисел, використовуючи різні форми стиснення. Результати такі:"

None     4000027
Deflate  2006803
Filtered 1391833
BZip2    427067
Lzma     255040

Схоже, LZMA ( алгоритм ланцюга Лемпель-Зів-Марков ) - хороший вибір для продовження. Я підготував простий 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

EDIT

Висновок:

  1. Не намагайтеся обдурити Природу
  2. Використовуйте більш просте стиснення з меншим слідом пам’яті
  3. Деякі додаткові підказки дійсно потрібні. Загальне бронезахисне рішення не видається здійсненним.

Етап 2: посилене стиснення, остаточний висновок

Як вже було сказано в попередньому розділі, будь-яка відповідна техніка стиснення може бути використана. Тож давайте позбудемось LZMA на користь більш простого та кращого (по можливості) підходу. Є багато хороших рішень, включаючи арифметичне кодування , дерево Radix тощо.

У будь-якому випадку, проста, але корисна схема кодування буде більш наочною, ніж ще одна зовнішня бібліотека, надаючи якийсь чудовий алгоритм. Справжнє рішення є досить простим: оскільки існують відра з частково відсортованими даними, замість чисел можна використовувати дельти.

схема кодування

Випадковий тест на введення показує дещо кращі результати:

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. дає не такі вже й погані результати

Повний код можна знайти тут , реалізацію BinaryInput та BinaryOutput можна знайти тут

Остаточний висновок

Немає остаточного висновку :) Іноді дуже зручно рухатись на один рівень вгору і переглядати завдання з точки зору мета рівня .

Було весело провести деякий час з цим завданням. До речі, нижче є багато цікавих відповідей. Дякую за увагу та щасливе кодування.


17
Я використовував Inkscape . Чудовий інструмент до речі. Ви можете використовувати це джерело діаграми як приклад.
Ренат Гілманов

21
Напевно LZMA вимагає занадто багато пам'яті, щоб бути корисним у цьому випадку? Як алгоритм призначений мінімізувати кількість даних, які потрібно зберігати чи передавати, а не бути ефективним у пам'яті.
Mjiig

67
Це нісенітниця ... Отримайте 1 мільйон випадкових 27-бітових цілих чисел, відсортуйте їх, стисніть 7zip, xz, що б ви не хотіли. Результат перевищує 1 Мб. Приміщення зверху - стиснення послідовних чисел. Кодування дельтою цього коду 0bit буде просто числом, наприклад 1000000 (скажімо, в 4 байти). З послідовними і дублікатами (без пропусків) число 1000000 і 1000000 біт = 128 КБ, з 0 для дублюючого числа і 1 для позначення наступного. Коли у вас випадкові прогалини, навіть невеликі, LZMA є смішним. Це не призначено для цього.
alecco

30
Це насправді не спрацює. Я запустив симуляцію, і хоча стислих даних більше 1 МБ (близько 1,5 МБ), він все ще використовує понад 100 Мб оперативної пам’яті для стиснення даних. Тож навіть стислі цілі числа не відповідають проблемі, не кажучи вже про використання часу оперативної пам'яті. Присудження вам винагороди - це моя найбільша помилка у ставковому потоці.
Улюблений Onwuemene

10
Ця відповідь настільки прихильна, оскільки багато програмістів люблять блискучі ідеї, а не перевірений код. Якби ця ідея спрацювала, ви побачили вибраний і перевірений фактичний алгоритм стиснення, а не просте твердження, що напевно є той, хто може це зробити ... коли цілком можливо, що там немає жодного, який би міг це зробити. .
Олате

185

Рішення можливе лише через різницю між 1 мегабайт і 1 мільйон байт. Є близько 2 потужностей 8093729.5 різних способів вибрати 1 мільйон 8-розрядних чисел із дозволеними дублікатами та замовити неважливо, тому машина з лише 1 мільйоном байтів оперативної пам’яті не має достатнього стану, щоб представити всі можливості. Але 1M (менше 2k для TCP / IP) - це 1022 * 1024 * 8 = 8372224 біт, тому рішення можливе.

Частина 1, початкове рішення

Цей підхід потребує трохи більше 1М, я вдосконалюю його, щоб він змістився в 1М пізніше.

Я буду зберігати компактний відсортований список номерів у діапазоні від 0 до 99999999 як послідовність підсписів 7-бітних чисел. Перший підпис має вміст чисел від 0 до 127, другий підліст містить цифри від 128 до 255 і т.д. 100000000/128 рівно 781250, тому 781250 таких підсписок знадобиться.

Кожен підпис складається з 2-розрядного заголовка підпису, за яким йде тіло. Тіло підпису займає 7 біт на запис підпису. Усі сублістини об'єднані разом, а формат дає змогу визначити, де закінчується один підпис і починається наступний. Загальний обсяг пам’яті, необхідний для повністю заселеного списку, становить 2 * 781250 + 7 * 1000000 = 8562500 біт, що становить приблизно 1,021 М-байт.

4 можливі значення заголовка підспісу:

00 Порожній підпис, нічого не випливає.

01 Сінглтон, у підписці є лише один запис, і наступні 7 біт містять його.

10 У списку є щонайменше 2 різних числа. Записи зберігаються у зменшенному порядку, за винятком того, що останній запис менший або рівний першому. Це дозволяє ідентифікувати кінець підспису. Наприклад, числа 2,4,6 зберігатимуться як (4,6,2). Числа 2,2,3,4,4 зберігатимуться як (2,3,4,4,2).

11 У списку вміщено 2 та більше повторень одного номера. Наступні 7 біт дають число. Потім приходять нульові або більше 7-бітних записів зі значенням 1, а потім 7-бітний запис зі значенням 0. Довжина тіла підпису диктує кількість повторень. Наприклад, числа 12,12 зберігатимуться як (12,0), числа 12,12,12 зберігатимуться як (12,1,0), 12,12,12,12 - (12,1) , 1,0) тощо.

Я починаю з порожнього списку, читаю купу чисел і зберігаю їх у вигляді 32-бітових цілих чисел, сортую нові числа на місці (можливо, використовуючи велику групу), а потім об'єдную їх у новий компактний відсортований список. Повторюйте, поки не буде читання більше чисел, а потім ще раз пройдіться компактним списком, щоб отримати результат.

Рядок нижче представляє пам'ять безпосередньо перед початком операції злиття списку. "O" s - це область, яка містить відсортовані 32-бітні цілі числа. "X" - це область, що містить старий компактний список. Знаки "=" - це приміщення розширення для компактного списку, 7 біт для кожного цілого числа в "O" s. "Z" - це інші випадкові накладні витрати.

ZZZOOOOOOOOOOOOOOOOOOOOOOOOOO==========XXXXXXXXXXXXXXXXXXXXXXXXXX

Підпрограма злиття починається з читання в крайньому лівому "O" та в лівій лівій "X", і починає писати в крайній лівій частині "=". Покажчик запису не вловлює покажчик зчитування компактного списку, поки всі нові цілі числа не об'єднані, тому що обидва вказівники просувають 2 біти для кожного підпису та 7 біт для кожного запису у старому компактному списку, і є достатньо місця для 7-бітні записи для нових номерів.

Частина 2, набивши її в 1М

Щоб видавити рішення вище в 1 М, мені потрібно зробити формат списку компактніше. Я позбудусь одного з типів списків, щоб було лише 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 Підпис складається з 2 або більше повторів одного числа.

Мої 3 значення заголовка підспісу будуть "A", "B" і "C", тому мені потрібен спосіб представити підсистеми типу D.

Припустимо, у мене є заголовок підсистеми типу C, який супроводжується 3-ма записами, такими як "C [17] [101] [58]". Це не може бути частиною дійсного списку типу С, як описано вище, оскільки третій запис менший за другий, але більше, ніж перший. Я можу використовувати цей тип конструкції для представлення підспису типу 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 "s. Це 2 біти занадто довго.

Мені доведеться позичити 2 біти і повернути їх з 4 невикористаних біт у цій схемі. Коли читаєте, зустрічаючи "C {0NNNNNN} {11N00AB} 10", виведіть 2 екземпляри числа у "N" s, перезапишіть "10" в кінці бітами A і B і перемотайте вказівник читання на 2 біт. Для цього алгоритму руйнівні читання є нормальними, оскільки кожен компактний список проходить лише один раз.

Коли ви пишете підпис із 2-х повторів одного числа, напишіть "C {0NNNNNN} 11N00" і встановіть лічильник запозичених бітів на 2. При кожному записі, де запозичений лічильник бітів не дорівнює нулю, він зменшується для кожного записаного біта і "10" пишеться, коли лічильник дорівнює нулю. Тож наступні два біти, записані, перейдуть до слотів A і B, і тоді "10" потрапить до кінця.

З 3-х значень заголовка підспісу, представлених "00", "01" та "1", я можу призначити "1" найпопулярнішому типу підпису. Мені знадобиться невелика таблиця, щоб зіставити значення заголовків підсписів на типи підполістів, і мені знадобиться лічильник зустрічей для кожного типу підспісу, щоб я знав, що найкраще відображення заголовка підспісу.

Найгірший випадок мінімального представлення повнонаселеного компактного списку відбувається тоді, коли всі типи списків однаково популярні. У цьому випадку я зберігаю 1 біт на кожні 3 заголовки списку, тому розмір списку становить 2 * 781250 + 7 * 1000000 - 781250/3 = 8302083,3 біт. Округлення до 32-бітової межі слова, це 8302112 біт або 1037764 байт.

1М мінус 2k для стану TCP / IP та буферів - 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 байт для повноцінно заповненого компактного списку, це залишає мені 8764 - 4096 = 4668 байт для "Z" s на карті пам'яті.

Цього має бути достатньо для 10 таблиць відображення заголовків підсписів, 30 підрахунків заголовок підспісу та інших кількох лічильників, покажчиків та невеликих буферів, які мені знадобляться, та місця, яке я використав, не помічаючи, як простір для стеків для повернення функцій зворотного виклику та локальні змінні.

Частина 3, скільки часу знадобиться бігти?

При порожньому компактному списку 1-бітний заголовок списку буде використовуватися для порожнього підпису, а початковий розмір списку становитиме 781250 біт. У гіршому випадку список зростає 8 біт на кожне додане число, тому 32 + 8 = 40 біт вільного простору потрібно, щоб кожне з 32-бітових номерів було розміщене у верхній частині буфера списку, а потім відсортовано та об'єднано. У гіршому випадку зміна відображення заголовка підспісу призводить до використання місця 2 * 781250 + 7 * записів - 781250/3 біт.

При політиці зміни відображення заголовка підспісу після кожного п’ятого злиття, коли в списку буде щонайменше 800000 номерів, найгірший випадок залучатиме до загальної кількості близько 30 мільйонів активних читання та запису списків.

Джерело:

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


15
Я не думаю, що кращого рішення неможливо (у випадку, якщо нам потрібно працювати з будь-якими незжатими значеннями). Але цей, можливо, трохи вдосконалений. Не потрібно змінювати заголовки списків між 1-бітним та 2-бітним поданням. Натомість ви можете використовувати арифметичне кодування , що спрощує алгоритм, а також зменшує найгірший випадок бітів на заголовок з 1,67 до 1,58. І вам не потрібно переміщувати компактний список в пам'яті; замість цього використовуйте круговий буфер і змінюйте лише покажчики.
Євгеній Клюєв

5
Отже, нарешті, це було питання інтерв'ю?
mlvljr

2
Іншим можливим вдосконаленням є використання 100-елементних списків замість 128-елементних списків (тому що ми отримуємо найбільш компактне представлення, коли кількість списків дорівнює кількості елементів у наборі даних). Кожне значення підспису, яке слід кодувати арифметичним кодуванням (з однаковою частотою 1/100 для кожного значення). Це може заощадити близько 10000 біт, що набагато менше, ніж стиснення заголовків списків.
Євгеній Клюєв

У випадку C ви говорите: "Записи зберігаються у зменшенному порядку, за винятком того, що останній запис менший або рівний першому". Як тоді ви кодували 2,2,2,3,5? {2,2,3,5,2} виглядатиме як всього 2,2
Роллі

1
Можливе більш просте рішення кодування заголовка підспісу з однаковим коефіцієнтом стиснення 1,67 біта на підзаголовок без складного перемикання відображення. Ви можете комбінувати кожні 3 послідовних підзаголовки разом, які можна легко закодувати в 5 біт, оскільки 3 * 3 * 3 = 27 < 32. Ви їх поєднуєте combined_subheader = subheader1 + 3 * subheader2 + 9 * subheader3.
hynekcer

57

Відповідь Гілманова в своїх припущеннях дуже помилкова. Він починає спекулювати на основі безглуздої міри мільйона послідовних цілих чисел. Це означає відсутність прогалин. Ці випадкові прогалини, хоч і невеликі, справді роблять це поганою ідеєю.

Спробуйте самі. Отримайте 1 мільйон випадкових 27-бітових цілих чисел, відсортуйте їх, стисніть 7-Zip , xz, будь-який LZMA. Результат - понад 1,5 Мб. Приміщення зверху - стиснення послідовних чисел. Навіть дельта-кодування цього понад 1,1 Мб . І неважливо, що для стиснення використовується понад 100 Мб оперативної пам’яті. Тому навіть стислі цілі числа не відповідають проблемі, і неважливо використовувати час роботи оперативної пам'яті .

Мене прикро, як люди просто підтримують гарну графіку та раціоналізацію.

#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);

}

Тепер стисніть ints.bin за допомогою LZMA ...

$ 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
будь-який алгоритм з участю словника стиснення , заснований тільки за відсталий, я закодовані кілька користувальницьких з них і всі вони займають зовсім небагато пам'яті просто розмістити свої власні хеш - таблиці (і не HashMap в Java , як це зайва голодним на ресурси). Найближчим рішенням буде дельта, що кодує w / змінну довжину бітів і відскакує назад пакети TCP, які вам не подобаються. Ровесник повторно передаватиметься, все ще хитрий у кращому випадку.
bestsss

@bestsss так! перевірити мою останню відповідь, що не працює. Я думаю , що це може бути можливо.
alecco

3
Вибачте, але, здається, це також не відповідає на питання .
n611x007

@naxa так, він відповідає: це неможливо зробити в межах початкових параметрів питання. Це можна зробити лише в тому випадку, якщо розподіл чисел має дуже низьку ентропію.
alecco

1
Усі ці відповіді показують, що стандартні процедури стиснення мають труднощі при стисненні даних нижче 1 Мб. Може бути або не бути схемою кодування, яка може стискати дані, вимагаючи менше 1 МБ, але ця відповідь не доводить, що не існує схеми кодування, яка б так стискала дані.
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 біт. Я думаю, що цікавим прийомом було б припустити, що це все замовлення, пошук коду для цього замовлення, а потім, коли ви отримуєте новий номер, повертаючись назад і оновлюючи попередній код. Рука хвиля ручної хвилі.


Це дійсно багато махає рукою. З одного боку, теоретично це рішення, тому що ми можемо просто написати велику - але все ж кінцеву - державну машину; з іншого боку, розмір вказівного інструменту для цієї великої державної машини може бути більше одного мегабайт, що робить це нестартером. Це дійсно вимагає трохи більше роздумів, ніж це, щоб реально вирішити дану проблему. Нам потрібно не тільки представляти всі стани, але і всі перехідні стани, необхідні для обчислення того, що потрібно робити на будь-якому даному наступному вхідному номері.
Даніель Вагнер

4
Я думаю, що інші відповіді є просто більш тонкими щодо махання рукою. З огляду на те, що тепер ми знаємо розмір простору результату, ми знаємо, скільки простору нам потрібно. Жодна інша відповідь не зможе зберігати будь-яку можливу відповідь у чомусь меншому розмірі, ніж 8093700 біт, оскільки саме стільки можуть бути кінцевих станів. Робота компресу (остаточний стан) може в кращому випадку іноді скоротити простір, але завжди знайдеться відповідь, яка потребує повного простору (жоден алгоритм стиснення не може стискати кожен вхід).
Франциско Райан Толмаський I

Кілька інших відповідей у ​​будь-якому випадку вже згадували жорстку нижню межу (наприклад, друге речення оригінальної відповіді запитувача), тому я не впевнений, що бачу, що ця відповідь додає до гештальту.
Даніель Вагнер

Ви посилаєтесь на 3.5M для зберігання потоку сировини? (Якщо ні, мої вибачення та ігнорування цієї відповіді). Якщо так, то це абсолютно не пов'язана нижня межа. Моя нижня межа - скільки місця займає результат, нижня межа - скільки місця займають входи, якщо потрібно їх зберігати - враховуючи, що питання було сформульовано як потік, що надходить з TCP-з'єднання. Не ясно, чи справді вам це потрібно, ви можете читати по одному номеру одночасно і оновлювати свій стан, таким чином, вам не знадобиться 3.5М - в будь-якому випадку, що 3.5 є ортогональним для цього обчислення.
Франциско Райан Толмаський I

"Існує приблизно 2 різних способів 8093729.5 для вибору 1 мільйона 8-розрядних чисел з дозволеними дублікатами та замовлення незначного" <- з оригінальної відповіді запитувача. Не знаю, як бути більш зрозумілим щодо того, про яку межу я говорю. Я досить конкретно посилався на це речення в останньому коментарі.
Даніель Вагнер

20

Мої пропозиції тут багато завдячують рішенню Дена

Спочатку я припускаю, що рішення має обробляти всі можливі списки введення. Я думаю, що популярні відповіді не припускають цього припущення (що IMO - це величезна помилка).

Відомо, що жодна форма стиснення без втрат не зменшить розмір усіх входів.

Усі популярні відповіді передбачають, що вони зможуть застосувати стиснення досить ефективно, щоб забезпечити їм додатковий простір. Насправді шматок зайвого простору, достатньо великий, щоб вмістити частину їх частково заповненого списку в нестисненому вигляді і дати їм змогу виконувати свої сортування. Це лише погане припущення.

Для такого рішення кожен, хто знає, як вони стискають, зможе спроектувати деякі вхідні дані, які не добре стискаються для цієї схеми, і "рішення", швидше за все, потім зламається через нестачу місця.

Натомість я використовую математичний підхід. Наші можливі результати - це всі списки довжини LEN, що складаються з елементів у діапазоні 0..MAX. Тут LEN - 1 000 000, а наш MAX - 100 000 000.

Для довільних LEN та MAX кількість бітів, необхідних для кодування цього стану, становить:

Log2 (MAX Multichoose LEN)

Тож для нашого числа, після того як ми завершимо отримання та сортування, нам знадобиться принаймні Log2 (100 000 000 МС 1 000 000) біт, щоб зберегти наш результат таким чином, що дозволяє однозначно розрізнити всі можливі результати.

Це ~ = 988 кб . Тож насправді у нас достатньо місця, щоб утримати результат. З цієї точки зору це можливо.

[Видалено безглузді скандали, коли існують кращі приклади ...]

Найкраща відповідь тут .

Ще одна гарна відповідь є тут і в основному використовує сортування вставки як функцію розширення списку одним елементом (буферизує декілька елементів і попередньо сортувати, щоб дозволити вставляти більше одного за один раз, економить трохи часу). використовує також приємне компактне кодування стану, відра з семи бітових дельт


Завжди весело перечитати власну відповідь на наступний день ... Тож, хоча найвища відповідь є неправильною, прийнята одна stackoverflow.com/a/12978097/1763801 є досить хорошою. В основному використовує сортування вставки як функцію приймати список LEN-1 та повертати LEN. Виграє той факт, що якщо ви складаєте невеликий набір, ви можете вставити їх за один прохід, щоб підвищити ефективність. Представлення штату досить компактне (відра з 7-бітовими номерами) краще, ніж моє ручно-хвилясте пропозиція, і більш інтуїтивне. мої думки щодо географічних думок були сміливими, пробачте про це
davec

1
Я думаю, що ваша арифметика трохи відключена. Я отримую lg2 (100999999! / (99999999! * 1000000!)) = 1011718.55
NovaDenizen

Так, спасибі, це було 988 кб, а не 965. Я був неохайним в плані 1024 проти 1000. Нам все ще залишається близько 35 кб, щоб пограти. У відповідь я додав посилання на математичний розрахунок.
davec

18

Припустимо, це завдання можливе. Незадовго до виходу з'явиться в пам'яті представлення мільйона відсортованих чисел. Скільки існує різних таких уявлень? Оскільки може бути повторне число, ми не можемо використовувати nCr (вибирати), але є операція під назвою мультикоссей, яка працює на мультисетах .

  • Існує 2.2e2436455 способів вибрати мільйон чисел у діапазоні 0..99,999,999.
  • Для цього потрібно 8 093 730 біт, щоб представити будь-яку можливу комбінацію, або 1011 717 байт.

Тож теоретично це можливо, якщо ви зможете скласти здорове (достатньо) подання відсортованого списку чисел. Наприклад, для божевільного представлення може знадобитися таблиця пошуку 10 МБ або тисячі рядків коду.

Однак якщо "1М оперативної пам'яті" означає мільйон байт, то явно місця не вистачає. Той факт, що на 5% більше пам’яті робить теоретично можливим, підказує мені, що представлення повинно бути ДУЖЕ ефективним і, ймовірно, не розумним.


Кількість способів вибору мільйона чисел (2.2e2436455) наближається до (256 ^ (1024 * 988)), тобто (2.0e2436445). Ерго, якщо ви забираєте в 1М близько 32 КБ пам'яті, проблему вирішити не вдалося. Також пам’ятайте, що принаймні 3 КБ пам’яті було зарезервовано.
johnwbyrd

Звичайно, це передбачає цілком випадкові дані. Наскільки ми знаємо, це так, але я просто кажу :)
Торарін

Умовний спосіб представити цю кількість можливих станів - це взяття бази журналів 2 та повідомлення кількості бітів, необхідних для їх представлення.
NovaDenizen

@Thorarin, так, я не бачу сенсу в "рішенні", яке працює лише для деяких входів.
День

12

(Моя оригінальна відповідь була неправильною, вибачте за погану математику. Дивіться нижче перерви.)

Як щодо цього?

Перші 27 біт зберігають найменше число, яке ви бачили, потім різницю до наступного баченого числа кодуєте так: 5 біт для зберігання кількості бітів, що використовуються для зберігання різниці, а потім різницю. Використовуйте 00000, щоб вказати, що ви знову побачили це число.

Це працює, тому що як більше вставлених чисел, середня різниця між числами зменшується, тому ви використовуєте менше біт для зберігання різниці, коли ви додаєте більше чисел. Я вважаю, що це називається дельта-списком.

Найгірший випадок, який я можу придумати, - це всі числа, рівномірно розташовані (на 100), наприклад, якщо припустити 0 - це перше число:

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

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

Reddit на допомогу!

Якби все, що вам потрібно було, сортувати їх, ця проблема була б легкою. Для зберігання чисел, які ви бачили, потрібно 122 к (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,7 к.

Ви можете внести подальші вдосконалення, якщо використовувати таке кодування:

0 означає, що ви не бачили цифру 10, значить ви бачили її один раз 11 - це як ви постійно рахуєте

Що в середньому призведе до 280,7 тис. Сховищ.

EDIT: моя математика в неділю вранці була неправильною.

Найгірший випадок - ми бачимо 500 000 чисел двічі, тож математика стає:

500 000 * 27 + 500 000 * 2 = 1,77М

Чергове кодування призводить до середнього зберігання

500 000 * 27 + 500 000 = 1,70 млн

: (


1
Ну ні, оскільки другий номер буде 500000.
jfernand

Можливо, додайте якийсь проміжний, наприклад, коли 11 означає, що ви бачили число до 64 разів (використовуючи наступні 6 біт), а 11000000 означає використовувати ще 32 біти, щоб зберегти кількість разів, коли ви його бачили.
τεκ

10
Звідки ви взяли номер "1 мільйон біт"? Ви сказали, що 2300-й біт означає, чи було видно 2300. (Я думаю, ви насправді мали на увазі 2301-е.) Який біт являє собою, чи бачили 99,999,999 (найбільше 8-розрядне число)? Імовірно, це був би 100-мільйонний біт.
user94559

Ви отримали один мільйон і сто мільйонів назад. Найбільше значення, можливо, може становити 1 мільйон, і вам знадобиться лише 20 біт, щоб представити кількість входів значення. Так само вам потрібно 100 000 000 бітових полів (не 1 мільйон), по одному для кожного можливого значення.
Тім Р.

Ага, 27 + 1000000 * (5 + 7) = 12000027 біт = 1,43М, а не 427К.
Даніель Вагнер

10

Існує одне рішення цієї проблеми на всіх можливих входах. Чит.

  1. Прочитайте значення m через TCP, де m знаходиться біля максимуму, який можна відсортувати в пам'яті, можливо n / 4.
  2. Сортуйте 250 000 (або близько того) чисел та виведіть їх.
  3. Повторіть для інших 3 чверті.
  4. Нехай одержувач об'єднує 4 списки цифр, які він отримав під час їх обробки. (Це не набагато повільніше, ніж використання одного списку.)

7

Я б спробував дерево Радикса . Якщо ви могли зберігати дані в дереві, ви могли б зробити проїзд по порядку для передачі даних.

Я не впевнений, що ви могли це вкласти в 1 Мб, але я думаю, що варто спробувати.


7

Який комп'ютер ви використовуєте? Він може не мати іншого "звичайного" локального сховища, але чи має він, наприклад, відео оперативну пам’ять? 1 мегапіксель x 32 біта на піксель (скажімо) досить близький до потрібного розміру введення даних.

(Я багато в чому запитую на пам'ять старого ПК Acorn RISC , який міг би "запозичити" VRAM для розширення доступної системної оперативної пам'яті, якщо ви вибрали режим низького дозволу або низьку кольорову глибину!). Це було досить корисно на машині з лише декількома МБ звичайної оперативної пам’яті.


1
Хочете коментувати, downvoter? - Я просто намагаюся розкрити очевидні протиріччя питання (тобто творчо обманювати ;-)
ДНК

Можливо, взагалі немає комп’ютера, оскільки відповідна тема в новинах хакера згадує, що колись це було питання інтерв'ю Google.
mlvljr

1
Так - я відповів перед тим, як питання було відредаговано, щоб вказати, що це питання інтерв'ю!
ДНК

6

Представлення радиксного дерева наблизиться до вирішення цієї проблеми, оскільки дерево radix використовує перевагу "стиснення префікса". Але важко уявити радіальне зображення дерева, яке могло б представляти один вузол в одному байті - два, мабуть, стосуються межі.

Але, незалежно від того, як представлені дані, після їх сортування вони можуть зберігатися у стисненому префіксом вигляді, де числа 10, 11 та 12 будуть представлені, скажімо, 001b, 001b, 001b, що вказує на збільшення 1 від попереднього числа. Можливо, тоді 10101b буде представляти приріст 5, 1101001b з приростом 9 і т.д.


6

Є 10 ^ 6 значень у діапазоні 10 ^ 8, тому в середньому є одне значення на сто кодових балів. Зберігайте відстань від N-ї точки до (N + 1) -ї. Дублікати значень мають пропуск 0. Це означає, що для зберігання потрібно в середньому трохи менше 7 біт, тому мільйон з них із задоволенням впишеться в наші 8 мільйонів біт пам’яті.

Ці пропуски повинні бути закодовані в бітовий потік, скажімо, кодування Huffman. Вставка полягає в ітерації через бітовий потік і переписуванні після нового значення. Вивести шляхом повторення та виписання мається на увазі значення. Для практичності це, мабуть, хочеться зробити так, як, скажімо, 10 ^ 4 списки, що охоплюють 10 ^ 4 кодових пункту (і в середньому 100 значень) кожен.

Гарне дерево Хаффмана для випадкових даних можна побудувати апріорі, якщо припустити розподіл Пуассона (середнє = дисперсія = 100) по довжині пропусків, але реальну статистику можна вести на вхідних даних і використовувати для створення оптимального дерева для вирішення патологічні випадки.


5

У мене є комп’ютер з 1М оперативної пам’яті та відсутнім іншим локальним сховищем

Ще один спосіб обману: ви можете використовувати натомість не локальну (мережеву) пам’ять (ваше питання цього не виключає) та зателефонувати в мережевий сервіс, який міг би використовувати простого дискового об'єднання (або просто достатньої кількості оперативної пам’яті для сортування в пам’яті, оскільки ви потрібно лише приймати 1М номери), не потребуючи вже наданих (мабуть, надзвичайно геніальних) рішень.

Це може бути обманом, але незрозуміло, чи шукаєте ви вирішення проблеми в реальному світі, або пазл, який запрошує згинати правила ... якщо останнє, то простий чит може отримати кращі результати, ніж складний але "справжнє" рішення (яке, як зазначали інші, може працювати тільки для стислих входів).


5

Я думаю, що рішення полягає в поєднанні методів кодування відео, а саме дискретного перетворення косинусів. У цифровому відео, а не записуючи зміни яскравості або кольору відео як звичайні значення, такі як 110 112 115 116, кожне віднімається від останнього (подібно до кодування довжини виконання). 110 112 115 116 стає 110 2 3 1. Значенням 2 3 1 потрібно менше бітів, ніж оригінали.

Отже, скажемо, що ми створюємо список вхідних значень, коли вони надходять у сокет. Ми зберігаємо в кожному елементі не значення, а зміщення того, що передує. Ми розбираємося в міру того, як йдемо, тож компенсації будуть лише позитивними. Але зміщення може становити 8 знаків після десяткової цифри, що відповідає 3 байтам. Кожен елемент не може бути 3 байти, тому нам потрібно спакувати їх. Ми могли б використовувати верхній біт кожного байта як "біт продовження", вказуючи на те, що наступний байт є частиною числа, а нижні 7 біт кожного байта потрібно поєднувати. нуль дійсний для дублікатів.

По мірі заповнення списку цифри повинні зближуватися, тобто в середньому використовується лише 1 байт для визначення відстані до наступного значення. 7 біт значення та 1 біт зміщення, якщо це зручно, але може бути солодке місце, яке вимагає менше 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);
    }
}

4

Ми могли б пограти з мережевим стеком, щоб надіслати номери в упорядкованому порядку, перш ніж мати всі номери. Якщо ви надішлете 1М даних, TCP / IP розбить їх на 1500 байт-пакетів та передасть їх у цільове значення. Кожному пакету буде наданий порядковий номер.

Ми можемо це зробити вручну. Незадовго до того, як ми заповнимо оперативну пам’ять, ми зможемо сортувати те, що у нас є, і відправити список до нашої цілі, але залишити отвори в нашій послідовності навколо кожного номера. Потім обробіть 2 1/2 1/2 чисел так само, використовуючи ці отвори в послідовності.

Стек мереж на дальньому кінці буде збирати отриманий потік даних у порядку послідовності, перш ніж передавати його додатку.

Це використовується мережа для здійснення сортування злиття. Це тотальний злом, але мене надихнули інші перелічені мережеві хаки.


4

Google «s (поганий) підхід, з HN нитки. Зберігайте підрахунки в стилі RLE.

Ваша початкова структура даних - "99999999: 0" (усі нулі, не бачили жодного числа), а потім скажемо, що ви бачите число 3 866 344, тож ваша структура даних стає "3866343: 0,1: 1,96133654: 0" Ви можете бачити, що числа завжди будуть чергуватися між кількістю нульових бітів і кількістю '1' бітів, тому ви можете просто припустити, що непарні числа представляють 0 біт, а парні числа - 1 біт. Це стає (3866343,1 96133654)

Схоже, їхня проблема не стосується дублікатів, але скажімо, що вони використовують "0: 1" для дублікатів.

Велика проблема №1: вставки для 1M цілих чисел займуть віків .

Велика проблема №2: як і всі звичайні рішення для кодування дельти, деякі розподіли неможливо покрити таким чином. Наприклад, 1м цілих чисел з відстанями 0:99 (наприклад, +99 кожне). Тепер подумайте так само, але з випадковою відстані в діапазоні 0:99 . (Примітка: 99999999/1000000 = 99,99)

Підхід Google є і недостойним (повільним), і неправильним. Але на їх захист, проблема їх може бути дещо іншою.


3

Для представлення відсортованого масиву можна просто зберегти перший елемент та різницю між суміжними елементами. Таким чином, ми маємо на увазі кодування 10 ^ 6 елементів, які можуть скласти не більше 10 ^ 8. Давайте назвемо це D . Для кодування елементів D можна використовувати код Хаффмана . Словник для коду Хаффмана можна створити на ходу, а масив оновлюється кожного разу, коли в відсортований масив (сортування вставки) новий елемент вставляється. Зауважте, що при зміні словника через новий елемент весь масив повинен бути оновлений, щоб він відповідав новому кодуванню.

Середня кількість бітів для кодування кожного елемента D максимальна, якщо ми маємо рівну кількість кожного унікального елемента. Скажіть, елементи d1 , d2 , ..., dN в D кожен з'являються F разів. У такому випадку (в гіршому випадку ми маємо і 0, і 10 ^ 8 у послідовності введення)

сума (1 <= я <= N ) F . di = 10 ^ 8

де

сума (1 <= i <= N ) F = 10 ^ 6, або F = 10 ^ 6 / N, а нормалізована частота буде p = F / 10 ^ = 1 / N

Середня кількість бітів буде -log2 (1 / P ) = log2 ( N ). У цих умовах ми повинні знайти випадок , який максимізує N . Це відбувається, якщо у нас є послідовні числа для di, починаючи з 0, або, di = i -1, отже

10 ^ 8 = сума (1 <= я <= 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

Для дублюючих чисел різниця між сусідніми числами буде нульовою. Не створює жодних проблем. Код Хаффмана не вимагає ненульових значень.
Мохсен Носратінія

3

Я б скористався поведінкою ретрансляції TCP.

  1. Зробіть так, щоб компонент TCP створив велике вікно прийому.
  2. Отримайте деяку кількість пакетів, не надсилаючи для них ACK.
    • Обробляйте ті проходи, створюючи деяку (префікс) стиснуту структуру даних
    • Надішліть дублікат 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 та 0 0.

Сума всіх різниць наблизиться до максимального значення, яке підтримує алгоритм, а кількість усіх різниць наблизиться до кількості значень, вставлених в алгоритм. Це означає, що ми очікуємо, що потік в кінці буде містити максимум 1 і рахувати 0. Це дозволяє обчислити очікувану ймовірність 0 і 1 у потоці. А саме, ймовірність 0 є, count/(count+maxval)а ймовірність a 1 єmaxval/(count+maxval) .

Ми використовуємо ці ймовірності для визначення моделі арифметичної кодування над цим бітовим потоком. Цей арифметичний код буде кодувати саме ці величини 1 і 0 в оптимальному просторі. Ми можемо обчислити простір , що використовується цієї моделі для будь-якого проміжного бітового потоку , як: bits = encoded * log2(1 + amount / maxval) + maxval * log2(1 + maxval / amount). Щоб обчислити загальний необхідний простір для алгоритму, встановіть encodedрівну суму.

Щоб не вимагати смішної кількості ітерацій, до буфера можна додати невелику накладну. Це забезпечить, що алгоритм буде принаймні діяти на кількість чисел, що вміщуються в цьому режимі, оскільки на сьогодні найбільша часова вартість алгоритму - це стиснення та декомпресія арифметичного кодування кожного циклу.

Поряд із цим, деякий накладний обсяг необхідний для зберігання даних бухгалтерського обліку та обробки незначних неточностей у наближенні до фіксованої точки алгоритму арифметичного кодування, але в цілому алгоритм здатний вміститися в 1 Мбіт простору навіть із додатковим буфером, який може містити 8000 чисел, що становить 1043916 байт простору.

Оптимальність

Поза зменшенням (невеликих) накладних витрат алгоритму теоретично не можна отримати менший результат. Щоб просто містити ентропію кінцевого результату, було б необхідно 1011717 байт. Якщо відняти додатковий буфер, доданий для ефективності, цей алгоритм використовував 1011916 байт для зберігання кінцевого результату + накладних витрат.


2

Якби вхідний потік можна було отримати кілька разів, це було б набагато простіше (немає інформації про це, ідею та проблему, пов'язану з виконанням часу).

Потім ми могли порахувати десяткові значення. За допомогою підрахованих значень було б легко зробити вихідний потік. Стисніть, підраховуючи значення. Це залежить від того, що було б у вхідному потоці.


1

Якщо вхідний потік можна було отримати декілька разів, це було б набагато простіше (немає інформації про це, ідею та проблему, що стосується часу). Потім ми могли порахувати десяткові значення. За допомогою підрахованих значень було б легко зробити вихідний потік. Стисніть, підраховуючи значення. Це залежить від того, що було б у вхідному потоці.


1

Сортування тут є вторинною проблемою. Як говорилося в іншому, просто зберігати цілі числа важко і не може працювати на всіх вхідних даних , оскільки 27 біт на число буде необхідним.

Я вважаю: зберігайте лише відмінності між послідовними (відсортованими) цілими числами, оскільки вони, швидше за все, будуть невеликими. Потім використовуйте схему стиснення, наприклад, з 2 додатковими бітами на кожне вхідне число, щоб кодувати, скільки біт зберігається в номері. Щось на зразок:

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

Потрібно мати можливість зберігати достатню кількість можливих списків введення в межах заданого обмеження пам'яті. Математика, як вибрати схему стиснення, щоб вона працювала на максимальній кількості входів, не в мене.

Я сподіваюсь, що ви, можливо, зможете скористатись знаннями вашого домену для конкретного домену, щоб знайти достатньо хорошу цілочисленну схему стиснення на основі цього.

Так і тоді ви робите вставку в цьому сортованому списку під час отримання даних.


1

Тепер націлене на власне рішення, що охоплює всі можливі випадки введення в 8-значний діапазон із лише 1 Мб оперативної пам’яті. ПРИМІТКА: робота триває, завтра буде продовжено. Використовуючи арифметичне кодування дельт відсортованих ints, найгірший випадок для 1M відсортованих int коштуватиме приблизно 7 біт на запис (оскільки 99999999/1000000 - 99, а log2 (99) - майже 7 біт).

Але вам потрібно відсортовано цілі 1 м, щоб дістатися до 7 або 8 біт! Більш короткі серії матимуть великі дельти, тому більше біт на елемент.

Я працюю над тим, щоб взяти якомога більше і стиснути (майже) на місці. Для першої партії близько 250 К в кращому випадку знадобиться близько 9 біт. Таким чином, результат займе близько 275 КБ. Повторіть, залишивши вільну пам'ять кілька разів. Потім декомпрес-злиття на місці стискають ці стислі шматки. Це досить важко , але можливо. Я думаю.

Об'єднані списки наближаються і наближаються до цілі 7 біт на ціле число. Але я не знаю, скільки ітерацій знадобиться циклу злиття. Можливо, 3.

Але неточність реалізації арифметичного кодування може унеможливити. Якщо ця проблема взагалі можлива, це було б надзвичайно жорстко.

Будь-які волонтери?


Арифметичне кодування працює. Це може допомогти помітити, що кожна послідовна дельта виводиться з негативного біноміального розподілу.
скупчення

1

Вам просто потрібно зберегти відмінності між числами в послідовності і використовувати кодування для стиснення цих порядкових чисел. Маємо 2 ^ 23 біта. Ми поділимо його на 6-бітні шматки, а останній біт вкаже, чи поширюється число на ще 6 біт (5 біт плюс розтягнутий фрагмент).

Таким чином, 000010 - це 1, а 000100 - 2. 000001100000 - це 128. Тепер ми вважаємо найгірший показник у представленні відмінностей у послідовності чисел до 1000000. Може бути 10 000 000/2 ^ 5 різниць, більших за 2 ^ 5, 10 000 000/2 ^ 10 різниць, більших за 2 ^ 10, і 10 000 000/2 ^ 15 різниць, більше 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, у нас легко має бути достатньо пам’яті. Нам, напевно, знадобиться ще трохи пам'яті, щоб зберігати суму, де ми вставляємо нові числа. Потім ми проходимо послідовність і знаходимо, куди потрібно вставити наше нове число, зменшуємо наступну різницю, якщо потрібно, і переміщуємо все після цього правильно.


Я вважаю, що мій аналіз тут показує, що ця схема не працює (і не може навіть якщо ми обираємо інший розмір, ніж п'ять біт).
Даніель Вагнер

@Daniel Wagner - Вам не доведеться використовувати однакову кількість біт на шматок, і вам навіть не доведеться використовувати цілу кількість біт на шматок.
скупчення

@crowding Якщо у вас є конкретна пропозиція, я хотів би її почути. =)
Даніель Вагнер

@crowding Робіть математику про те, скільки місця займає арифметичне кодування. Поплакайте трохи. Тоді думати важче.
Даніель Вагнер

Вивчайте більше. Повний умовний розподіл символів у правій проміжному поданні (Франциско має найпростіше проміжне подання, як і Стріланк) легко обчислити. Таким чином, модель кодування може бути буквально досконалою і може входити в межах одного біта ентропічного межі. Кінцева арифметика точності може додати кілька біт.
скупчення

1

Якщо ми нічого не знаємо про ці числа, нас обмежують наступні обмеження:

  • нам потрібно завантажити всі номери, перш ніж ми зможемо їх сортувати,
  • набір чисел не стискається.

Якщо ці припущення виконуються, немає ніякого способу виконати своє завдання, оскільки вам знадобиться щонайменше 26,575,425 біт пам’яті (3,321,929 байт).

Що ви можете сказати нам про свої дані?


1
Ви читаєте їх і сортуєте по ходу. Теоретично для цього потрібні біти lg2 (100999999! / (99999999! * 1000000!)) Для зберігання 1М нерозрізних предметів у колах, розміщених 100М, що складає 96,4% від 1 МБ.
NovaDenizen

1

Хитрість полягає в тому, щоб представити стан алгоритмів, який є цілим множинним набором, як стислий потік "лічильника приросту" = "+" та "лічильника виводу" = "!" символів. Наприклад, набір {0,3,3,4} буде представлений як "! +++ !! +!", А потім будь-яка кількість символів "+". Щоб змінити множину, ви надсилаєте символи, зберігаючи лише постійну кількість, декомпресовану за один раз, і вносите зміни на місці, перш ніж передати їх назад у стисненому вигляді.

Деталі

Ми знаємо, що у фінальному наборі рівно 10 ^ 6 чисел, тож є максимум 10 ^ 6 "!" символів. Ми також знаємо, що наш діапазон має розмір 10 ^ 8, тобто більше 10 ^ 8 "+" символів. Кількість способів впорядкування 10 ^ 6 "!" Серед 10 ^ 8 "+" s є (10^8 + 10^6) choose 10^6, і тому вказівка ​​деякого конкретного розташування займає ~ 0,965 МіБ `даних. Це буде дуже тісно.

Ми можемо ставитися до кожного персонажа як до незалежного, не перевищуючи квоти. Існує рівно в 100 разів більше символів "+", ніж "!" символів, що спрощує до 100: 1 шанси кожного символу "+", якщо ми забудемо, що вони залежні. Коефіцієнт 100: 101 відповідає ~ 0,08 біт на символ , майже на загальну суму ~ 0,965 МіБ (ігнорування залежності в цьому випадку коштує лише ~ 12 біт !).

Найпростіша техніка зберігання незалежних символів з відомою попередньою ймовірністю - кодування Хаффмана . Зауважте, що нам потрібне непрактично велике дерево (Дерево Хаффмана для блоків з 10 символів має середню вартість за блок близько 2,4 біт, загалом ~ 2,9 миб. Дерево хаффмана для блоків з 20 символів має середню вартість за блок приблизно 3 біти, що в цілому становить ~ 1,8 Мбіт. Напевно, нам знадобиться блок розміром на сто, що передбачає більше вузлів у нашому дереві, ніж може зберігати все комп'ютерне обладнання, яке коли-небудь існувало. ). Однак, ПЗУ технічно "вільний" відповідно до проблеми, і практичні рішення, які використовують перевагу регулярності на дереві, будуть виглядати по суті однаково.

Псевдокод

  • Мають достатньо велике дерево хаффмана (або подібні дані блокування за блоком), що зберігаються в ПЗУ
  • Почніть зі стисненого рядка з 10 ^ 8 "+" символів.
  • Щоб вставити число N, передайте стиснуту рядок до тих пір, поки символи N "+" не пройдуть, а потім вставіть "!". Передайте рекомпресовану рядок назад по попередній під час руху, зберігаючи постійну кількість блокованих блоків, щоб уникнути перевиконання / недоопрацювання.
  • Повторіть один мільйон разів: [введення, потік декомпресії> вставка> стиснення], а потім декомпресія для виведення

1
Поки що це єдина відповідь, яку я бачу, що насправді відповідає на проблему! Я думаю, що арифметичне кодування є більш простим пристосуванням, ніж кодування Хаффмана, оскільки воно унеможливлює зберігання кодової книги та турбується про межі символів. Ви також можете пояснити залежність.
скупчення

Цілі числа введення НЕ сортуються. Сортувати потрібно спочатку.
alecco

1
@alecco Алгоритм сортує їх по мірі прогресування. Вони ніколи не зберігаються несортованими.
Крейг Гідні

1

У нас є 1 МБ - 3 КБ оперативної пам’яті = 2 ^ 23 - 3 * 2 ^ 13 біт = 8388608 - 24576 = 8364032 біт.

Нам дано 10 ^ 6 чисел у діапазоні 10 ^ 8. Це дає середній розрив ~ 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', тоді я думаю, що ми можемо тримати представлення розриву досить коротким, але це потрібно більше аналізу.


0

Під час отримання потоку виконайте ці дії.

1-й набір трохи розумного розміру

Ідея псевдокоду:

  1. Першим кроком було б знайти всі дублікати і вставити їх у словник з його кількістю та видалити їх.
  2. Третім кроком буде розміщення чисел, що існують у послідовності їх алгоритмічних кроків, і розміщення їх у лічильниках спеціальних словників із першим числом та їх кроком, як n, n + 1 ..., n + 2, 2n, 2n + 1, 2n + 2 ...
  3. Починайте стискати шматками деякі розумні діапазони чисел, як кожні 1000 чи колись 10000, інші числа, які рідше повторюються.
  4. Скасуйте цей діапазон, якщо знайдено число, і додайте його до діапазону і залиште його нестисненим на деякий час довше.
  5. В іншому випадку просто додайте це число до байту [chunkSize]

Продовжуйте перші 4 кроки, отримуючи потік. Останнім кроком буде або провал, якщо ви перевищили пам’ять або почали виводити результат, як тільки всі дані будуть зібрані, почавши сортувати діапазони та виплювати результати в порядку та розпаковувати ті, для того, щоб їх потрібно було нестискати і сортувати їх, коли ти потрапляєш до них.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.