Сортировка 1 миллиона 8-значных чисел в 1 МБ ОЗУ

У меня есть компьютер с 1 МБ ОЗУ и никакое другое локальное хранилище. Я должен использовать его, чтобы принять 1 миллион 8-значных десятичных чисел по TCP-соединению, отсортировать их, а затем отправить отсортированный список по другому TCP-соединению.

Список номеров может содержать дубликаты, которые я не должен отбрасывать. Код будет помещен в ПЗУ, поэтому мне не нужно вычитать размер моего кода из 1 MB. У меня уже есть код для управления портом Ethernet и обработки TCP/IP-соединений, и для его данных состояния требуется 2 KB, включая буфер 1 килобайт, через который код будет считывать и записывать данные. Есть ли решение этой проблемы?

Источники вопроса и ответа:
slashdot.org

cleaton.net

Ответ 1

Решение возможно только из-за разницы между 1 мегабайтом и 1 миллионом байтов. Существует около 2 возможностей 8093729.5 различными способами выбрать 1 миллион 8-значных чисел с допустимыми дубликатами и сделать заказ несущественными, поэтому машина с 1 миллионом байт ОЗУ не имеет достаточных состояний для представления всех возможностей. Но 1M (менее 2k для TCP/IP) составляет 1022 * 1024 * 8 = 8372224 бит, поэтому возможно решение.

Часть 1, начальное решение

Этот подход требует немного больше, чем 1M, я уточню его, чтобы вписаться в 1M позже.

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

Каждый подсчет состоит из 2-битного заголовка подзаголовка, за которым следует сублистовое тело. Тело подсписок занимает 7 бит на запись подписок. Подсети все объединяются вместе, и формат позволяет указать, где заканчивается один подсписк, и начинается следующее. Общее хранилище, необходимое для полностью заполненного списка, составляет 2 * 781250 + 7 * 1000000 = 8562500 бит, что составляет около 1.021 Мбайт.

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-битные целые числа, сортировать новые числа на месте (используя, вероятно, heapsort), а затем объединять их в новый компактный отсортированный список. Повторяйте до тех пор, пока не будет больше номеров для чтения, затем еще раз просмотрите компактный список для создания вывода.

В приведенной ниже строке представлена ​​память непосредственно перед началом операции слияния списка. "O" - это область, в которой хранятся отсортированные 32-битные целые числа. "X" - это область, в которой хранится старый компактный список. Знаки "=" - это комната расширения для компактного списка, 7 бит для каждого целого числа в "О". "Z" - другие случайные служебные данные.

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

Процедура слияния начинает отсчет в крайнем левом "О" и в крайнем левом "X" и начинает писать в самом левом "=". Указатель записи не улавливает указатель чтения компакт-листа до тех пор, пока все новые целые числа не будут объединены, поскольку оба указателя продвигают 2 бита для каждого подсписника и 7 бит для каждой записи в старом компактном списке, и есть достаточно дополнительного места для 7-битные записи для новых номеров.

Часть 2, перебирая ее в 1M

Чтобы выжать решение выше в 1M, мне нужно сделать компактный формат списка немного более компактным. Я избавлюсь от одного из типов подписок, так что будет только 3 разных возможных значения заголовка подписок. Затем я могу использовать "00", "01" и "1" в качестве значений заголовка подписок и сохранять несколько бит. Типы подписок:

Пустой подсписк, ничего не следует.

B Singleton, в подписок есть только одна запись, а следующие 7 бит сохраняют ее.

C Подсветчик содержит не менее 2 различных чисел. Записи сохраняются в неубывающем порядке, за исключением того, что последняя запись меньше или равна первой. Это позволяет идентифицировать конец подсписчика. Например, цифры 2,4,6 будут храниться как (4,6,2). Числа 2,2,3,4,4 будут храниться как (2,3,4,4,2).

D Подсчет состоит из двух или более повторений одного числа.

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

Предположим, что у меня есть заголовок подтипа C-типа, за которым следуют 3 записи, такие как "C [17] [101] [58]". Это не может быть частью релевантного C-типа, как описано выше, поскольку третья запись меньше второй, но больше первой. Я могу использовать этот тип конструкции для представления подписок D-типа. В битных терминах, где бы я ни находился, "C {00?????} {1??????} {01?????}" является невозможным подписок C-типа. Я буду использовать это, чтобы представить подсписку, состоящую из 3 или более повторений одного числа. Первые два 7-битных слова кодируют число (бит "N" ниже), за которым следуют ноль или более слов {0100001}, а затем слово {0100000}.

