Ordinamento di 1 milione di numeri a 8 cifre decimali con 1 MB di RAM


726

Ho un computer con 1 MB di RAM e nessun altro archivio locale. Devo usarlo per accettare 1 milione di numeri decimali a 8 cifre su una connessione TCP, ordinarli e quindi inviare l'elenco ordinato su un'altra connessione TCP.

L'elenco dei numeri può contenere duplicati, che non devo scartare. Il codice verrà inserito nella ROM, quindi non è necessario sottrarre la dimensione del mio codice da 1 MB. Ho già un codice per guidare la porta Ethernet e gestire le connessioni TCP / IP, e richiede 2 KB per i suoi dati di stato, incluso un buffer da 1 KB tramite il quale il codice leggerà e scriverà i dati. C'è una soluzione a questo problema?

Fonti di domande e risposte:

slashdot.org

cleaton.net


45
Ehm, un milione di volte un numero decimale a 8 cifre (min.
Intero

15
1 M di RAM significa 2 ^ 20 byte? E quanti bit ci sono in un byte su questa architettura? E il "milione" in "1 milione di cifre decimali a 8 cifre" è un milione di SI (10 ^ 6)? Che cos'è un numero decimale di 8 cifre, un numero naturale <10 ^ 8, un numero razionale la cui rappresentazione decimale accetta 8 cifre escluso il punto decimale o qualcos'altro?

13
1 milione di numeri decimali a 8 cifre o 1 milione di numeri a 8 bit?
Patrick White,

13
mi ricorda un articolo nel "Dr Dobb's Journal" (da qualche parte tra il 1998-2001), in cui l'autore ha usato un tipo di inserimento per ordinare i numeri di telefono mentre li leggeva: quella era la prima volta che mi rendevo conto che, a volte, un tempo più lento l'algoritmo potrebbe essere più veloce ...
Adrien Plisson,

103
C'è un'altra soluzione che nessuno ha ancora menzionato: acquista hardware con 2 MB di RAM. Non dovrebbe essere molto più costoso e renderà il problema molto, molto più semplice da risolvere.
Daniel Wagner,

Risposte:


716

C'è un trucco piuttosto subdolo non menzionato qui finora. Partiamo dal presupposto che non hai un modo aggiuntivo per archiviare i dati, ma ciò non è strettamente vero.

Un modo per aggirare il tuo problema è fare la seguente cosa orribile, che non dovrebbe essere tentata da nessuno in nessun caso: Usa il traffico di rete per archiviare i dati. E no, non intendo NAS.

È possibile ordinare i numeri con solo pochi byte di RAM nel modo seguente:

  • Per prima cosa prendi 2 variabili: COUNTERe VALUE.
  • Innanzitutto impostare tutti i registri su 0;
  • Ogni volta che ricevi un numero intero I, incrementale COUNTERe impostato VALUEsu max(VALUE, I);
  • Quindi inviare un pacchetto di richiesta echo ICMP con set di dati Ial router. Cancella Ie ripeti.
  • Ogni volta che ricevi il pacchetto ICMP restituito, devi semplicemente estrarre il numero intero e rispedirlo nuovamente in un'altra richiesta di eco. Ciò produce un numero enorme di richieste ICMP che si spostano avanti e indietro contenenti gli interi.

Una volta COUNTERraggiunto 1000000, hai tutti i valori memorizzati nel flusso incessante di richieste ICMP e VALUEora contiene il numero intero massimo. Scegli un po ' threshold T >> 1000000. Impostare COUNTERsu zero. Ogni volta che si riceve un pacchetto ICMP, incrementare COUNTERe rinviare il numero intero contenuto Iin un'altra richiesta di eco, a meno che I=VALUE, nel qual caso, trasmetterlo alla destinazione per i numeri interi ordinati. Una volta COUNTER=T, decrementa VALUEdi 1, ripristina COUNTERa zero e ripeti. Una volta VALUEraggiunto lo zero dovresti aver trasmesso tutti gli interi in ordine dal più grande al più piccolo alla destinazione e hai usato solo circa 47 bit di RAM per le due variabili persistenti (e qualsiasi piccola quantità di cui hai bisogno per i valori temporanei).

So che è orribile, e so che possono esserci tutti i tipi di problemi pratici, ma ho pensato che potesse far ridere qualcuno o almeno spaventarti.


27
Quindi stai fondamentalmente sfruttando la latenza della rete e trasformando il tuo router in una sorta di que?
Eric R.,

335
Questa soluzione non è solo fuori dagli schemi; sembra aver dimenticato la sua scatola in casa: D
Vladislav Zorov il

28
Ottima risposta ... Adoro queste risposte perché espongono davvero la varietà di una soluzione a un problema
StackOverflow

33
ICMP non è affidabile.
sleeplessnerd,

13
@MDMarra: Noterai proprio in alto che dico "Un modo per aggirare il tuo problema è fare la seguente cosa orribile, che non dovrebbe essere tentata da nessuno in nessuna circostanza". C'era una ragione per cui l'ho detto.
Joe Fitzsimons,

423

Ecco del codice C ++ funzionante che risolve il problema.

Prova che i vincoli di memoria sono soddisfatti:

Editor: non ci sono prove dei requisiti di memoria massima offerti dall'autore in questo post o nei suoi blog. Poiché il numero di bit necessari per codificare un valore dipende dai valori precedentemente codificati, tale prova è probabilmente non banale. L'autore osserva che la più grande dimensione codificata su cui poteva inciampare empiricamente era 1011732e ha scelto 1013000arbitrariamente la dimensione del buffer .

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

    ...

Insieme, questi due array occupano 1045000 byte di memoria. Ciò lascia 1048576 - 1045000 - 2 × 1024 = 1528 byte per le variabili rimanenti e lo spazio dello stack.

Funziona in circa 23 secondi sul mio Xeon W3520. Puoi verificare che il programma funzioni usando il seguente script Python, assumendo un nome di programma 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!')

Una spiegazione dettagliata dell'algoritmo è disponibile nelle seguenti serie di post:


8
@preshing sì, vogliamo davvero una spiegazione dettagliata di questo.
T Suds,

25
Penso che l'osservazione chiave sia che un numero di 8 cifre ha circa 26,6 bit di informazioni e un milione è di 19,9 bit. Se delta comprimi l'elenco (memorizzi le differenze dei valori adiacenti), le differenze vanno da 0 (0 bit) a 99999999 (26,6 bit) ma non puoi avere il delta massimo tra ogni coppia. Il caso peggiore dovrebbe in realtà essere un milione di valori distribuiti uniformemente, che richiedono delta di (26,6-19,9) o circa 6,7 ​​bit per delta. La memorizzazione di un milione di valori di 6,7 bit si adatta facilmente a 1M. La compressione Delta richiede un ordinamento continuo delle fusioni, in modo da renderlo quasi gratuito.
Ben Jackson,

4
soluzione dolce. dovreste visitare il suo blog per la spiegazione preshing.com/20121025/…
davec

9
@BenJackson: c'è un errore da qualche parte nei tuoi calcoli. Esistono 2.265 x 10 ^ 2436455 output possibili univoci (set ordinati di 10 ^ 6 numeri interi a 8 cifre) che richiedono 8,094 x 10 ^ 6 bit per memorizzare (cioè un capello sotto un megabyte). Nessuno schema intelligente può comprimere oltre questo limite teorico di informazioni senza perdita. La tua spiegazione implica che hai bisogno di molto meno spazio ed è quindi sbagliato. In effetti, "circolare" nella soluzione sopra è abbastanza grande da contenere le informazioni necessarie, quindi la presunzione sembra averne preso in considerazione, ma ti manca.
Joe Fitzsimons,

5
@JoeFitzsimons: non avevo elaborato la ricorsione (insiemi univoci ordinati di n numeri da 0..m è (n+m)!/(n!m!)) quindi devi avere ragione. Probabilmente è la mia stima che un delta di b bit richiede b bit per la memorizzazione - chiaramente i delta di 0 non richiedono 0 bit per la memorizzazione.
Ben Jackson,

371

Vedere la prima risposta corretta o la risposta successiva con codifica aritmetica . Di seguito puoi trovare una soluzione divertente, ma non al 100% a prova di proiettile.

Questo è un compito abbastanza interessante ed ecco un'altra soluzione. Spero che qualcuno possa trovare utile il risultato (o almeno interessante).

Fase 1: struttura iniziale dei dati, approccio approssimativo alla compressione, risultati di base

Facciamo un po 'di matematica semplice: abbiamo inizialmente 1M (1048576 byte) di RAM per memorizzare numeri decimali da 8 ^ 6 cifre. [0; 99999999]. Quindi per memorizzare un numero sono necessari 27 bit (supponendo che verranno utilizzati numeri senza segno). Pertanto, per archiviare un flusso non elaborato saranno necessari ~ 3,5 M di RAM. Qualcuno ha già detto che non sembra fattibile, ma direi che il compito può essere risolto se l'input è "abbastanza buono". Fondamentalmente, l'idea è quella di comprimere i dati di input con un fattore di compressione di 0,29 o superiore e fare l'ordinamento in modo corretto.

Risolviamo prima il problema di compressione. Sono già disponibili alcuni test rilevanti:

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

"Ho eseguito un test per comprimere un milione di numeri interi consecutivi utilizzando varie forme di compressione. I risultati sono i seguenti:"

None     4000027
Deflate  2006803
Filtered 1391833
BZip2    427067
Lzma     255040

Sembra che LZMA ( algoritmo della catena Lempel – Ziv – Markov ) sia una buona scelta per continuare. Ho preparato un semplice PoC, ma ci sono ancora alcuni dettagli da evidenziare:

  1. La memoria è limitata, quindi l'idea è quella di preordinare i numeri e utilizzare secchi compressi (dimensioni dinamiche) come memoria temporanea
  2. È più facile ottenere un miglior fattore di compressione con dati preordinati, quindi esiste un buffer statico per ogni bucket (i numeri dal buffer devono essere ordinati prima di LZMA)
  3. Ogni bucket ha un intervallo specifico, quindi l'ordinamento finale può essere eseguito separatamente per ogni bucket
  4. Le dimensioni del bucket possono essere impostate correttamente, quindi ci sarà memoria sufficiente per decomprimere i dati memorizzati ed eseguire l'ordinamento finale per ciascun bucket separatamente

Ordinamento in memoria

Si noti che il codice allegato è un POC , non può essere utilizzato come soluzione finale, dimostra solo l'idea di utilizzare diversi buffer più piccoli per memorizzare i numeri preordinati in modo ottimale (eventualmente compresso). LZMA non è proposto come soluzione finale. È usato come un modo più veloce possibile per introdurre una compressione a questo PoC.

Vedi sotto il codice PoC (ti preghiamo di notare che è solo una demo, per compilarlo sarà necessario 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);
}
}