For example, 3 repetitions: "C{00NNNNN}{1NN0000}{0100000}", 4 repetitions: "C{00NNNNN}{1NN0000}{0100001}{0100000}", and so on.

Это просто оставляет списки, которые содержат ровно 2 повторения одного числа. Я буду представлять те, у кого есть еще один невозможный подвыбор C-типа: "C {0??????} {11?????} {10?????}". Там много места для 7 бит номера в первых двух словах, но этот шаблон длиннее, чем подсписок, который он представляет, что делает вещи немного сложнее. Пять вопросительных знаков в конце можно считать не частью шаблона, поэтому у меня есть: "C {0NNNNNN} {11N????} 10" в качестве моего шаблона, номер которого должен быть повторен в "N" "s. Это 2 бит слишком долго.

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

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

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

Наихудшее минимальное представление полностью заполненного компактного списка происходит, когда все типы подписок одинаково популярны. В этом случае я сохраняю 1 бит на каждые 3 заголовка подсписок, поэтому размер списка составляет 2 * 781250 + 7 * 1000000 - 781250/3 = 8302083.3 бит. Округление до 32-битной границы слова, это 8302112 бит или 1037764 байт.

1M минус 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" на карте памяти.

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

Часть 3, сколько времени потребуется для запуска?

С пустым компактным списком 1-битный заголовок списка будет использоваться для пустого подписок, а начальный размер списка будет 781250 бит. В худшем случае список увеличивается на 8 бит для каждого добавленного номера, поэтому для каждого из 32-разрядных чисел, которые должны быть размещены в верхней части буфера списка, необходимо 32 + 8 = 40 бит свободного пространства, а затем отсортированы и объединены. В худшем случае изменение отображения заголовка подсети приводит к использованию пространства 2 * 781250 + 7 * записей - 781250/3 бит.

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

Источник:

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

Ответ 2

Вот некоторый рабочий код С++, который решает проблему.

Доказательство того, что ограничения памяти выполнены:

Редактор: Нет доказательств максимальной потребности в памяти, предлагаемой автором либо в этом посте, либо в его блогах. Поскольку количество бит, необходимое для кодирования значения, зависит от ранее закодированных значений, такое доказательство, вероятно, является нетривиальным. Автор отмечает, что наибольший закодированный размер, который он мог наложить эмпирически, был 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 & times; 1024 = 1528 байтов для оставшихся переменных и пространства стека.

Он работает примерно через 23 секунды на моем Xeon W3520. Вы можете проверить, работает ли программа, используя следующий Python script, предполагая имя программы 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!')

Подробное объяснение алгоритма можно найти в следующих сериях сообщений:

Ответ 3

Есть один довольно хитрый трюк, не упомянутый здесь до сих пор. Мы предполагаем, что у вас нет дополнительного способа хранения данных, но это не совсем так.

Один из способов вашей проблемы - сделать следующее ужасное дело, которое ни при каких обстоятельствах не должно пытаться: использовать сетевой трафик для хранения данных. И нет, я не имею в виду NAS.

Вы можете сортировать числа с несколькими байтами ОЗУ следующим образом:

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

Как только COUNTER достигает 1000000, у вас есть все значения, сохраненные в непрерывном потоке ICMP-запросов, а VALUE теперь содержит максимальное целое число. Выберите несколько threshold T >> 1000000. Установите COUNTER на ноль. Каждый раз, когда вы получаете ICMP-пакет, увеличивайте COUNTER и отправляете содержащееся целое число обратно в другой запрос эха, если I=VALUE, и в этом случае не передайте его адресату для отсортированных целых чисел. После COUNTER=T уменьшите VALUE на 1, reset COUNTER до нуля и повторите. Как только VALUE достигнет нуля, вы должны были передать все целые числа в порядке от самого большого к наименьшему до места назначения и использовать только 47 бит ОЗУ для двух постоянных переменных (и любой небольшой суммы, необходимой для временных значений).

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

Ответ 4

Пожалуйста, см. первый правильный ответ или более поздний ответ с арифметическим кодированием. Ниже вы можете найти какое-то удовольствие, но не 100% пуленепробиваемое решение.

Это довольно интересная задача, и вот еще одно решение. Я надеюсь, что кто-то найдет результат полезным (или, по крайней мере, интересным).

Этап 1: начальная структура данных, приблизительный подход к сжатию, основные результаты

Давайте сделаем простую математику: у нас есть 1M (1048576 байт) оперативной памяти, изначально доступной для хранения десятичных чисел в 10 цифр. [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, но есть еще некоторые детали, которые нужно выделить:

  • Память ограничена, поэтому идея состоит в том, чтобы предварять номера и использовать сжатые ведра (динамический размер) в качестве временного хранилища.
  • Легче добиться лучшего коэффициента сжатия с предварительным данных, поэтому для каждого ведра есть статический буфер (номера из буфера должны быть отсортированы до LZMA).
  • Каждое ведро имеет определенный диапазон, поэтому окончательный вид может быть выполнен для каждый ведро отдельно
  • Размер ведра может быть правильно установлен, поэтому будет достаточно памяти для распаковать сохраненные данные и выполнить окончательную сортировку для каждого ведра отдельно

In-memory sorting

Обратите внимание: прикрепленный код 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

ИЗМЕНИТЬ

Вывод:

  • Не пытайтесь обмануть Nature
  • Используйте более простое сжатие с меньшим объемом памяти
  • Некоторые дополнительные подсказки действительно необходимы. Обычное пуленепробиваемое решение кажется нецелесообразным.

Этап 2: расширенное сжатие, окончательное заключение

Как уже упоминалось в предыдущем разделе, можно использовать любую подходящую технику сжатия. Поэтому позвольте избавиться от LZMA в пользу более простого и лучшего (если возможно) подхода. Есть много хороших решений, в том числе Арифметическое кодирование, Radix tree и т.д.

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

encoding scheme

Случайный входной тест показывает несколько лучшие результаты:

bucket[0] contains: 10103 numbers, compressed size: 13683 compression factor: 0.34
bucket[1] contains: 9885 numbers, compressed size: 13479 compression factor: 0.34
...
bucket[98] contains: 10026 numbers, compressed size: 13612 compression factor: 0.34
bucket[99] contains: 10058 numbers, compressed size: 13701 compression factor: 0.34
Data size: 3.85M compressed 1.31M compression factor 0.34

Пример кода

  public static void encode(int[] buffer, int length, BinaryOut output) {
    short size = (short)(length & 0x7FFF);

    output.write(size);
    output.write(buffer[0]);

    for(int i=1; i< size; i++) {
        int next = buffer[i] - buffer[i-1];
        int bits = getBinarySize(next);

        int len = bits;

        if(bits > 24) {
          output.write(3, 2);
          len = bits - 24;
        }else if(bits > 16) {
          output.write(2, 2);
          len = bits-16;
        }else if(bits > 8) {
          output.write(1, 2);
          len = bits - 8;
        }else{
          output.write(0, 2);
        }

        if (len > 0) {
            if ((len % 2) > 0) {
                len = len / 2;
                output.write(len, 2);
                output.write(false);
            } else {
                len = len / 2 - 1;
                output.write(len, 2);
            }

            output.write(next, bits);
        }
    }
}

public static short decode(BinaryIn input, int[] buffer, int offset) {
    short length = input.readShort();
    int value = input.readInt();
    buffer[offset] = value;

    for (int i = 1; i < length; i++) {
        int flag = input.readInt(2);

        int bits;
        int next = 0;
        switch (flag) {
            case 0:
                bits = 2 * input.readInt(2) + 2;
                next = input.readInt(bits);
                break;
            case 1:
                bits = 8 + 2 * input.readInt(2) +2;
                next = input.readInt(bits);
                break;
            case 2:
                bits = 16 + 2 * input.readInt(2) +2;
                next = input.readInt(bits);
                break;
            case 3:
                bits = 24 + 2 * input.readInt(2) +2;
                next = input.readInt(bits);
                break;
        }

        buffer[offset + i] = buffer[offset + i - 1] + next;
    }

   return length;
}

Обратите внимание, что этот подход:

  • не потребляет много памяти
  • работает с потоками
  • обеспечивает не очень плохие результаты.

Полный код можно найти здесь, реализации BinaryInput и BinaryOutput можно найти здесь

Окончательный вывод

Нет окончательного вывода:) Иногда очень хорошая идея переместиться на один уровень вверх и просмотреть задачу из метауровня точки Посмотреть.