Con numeri casuali produce quanto segue:

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

Per una semplice sequenza ascendente (viene utilizzato un bucket) produce:

bucket[0] contains: 1000000 numbers, compressed size: 256700 compression factor: 0.06
Data size: 3.85M compressed 0.25M compression factor 0.06

MODIFICARE

Conclusione:

  1. Non cercare di ingannare la natura
  2. Usa una compressione più semplice con un footprint di memoria inferiore
  3. Alcuni indizi aggiuntivi sono davvero necessari. La soluzione comune antiproiettile non sembra fattibile.

Fase 2: compressione migliorata, conclusione finale

Come già accennato nella sezione precedente, è possibile utilizzare qualsiasi tecnica di compressione adatta. Quindi liberiamoci di LZMA a favore di un approccio più semplice e migliore (se possibile). Esistono molte buone soluzioni tra cui la codifica aritmetica , l' albero di Radix ecc.

Comunque, uno schema di codifica semplice ma utile sarà più illustrativo di un'altra libreria esterna, fornendo un algoritmo elegante. La soluzione effettiva è piuttosto semplice: poiché esistono bucket con dati parzialmente ordinati, è possibile utilizzare delta anziché numeri.

schema di codifica

Il test di input casuale mostra risultati leggermente migliori:

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

Codice di esempio

  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;
}

Si noti che questo approccio:

  1. non consuma molta memoria
  2. funziona con flussi
  3. fornisce risultati non così negativi

Il codice completo è disponibile qui , le implementazioni BinaryInput e BinaryOutput sono disponibili qui

Conclusione finale

Nessuna conclusione finale :) A volte è davvero una buona idea salire di un livello e rivedere l'attività da un punto di vista a livello meta .

È stato divertente passare un po 'di tempo con questo compito. A proposito, ci sono molte risposte interessanti qui sotto. Grazie per la vostra attenzione e buona programmazione.


17
Ho usato Inkscape . Ottimo strumento a proposito. È possibile utilizzare questa sorgente del diagramma come esempio.
Renat Gilmanov,

21
Sicuramente LZMA richiede troppa memoria per essere utile in questo caso? Come algoritmo ha lo scopo di ridurre al minimo la quantità di dati che devono essere memorizzati o trasmessi, piuttosto che essere efficienti in memoria.
Mjiig,

67
Questa è una sciocchezza ... Ottieni 1 milione di numeri casuali a 27 bit, ordinali, comprimi con 7zip, xz, qualunque LZMA desideri. Il risultato è oltre 1 MB. La premessa in cima è la compressione di numeri sequenziali. La codifica delta di quella con 0 bit sarebbe solo il numero, ad esempio 1000000 (diciamo in 4 byte). Con sequenziali e duplicati (senza spazi vuoti), il numero 1000000 e 1000000 bit = 128 KB, con 0 per il numero duplicato e 1 per contrassegnare il successivo. Quando hai spazi vuoti casuali, anche piccoli, LZMA è ridicolo. Non è progettato per questo.
alecco,

30
Questo in realtà non funzionerà. Ho eseguito una simulazione e mentre i dati compressi superano 1 MB (circa 1,5 MB), per comprimere i dati vengono comunque utilizzati oltre 100 MB di RAM. Quindi anche gli interi compressi non si adattano al problema per non parlare dell'utilizzo della RAM in fase di esecuzione. Assegnarti la generosità è il mio più grande errore su StackOverflow.
Preferito Onwuemene il

10
Questa risposta è così votata perché molti programmatori preferiscono idee brillanti piuttosto che codice comprovato. Se questa idea funzionasse, vedresti un vero algoritmo di compressione scelto e provato piuttosto che una semplice affermazione che sicuramente ce n'è uno là fuori che può farlo ... quando è del tutto possibile che non ce ne sia uno là fuori che possa farlo .
Olathe,

185

Una soluzione è possibile solo a causa della differenza tra 1 megabyte e 1 milione di byte. Ci sono circa 2 alla potenza 8093729.5 diversi modi per scegliere 1 milione di numeri a 8 cifre con duplicati consentiti e ordini non importanti, quindi una macchina con solo 1 milione di byte di RAM non ha abbastanza stati per rappresentare tutte le possibilità. Ma 1M (meno 2k per TCP / IP) è 1022 * 1024 * 8 = 8372224 bit, quindi è possibile una soluzione.

Parte 1, soluzione iniziale

Questo approccio richiede poco più di 1 milione, lo perfezionerò per adattarlo a 1 milione più tardi.

Memorizzerò un elenco compatto di numeri compreso tra 0 e 99999999 come una sequenza di elenchi di numeri a 7 bit. Il primo elenco secondario contiene numeri da 0 a 127, il secondo elenco secondario contiene numeri da 128 a 255, ecc. 100000000/128 è esattamente 781250, quindi saranno necessari 781250 tali elenchi.

Ogni elenco secondario è costituito da un'intestazione dell'elenco secondario a 2 bit seguita da un corpo dell'elenco secondario. Il corpo della lista secondaria occupa 7 bit per voce della lista secondaria. Le liste secondarie sono tutte concatenate insieme e il formato consente di dire dove finisce una lista secondaria e inizia la successiva. La memoria totale richiesta per un elenco completamente popolato è 2 * 781250 + 7 * 1000000 = 8562500 bit, ovvero circa 1,021 M-byte.

I 4 possibili valori dell'intestazione dell'elenco secondario sono:

00 Elenco vuoto, non segue nulla.

01 Singleton, c'è solo una voce nell'elenco secondario e i successivi 7 bit lo tengono.

10 L'elenco secondario contiene almeno 2 numeri distinti. Le voci sono memorizzate in ordine non decrescente, tranne per il fatto che l'ultima voce è inferiore o uguale alla prima. Ciò consente di identificare la fine dell'elenco secondario. Ad esempio, i numeri 2,4,6 verrebbero memorizzati come (4,6,2). I numeri 2,2,3,4,4 verrebbero memorizzati come (2,3,4,4,2).

11 L'elenco secondario contiene 2 o più ripetizioni di un singolo numero. I successivi 7 bit danno il numero. Quindi arrivano zero o più voci a 7 bit con il valore 1, seguite da una voce a 7 bit con il valore 0. La lunghezza del corpo dell'elenco secondario determina il numero di ripetizioni. Ad esempio, i numeri 12,12 verrebbero memorizzati come (12,0), i numeri 12,12,12 verrebbero memorizzati come (12,1,0), 12,12,12,12 sarebbero (12,1 , 1,0) e così via.

Comincio con un elenco vuoto, leggo un mucchio di numeri e li memorizzo come numeri interi a 32 bit, ordina i nuovi numeri sul posto (usando heapsort, probabilmente) e poi li unisco in un nuovo elenco ordinato compatto. Ripetere fino a quando non ci sono più numeri da leggere, quindi percorrere di nuovo l'elenco compatto per generare l'output.

La riga seguente rappresenta la memoria poco prima dell'inizio dell'operazione di unione elenco. Le "O" sono la regione che contiene gli interi a 32 bit ordinati. Le "X" sono la regione che contiene il vecchio elenco compatto. I segni "=" sono lo spazio di espansione per l'elenco compatto, 7 bit per ogni numero intero nelle "O". Le "Z" sono altre spese generali casuali.

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

La routine di unione inizia a leggere all'estrema sinistra "O" e all'estrema sinistra "X", e inizia a scrivere all'estrema sinistra "=". Il puntatore di scrittura non rileva il puntatore di lettura dell'elenco compatto fino a quando tutti i nuovi numeri interi non vengono uniti, poiché entrambi i puntatori avanzano di 2 bit per ciascun elenco secondario e di 7 bit per ciascuna voce dell'elenco compatto precedente e non vi è spazio aggiuntivo sufficiente per Voci a 7 bit per i nuovi numeri.

Parte 2, stipandolo in 1M