Было весело провести некоторое время с этой задачей. Кстати, есть много интересных ответов ниже. Благодарим вас за внимание и радость.

Ответ 5

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

Попробуйте сами. Получите 1 миллион случайных 27-битных целых чисел, отсортируйте их, скомпилируйте 7-Zip, xz, независимо от того, какой LZMA вы хотите. Результат - более 1,5 Мб. Предпосылкой сверху является сжатие последовательных чисел. Даже дельта-кодирование составляет более 1,1 Мб. И неважно, что это использует более 100 МБ ОЗУ для сжатия. Таким образом, даже сжатые целые числа не вписываются в проблему и не обращают внимания на время использования 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);

}

Теперь сжимаем 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

Ответ 6

Я думаю, что один из способов думать об этом - с точки зрения комбинаторики: сколько возможных комбинаций упорядоченных порядков чисел? Если мы дадим комбинацию 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-позиция ноги представляет значение.

enter image description here

Итак, если путь просто перемещается вправо до конца, он перескакивает до самого верха, что эквивалентно порядку 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 бит. Я предполагаю, что интересная техника заключается в том, что в каждой точке предполагается, что это весь заказ, нахождение кода для этого заказа, а затем, когда вы получаете новый номер, возвращающийся и обновляющий предыдущий код. Ручная волна ручной волны.

Ответ 7

Мои предложения здесь во многом обязаны решению Dan

Прежде всего, я предполагаю, что решение должно обрабатывать все возможные входные списки. Я думаю, что популярные ответы не делают этого предположения (что ИМО является большой ошибкой).

Известно, что никакая форма сжатия без потерь будет уменьшать размер всех входов.

Все популярные ответы предполагают, что они смогут применять сжатие достаточно эффективно, чтобы позволить им дополнительное пространство. Фактически, кусок дополнительного пространства достаточно велик, чтобы удерживать часть частично заполненного списка в несжатом виде и позволять им выполнять свои операции сортировки. Это просто плохое предположение.

Для такого решения любой, кто знает, как они делают свое сжатие, сможет проектировать некоторые входные данные, которые плохо сжимаются для этой схемы, и "решение", скорее всего, сломается из-за нехватки места.

Вместо этого я беру математический подход. Нашими возможными выходами являются все списки длины LEN, состоящие из элементов в диапазоне 0..MAX. Здесь LEN составляет 1,000,000, а наш MAX составляет 100 000 000.

Для произвольных LEN и MAX количество бит, необходимых для кодирования этого состояния, равно:

Log2 (MAX Multichoose LEN)

Итак, для наших номеров, как только мы закончим получение и сортировку, нам понадобятся биты Log2 (100 000 000 MC 1,000,000), чтобы сохранить наш результат таким образом, чтобы он мог однозначно различать все возможные выходы.

Это ~ = 988kb. Таким образом, у нас на самом деле достаточно места для хранения нашего результата. С этой точки зрения это возможно.

[Удалено бессмысленное прерывание теперь, когда существуют лучшие примеры...]

Лучший ответ здесь.

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

Ответ 8

Предположим, что эта задача возможна. Как раз перед выходом, будет отображаться в миллисекундах миллионов отсортированных чисел. Сколько таких разных представлений существует? Поскольку числа могут повторяться, мы не можем использовать nCr (select), но есть операция multichoose, которая работает на multisets.

  • Есть 2.2e2436455 способы выбора миллиона чисел в диапазоне 0..99,999,999.
  • Для этого требуется 8,093,730 бит для представления каждой возможной комбинации, или 1 011 717 байт.

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

Однако, если "1M RAM" означает миллион байт, то, очевидно, недостаточно места. Тот факт, что теоретически возможно на 5% больше памяти, говорит мне, что представление должно быть ОЧЕНЬ эффективным и, вероятно, не нормальным.

Ответ 9

(Мой оригинальный ответ был неправильным, извините за плохую математику, см. ниже перерыва.)

Как насчет этого?

Первые 27 бит хранят самое низкое число, которое вы видели, а затем разницу в следующем количестве, закодированном следующим образом: 5 бит, чтобы сохранить количество бит, используемых для хранения разницы, а затем разницу. Используйте 00000, чтобы указать, что вы снова видели это число.

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