Per comprimere la soluzione sopra in 1M, devo rendere il formato dell'elenco compatto un po 'più compatto. Mi libererò di uno dei tipi di elenco secondario, in modo che ci siano solo 3 diversi valori di intestazione dell'elenco secondario possibili. Quindi posso usare "00", "01" e "1" come valori dell'intestazione dell'elenco secondario e salvare alcuni bit. I tipi di elenco secondario sono:

Un elenco secondario vuoto, non segue nulla.

B Singleton, c'è solo una voce nell'elenco secondario e i successivi 7 bit lo tengono.

C L'elenco secondario contiene almeno 2 numeri distinti. Le voci sono memorizzate in ordine non decrescente, tranne per il fatto che l'ultima voce è inferiore o uguale alla prima. Ciò consente di identificare la fine dell'elenco secondario. Ad esempio, i numeri 2,4,6 verrebbero memorizzati come (4,6,2). I numeri 2,2,3,4,4 verrebbero memorizzati come (2,3,4,4,2).

D La lista secondaria è composta da 2 o più ripetizioni di un singolo numero.

I miei 3 valori di intestazione dell'elenco secondario saranno "A", "B" e "C", quindi ho bisogno di un modo per rappresentare gli elenchi secondari di tipo D.

Supponiamo di avere l'intestazione dell'elenco secondario di tipo C seguita da 3 voci, ad esempio "C [17] [101] [58]". Questo non può far parte di un elenco secondario di tipo C valido come descritto sopra, poiché la terza voce è inferiore alla seconda ma più della prima. Posso usare questo tipo di costrutto per rappresentare un elenco secondario di tipo D. In parole povere, ovunque io abbia "C {00 ?????} {1 ??????} {01 ?????}" è un elenco secondario di tipo C impossibile. Userò questo per rappresentare un elenco secondario composto da 3 o più ripetizioni di un singolo numero. Le prime due parole a 7 bit codificano il numero (i bit "N" in basso) e sono seguite da zero o più parole {0100001} seguite da una parola {0100000}.

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

Ciò lascia solo elenchi che contengono esattamente 2 ripetizioni di un singolo numero. Rappresenterò quelli con un altro schema di sublist di tipo C impossibile: "C {0 ??????} {11 ?????} {10 ?????}". C'è un sacco di spazio per i 7 bit del numero nelle prime 2 parole, ma questo schema è più lungo dell'elenco secondario che rappresenta, il che rende le cose un po 'più complesse. I cinque punti interrogativi alla fine possono essere considerati non parte del modello, quindi ho: "C {0NNNNNN} {11N ????} 10" come modello, con il numero da ripetere memorizzato nella "N "S. Sono 2 bit troppo lunghi.

Dovrò prendere in prestito 2 bit e ripagarli dai 4 bit non utilizzati in questo modello. Durante la lettura, quando si incontra "C {0NNNNNN} {11N00AB} 10", emettere 2 istanze del numero nelle "N", sovrascrivere "10" alla fine con i bit A e B e riavvolgere il puntatore di lettura di 2 bit. Letture distruttive sono ok per questo algoritmo, poiché ogni elenco compatto viene camminato una sola volta.

Quando si scrive un elenco secondario di 2 ripetizioni di un singolo numero, scrivere "C {0NNNNNN} 11N00" e impostare il contatore dei bit presi in prestito su 2. Ad ogni scrittura in cui il contatore dei bit presi in prestito è diverso da zero, viene ridotto per ogni bit scritto e "10" viene scritto quando il contatore colpisce zero. Quindi i successivi 2 bit scritti andranno negli slot A e B, quindi i "10" verranno rilasciati alla fine.

Con 3 valori di intestazione dell'elenco secondario rappresentati da "00", "01" e "1", posso assegnare "1" al tipo di elenco secondario più popolare. Avrò bisogno di una piccola tabella per mappare i valori dell'intestazione dell'elenco secondario ai tipi di elenco secondario e avrò bisogno di un contatore di occorrenze per ciascun tipo di elenco secondario in modo da sapere qual è il migliore mapping dell'intestazione dell'elenco secondario.

La rappresentazione minima del caso peggiore di un elenco compatto completamente popolato si verifica quando tutti i tipi di elenco secondario sono ugualmente popolari. In tal caso, salvo 1 bit per ogni 3 intestazioni dell'elenco secondario, quindi la dimensione dell'elenco è 2 * 781250 + 7 * 1000000 - 781250/3 = 8302083,3 bit. Arrotondando per eccesso a un limite di parola a 32 bit, sono 8302112 bit o 1037764 byte.

1M meno il 2k per lo stato e i buffer TCP / IP è 1022 * 1024 = 1046528 byte, lasciandomi 8764 byte con cui giocare.

Ma per quanto riguarda il processo di modifica del mapping dell'intestazione dell'elenco secondario? Nella mappa di memoria in basso, "Z" è un overhead casuale, "=" è spazio libero, "X" è l'elenco compatto.

ZZZ=====XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Inizia a leggere all'estrema sinistra "X" e inizia a scrivere all'estrema sinistra "=" e lavora a destra. Al termine, l'elenco compatto sarà un po 'più corto e si troverà nella parte sbagliata della memoria:

ZZZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=======

Quindi dovrò spostarlo a destra:

ZZZ=======XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Nel processo di modifica della mappatura delle intestazioni, fino a 1/3 delle intestazioni della sublist passerà da 1-bit a 2-bit. Nel peggiore dei casi questi saranno tutti in testa alla lista, quindi avrò bisogno di almeno 781250/3 bit di spazio di archiviazione gratuito prima di iniziare, il che mi riporta ai requisiti di memoria della versione precedente dell'elenco compatto: (

Per ovviare a questo, dividerò le 781250 liste secondarie in 10 gruppi di liste secondarie di 78125 liste secondarie ciascuna. Ogni gruppo ha il proprio mapping di intestazione dell'elenco secondario indipendente. Usando le lettere dalla A alla J per i gruppi:

ZZZ=====AAAAAABBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ

Ogni gruppo di elenchi secondari si restringe o rimane lo stesso durante una modifica del mapping dell'intestazione dell'elenco secondario:

ZZZ=====AAAAAABBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAA=====BBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABB=====CCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCC======DDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDD======EEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEE======FFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFF======GGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGG=======HHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHH=======IJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHI=======JJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ=======
ZZZ=======AAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ

L'espansione temporanea del caso peggiore di un gruppo di elenchi secondari durante una modifica della mappatura è 78125/3 = 26042 bit, inferiore a 4k. Se consento 4k più i 1037764 byte per un elenco compatto completamente popolato, ciò mi lascia 8764 - 4096 = 4668 byte per le "Z" nella mappa di memoria.

Questo dovrebbe essere sufficiente per le 10 tabelle di mappatura delle intestazioni della sublist, 30 conteggi delle occorrenze delle intestazioni della sublist e gli altri pochi contatori, puntatori e piccoli buffer di cui avrò bisogno, e lo spazio che ho usato senza notare, come lo spazio dello stack per gli indirizzi di ritorno delle chiamate di funzione e variabili locali.

Parte 3, quanto tempo ci vorrebbe per funzionare?

Con un elenco compatto vuoto, l'intestazione dell'elenco a 1 bit verrà utilizzata per un elenco secondario vuoto e la dimensione iniziale dell'elenco sarà 781250 bit. Nel peggiore dei casi l'elenco aumenta di 8 bit per ogni numero aggiunto, quindi sono necessari 32 + 8 = 40 bit di spazio libero per ciascuno dei numeri a 32 bit da posizionare nella parte superiore del buffer dell'elenco e quindi ordinato e unito. Nel peggiore dei casi, la modifica della mappatura dell'intestazione dell'elenco secondario comporta un utilizzo dello spazio di 2 * 781250 + 7 * voci - 781250/3 bit.

Con una politica di modifica del mapping dell'intestazione dell'elenco secondario dopo ogni quinta unione una volta che ci sono almeno 800000 numeri nell'elenco, un caso peggiore comporterebbe un totale di circa 30 milioni di attività di lettura e scrittura dell'elenco compatto.

Fonte:

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


15
Non penso che sia possibile una soluzione migliore (nel caso in cui dobbiamo lavorare con valori incomprimibili). Ma questo potrebbe essere leggermente migliorato. Non è necessario modificare le intestazioni dell'elenco secondario tra rappresentazioni a 1 e 2 bit. Invece è possibile utilizzare la codifica aritmetica , che semplifica l'algoritmo e riduce anche il numero di bit nel caso peggiore per intestazione da 1,67 a 1,58. E non è necessario spostare l'elenco compatto in memoria; utilizzare invece il buffer circolare e modificare solo i puntatori.
Evgeny Kluev,

5
Quindi, alla fine, è stata una domanda per l'intervista?
mlvljr,

2
Un altro possibile miglioramento consiste nell'utilizzare elenchi a 100 elementi anziché in elenchi a 128 elementi (poiché otteniamo una rappresentazione più compatta quando il numero di elenchi secondari è uguale al numero di elementi nel set di dati). Ogni valore dell'elenco secondario da codificare con codifica aritmetica (con frequenza uguale a 1/100 per ciascun valore). Ciò consente di risparmiare circa 10000 bit, molto meno della compressione delle intestazioni degli elenchi secondari.
Evgeny Kluev,

Per il caso C, si dice "Le voci sono memorizzate in ordine non decrescente, tranne per il fatto che l'ultima voce è inferiore o uguale alla prima." Come codificheresti quindi 2,2,2,3,5? {2,2,3,5,2} sembrerebbe solo 2,2
Rollie il