В худшем случае я могу думать, что все числа равномерно распределены (на 100), например. Предполагая, что 0 - первое число:

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

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

Reddit на помощь!

Если бы все, что вам нужно было сделать, это сортировать их, эта проблема была бы простой. Требуется 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,7 тыс.

Вы можете внести дополнительные улучшения, если используете следующую кодировку:

0 означает, что вы не видите номер 10 означает, что вы видели его один раз 11 - то, как вы держите счет

Что в среднем приведет к 280,7 тыс. памяти.

EDIT: моя утренняя утренняя математика была неправильной.

В худшем случае мы видим 500 000 номеров дважды, поэтому математика становится:

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

Альтернативное кодирование приводит к среднему хранению

500 000 * 27 + 500 000 = 1,70 М

: (

Ответ 10

Существует одно решение этой проблемы на всех возможных входах. Чит.

  • Чтение значений m по TCP, где m находится рядом с max, которые могут быть отсортированы в памяти, возможно, n/4.
  • Отсоедините 250 000 (или около того) номеров и выведите их.
  • Повторите для остальных 3 кварталов.
  • Пусть приемник объединяет 4 списка полученных им чисел по мере их обработки. (Это не намного медленнее, чем использование одного списка.)

Ответ 11

Я бы попробовал Radix Tree. Если вы могли бы хранить данные в дереве, вы могли бы выполнить траверс в порядке для передачи данных.

Я не уверен, что вы можете поместить это в 1MB, но я думаю, что стоит попробовать.

Ответ 12

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

(Я в основном спрашиваю в память старого Acorn RISC PC, который мог бы "заимствовать" VRAM для расширения доступной системной RAM, если вы выбрали низкое разрешение или режим с низкой глубиной цвета!). Это было довольно полезно на машине с только несколькими МБ нормальной оперативной памяти.

Ответ 13

Представление дерева оснований приблизилось бы к решению этой проблемы, поскольку дерево оснований использует преимущества префиксного сжатия. Но трудно представить представление дерева оснований, которое могло бы представлять один node в одном байте - два, вероятно, имеют предел.

Но независимо от того, как представлены данные, после сортировки он может быть сохранен в префикс-сжатой форме, где числа 10, 11 и 12 будут представлены, например, 001b, 001b, 001b, указывая приращение 1 от предыдущего номера. Возможно, тогда 10101b будет представлять приращение 5, 1101001b с шагом 9 и т.д.

Ответ 14

У меня есть компьютер с 1 М оперативной памятью и никакое другое локальное хранилище

Еще один способ обмана: вместо этого вы можете использовать нелокальное (сетевое) хранилище (ваш вопрос не исключает этого) и вызовите сетевой сервис, который может использовать простое слияние на основе дисков (или достаточно ОЗУ для сортировки в памяти, так как вам нужно только принимать номера 1M), не требуя (по общему признанию, чрезвычайно изобретательных) решений, уже предоставленных.

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

Ответ 15

Есть 10 ^ 6 значений в диапазоне 10 ^ 8, поэтому в среднем на сто кодовых точек приходится одно значение. Храните расстояние от N-й точки до (N + 1) -й. Дублирующиеся значения имеют пропуск 0. Это означает, что для пропуска требуется среднее значение менее 7 бит, поэтому миллион из них будет с удовольствием вписываться в наши 8 миллионов бит памяти.

Эти пропуски должны быть закодированы в битовый поток, например, по кодировке Хаффмана. Вставка - путем итерации через бит-поток и перезаписи после нового значения. Вывод путем повторения и записи подразумеваемых значений. Для практичности он, вероятно, хочет сделать, например, 10 ^ 4 списков, охватывающих 10 ^ 4 кодовых пункта (и в среднем 100 значений) каждый.

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

Ответ 16

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

Итак, скажем, мы создаем список входных значений по мере их поступления в сокет. Мы сохраняем в каждом элементе, а не в значении, а смещаем его перед ним. Мы сортируем, как мы идем, поэтому смещения будут только позитивными. Но смещение может составлять 8 десятичных цифр, что соответствует 3 байтам. Каждый элемент не может быть 3 байта, поэтому нам нужно их упаковать. Мы могли бы использовать верхний бит каждого байта как "бит продолжения", указывая, что следующий байт является частью числа, а младшие 7 бит каждого байта должны быть объединены. нуль действителен для дубликатов.

По мере того, как список заполняется, цифры должны быть ближе друг к другу, в среднем в среднем только 1 байт используется для определения расстояния до следующего значения. 7 бит значения и 1 бит смещения, если это удобно, но может быть сладкое пятно, которое требует меньше 8 бит для значения "продолжить".

Во всяком случае, я сделал некоторый эксперимент. Я использую генератор случайных чисел, и я могу подогнать миллион отсортированных 8 цифр десятичных чисел примерно в 1279000 байт. Среднее пространство между каждым числом равно 99...

public class Test {
    public static void main(String[] args) throws IOException {
        // 1 million values
        int[] values = new int[1000000];

        // create random values up to 8 digits lrong
        Random random = new Random();
        for (int x=0;x<values.length;x++) {
            values[x] = random.nextInt(100000000);
        }
        Arrays.sort(values);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        int av = 0;    
        writeCompact(baos, values[0]);     // first value
        for (int x=1;x<values.length;x++) {
            int v = values[x] - values[x-1];  // difference
            av += v;
            System.out.println(values[x] + " diff " + v);
            writeCompact(baos, v);
        }

        System.out.println("Average offset " + (av/values.length));
        System.out.println("Fits in " + baos.toByteArray().length);
    }

    public static void writeCompact(OutputStream os, long value) throws IOException {
        do {
            int b = (int) value & 0x7f;
            value = (value & 0x7fffffffffffffffl) >> 7;
            os.write(value == 0 ? b : (b | 0x80));
        } while (value != 0);
    }
}

Ответ 17

Мы могли бы играть в сетевом стеке, чтобы отправить числа в отсортированном порядке, прежде чем у нас будут все номера. Если вы отправляете 1M данных, TCP/IP разбивает его на 1500 байтовых пакетов и передает их для цели. Каждому пакету будет присвоен порядковый номер.

Мы можем сделать это вручную. Как раз перед тем, как мы заполним нашу оперативную память, мы можем сортировать то, что у нас есть, и отправлять список в нашу цель, но оставлять дыры в нашей последовательности вокруг каждого номера. Затем обработайте вторую половину чисел таким же образом, используя эти отверстия в последовательности.

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

Он использует сеть для выполнения сортировки слияния. Это общий взлом, но я был вдохновлен другим сетевым хаком, перечисленным ранее.

Ответ 18

Google (плохой) подход, из потока HN. Храните метки в стиле RLE.

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

Их проблема не охватывает дубликаты, но пусть они используют "0: 1" для дубликатов.

Большая проблема # 1: вставки для целых чисел 1М занимают возрасты.

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

Подход Google является недостойным (медленным) и неправильным. Но для их защиты их проблема могла быть несколько иной.

Ответ 19

Я бы использовал поведение повторной передачи TCP.

  • Сделать компонент TCP создать большое окно получения.
  • Получить некоторое количество пакетов без отправки ACK для них.
    • Обработать те, которые находятся в проходах, создавая некоторую (префикс) сжатую структуру данных.
    • Отправить дубликат ack для последнего пакета, который больше не нужен/дождитесь тайм-аута повторной передачи
    • Перейти 2
  • Все пакеты были приняты

Это предполагает некоторую выгоду от ковшей или нескольких проходов.

Возможно, путем сортировки партий/ведер и их слияния. → деревья оснований

Используйте этот метод для приема и сортировки первых 80%, а затем прочитайте последние 20%, убедитесь, что последние 20% не содержат чисел, которые попали бы в первые 20% самых низких чисел. Затем отправьте 20% наименьших чисел, удалите из памяти, примите оставшиеся 20% новых чисел и слейте. **

Ответ 20

Для представления отсортированного массива можно просто сохранить первый элемент и разницу между соседними элементами. Таким образом, мы имеем дело с кодированием элементов 10 ^ 6, которые могут суммироваться не более чем на 10 ^ 8. Позвольте называть это D. Для кодирования элементов D можно использовать код Хаффмана. Словарь для кода Хаффмана может быть создан на ходу, и массив обновляется каждый раз, когда новый элемент вставляется в отсортированный массив (сортировка вставки). Обратите внимание, что когда словарь изменяется из-за нового элемента, весь массив должен быть обновлен в соответствии с новой кодировкой.

Среднее число бит для кодирования каждого элемента D максимизируется, если мы имеем равное количество каждого уникального элемента. Скажем, что элементы d1, d2,..., dN в D кажутся F раз. В этом случае (в худшем случае мы имеем 0 и 10 ^ 8 во входной последовательности), имеем

sum (1 <= я <= N) F. di = 10 ^ 8

где

sum (1 <= я <= 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 = sum (1 <= я <= N) F. di = sum (1 <= я <= N) (10 ^ 6/N) (i-1) = (10 ^ 6/N) N (N-1)/2

то есть.

N <= 201. И для этого случая среднее число бит равно log2 (201) = 7,6511, что означает, что для сохранения отсортированного массива нам потребуется около 1 байт на входной элемент. Обратите внимание, что это не означает, что D вообще не может содержать более 201 элемента. Он просто сеет, что если элементы D равномерно распределены, он не может иметь более 201 уникальных значений.

Ответ 21

У нас есть 1 MB - 3 KB RAM = 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", тогда я думаю, что мы можем сохранить представление пробела достаточно коротким, но он нуждается в большем анализе.

Ответ 22

Если входной поток может быть принят несколько раз, это будет много проще (нет информации об этом, проблема идеи и времени).

Тогда мы могли бы считать десятичные значения. С подсчитанными значениями это будет легко сделать выходной поток. Сжатие путем подсчета значений. Это зависит от того, что было бы во входном потоке.

Ответ 23

Если поток ввода можно было бы получить несколько раз, это было бы намного проще (информация об этом, идея и проблема с производительностью по времени). Затем мы могли бы подсчитать десятичные значения. С подсчитанными значениями было бы легко сделать выходной поток. Сжатие путем подсчета значений. Это зависит от того, что будет во входном потоке.

Ответ 24

Сортировка - вторичная проблема. Как и другие, просто хранить целые числа сложно, а не может работать на всех входах, так как потребуется 27 бит на номер.

Мое занятие: хранить только различия между последовательными (отсортированными) целыми числами, так как они, скорее всего, будут малыми. Затем используют схему сжатия, например. с 2 дополнительными битами на номер входа, чтобы закодировать, сколько бит хранится в памяти. Что-то вроде:

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

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

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

О, а затем вы вносите сортировку в этот отсортированный список, когда получаете данные.

Ответ 25

Теперь нацелившись на реальное решение, охватывающее все возможные случаи ввода в 8-разрядном диапазоне с 1 МБ ОЗУ. ПРИМЕЧАНИЕ: работа продолжается, завтра будет продолжаться. Используя арифметическое кодирование дельт сортированных ints, наихудший случай для 1M сортированных ints будет стоить около 7 бит на запись (с 99999999/1000000 - 99, а log2 (99) - почти 7 бит).

Но вам нужны целые числа 1 м, отсортированные для получения 7 или 8 бит! У более коротких серий будут большие дельта, поэтому больше бит на элемент.

Я работаю над тем, чтобы максимально использовать и сжимать (почти) на месте. Для первой партии близких к 250 тыс. Ints в лучшем случае потребуется около 9 бит. Таким образом, результат составит около 275 КБ. Повторяйте с оставшейся свободной памятью несколько раз. Затем распакуйте-слить-на-место-сжать эти сжатые куски. Это довольно сложно, но возможно. Я думаю.

Объединенные списки будут ближе и ближе к 7-битной цельной цели. Но я не знаю, сколько итераций потребуется для цикла слияния. Возможно, 3.

Но неточность реализации арифметического кодирования может сделать невозможным. Если эта проблема вообще возможна, она будет чрезвычайно жесткой.

Любые волонтеры?

Ответ 26

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

Таким образом, 000010 равно 1, а 000100 равно 2. 000001100000 равно 128. Теперь мы рассматриваем наихудший результат в представлении различий в последовательности чисел до 10 000 000. Могут быть разницы в 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 + roundup (10,000,000/2 ^ 5) * 6 + roundup (10,000,000/2 ^ 10) * 6 + roundup (10,000,000/2 ^ 15) * 6 + roundup (10,000,000/2 ^ 20) * 4 = 7935479.

2 ^ 24 = 8388608. С 8388608 > 7935479 нам должно быть достаточно памяти. Нам, вероятно, понадобится еще немного бит памяти для хранения суммы того, где мы вставляем новые числа. Затем мы переходим через последовательность и находим, где нужно вставлять новое число, при необходимости уменьшаем следующее различие и сдвигаем все после правильного.

Ответ 27

Если мы ничего не знаем об этих числах, мы ограничены следующими ограничениями:

  • нам нужно загрузить все числа, прежде чем мы сможем их отсортировать,
  • набор чисел не сжимается.

Если эти допущения выполняются, вы не сможете выполнить свою задачу, так как вам потребуется не менее 26 575 425 бит памяти (3 321 929 байт).

Что вы можете рассказать нам о своих данных?

Ответ 28

Трюк состоит в том, чтобы представить состояние алгоритмов, которое является целочисленным множеством, как сжатый поток "счетчик приращений" = "+" и "счетчик вывода" = "!" . персонажи. Например, набор {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 MiB `данных. Это будет плотно пригонка.

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

Самый простой способ хранения независимых символов с известной вероятностью - кодирование Хаффмана. Обратите внимание, что нам нужно непрактично большое дерево (дерево huffman для блоков из 10 символов имеет среднюю стоимость на блок около 2,4 бит, в общей сложности ~ 2.9 Mib. Дерево хаффмана для блоков из 20 символов имеет среднюю стоимость за блок около 3 бит, что составляет в общей сложности ~ 1,8 млн. Мы, вероятно, понадобится блок размером порядка 100, подразумевая большее количество узлов в нашем дереве, чем все компьютерное оборудование, которое когда-либо существовало, может хранить.). Однако ROM технически "свободен" в соответствии с проблемой, и практические решения, которые используют преимущества регулярности в дереве, будут выглядеть по существу одинаково.

Псевдо-код

  • Достаточно большое дерево huffman (или аналогичные поблоковые данные сжатия), хранящиеся в ROM
  • Начните с сжатой строки из символов 10 ^ 8 "+" .
  • Чтобы вставить номер N, выпустите сжатую строку до тех пор, пока символы "+" не пройдут, а затем вставьте "!" . Поток повторно сжатой строки обратно по предыдущей, когда вы идете, сохраняя постоянное количество буферизованных блоков, чтобы избежать превышения/под-запусками.
  • Повторите один миллион раз: [ввод, поток распаковки > вставить > сжатие], затем выполните распаковку для вывода

Ответ 29

При получении потока выполните следующие действия.

1-й установить некоторый разумный размер куска

Идея псевдокода:

  • Первым шагом будет найти все дубликаты и вставить их в словарь со своим счетчиком и удалить их.
  • Третий шаг состоял бы в том, чтобы поместить число, которое существует в последовательности их алгоритмических шагов, и поместить их в счетчики специальных словарей с первым числом и их шагом, как n, n + 1..., n + 2, 2n, 2n +1, 2n + 2...
  • Начните сжимать в кусках некоторые разумные диапазоны чисел, например, каждые 1000 или 10000 оставшихся чисел, которые появляются менее часто для повторения.
  • Раскройте этот диапазон, если число найдено и добавьте его в диапазон, и оставьте его несжатым еще некоторое время.
  • В противном случае просто добавьте это число в байт [chunkSize]

Продолжайте выполнение первых 4 шагов при получении потока. Последним шагом будет либо сбой, если вы превысите память или начнете выдавать результат после того, как все данные будут собраны, начиная сортировать диапазоны и выплевывать результаты по порядку и разжимать их, чтобы их нужно было несжато и сортировать, когда вы добираетесь до них.

Ответ 30

Сорт вроде sleepsort:

for i = 1 to 1000000
    input N
        Send a message on the network with delay of (N * some constant)

КЭД

Как вы это делаете, спросите вы? Ну:

http://www.shysecurity.com/posts/PingFS

PingFS - это набор скриптов python [sic], которые в Linux предоставляют хранилище виртуальных дисков в сети. Каждый файл разбивается на блоки размером 64-1024 байта, отправляется по кабелю в эхо-запросе ICMP и быстро удаляется из памяти. Каждый раз, когда сервер отправляет обратно ваш пакет радости, PingFS распознает свои данные и отправляет другой подарок [данные]. Загрузите внизу страницы.