1
Una soluzione più semplice della codifica dell'intestazione dell'elenco secondario è possibile con lo stesso rapporto di compressione 1,67 bit per sottotitolo senza cambiare complicato il mapping. È possibile combinare ogni 3 sottotitoli consecutivi insieme, che può essere facilmente codificato in 5 bit perché 3 * 3 * 3 = 27 < 32. Li combini combined_subheader = subheader1 + 3 * subheader2 + 9 * subheader3.
hynekcer,

57

La risposta di Gilmanov è molto sbagliata nelle sue ipotesi. Inizia a speculare in base a una misura inutile di un milione di numeri interi consecutivi . Ciò significa che non ci sono lacune. Quelle lacune casuali, per quanto piccole, lo rendono davvero una cattiva idea.

Prova tu stesso. Ottieni 1 milione di numeri casuali a 27 bit, ordinali, comprimi con 7-Zip , xz, qualunque LZMA desideri. Il risultato è oltre 1,5 MB. La premessa in cima è la compressione di numeri sequenziali. Anche la codifica delta è superiore a 1,1 MB . E non importa che questo stia usando oltre 100 MB di RAM per la compressione. Quindi anche gli interi compressi non si adattano al problema e non importa il tempo di esecuzione della RAM .

Mi rattrista il modo in cui le persone votano semplicemente la grafica e la razionalizzazione.

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

}

Ora comprimi ints.bin con 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
qualsiasi algoritmo che coinvolge la compressione basata su dizionario è appena al di là del ritardo, ne ho codificati alcuni personalizzati e tutto ciò che serve un bel po 'di memoria solo per posizionare le proprie tabelle hash (e nessun HashMap in Java perché ha molta fame di risorse). La soluzione più vicina sarebbe la codifica delta con lunghezza bit variabile e il ripristino dei pacchetti TCP che non ti piacciono. Il peer verrà ritrasmesso, nel migliore dei casi ancora bizzarro.
bestsss

@bestsss yeah! controlla la mia ultima risposta in corso. Penso che potrebbe essere possibile.
alecco,

3
Mi dispiace ma questo non sembra rispondere alla domanda neanche, in realtà.
n611x007,

@naxa si risponde: non può essere fatto entro i parametri della domanda originale. Può essere fatto solo se la distribuzione dei numeri ha un'entropia molto bassa.
alecco,

1
Tutta questa risposta mostra che le routine di compressione standard hanno difficoltà a comprimere i dati al di sotto di 1 MB. Potrebbe esserci o meno uno schema di codifica che può comprimere i dati per richiedere meno di 1 MB, ma questa risposta non dimostra che non esiste uno schema di codifica che comprimerà i dati così tanto.
Itsme2003,

41

Penso che un modo di pensarci sia dal punto di vista combinatorio: quante possibili combinazioni di ordinamenti numerici ordinati ci sono? Se diamo alla combinazione 0,0,0, ...., 0 il codice 0 e 0,0,0, ..., 1 il codice 1 e 99999999, 99999999, ... 99999999 il codice N, che cos'è N? In altre parole, quanto è grande lo spazio dei risultati?

Bene, un modo per pensare a questo è notare che questa è una biiezione del problema di trovare il numero di percorsi monotonici in una griglia N x M, dove N = 1.000.000 e M = 100.000.000. In altre parole, se hai una griglia larga 1.000.000 e alta 100.000.000, quanti sono i percorsi più corti dalla parte inferiore sinistra alla parte superiore destra? Naturalmente i percorsi più brevi richiedono sempre o solo di spostarsi a destra o in alto (se si spostasse in basso o a sinistra, si annullerebbero i progressi compiuti in precedenza). Per vedere come questa è una biiezione del nostro problema di ordinamento dei numeri, osservare quanto segue:

Puoi immaginare qualsiasi gamba orizzontale nel nostro percorso come un numero nel nostro ordine, in cui la posizione Y della gamba rappresenta il valore.

inserisci qui la descrizione dell'immagine

Quindi se il percorso si sposta semplicemente verso destra fino alla fine, quindi salta fino in cima, ciò equivale all'ordinamento 0,0,0, ..., 0. se invece inizia saltando fino in cima e poi si sposta a destra 1.000.000 di volte, ciò equivale a 99999999,99999999, ..., 99999999. Un percorso in cui si sposta a destra una volta, poi su una volta, poi a destra , poi su una volta, ecc. fino alla fine (quindi salta necessariamente fino in cima), equivale a 0,1,2,3, ..., 999999.

Fortunatamente per noi questo problema è già stato risolto, tale griglia ha (N + M) Scegli (M) percorsi:

(1.000.000 + 100.000.000) Scegli (100.000.000) ~ = 2,27 * 10 ^ 2436455

N equivale quindi a 2,27 * 10 ^ 2436455, e quindi il codice 0 rappresenta 0,0,0, ..., 0 e il codice 2,27 * 10 ^ 2436455 e alcune modifiche rappresentano 99999999,99999999, ..., 99999999.

Per memorizzare tutti i numeri da 0 a 2,27 * 10 ^ 2436455 è necessario lg2 (2,27 * 10 ^ 2436455) = 8,0937 * 10 ^ 6 bit.

1 megabyte = 8388608 bit> 8093700 bit

Quindi sembra che almeno in realtà abbiamo abbastanza spazio per memorizzare il risultato! Ora, ovviamente, il bit interessante sta facendo l'ordinamento mentre i numeri scorrono. Non sono sicuro che l'approccio migliore sia dato dal fatto che rimangono 294908 bit. Immagino che una tecnica interessante sarebbe quella di supporre che quello sia l'intero ordinamento, trovare il codice per quell'ordinamento, e quindi quando si riceve un nuovo numero tornando indietro e aggiornando il codice precedente. Mano onda mano onda


Questo è davvero un sacco di agitando la mano. Da un lato, teoricamente questa è la soluzione perché possiamo semplicemente scrivere una macchina a stati grandi, ma ancora finiti; d'altra parte, la dimensione del puntatore dell'istruzione per quella grande macchina a stati potrebbe essere più di un megabyte, rendendolo un dispositivo di avviamento. Richiede davvero un po 'più di pensiero di questo per risolvere effettivamente il problema dato. Dobbiamo rappresentare non solo tutti gli stati, ma anche tutti gli stati di transizione necessari per calcolare cosa fare su un dato numero di input successivo.
Daniel Wagner,

4
Penso che le altre risposte siano solo più sottili riguardo al loro agitando la mano. Dato che ora conosciamo le dimensioni dello spazio dei risultati, sappiamo di quanto spazio abbiamo assolutamente bisogno. Nessun'altra risposta sarà in grado di memorizzare ogni possibile risposta in qualcosa di più piccolo di 8093700 bit, poiché questo è quanti stati finali ci possono essere. Fare compress (stato finale) può a volte ridurre lo spazio, ma ci sarà sempre qualche risposta che richiede lo spazio completo (nessun algoritmo di compressione può comprimere ogni input).
Francisco Ryan Tolmasky I,

Diverse altre risposte hanno già citato comunque il limite inferiore duro (ad esempio la seconda frase della risposta originale di chi pone domande), quindi non sono sicuro di vedere cosa aggiunge questa risposta alla gestalt.
Daniel Wagner,

Ti riferisci a 3.5M per archiviare il flusso non elaborato? (In caso contrario, mi scuso e ignoro questa risposta). In tal caso, si tratta di un limite inferiore completamente non correlato. Il mio limite inferiore è quanto spazio occuperà il risultato, quel limite inferiore è quanto spazio occuperebbero gli input se fosse necessario memorizzarli - dato che la domanda era formulata come un flusso proveniente da una connessione TCP non è chiaro se sia effettivamente necessario, potresti leggere un numero alla volta e aggiornare il tuo stato, quindi non è necessario il 3.5M - in entrambi i casi, il 3.5 è ortogonale a questo calcolo.
Francisco Ryan Tolmasky I,

"Ci sono circa 2 alla potenza 8093729.5 diversi modi per scegliere 1 milione di numeri a 8 cifre con i duplicati consentiti e ordinare senza importanza" <- dalla risposta originale del richiedente. Non so come essere più chiaro su ciò di cui sto parlando. Nel mio ultimo commento ho fatto un riferimento specifico a questa frase.
Daniel Wagner,

20

I miei suggerimenti qui devono molto alla soluzione di Dan

Prima di tutto suppongo che la soluzione debba gestire tutti i possibili elenchi di input. Penso che le risposte popolari non facciano questo presupposto (che l'IMO è un grosso errore).

È noto che nessuna forma di compressione senza perdita ridurrà la dimensione di tutti gli input.

Tutte le risposte più diffuse presumono che saranno in grado di applicare una compressione abbastanza efficace da consentire loro uno spazio aggiuntivo. In effetti, un pezzo di spazio extra abbastanza grande da contenere una parte dell'elenco parzialmente completato in una forma non compressa e consentire loro di eseguire le operazioni di ordinamento. Questa è solo una cattiva ipotesi.

Per tale soluzione, chiunque sia a conoscenza del modo in cui esegue la compressione sarà in grado di progettare alcuni dati di input che non si comprimono bene per questo schema, e la "soluzione" molto probabilmente si interromperà a causa dell'esaurimento dello spazio.

Invece prendo un approccio matematico. Le nostre possibili uscite sono tutte le liste di lunghezza LEN costituite da elementi nella gamma 0..MAX. Qui il LEN è 1.000.000 e il nostro MAX è 100.000.000.

Per LEN e MAX arbitrari, la quantità di bit necessari per codificare questo stato è:

Log2 (MAX Multichoose LEN)

Quindi, per i nostri numeri, una volta che abbiamo completato la ricezione e l'ordinamento, avremo bisogno di almeno i bit Log2 (100.000.000 MC 1.000.000) per memorizzare il nostro risultato in un modo che possa distinguere in modo univoco tutti i possibili output.

Questo è ~ = 988kb . Quindi in realtà abbiamo abbastanza spazio per contenere il nostro risultato. Da questo punto di vista, è possibile.

[Cancellati inutili vagabondi ora che esistono esempi migliori ...]

La migliore risposta è qui .

Un'altra buona risposta è qui e fondamentalmente utilizza l'ordinamento di inserzione come funzione per espandere l'elenco di un elemento (buffer di alcuni elementi e pre-ordinamento, per consentire l'inserimento di più di uno alla volta, consente di risparmiare un po 'di tempo). usa anche una bella codifica a stato compatto, bucket di delta a sette bit


Sempre divertente rileggere la propria risposta il giorno successivo ... Quindi, mentre la risposta migliore è sbagliata, quella accettata stackoverflow.com/a/12978097/1763801 è piuttosto buona. Fondamentalmente usa l'ordinamento di inserzione come funzione per prendere l'elenco LEN-1 e restituire LEN. Sfrutta al massimo il fatto che se prevedi un piccolo set puoi inserirli tutti in un unico passaggio, per aumentare l'efficienza. La rappresentazione dello stato è piuttosto compatta (secchi di numeri a 7 bit) meglio del mio suggerimento ondulato a mano e più intuitiva. i miei pensieri comp geo erano bulllocks, mi dispiace per quello
davec

1
Penso che la tua aritmetica sia un po 'fuori. Ottengo lg2 (100999999! / (99999999! * 1000000!)) = 1011718.55
NovaDenizen,

Sì, grazie era 988kb non 965. Ero sciatto in termini di 1024 contro 1000. Ci rimangono ancora circa 35kb con cui giocare. Ho aggiunto un link al calcolo matematico nella risposta.
davec,

18

Supponiamo che questa attività sia possibile. Poco prima dell'output, ci sarà una rappresentazione in memoria dei milioni di numeri ordinati. Quante rappresentazioni diverse ci sono? Dato che potrebbero esserci numeri ripetuti, non possiamo usare nCr (scegliere), ma esiste un'operazione chiamata multichoose che funziona su multiset .

  • Ci sono 2.2e2436455 modi per scegliere un milione di numeri nell'intervallo 0..99.999.999.
  • Ciò richiede 8.093.730 bit per rappresentare ogni possibile combinazione, o 1.011.717 byte.

Quindi teoricamente potrebbe essere possibile, se riesci a trovare una rappresentazione sana (sufficiente) dell'elenco ordinato dei numeri. Ad esempio, una rappresentazione folle potrebbe richiedere una tabella di ricerca da 10 MB o migliaia di righe di codice.

Tuttavia, se "1M RAM" significa un milione di byte, allora chiaramente non c'è abbastanza spazio. Il fatto che il 5% di memoria in più lo renda teoricamente possibile mi suggerisce che la rappresentazione dovrà essere MOLTO efficiente e probabilmente non sana.


Il numero di modi per scegliere un milione di numeri (2.2e2436455) sembra essere vicino a (256 ^ (1024 * 988)), che è (2.0e2436445). Ergo, se si toglie circa 32 KB di memoria dall'1M, il problema non può essere risolto. Ricorda inoltre che sono stati riservati almeno 3 KB di memoria.
johnwbyrd,

Questo ovviamente presuppone che i dati siano completamente casuali. Per quanto ne sappiamo, lo è, ma sto solo dicendo :)
Thorarin,

Il modo convenzionale di rappresentare questo numero di stati possibili è quello di prendere la base di log 2 e riportare il numero di bit necessari per rappresentarli.
NovaDenizen,

@Thorarin, sì, non vedo alcun punto in una "soluzione" che funziona solo per alcuni input.
Dan,

12

(La mia risposta originale era sbagliata, scusa per la cattiva matematica, vedi sotto l'interruzione.)

Cosa ne pensi di questo?

I primi 27 bit memorizzano il numero più basso che hai visto, quindi la differenza con il numero successivo visto, codificato come segue: 5 bit per memorizzare il numero di bit utilizzati per memorizzare la differenza, quindi la differenza. Usa 00000 per indicare che hai visto di nuovo quel numero.

Ciò funziona perché quando vengono inseriti più numeri, la differenza media tra i numeri diminuisce, quindi si utilizzano meno bit per memorizzare la differenza quando si aggiungono più numeri. Credo che questo sia chiamato un elenco delta.

Il caso peggiore che mi viene in mente è che tutti i numeri siano equidistanti (per 100), ad esempio Supponendo che 0 sia il primo numero:

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

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

Reddit in soccorso!

Se tutto ciò che dovevi fare fosse ordinarli, questo problema sarebbe facile. Ci vogliono 122k (1 milione di bit) per memorizzare quali numeri hai visto (0 ° bit se è stato visto 0, 2300 ° bit se è stato visto 2300, ecc.

Leggi i numeri, li memorizzi nel campo dei bit e poi li sposti mantenendo un conteggio.

MA, devi ricordare quanti ne hai visti. Sono stato ispirato dalla risposta dell'elenco secondario sopra per elaborare questo schema:

Invece di utilizzare un bit, utilizzare 2 o 27 bit:

  • 00 significa che non hai visto il numero.
  • 01 significa che l'hai visto una volta
  • 1 significa che l'hai visto, e i successivi 26 bit sono il conteggio di quante volte.

Penso che funzioni: se non ci sono duplicati, hai un elenco di 244k. Nel peggiore dei casi vedi ogni numero due volte (se vedi un numero tre volte, accorcia il resto della lista per te), ciò significa che hai visto 50.000 più di una volta e hai visto 950.000 articoli 0 o 1 volte.

50.000 * 27 + 950.000 * 2 = 396,7k.

Puoi apportare ulteriori miglioramenti se usi la seguente codifica:

0 significa che non hai visto il numero 10 significa che l'hai visto una volta 11 è il modo in cui conti

Ciò comporterà in media 280,7 k di spazio di archiviazione.

EDIT: la mia matematica di domenica mattina era sbagliata.

Il caso peggiore è che vediamo 500.000 numeri due volte, quindi la matematica diventa:

500.000 * 27 + 500.000 * 2 = 1,77 milioni

La codifica alternativa risulta in una memoria media di

500.000 * 27 + 500.000 = 1.70 milioni

: (


1
Bene, no, dato che il secondo numero sarebbe 500000.
jfern e

Forse aggiungi qualche intermedio, come dove 11 significa che hai visto il numero fino a 64 volte (usando i successivi 6 bit) e 11000000 significa usa altri 32 bit per memorizzare il numero di volte che l'hai visto.
τεκ

10
Dove hai preso il numero "1 milione di bit"? Hai detto che il 2300 bit rappresenta se 2300 è stato visto. (Penso che tu abbia effettivamente inteso il 2301.) Quale bit rappresenta se è stato visto 99.999.999 (il più grande numero di 8 cifre)? Presumibilmente, sarebbe il centesimo milionesimo.
user94559

Hai il tuo milione e i tuoi cento milioni al contrario. La maggior parte delle volte che può verificarsi un valore è 1 milione e sono necessari solo 20 bit per rappresentare il numero di occorrenze di un valore. Allo stesso modo sono necessari campi da 100.000.000 di bit (non 1 milione), uno per ogni possibile valore.
Tim R.

Uh, 27 + 1000000 * (5 + 7) = 12000027 bit = 1,43 M, non 427 K.
Daniel Wagner,

10

C'è una soluzione a questo problema tra tutti i possibili input. Truffare.

  1. Leggi i valori di m su TCP, dove m è vicino al massimo che può essere ordinato in memoria, forse n / 4.
  2. Ordinare i 250.000 (circa) numeri e produrli.
  3. Ripetere l'operazione per gli altri 3 trimestri.
  4. Lascia che il destinatario unisca i 4 elenchi di numeri che ha ricevuto mentre li elabora. (Non è molto più lento dell'uso di un singolo elenco.)

7

Vorrei provare un Radix Tree . Se è possibile memorizzare i dati in un albero, è possibile eseguire una traversata in ordine per trasmettere i dati.

Non sono sicuro che potresti inserirlo in 1 MB, ma penso che valga la pena provare.


7

Che tipo di computer stai usando? Potrebbe non avere altra memoria locale "normale", ma ha RAM video, ad esempio? 1 megapixel x 32 bit per pixel (diciamo) è abbastanza vicino alla dimensione di input dei dati richiesta.

(Chiedo in gran parte in memoria del vecchio PC RISC Acorn , che potrebbe "prendere in prestito" la VRAM per espandere la RAM di sistema disponibile, se si sceglie una modalità schermo a bassa risoluzione o con profondità di colore ridotta!). Ciò era piuttosto utile su una macchina con solo pochi MB di RAM normale.


1
Vuoi commentare, downvoter? - Sto solo cercando di estendere gli apparenti contorni della domanda (cioè imbrogliare in modo creativo ;-)
DNA

Potrebbe non esserci alcun computer, poiché il thread pertinente sulle notizie degli hacker menziona questa volta una volta era una domanda di intervista di Google.
mlvljr,

1
Sì, ho risposto prima che la domanda fosse modificata per indicare che si tratta di una domanda di intervista!
DNA,

6

Una rappresentazione ad albero radix si avvicina alla gestione di questo problema, dal momento che l'albero radix sfrutta la "compressione prefisso". Ma è difficile concepire una rappresentazione ad albero radicale che possa rappresentare un singolo nodo in un byte - due probabilmente riguarda il limite.

Tuttavia, indipendentemente dal modo in cui i dati sono rappresentati, una volta ordinati possono essere memorizzati in forma di prefisso compresso, dove i numeri 10, 11 e 12 sarebbero rappresentati da, diciamo 001b, 001b, 001b, indicando un incremento di 1 dal numero precedente. Forse, quindi, 10101b rappresenterebbe un incremento di 5, 1101001b un incremento di 9, ecc.


6

Ci sono 10 ^ 6 valori in un intervallo di 10 ^ 8, quindi c'è un valore per cento punti di codice in media. Memorizza la distanza dal punto N al punto (N + 1) th. I valori duplicati hanno un salto di 0. Ciò significa che il salto ha bisogno di una media di poco meno di 7 bit per essere archiviato, quindi un milione di loro si adatterà felicemente ai nostri 8 milioni di bit di archiviazione.

Questi salti devono essere codificati in un flusso di bit, ad esempio con la codifica Huffman. L'inserimento avviene ripetendo il flusso di bit e riscrivendo il nuovo valore. Output eseguendo iterando e scrivendo i valori impliciti. Per praticità, probabilmente vuole essere fatto come, diciamo, 10 ^ 4 elenchi che coprono 10 ^ 4 punti di codice (e una media di 100 valori) ciascuno.

Un buon albero di Huffman per dati casuali può essere costruito a priori assumendo una distribuzione di Poisson (media = varianza = 100) sulla lunghezza dei salti, ma è possibile mantenere statistiche reali sull'input e utilizzare per generare un albero ottimale da gestire casi patologici.


5

Ho un computer con 1M di RAM e nessun altro archivio locale

Un altro modo per imbrogliare: è possibile utilizzare l'archiviazione non locale (in rete) (la domanda non lo preclude) e chiamare un servizio in rete che potrebbe utilizzare un semplice mergesort basato su disco (o solo RAM sufficiente per ordinare in memoria, poiché devono solo accettare numeri 1M), senza aver bisogno delle soluzioni (dichiaratamente estremamente ingegnose) già fornite.

Questo potrebbe essere un imbroglio, ma non è chiaro se stai cercando una soluzione a un problema del mondo reale o un enigma che invita a piegare le regole ... se quest'ultimo, un semplice imbroglio può ottenere risultati migliori di un complesso ma una soluzione "genuina" (che come altri hanno sottolineato, può funzionare solo con input comprimibili).


5

Penso che la soluzione sia combinare le tecniche della codifica video, vale a dire la discreta trasformazione del coseno. Nel video digitale, piuttosto registrando la modifica della luminosità o del colore del video come valori regolari come 110 112 115 116, ciascuno viene sottratto dall'ultimo (simile alla codifica della lunghezza della corsa). 110 112 115 116 diventa 110 2 3 1. I valori, 2 3 1 richiedono meno bit rispetto agli originali.

Supponiamo quindi di creare un elenco dei valori di input quando arrivano sul socket. Stiamo memorizzando in ciascun elemento, non il valore, ma l'offset di quello precedente. Ordiniamo mentre andiamo, quindi gli offset saranno positivi. Ma l'offset potrebbe essere largo 8 cifre decimali che si adatta in 3 byte. Ogni elemento non può essere di 3 byte, quindi dobbiamo comprimerli. Potremmo usare il bit superiore di ogni byte come "bit di continuazione", indicando che il byte successivo fa parte del numero e che i 7 bit inferiori di ciascun byte devono essere combinati. zero è valido per i duplicati.

Man mano che l'elenco si riempie, i numeri dovrebbero avvicinarsi, il che significa che in media viene utilizzato solo 1 byte per determinare la distanza dal valore successivo. 7 bit di valore e 1 bit di offset, se conveniente, ma potrebbe esserci un punto debole che richiede meno di 8 bit per un valore "continua".

Comunque, ho fatto qualche esperimento. Uso un generatore di numeri casuali e posso inserire un milione di numeri decimali a 8 cifre ordinati in circa 1279000 byte. Lo spazio medio tra ogni numero è costantemente 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

Potremmo giocare con lo stack di rete per inviare i numeri in ordine prima di avere tutti i numeri. Se si inviano 1 milione di dati, TCP / IP li suddividerà in pacchetti da 1500 byte e li trasmetterà in streaming alla destinazione. A ciascun pacchetto verrà assegnato un numero progressivo.

Possiamo farlo a mano. Poco prima di riempire la nostra RAM, possiamo ordinare ciò che abbiamo e inviare l'elenco al nostro obiettivo, ma lasciare buchi nella nostra sequenza attorno a ciascun numero. Quindi elaborare il 2 ° 1/2 dei numeri allo stesso modo usando quei buchi nella sequenza.

Lo stack di rete sul lato opposto assemblerà il flusso di dati risultante in ordine di sequenza prima di consegnarlo all'applicazione.

Sta utilizzando la rete per eseguire un ordinamento di tipo merge. Questo è un hack totale, ma sono stato ispirato dall'altro hack di rete elencato in precedenza.


4

Approccio (cattivo) di Google , dal thread HN. Archivia i conteggi in stile RLE.

La struttura dei dati iniziale è '99999999: 0' (tutti zeri, non ho visto alcun numero) e quindi diciamo che vedi il numero 3.866.344, quindi la struttura dei dati diventa '3866343: 0,1: 1,96133654: 0' come te può vedere che i numeri si alterneranno sempre tra il numero di zero bit e il numero di bit "1" in modo da poter supporre che i numeri dispari rappresentino 0 bit e i numeri pari 1 bit. Questo diventa (3866343,1,96133654)

Il loro problema non sembra riguardare i duplicati, ma diciamo che usano "0: 1" per i duplicati.

Grande problema n. 1: gli inserimenti per numeri interi 1M richiederebbero anni .

Grande problema n. 2: come tutte le semplici soluzioni di codifica delta, alcune distribuzioni non possono essere coperte in questo modo. Ad esempio, numeri interi di 1m con distanze 0:99 (ad es. +99 ciascuno). Ora pensa lo stesso ma con la distanza a caso nella gamma di 0:99 . (Nota: 99999999/1000000 = 99.99)

L'approccio di Google è sia indegno (lento) che errato. Ma a loro difesa, il loro problema potrebbe essere stato leggermente diverso.


3

Per rappresentare l'array ordinato si può semplicemente memorizzare il primo elemento e la differenza tra elementi adiacenti. In questo modo ci occupiamo della codifica di 10 ^ 6 elementi che possono riassumere al massimo 10 ^ 8. Chiamiamo questo D . Per codificare gli elementi di D si può usare un codice Huffman . Il dizionario per il codice Huffman può essere creato in movimento e l'array viene aggiornato ogni volta che un nuovo elemento viene inserito nell'array ordinato (ordinamento per inserzione). Si noti che quando il dizionario cambia a causa di un nuovo elemento, l'intero array deve essere aggiornato in modo che corrisponda alla nuova codifica.

Il numero medio di bit per la codifica di ciascun elemento di D è massimizzato se abbiamo un numero uguale di ciascun elemento univoco. Dire che gli elementi d1 , d2 , ..., dN in D appaiono ciascuno F volte. In tal caso (nel peggiore dei casi abbiamo sia 0 che 10 ^ 8 nella sequenza di input) che abbiamo

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

dove

somma (1 <= i <= N ) F = 10 ^ 6 o F = 10 ^ 6 / N e la frequenza normalizzata sarà p = F / 10 ^ = 1 / N

Il numero medio di bit sarà -log2 (1 / P ) = log2 ( N ). In queste circostanze dovremmo trovare un caso che massimizza N . Ciò accade se abbiamo numeri consecutivi per di a partire da 0 o di = i -1, quindi

10 ^ 8 = sum (1 <= i <= N ) F . di = somma (1 <= i <= N ) (10 ^ 6 / N ) (i-1) = (10 ^ 6 / N ) N ( N -1) / 2

vale a dire

N <= 201. E in questo caso il numero medio di bit è log2 (201) = 7.6511 che significa che avremo bisogno di circa 1 byte per elemento di input per salvare l'array ordinato. Nota che questo non significa che D in generale non può avere più di 201 elementi. Scrive solo che se gli elementi di D sono distribuiti uniformemente, non può avere più di 201 valori univoci.


1
Penso che tu abbia dimenticato che il numero può essere duplicato.
bestsss

Per i numeri duplicati la differenza tra i numeri adiacenti sarà zero. Non crea alcun problema. Il codice Huffman non richiede valori diversi da zero.
Mohsen Nosratinia,

3

Vorrei sfruttare il comportamento di ritrasmissione di TCP.

  1. Fare in modo che il componente TCP crei una grande finestra di ricezione.
  2. Ricevi alcuni pacchetti senza inviare loro un ACK.
    • Elaborare quelli nei passaggi creando una struttura di dati compressi (prefisso)
    • Invia duplicati ack per l'ultimo pacchetto che non è più necessario / attendi il timeout di ritrasmissione
    • Vai a 2
  3. Sono stati accettati tutti i pacchetti

Questo presuppone una sorta di vantaggio di secchi o passaggi multipli.

Probabilmente ordinando i lotti / i secchi e unendoli. -> alberi radix

Utilizzare questa tecnica per accettare e ordinare il primo 80%, quindi leggere l'ultimo 20%, verificare che l'ultimo 20% non contenga numeri che potrebbero atterrare nel primo 20% dei numeri più bassi. Quindi invia il 20% di numeri più bassi, rimuovi dalla memoria, accetta il restante 20% di nuovi numeri e unisci. **


3

Ecco una soluzione generalizzata a questo tipo di problema:

Procedura generale

L'approccio adottato è il seguente. L'algoritmo funziona su un singolo buffer di parole a 32 bit. Esegue la seguente procedura in un ciclo:

  • Iniziamo con un buffer pieno di dati compressi dall'ultima iterazione. Il buffer si presenta così

    |compressed sorted|empty|

  • Calcola la quantità massima di numeri che possono essere memorizzati in questo buffer, sia compressi che non compressi. Dividere il buffer in queste due sezioni, iniziando con lo spazio per i dati compressi, terminando con i dati non compressi. Sembra il buffer

    |compressed sorted|empty|empty|

  • Riempi la sezione non compressa con i numeri da ordinare. Sembra il buffer

    |compressed sorted|empty|uncompressed unsorted|

  • Ordina i nuovi numeri con un ordinamento sul posto. Sembra il buffer

    |compressed sorted|empty|uncompressed sorted|

  • Allineare a destra tutti i dati già compressi dall'iterazione precedente nella sezione compressa. A questo punto il buffer è partizionato

    |empty|compressed sorted|uncompressed sorted|

  • Eseguire una decompressione-ricompressione in streaming sulla sezione compressa, unendo i dati ordinati nella sezione non compressa. La vecchia sezione compressa viene consumata man mano che aumenta la nuova sezione compressa. Sembra il buffer

    |compressed sorted|empty|

Questa procedura viene eseguita fino a quando tutti i numeri non sono stati ordinati.

Compressione

Questo algoritmo ovviamente funziona solo quando è possibile calcolare la dimensione compressa finale del nuovo buffer di ordinamento prima di sapere effettivamente cosa verrà effettivamente compresso. Accanto a questo, l'algoritmo di compressione deve essere abbastanza buono per risolvere il problema reale.

L'approccio utilizzato prevede tre passaggi. Innanzitutto, l'algoritmo memorizzerà sempre le sequenze ordinate, quindi possiamo invece memorizzare puramente le differenze tra voci consecutive. Ogni differenza è nell'intervallo [0, 99999999].

Queste differenze vengono quindi codificate come flusso di bit unario. Un 1 in questo flusso significa "Aggiungi 1 all'accumulatore, A 0 significa" Emetti l'accumulatore come una voce e ripristina ". Quindi la differenza N sarà rappresentata da N 1 e uno 0.

La somma di tutte le differenze si avvicinerà al valore massimo supportato dall'algoritmo e il conteggio di tutte le differenze si avvicinerà alla quantità di valori inseriti nell'algoritmo. Ciò significa che ci aspettiamo che lo stream contenga, alla fine, un valore massimo di 1 e conteggi 0. Questo ci consente di calcolare la probabilità attesa di uno 0 e 1 nello stream. Vale a dire, la probabilità di uno 0 è count/(count+maxval)e la probabilità di un 1 èmaxval/(count+maxval) .

Usiamo queste probabilità per definire un modello di codifica aritmetica su questo flusso di bit. Questo codice aritmetico codificherà esattamente questa quantità di 1 e 0 nello spazio ottimale. Siamo in grado di calcolare lo spazio utilizzato da questo modello per ogni flusso di bit intermedi come: bits = encoded * log2(1 + amount / maxval) + maxval * log2(1 + maxval / amount). Per calcolare lo spazio totale richiesto per l'algoritmo, impostare encodeduguale a quantità.

Per non richiedere una quantità ridicola di iterazioni, è possibile aggiungere un piccolo overhead al buffer. Ciò garantirà che l'algoritmo opererà almeno sulla quantità di numeri che rientrano in questo sovraccarico, poiché il costo di gran lunga maggiore dell'algoritmo è la compressione e la decompressione della codifica aritmetica ogni ciclo.

Accanto a ciò, è necessario un certo sovraccarico per archiviare i dati di contabilità e gestire lievi inesattezze nell'approssimazione a virgola fissa dell'algoritmo di codifica aritmetica, ma in totale l'algoritmo è in grado di adattarsi a 1 MiB di spazio anche con un buffer aggiuntivo che può contenere 8000 numeri, per un totale di 1043916 byte di spazio.

ottimalità

Al di fuori della riduzione del (piccolo) sovraccarico dell'algoritmo dovrebbe essere teoricamente impossibile ottenere un risultato più piccolo. Per contenere solo l'entropia del risultato finale, sarebbero necessari 1011717 byte. Se sottraggiamo il buffer aggiuntivo aggiunto per efficienza questo algoritmo utilizza 1011916 byte per memorizzare il risultato finale + spese generali.


2

Se il flusso di input potesse essere ricevuto alcune volte, ciò sarebbe molto più semplice (nessuna informazione al riguardo, idea e problema di prestazioni nel tempo).

Quindi, potremmo contare i valori decimali. Con i valori conteggiati sarebbe facile creare il flusso di output. Comprimi contando i valori. Dipende da cosa sarebbe nel flusso di input.


1

Se il flusso di input potesse essere ricevuto alcune volte, ciò sarebbe molto più semplice (nessuna informazione al riguardo, idea e problema di prestazioni nel tempo). Quindi, potremmo contare i valori decimali. Con i valori conteggiati sarebbe facile creare il flusso di output. Comprimi contando i valori. Dipende da cosa sarebbe nel flusso di input.


1

L'ordinamento è un problema secondario qui. Come altri hanno detto, solo memorizzare gli interi è difficile e non può funzionare su tutti gli input , poiché sarebbero necessari 27 bit per numero.

La mia opinione su questo è: memorizzare solo le differenze tra gli interi consecutivi (ordinati), poiché molto probabilmente saranno piccoli. Quindi utilizzare uno schema di compressione, ad esempio con 2 bit aggiuntivi per numero di ingresso, per codificare su quanti bit è memorizzato il numero. Qualcosa di simile a:

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

Dovrebbe essere possibile memorizzare un discreto numero di possibili liste di input all'interno del vincolo di memoria dato. La matematica su come scegliere lo schema di compressione per farlo funzionare sul numero massimo di input, è al di là di me.

Spero che tu possa essere in grado di sfruttare la conoscenza specifica del dominio dei tuoi input per trovare uno schema di compressione intero abbastanza buono basato su questo.

Oh e poi, fai un ordinamento di inserzione su quell'elenco ordinato mentre ricevi i dati.


1

Ora punta a una soluzione reale, che copra tutti i possibili casi di input nell'intervallo di 8 cifre con solo 1 MB di RAM. NOTA: lavori in corso, domani continuerà. Utilizzando la codifica aritmetica dei delta degli ints ordinati, il caso peggiore per ints ordinati 1M costerebbe circa 7 bit per voce (poiché 99999999/1000000 è 99 e log2 (99) è quasi 7 bit).

Ma hai bisogno degli interi da 1m ordinati per arrivare a 7 o 8 bit! Le serie più brevi avrebbero delta più grandi, quindi più bit per elemento.

Sto lavorando per prenderne il maggior numero possibile e comprimere (quasi) sul posto. Il primo lotto di quasi 250 K di ints richiederebbe al massimo circa 9 bit ciascuno. Quindi il risultato richiederebbe circa 275 KB. Ripetere l'operazione con la memoria rimanente libera alcune volte. Quindi decomprimi-unisci-sul-posto-comprimi quei pezzi compressi. Questo è abbastanza difficile , ma possibile. Penso.

Le liste unite si avvicinerebbero sempre più al target a 7 bit per intero. Ma non so quante iterazioni ci vorrebbe del ciclo di unione. Forse 3.

Ma l'imprecisione dell'implementazione della codifica aritmetica potrebbe renderlo impossibile. Se questo problema fosse possibile, sarebbe estremamente stretto.

Qualche volontario?


La codifica aritmetica è praticabile. Potrebbe essere utile notare che ogni delta successivo è tratto da una distribuzione binomiale negativa.
affollamento il

1

Devi solo memorizzare le differenze tra i numeri in sequenza e utilizzare una codifica per comprimere questi numeri di sequenza. Abbiamo 2 ^ 23 bit. Lo divideremo in blocchi a 6 bit e lasciamo che l'ultimo bit indichi se il numero si estende ad altri 6 bit (5 bit più il blocco di estensione).

Pertanto, 000010 è 1 e 000100 è 2. 000001100000 è 128. Ora, consideriamo il cast peggiore nel rappresentare differenze nella sequenza di un numero fino a 10.000.000. Ci possono essere 10.000.000 / 2 ^ 5 differenze maggiori di 2 ^ 5, 10.000.000 / 2 ^ 10 differenze maggiori di 2 ^ 10 e 10.000.000 / 2 ^ 15 differenze maggiori di 2 ^ 15, ecc.

Quindi, aggiungiamo quanti bit ci vorranno per rappresentare la nostra sequenza. Abbiamo 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 = 7.935.479.

2 ^ 24 = 8388608. Poiché 8388608> 7935479, dovremmo avere abbastanza memoria. Probabilmente avremo bisogno di un altro po 'di memoria per memorizzare la somma di dove sono quando inseriamo nuovi numeri. Analizziamo quindi la sequenza e scopriamo dove inserire il nostro nuovo numero, se necessario diminuiamo la differenza successiva e spostiamo tutto dopo.


Credo che la mia analisi qui mostra che questo schema non funziona (e non può nemmeno se scegliamo una dimensione diversa da cinque bit).
Daniel Wagner,

@Daniel Wagner - Non è necessario utilizzare un numero uniforme di bit per blocco e non è nemmeno necessario utilizzare un numero intero di bit per blocco.
affollamento il

@crowding Se hai una proposta concreta, mi piacerebbe ascoltarla. =)
Daniel Wagner,

@crowding Fai i calcoli su quanto spazio occuperebbe la codifica aritmetica. Piangi un po Quindi pensa più intensamente.
Daniel Wagner,

Per saperne di più. Una distribuzione condizionale completa di simboli nella giusta rappresentazione intermedia (Francisco ha la rappresentazione intermedia più semplice, così come Strilanc) è facile da calcolare. Pertanto, il modello di codifica può essere letteralmente perfetto e può rientrare in un bit del limite entropico. L'aritmetica di precisione finita potrebbe aggiungere alcuni bit.
affollamento il

1

Se non sappiamo nulla di questi numeri, siamo limitati dai seguenti vincoli:

  • dobbiamo caricare tutti i numeri prima di poterli ordinare,
  • l'insieme dei numeri non è comprimibile.

Se queste ipotesi valgono, non c'è modo di svolgere il tuo compito, poiché avrai bisogno di almeno 26.575.425 bit di memoria (3.321.929 byte).

Cosa puoi dirci dei tuoi dati?


1
Li leggi e li ordina mentre procedi. Richiede teoricamente bit lg2 (100999999! / (99999999! * 1000000!)) Per memorizzare oggetti indistinguibili 1M in scatole distinte da 100M, che raggiungono il 96,4% di 1 MB.
NovaDenizen,

1

Il trucco è rappresentare lo stato degli algoritmi, che è un set multiplo intero, come flusso compresso di "increment counter" = "+" e "output counter" = "!" personaggi. Ad esempio, l'insieme {0,3,3,4} verrebbe rappresentato come "! +++ !! +!", Seguito da un numero qualsiasi di caratteri "+". Per modificare il set multiplo, esegui lo streaming dei caratteri, mantenendo decompresso solo una quantità costante alla volta, e apporta le modifiche sul posto prima di eseguirne lo streaming in forma compressa.

Dettagli

Sappiamo che ci sono esattamente 10 ^ 6 numeri nel set finale, quindi ci sono al massimo 10 ^ 6 "!" personaggi. Sappiamo anche che la nostra gamma ha dimensioni 10 ^ 8, il che significa che ci sono al massimo 10 ^ 8 "+" caratteri. Il numero di modi in cui possiamo sistemare 10 ^ 6 "!" Tra 10 ^ 8 "+" s è (10^8 + 10^6) choose 10^6, e quindi specificare un accordo particolare richiede ~ 0,965 MiB `di dati. Sarà perfetto.

Possiamo considerare ogni personaggio come indipendente senza superare la nostra quota. Ci sono esattamente 100 volte più caratteri "+" rispetto a "!" personaggi, il che semplifica le probabilità 100: 1 di ogni personaggio essendo un "+" se dimentichiamo che sono dipendenti. Le probabilità di 100: 101 corrispondono a ~ 0,08 bit per carattere , per un totale quasi identico di ~ 0,965 MiB (ignorare la dipendenza ha un costo di soli ~ 12 bit in questo caso!).

La tecnica più semplice per memorizzare caratteri indipendenti con probabilità nota prima è la codifica di Huffman . Nota che abbiamo bisogno di un albero poco grande (un albero di Huffman per blocchi di 10 caratteri ha un costo medio per blocco di circa 2,4 bit, per un totale di ~ 2,9 Mib. Un albero di Huffman per blocchi di 20 caratteri ha un costo medio per blocco di circa 3 bit, per un totale di ~ 1,8 MiB. Probabilmente avremo bisogno di un blocco di dimensioni nell'ordine di un centinaio, implicando più nodi nel nostro albero di quanti siano in grado di memorizzare tutte le apparecchiature informatiche che siano mai esistite. ). Tuttavia, la ROM è tecnicamente "libera" in base al problema e le soluzioni pratiche che sfruttano la regolarità nella struttura appariranno sostanzialmente le stesse.

Pseudo-codice

  • Avere un albero huffman sufficientemente grande (o simili dati di compressione blocco per blocco) memorizzati nella ROM
  • Inizia con una stringa compressa di 10 ^ 8 caratteri "+".
  • Per inserire il numero N, eseguire lo stream della stringa compressa fino a quando non sono passati i caratteri N "+", quindi inserire un "!". Riporta in streaming la stringa ricompressa su quella precedente mentre procedi, mantenendo una quantità costante di blocchi bufferizzati per evitare sovraccarichi / sottoesecuzioni.
  • Ripeti un milione di volte: [input, stream decompress> insert> compress], quindi decomprimilo in output

1
Finora, questa è l'unica risposta che vedo che in realtà risponde al problema! Penso che la codifica aritmetica sia più semplice della codifica di Huffman, poiché ovvia alla necessità di archiviare un libro di codici e preoccuparsi dei confini dei simboli. Puoi anche rendere conto della dipendenza.
affollamento il

Gli interi di input NON sono ordinati. Devi prima ordinare.
alecco,

1
@alecco L'algoritmo li ordina man mano che procede. Non vengono mai archiviati non ordinati.
Craig Gidney,

1

Abbiamo 1 MB - 3 KB RAM = 2 ^ 23 - 3 * 2 ^ 13 bit = 8388608 - 24576 = 8364032 bit disponibili.

Ci vengono dati 10 ^ 6 numeri in un intervallo di 10 ^ 8. Questo dà uno spazio medio di ~ 100 <2 ^ 7 = 128

Consideriamo innanzitutto il problema più semplice di numeri equidistanti quando tutti gli spazi sono <128. Questo è facile. Basta memorizzare il primo numero e gli spazi a 7 bit:

(27 bit) + 10 ^ 6 Numero di gap a 7 bit = 7000027 bit richiesti

Nota i numeri ripetuti hanno spazi vuoti di 0.

Ma cosa succede se abbiamo spazi più grandi di 127?

OK, supponiamo che una dimensione di gap <127 sia rappresentata direttamente, ma una dimensione di gap di 127 sia seguita da una codifica continua a 8 bit per la lunghezza effettiva del gap:

 10xxxxxx xxxxxxxx                       = 127 .. 16,383
 110xxxxx xxxxxxxx xxxxxxxx              = 16384 .. 2,097,151

eccetera.

Nota che questa rappresentazione numerica descrive la sua lunghezza, quindi sappiamo quando inizia il prossimo numero di gap.

Con solo piccoli spazi <127, ciò richiede ancora 7000027 bit.

Possono esserci fino a (10 ^ 8) / (2 ^ 7) = 781250 numero di gap a 23 bit, che richiede un ulteriore 16 * 781.250 = 12.500.000 bit che è troppo. Abbiamo bisogno di una rappresentazione più compatta e lentamente crescente delle lacune.

La dimensione media del gap è 100, quindi se li riordiniamo come [100, 99, 101, 98, 102, ..., 2, 198, 1, 199, 0, 200, 201, 202, ...] e indicizziamo questo con una densa codifica binaria di base Fibonacci senza coppie di zeri (ad esempio, 11011 = 8 + 5 + 2 + 1 = 16) con numeri delimitati da '00', quindi penso che possiamo mantenere la rappresentazione del gap abbastanza breve, ma ha bisogno più analisi.


0

Durante la ricezione del flusso, procedere come segue.

1 ° impostare una dimensione ragionevole del pezzo

Idea di codice pseudo:

  1. Il primo passo sarebbe quello di trovare tutti i duplicati e incollarli in un dizionario con il suo conteggio e rimuoverli.
  2. Il terzo passo sarebbe quello di posizionare il numero esistente in sequenza dei loro passi algoritmici e posizionarli in contatori dizionari speciali con il primo numero e il loro passo come n, n + 1 ..., n + 2, 2n, 2n + 1, 2n + 2 ...
  3. Inizia a comprimere in blocchi alcuni intervalli di numeri ragionevoli come ogni 1000 o mai 10000 i numeri rimanenti che appaiono meno spesso da ripetere.
  4. Non comprimere quell'intervallo se viene trovato un numero e aggiungerlo all'intervallo e lasciarlo non compresso per un po 'più a lungo.
  5. Altrimenti aggiungi quel numero a un byte [chunkSize]

Continua i primi 4 passaggi mentre ricevi lo stream. Il passaggio finale sarebbe quello di fallire se hai superato la memoria o iniziare a produrre il risultato una volta raccolti tutti i dati iniziando a ordinare gli intervalli e sputando i risultati in ordine e decomprimendo quelli in ordine che devono essere decompressi e ordinarli quando ci arrivi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.