web-dev-qa-db-fra.com

Tri de 1 million de nombres à 8 chiffres dans 1 Mo de RAM

J'ai un ordinateur avec 1 Mo de RAM et aucun autre stockage local. Je dois l'utiliser pour accepter 1 million de nombres décimaux à 8 chiffres sur une connexion TCP, les trier, puis envoyer la liste triée via une autre connexion TCP.

La liste des numéros peut contenir des doublons, que je ne dois pas éliminer. Le code sera placé dans la ROM, je n'ai donc pas besoin de soustraire la taille de mon code du 1 Mo. J'ai déjà du code pour piloter le port Ethernet et gérer les connexions TCP/IP. Il nécessite 2 Ko pour ses données d'état, y compris un tampon de 1 Ko via lequel le code lit et écrit les données. Y at-il une solution à ce problème?

Sources de questions et réponses:

slashdot.org

cleaton.net

636

Une solution n'est possible qu'en raison de la différence entre 1 mégaoctet et 1 million d'octets. Il y a environ 2 façons différentes de choisir 1 million de numéros à 8 chiffres avec duplicata autorisés et ordre sans importance, de sorte qu'un ordinateur avec seulement 1 million d'octets de RAM ne dispose pas d'assez d'états pour tout représenter les possibilités. Mais 1M (moins 2k pour TCP/IP) est 1022 * 1024 * 8 = 8372224 bits, une solution est donc possible.

Partie 1, solution initiale

Cette approche nécessite un peu plus de 1M, je vais l'affiner pour l'adapter à 1M plus tard.

Je vais stocker une liste compacte de numéros triés dans la plage allant de 0 à 99999999 en tant que séquence de sous-listes de nombres à 7 bits. La première sous-liste contient les numéros de 0 à 127, la deuxième sous-liste, les numéros de 128 à 255, etc. 100000000/128 correspond exactement à 781250, il en faudra donc 781250.

Chaque sous-liste comprend un en-tête de sous-liste de 2 bits suivi d'un corps de sous-liste. Le corps de sous-liste occupe 7 bits par entrée de sous-liste. Les sous-listes sont toutes concaténées ensemble et le format permet de dire où une sous-liste se termine et la suivante commence. La mémoire totale requise pour une liste complètement remplie est 2 * 781250 + 7 * 1000000 = 8562500 bits, ce qui correspond à environ 1,021 M-octets.

Les 4 valeurs possibles pour l'en-tête de sous-liste sont:

00 Sous-liste vide, rien ne suit.

01 Singleton, il n'y a qu'une entrée dans la sous-liste et les 7 bits suivants la conservent.

10 La sous-liste contient au moins 2 nombres distincts. Les entrées sont stockées dans un ordre non décroissant, sauf que la dernière entrée est inférieure ou égale à la première. Cela permet d'identifier la fin de la sous-liste. Par exemple, les nombres 2,4,6 seraient stockés sous la forme (4,6,2). Les nombres 2,2,3,4,4 seraient stockés sous la forme (2,3,4,4,2).

11 La sous-liste contient au moins deux répétitions d'un seul numéro. Les 7 bits suivants donnent le nombre. Viennent ensuite zéro ou plusieurs entrées de 7 bits avec la valeur 1, suivies d'une entrée de 7 bits avec la valeur 0. La longueur du corps de sous-liste dicte le nombre de répétitions. Par exemple, les numéros 12,12 seraient stockés sous la forme (12,0), les numéros 12,12,12 seraient stockés sous la forme (12,1,0), 12,12,12,12 seraient (12,1 , 1,0) et ainsi de suite.

Je commence avec une liste vide, lit plusieurs nombres et les stocke sous forme d'entiers 32 bits, trie les nouveaux nombres (en utilisant probablement heapsort), puis les fusionne dans une nouvelle liste triée compacte. Répétez l'opération jusqu'à ce qu'il n'y ait plus de chiffres à lire, puis parcourez une nouvelle fois la liste compacte pour générer le résultat.

La ligne ci-dessous représente la mémoire juste avant le début de l'opération de fusion de liste. Les "O" sont la région qui contient les entiers 32 bits triés. Les "X" sont la région qui contient l'ancienne liste compacte. Les signes "=" représentent la salle d’extension de la liste compacte, soit 7 bits pour chaque entier des "O". Les "Z" sont d'autres frais généraux aléatoires.

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

La routine de fusion commence à lire à l'extrême gauche "O" et à l'extrême gauche "X", et commence à écrire à l'extrême gauche "=". Le pointeur d'écriture n'accroche pas le pointeur de lecture de liste compacte tant que tous les nouveaux entiers ne sont pas fusionnés, car les deux pointeurs avancent de 2 bits pour chaque sous-liste et de 7 bits pour chaque entrée de l'ancienne liste compacte. Entrées 7 bits pour les nouveaux numéros.

Partie 2, en la bourrant dans 1M

Pour compresser la solution ci-dessus dans 1M, je dois rendre le format de liste compact un peu plus compact. Je vais supprimer l'un des types de sous-listes afin qu'il n'y ait que trois valeurs possibles pour les en-têtes de sous-listes. Ensuite, je peux utiliser "00", "01" et "1" comme valeurs d'en-tête de sous-liste et enregistrer quelques bits. Les types de sous-listes sont:

Une sous-liste vide, rien ne suit.

B Singleton, il n'y a qu'une seule entrée dans la sous-liste et les 7 bits suivants la conservent.

C La sous-liste contient au moins 2 numéros distincts. Les entrées sont stockées dans un ordre non décroissant, sauf que la dernière entrée est inférieure ou égale à la première. Cela permet d'identifier la fin de la sous-liste. Par exemple, les nombres 2,4,6 seraient stockés sous la forme (4,6,2). Les nombres 2,2,3,4,4 seraient stockés sous la forme (2,3,4,4,2).

D La sous-liste est composée de 2 répétitions ou plus d'un même numéro.

Mes 3 valeurs d'en-tête de sous-liste seront "A", "B" et "C", j'ai donc besoin d'un moyen de représenter les sous-listes de type D.

Supposons que j'ai l'en-tête de sous-liste de type C suivi de 3 entrées, tel que "C [17] [101] [58]". Cela ne peut pas faire partie d'une sous-liste valide de type C, comme décrit ci-dessus, car la troisième entrée est inférieure à la seconde mais supérieure à la première. Je peux utiliser ce type de construction pour représenter une sous-liste de type D. En termes de bits, n'importe où j'ai "C {00 ?????} {1 ??????} {01 ?????}" est une sous-liste de type C impossible. J'utiliserai ceci pour représenter une sous-liste composée de 3 répétitions ou plus d'un même numéro. Les deux premiers mots de 7 bits codent le nombre (les "N" bits ci-dessous) et sont suivis de zéro ou plus de {0100001} mots suivis d'un {0100000} mot.

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

Cela ne laisse que les listes qui contiennent exactement 2 répétitions d'un seul numéro. Je représenterai ceux avec un autre motif de sous-liste impossible de type C: "C {0 ??????} {11 ?????} {10 ?????}". Il y a beaucoup de place pour les 7 bits du nombre dans les 2 premiers mots, mais ce modèle est plus long que la sous-liste qu'il représente, ce qui rend les choses un peu plus complexes. Les cinq points d'interrogation à la fin peuvent être considérés comme ne faisant pas partie du motif. J'ai donc: "C {0NNNNNN} {11N ????} 10" comme motif, avec le numéro à répéter enregistré dans le "N "s. C'est 2 bits de trop.

Je vais devoir emprunter 2 bits et les rembourser à partir des 4 bits inutilisés de ce modèle. Lors de la lecture, en rencontrant "C {0NNNNNN} {11N00AB} 10", affichez 2 occurrences du nombre dans les "N", écrasez le "10" à la fin avec les bits A et B, et rembobinez le pointeur de lecture de 2 morceaux. Les lectures destructives sont acceptables pour cet algorithme, car chaque liste compacte est parcourue une seule fois.

Lors de l'écriture d'une sous-liste de 2 répétitions d'un seul numéro, écrivez "C {0NNNNNN} 11N00" et réglez le compteur de bits empruntés sur 2. À chaque écriture où le compteur de bits empruntés est non nul, il est décrémenté pour chaque bit écrit "10" est écrit lorsque le compteur atteint zéro. Ainsi, les 2 prochains bits écrits iront aux emplacements A et B, puis le "10" sera abandonné à la fin.

Avec 3 valeurs d'en-tête de sous-liste représentées par "00", "01" et "1", je peux affecter "1" au type de sous-liste le plus populaire. J'aurai besoin d'une petite table pour mapper les valeurs d'en-tête de sous-liste sur les types de sous-liste, ainsi que d'un compteur d'occurrences pour chaque type de sous-liste afin de connaître le meilleur mappage d'en-tête de sous-liste.

Dans le pire des cas, la représentation minimale d'une liste compacte entièrement remplie se produit lorsque tous les types de sous-listes sont également populaires. Dans ce cas, je sauvegarde 1 bit pour chaque 3 en-têtes de sous-liste. La taille de la liste est donc 2 * 781250 + 7 * 1000000 - 781250/3 = 8302083,3 bits. Arrondi à une limite de mot de 32 bits, 8302112 bits ou 1037764 octets.

1M moins le 2k pour l'état TCP/IP et les tampons est de 1022 * 1024 = 1046528 octets, me laissant ainsi 8764 octets à jouer.

Mais qu'en est-il du processus de modification du mappage d'en-tête de sous-liste? Dans la mappe de mémoire ci-dessous, "Z" représente une surcharge aléatoire, "=" représente un espace libre, "X" une liste compacte.

ZZZ=====XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Commencez à lire le "X" le plus à gauche et commencez à écrire le "=" le plus à gauche et travaillez à droite. Quand cela sera fait, la liste compacte sera un peu plus courte et sera mal placée:

ZZZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=======

Alors, je vais devoir le déplacer vers la droite:

ZZZ=======XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Dans le processus de changement de mappage d'en-tête, jusqu'à 1/3 des en-têtes de sous-liste passera de 1 bit à 2 bits. Dans le pire des cas, ils se trouveront tous en tête de la liste. Il me faudra donc au moins 781250/3 bits de mémoire libre avant de commencer, ce qui me ramène aux besoins en mémoire de la version précédente de la liste compacte: (

Pour résoudre ce problème, je vais scinder les sous-listes 781250 en 10 groupes de 78125 sous-listes chacun. Chaque groupe a son propre mappage d'en-tête de sous-liste indépendant. En utilisant les lettres A à J pour les groupes:

ZZZ=====AAAAAABBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ

Chaque groupe de sous-listes est réduit ou reste identique lors d'un changement de mappage d'en-tête de sous-liste:

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

Dans le pire des cas, l’extension temporaire d’un groupe de sous-listes lors d’un changement de mappage est de 78125/3 = 26042 bits, sous 4k. Si j'autorise 4k plus les 1037764 octets pour une liste compacte entièrement remplie, cela me laisse 8764 - 4096 = 4668 octets pour les "Z" de la mappe de mémoire.

Cela devrait suffire pour les 10 tables de mappage d'en-tête de sous-liste, le nombre d'occurrences d'en-tête de sous-liste et les quelques autres compteurs, pointeurs et petits tampons dont j'ai besoin, et l'espace que j'ai utilisé sans le remarquer, comme l'espace de pile pour les adresses de renvoi d'appel de fonction et variables locales.

Partie 3, combien de temps cela prend-il pour s'exécuter?

Avec une liste compacte vide, l'en-tête de liste à 1 bit sera utilisé pour une sous-liste vide et la taille de départ de la liste sera de 781250 bits. Dans le pire des cas, la liste augmente de 8 bits pour chaque nombre ajouté. Il faut donc 32 + 8 = 40 bits d'espace libre pour que chacun des nombres de 32 bits soit placé en haut de la mémoire tampon de la liste, puis trié et fusionné. Dans le pire des cas, la modification du mappage d'en-tête de sous-liste entraîne une utilisation d'espace de 2 * 781250 + 7 * entrées - 781250/3 bits.

Avec une politique de modification du mappage d'en-tête de sous-liste après chaque fusion, une fois qu'il y a au moins 800 000 numéros dans la liste, une exécution dans le pire des cas impliquerait un total d'environ 30 millions d'activités de lecture et d'écriture de liste compactes.

Source:

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

169

Il y a un tour plutôt sournois qui n'a pas encore été mentionné ici. Nous supposons que vous n'avez aucun moyen supplémentaire de stocker des données, mais ce n'est pas strictement vrai.

Une solution à votre problème consiste à effectuer l’horrible problème suivant, que personne, sous aucune circonstance, ne devrait toutefois tenter: Utilisez le trafic réseau pour stocker des données. Et non, je ne parle pas de NAS.

Vous pouvez trier les nombres avec seulement quelques octets de RAM de la manière suivante:

  • Prenez d’abord deux variables: COUNTER et VALUE.
  • Commencez par définir tous les registres sur 0;
  • Chaque fois que vous recevez un entier I, incrémentez COUNTER et définissez VALUE sur max(VALUE, I);
  • Envoyez ensuite un paquet de demande d'écho ICMP avec l'ensemble de données au routeur I. Effacer I et répéter.
  • Chaque fois que vous recevez le paquet ICMP renvoyé, il vous suffit d'extraire l'entier et de le renvoyer dans une autre demande d'écho. Cela produit un grand nombre de demandes ICMP qui vont et viennent en arrière et qui contiennent les entiers.

Une fois que COUNTER atteint 1000000, vous avez toutes les valeurs stockées dans le flux incessant de demandes ICMP et VALUE contient maintenant le nombre entier maximal. Choisissez quelques threshold T >> 1000000. Réglez COUNTER à zéro. Chaque fois que vous recevez un paquet ICMP, incrémentez COUNTER et renvoyez l'entier contenu I dans une autre requête d'écho, sauf si I=VALUE, auquel cas transmettez-le à la destination des entiers triés. Une fois que COUNTER=T, décrémentez VALUE de 1, réinitialisez COUNTER à zéro et répétez l'opération. Une fois que VALUE atteint zéro, vous devez avoir transmis tous les entiers, du plus grand au plus petit jusqu'à la destination, et n’avoir utilisé que 47 bits environ de RAM pour les deux variables persistantes (et le plus petit besoin des valeurs temporaires).

Je sais que c'est horrible, et je sais qu'il peut y avoir toutes sortes de problèmes pratiques, mais j'ai pensé que cela pourrait faire rire certains ou au moins vous horreur.

427
Joe Fitzsimons

Voici du code de travail C++ qui résout le problème.

Preuve que les contraintes de mémoire sont satisfaites:

Éditeur: Il n’existe aucune preuve des exigences maximales de mémoire offertes par l’auteur, ni dans ce billet ni dans ses blogs. Puisque le nombre de bits nécessaires pour coder une valeur dépend des valeurs précédemment codées, cette preuve est probablement non triviale. L'auteur note que la plus grande taille encodée sur laquelle il pouvait tomber empiriquement était 1011732, et a choisi la taille du tampon 1013000 de manière arbitraire.

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

    ...

Ensemble, ces deux baies prennent 1045 000 octets de stockage. Cela laisse 1048576 - 1045000 - 2 × 1024 = 1528 octets pour les variables restantes et l'espace de pile.

Il tourne en 23 secondes environ sur mon Xeon W3520. Vous pouvez vérifier que le programme fonctionne à l'aide du script Python suivant, en prenant pour nom de programme 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!')

Vous trouverez une explication détaillée de l’algorithme dans les séries de publications suivantes:

409
preshing

S'il vous plaît voir le première réponse correcte ou la dernière réponse avec encodage arithmétique . Ci-dessous vous pouvez trouver amusant, mais pas une solution à 100%.

C'est une tâche assez intéressante et voici une autre solution. J'espère que quelqu'un trouvera le résultat utile (ou au moins intéressant).

Étape 1: structure de données initiale, approche de compression approximative, résultats de base

Faisons quelques calculs simples: nous avons 1 M (1048576 octets) de RAM initialement disponible pour stocker 10 ^ 6 nombres décimaux à 8 chiffres. [0; 99999999]. Donc, pour stocker un numéro, il faut 27 bits (en supposant que des nombres non signés seront utilisés). Ainsi, pour stocker un flux brut, il vous faudra environ 3,5 M de RAM. Quelqu'un a déjà dit que cela ne semblait pas faisable, mais je dirais que la tâche peut être résolue si l'entrée est "assez bonne". L'idée est essentiellement de compresser les données d'entrée avec un facteur de compression de 0,29 ou plus et de procéder au tri de manière appropriée.

Commençons par résoudre le problème de la compression. Certains tests pertinents sont déjà disponibles:

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

"J'ai effectué un test pour compresser un million d'entiers consécutifs en utilisant différentes formes de compression. Les résultats sont les suivants:"

None     4000027
Deflate  2006803
Filtered 1391833
BZip2    427067
Lzma     255040

Il semble que LZMA ( algorithme de la chaîne de Lempel – Ziv – Markov ) est un bon choix pour continuer. J'ai préparé un PoC simple, mais il reste quelques détails à mettre en évidence:

  1. La mémoire étant limitée, l'idée est de trier les numéros et d'utiliser des compartiments compressés (taille dynamique) comme stockage temporaire.
  2. Il est plus facile d'obtenir un meilleur facteur de compression avec des données triées au préalable. Il existe donc un tampon statique pour chaque compartiment (les numéros du tampon doivent être triés avant le LZMA).
  3. Chaque compartiment contient une plage spécifique. Le tri final peut donc être effectué séparément pour chaque compartiment.
  4. La taille du compartiment peut être correctement définie, afin qu'il y ait suffisamment de mémoire pour décompresser les données stockées et effectuer le tri final pour chaque compartiment séparément.

In-memory sorting

Veuillez noter que le code joint est un POC , il ne peut pas être utilisé comme solution finale. Il montre simplement l'idée d'utiliser plusieurs tampons plus petits pour stocker nombres pré-triés de manière optimale (éventuellement compressés). LZMA n'est pas proposé comme solution finale. Il est utilisé comme moyen le plus rapide possible d’introduire une compression dans cette PoC .

Voir le code PoC ci-dessous (veuillez noter qu'il ne s'agit que d'une démo, pour le compiler LZMA-Java sera nécessaire):

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

Avec des nombres aléatoires, il produit les éléments suivants:

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

Pour une séquence ascendante simple (un seau est utilisé), il produit:

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

EDIT

Conclusion:

  1. N'essayez pas de tromper le Nature
  2. Utilisez une compression plus simple avec une empreinte mémoire réduite
  3. Quelques indices supplémentaires sont vraiment nécessaires. Une solution commune à l’épreuve des balles ne semble pas réalisable.

Étape 2: compression améliorée, conclusion finale

Comme cela a déjà été mentionné dans la section précédente, toute technique de compression appropriée peut être utilisée. Supprimons donc LZMA au profit d’une approche plus simple et meilleure (si possible). Il y a beaucoup de bonnes solutions, y compris Codage arithmétique , Arbre Radix etc.

Quoi qu'il en soit, un schéma de codage simple mais utile sera plus illustratif qu'une autre bibliothèque externe, fournissant un algorithme astucieux. La solution actuelle est assez simple: comme il existe des compartiments avec des données partiellement triées, des deltas peuvent être utilisés à la place des nombres.

encoding scheme

Le test d’entrée aléatoire donne des résultats légèrement meilleurs:

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

Exemple de code

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

S'il vous plaît noter, cette approche:

  1. ne consomme pas beaucoup de mémoire
  2. fonctionne avec des ruisseaux
  3. fournit pas si mauvais résultats

Le code complet peut être trouvé ici , les implémentations BinaryInput et BinaryOutput peuvent être trouvés ici

Conclusion finale

Pas de conclusion finale :) Parfois, il est vraiment judicieux de monter d'un niveau et de revoir la tâche d'un point de vue méta-nivea .

C'était amusant de passer du temps avec cette tâche. BTW, il y a beaucoup de réponses intéressantes ci-dessous. Merci pour votre attention et joyeux codding.

363
Renat Gilmanov

La réponse de Gilmanov est très fausse dans ses hypothèses. Il commence à spéculer sur la base d'une mesure inutile d'un million entiers consécutifs . Cela signifie pas de lacunes. Ces écarts aléatoires, aussi petits soient-ils, en font vraiment une mauvaise idée.

Essayez vous-même. Obtenez 1 million d'entiers aléatoires 27 bits, triez-les, compressez avec 7-Zip , xz, quel que soit le LZMA souhaité. Le résultat est supérieur à 1,5 Mo. La prémisse est la compression de nombres séquentiels. Même le codage delta de ce nombre est supérieur à 1,1 MB . Et qu’importe que cela utilise plus de 100 Mo de RAM pour la compression. Ainsi, même les nombres entiers compressés ne correspondent pas au problème et cela ne fait aucun doute que le temps d'exécution RAM usage .

Cela m'attriste de voir comment les gens ne font que remonter les beaux graphiques et la rationalisation.

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

}

Compressez maintenant ints.bin avec 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
55
alecco

Je pense qu’une façon de penser à cela est du point de vue de la combinatoire: combien de combinaisons possibles d’ordre des nombres triés existe-t-il? Si nous donnons à la combinaison 0,0,0, ...., 0 le code 0 et 0,0,0, ..., 1 le code 1, et 99999999, 99999999, ... 99999999 le code N, qu'est-ce que N? En d'autres termes, quelle est la taille de l'espace de résultat?

Eh bien, une façon de penser à cela est de noter que ceci est une bijection du problème de trouver le nombre de chemins monotones dans une grille N x M, où N = 1 000 000 et M = 100 000 000. En d’autres termes, si vous avez une grille de 1 000 000 de large et 100 000 000 de hauteur, combien y a-t-il de chemins les plus courts allant du bas à gauche au haut à droite? Bien sûr, les chemins les plus courts ne nécessitent que vous vous déplaciez vers la droite ou vers le haut (si vous aviez à descendre ou à gauche, vous annuleriez des progrès déjà accomplis). Pour voir en quoi cela constitue une bijection de notre problème de tri de numéros, observez les éléments suivants:

Vous pouvez imaginer n'importe quelle jambe horizontale sur notre chemin comme un nombre dans notre ordre, où l'emplacement Y de la jambe représente la valeur.

enter image description here

Ainsi, si le chemin se déplace simplement vers la droite et se termine vers la fin, il saute complètement vers le haut, ce qui équivaut à l'ordre 0,0,0, ..., 0. si au lieu de cela il commence par sauter jusqu'au sommet puis se déplace vers la droite 1 000 000 fois, ce qui équivaut à 99999999 99999999, ..., 99999999. Un chemin où il se déplace une fois, puis une fois, puis à droite , puis une fois, etc. jusqu’à la fin (puis saute nécessairement jusqu’au sommet), équivaut à 0,1,2,3, ..., 999999.

Heureusement pour nous ce problème a déjà été résolu, une telle grille a (N + M) Choisir les chemins (M):

(1 000 000 + 100 000 000) Choisissez (100 000 000) ~ = 2,27 * 10 ^ 2436455

N est donc égal à 2,27 * 10 ^ 2436455, et donc le code 0 représente 0,0,0, ..., 0 et le code 2,27 * 10 ^ 2436455 et certains changements représentent 99999999 99999999, ..., 99999999.

Afin de stocker tous les nombres de 0 à 2,27 * 10 ^ 2436455, vous avez besoin de lg2 (2,27 * 10 ^ 2436455) = 8,0937 * 10 ^ 6 bits.

1 mégaoctet = 8388608 bits> 8093700 bits

Il semble donc que nous ayons au moins assez d'espace pour stocker le résultat! Maintenant, bien sûr, le bit intéressant est le tri à mesure que les nombres entrent. Pas sûr que la meilleure approche à ce problème soit donnée, il reste 294908 bits. J'imagine qu'une technique intéressante serait de supposer à chaque étape qu'il s'agit de la commande complète, de la recherche du code correspondant à cette commande, puis de la remise d'un nouveau numéro et de la mise à jour du code précédent. Vague main vague main.

Mes suggestions ici doivent beaucoup à la solution de Dan

Tout d’abord, je suppose que la solution doit gérer toutes les listes de saisie possibles. Je pense que les réponses populaires ne font pas cette hypothèse (qui OMI est une énorme erreur).

On sait qu'aucune compression sans perte ne réduira la taille de toutes les entrées.

Toutes les réponses populaires supposent qu’ils seront capables d’appliquer une compression suffisamment efficace pour leur permettre un espace supplémentaire. En fait, un bloc d'espace supplémentaire suffisamment grand pour contenir une partie de leur liste partiellement complétée sous une forme non compressée et leur permettre d'effectuer leurs opérations de tri. Ceci est juste une mauvaise hypothèse.

Pour une telle solution, toute personne ayant une connaissance de la manière dont elle compresse sa compression sera capable de concevoir des données d’entrée qui ne se compressent pas bien pour ce schéma, et la "solution" cassera alors très probablement par manque d’espace.

Au lieu de cela, je prends une approche mathématique. Nos sorties possibles sont toutes les listes de longueur LEN constituées d'éléments de la plage 0..MAX. Ici, le LEN est de 1 000 000 et notre MAX de 100 000 000.

Pour LEN et MAX arbitraires, la quantité de bits nécessaire pour coder cet état est la suivante:

Log2 (MAX multichoose LEN)

Ainsi, pour nos nombres, une fois que nous aurons terminé la réception et le tri, nous aurons besoin d’au moins Log2 (100 000 000 MC 1 000 000) pour stocker notre résultat de manière à distinguer de manière unique toutes les sorties possibles.

Ceci est ~ = 988kb . Nous avons donc suffisamment d’espace pour conserver notre résultat. De ce point de vue, c'est possible.

[Supprimé décousu inutile maintenant que de meilleurs exemples existent ...]

Meilleure réponse est ici .

Une autre bonne réponse est ici et utilise essentiellement le tri par insertion en tant que fonction pour développer la liste d'un élément (en tamponnant quelques éléments et en pré-tri, pour permettre l'insertion de plusieurs éléments à la fois, enregistre une peu de temps). utilise également un encodage d'état compact et agréable, des seaux de deltas de sept bits

20
davec

Supposons que cette tâche est possible. Juste avant la sortie, il y aura une représentation en mémoire du million de nombres triés. Combien y a-t-il de telles représentations? Comme il peut y avoir des nombres répétés, nous ne pouvons pas utiliser nCr (choisir), mais il existe une opération appelée multichoose qui fonctionne sur multisets .

  • Il y a 2.2e2436455 façons de choisir un million de nombres dans la plage 0..99.999.999.
  • Cela nécessite 8.093.7 bits pour représenter toutes les combinaisons possibles, soit 1.011.717 octets.

Donc, théoriquement, cela peut être possible si vous pouvez obtenir une représentation saine (suffisante) de la liste triée de nombres. Par exemple, une représentation démente peut nécessiter une table de consultation de 10 Mo ou des milliers de lignes de code.

Cependant, si "1M de RAM" signifie un million d'octets, alors il est clair qu'il n'y a pas assez d'espace. Le fait que 5% de mémoire en plus le rend théoriquement possible me suggère que la représentation devra être TRES efficace et probablement pas saine d'esprit.

18
Dan

(Ma réponse initiale était fausse, désolée pour les mauvaises calculs, voir ci-dessous la pause.)

Que dis-tu de ça?

Les 27 premiers bits stockent le nombre le plus bas que vous avez vu, puis la différence par rapport au nombre suivant, codés comme suit: 5 bits pour stocker le nombre de bits utilisés pour stocker la différence, puis la différence. Utilisez 00000 pour indiquer que vous avez encore vu ce numéro.

Cela fonctionne parce que plus vous insérez de nombres, plus la différence moyenne entre les nombres diminue. Vous utilisez donc moins de bits pour stocker la différence lorsque vous ajoutez plus de nombres. Je crois que cela s'appelle une liste delta.

Le pire des cas auquel je puisse penser est que tous les nombres sont espacés de manière égale (par 100), par exemple. En supposant que 0 soit le premier numéro:

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

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

Reddit à la rescousse!

Si tout ce que vous deviez faire était de les trier, ce problème serait facile. Il faut 122k (1 million de bits) pour stocker les numéros que vous avez vus (0ème bit activé si 0 visible, 2300ème bit activé si 2300 visible, etc.).

Vous lisez les nombres, les stockez dans le champ de bits, puis décalez les bits tout en conservant un compte.

MAIS, vous devez vous rappeler combien vous en avez vu. La réponse de la sous-liste ci-dessus m'a inspiré pour proposer ce schéma:

Au lieu d'utiliser un bit, utilisez 2 ou 27 bits:

  • 00 signifie que vous n'avez pas vu le numéro.
  • 01 signifie que vous l'avez vu une fois
  • 1 signifie que vous l'avez vu et les 26 bits suivants représentent le nombre de fois.

Je pense que cela fonctionne: s'il n'y a pas de doublons, vous avez une liste de 244k. Dans le pire des cas, vous voyez chaque numéro deux fois (si vous voyez un numéro trois fois, le reste de la liste est raccourci), cela signifie que vous en avez vu 50 000 plus d'une fois et que vous avez vu 950 000 éléments 0 ou 1 fois.

50 000 * 27 + 950 000 * 2 = 396,7k.

Vous pouvez apporter d'autres améliorations si vous utilisez le codage suivant:

0 signifie que vous n'avez pas vu le chiffre 10 signifie que vous l'avez vu une fois 11, c'est ainsi que vous tenez compte

Ce qui, en moyenne, donnera 280,7k de stockage.

EDIT: mon calcul de dimanche matin était faux.

Le pire des cas, c’est que nous voyons deux fois 500 000 nombres. Le calcul est donc le suivant:

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

L’encodage alterné a pour résultat un stockage moyen de

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

: (

12
jfernand

Il existe une solution à ce problème parmi toutes les entrées possibles. Tricher.

  1. Lire les valeurs m sur TCP, où m est proche du maximum pouvant être trié en mémoire, peut-être n/4.
  2. Trier les 250 000 (ou plus) nombres et les sortir.
  3. Répétez l'opération pour les 3 autres trimestres.
  4. Laissez le destinataire fusionner les 4 listes de numéros qu’il a reçus lorsqu’il les traite. (Ce n'est pas beaucoup plus lent que d'utiliser une seule liste.)
10
xpda

Quel type d'ordinateur utilisez-vous? Il ne dispose peut-être d'aucun autre stockage local "normal", mais dispose-t-il d'une mémoire vidéo, par exemple? 1 mégapixel x 32 bits par pixel (disons) est assez proche de la taille de saisie de données requise.

(Je demande en grande partie à la mémoire de l'ancien Acorn RISC PC , qui pourrait "emprunter" de la mémoire VRAM pour étendre la RAM système disponible, si vous choisissez un mode d'écran basse résolution ou à faible profondeur de couleur!). Cela était plutôt utile sur une machine ne disposant que de quelques Mo de RAM normale.

7
DNA

Je voudrais essayer un Radix Tree . Si vous pouviez stocker les données dans un arbre, vous pourriez alors effectuer un cheminement dans l'ordre pour transmettre les données.

Je ne suis pas sûr que vous puissiez intégrer cela à 1 Mo, mais je pense que cela vaut la peine d'essayer.

7
Alex Chamberlain

Une représentation par arborescence de bases serait presque en mesure de résoudre ce problème, car elle utilise la "compression de préfixe". Mais il est difficile de concevoir une représentation radix pouvant représenter un seul nœud sur un octet - deux correspond probablement à la limite.

Mais, quelle que soit la manière dont les données sont représentées, une fois triées, elles peuvent être stockées sous une forme préfixée-comprimée, où les chiffres 10, 11 et 12 seraient représentés par, par exemple, 001b, 001b, 001b, indiquant un incrément de 1. du numéro précédent. 10101b représenterait alors peut-être une augmentation de 5, 1101001b une augmentation de 9, etc.

6
Hot Licks

Il y a 10 ^ 6 valeurs dans une plage de 10 ^ 8, il y a donc une valeur pour cent points de code en moyenne. Mémorise la distance entre le Nième et le (N + 1) ème. Les valeurs en double ont un saut de 0. Cela signifie que le saut a besoin d'une moyenne d'un peu moins de 7 bits à stocker, donc un million d'entre elles s'intégreront avec bonheur dans nos 8 millions de bits de stockage.

Ces sauts doivent être codés dans un train de bits, par exemple par le codage de Huffman. L'insertion consiste à parcourir le flux binaire et à réécrire après la nouvelle valeur. Sortie en parcourant et en écrivant les valeurs impliquées. Pour des raisons pratiques, il est probablement souhaitable de définir 10 ^ 4 listes couvrant chacune 10 ^ 4 points de code (et une moyenne de 100 valeurs).

Un bon arbre de Huffman pour des données aléatoires peut être construit a priori en supposant une distribution de Poisson (moyenne = variance = 100) sur la longueur des sauts, mais des statistiques réelles peuvent être conservées en entrée et utilisées pour générer un arbre optimal à traiter. cas pathologiques.

6
Russ Williams

J'ai un ordinateur avec 1 M de RAM et aucun autre stockage local

Un autre moyen de tricher: vous pouvez utiliser un stockage non local (en réseau) (votre question ne l'exclut pas) et appeler un service en réseau pouvant utiliser un fusionnement simple sur disque (ou juste assez de RAM pour trier en mémoire, puisque vous devez seulement accepter les nombres 1M), sans avoir besoin des solutions (certes extrêmement ingénieuses) déjà données.

Il s’agit peut-être de tricherie, mais il n’est pas clair si vous cherchez une solution à un problème réel, ou un casse-tête qui invite à contourner les règles ... Si ces dernières sont contraires, une simple fraude peut donner de meilleurs résultats qu’un complexe mais "véritable" solution (qui, comme d'autres l'ont souligné, ne peut fonctionner que pour des entrées compressibles).

5
DNA

Je pense que la solution consiste à combiner des techniques d’encodage vidéo, à savoir la transformation en cosinus discrète. En vidéo numérique, enregistrer plutôt le changement de luminosité ou de couleur de la vidéo sous forme de valeurs normales telles que 110 112 115 116, chacune d’elles étant soustraite de la dernière (similaire au codage par longueur). 110 112 115 116 devient 110 2 3 1. Les valeurs, 2 3 1 nécessitent moins de bits que les originaux.

Disons donc que nous créons une liste des valeurs d'entrée au fur et à mesure qu'elles arrivent sur le socket. Nous stockons dans chaque élément, pas la valeur, mais le décalage de celui qui le précède. Nous trions au fur et à mesure, les compensations ne seront que positives. Mais le décalage pourrait être large de 8 chiffres décimaux, ce qui correspond à 3 octets. Chaque élément ne peut pas être 3 octets, nous devons donc les emballer. Nous pourrions utiliser le bit le plus haut de chaque octet comme "bit de suite", indiquant que l'octet suivant fait partie du nombre et que les 7 bits inférieurs de chaque octet doivent être combinés. zéro est valable pour les doublons.

Au fur et à mesure que la liste se remplit, les nombres doivent être rapprochés, ce qui signifie qu'en moyenne, un octet seulement est utilisé pour déterminer la distance jusqu'à la valeur suivante. 7 bits de valeur et 1 bit de décalage si cela vous convient, mais il peut exister un point idéal nécessitant moins de 8 bits pour une valeur "continue".

En tout cas, j'ai fait des expériences. J'utilise un générateur de nombres aléatoires et je peux insérer un million de nombres décimaux à 8 chiffres triés dans environ 1279 000 octets. L'espace moyen entre chaque nombre est toujours 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);
    }
}
5
slipperyseal

Google (mauvaise) approche, du fil HN. Stockez les comptes à la RLE.

Votre structure de données initiale est '99999999: 0' (tous les zéros, vous n'avez pas encore vu de chiffres), puis supposons que vous voyez le nombre 3 866 344, votre structure de données devient alors '3866343: 0,1: 1,96133654: 0'. Vous pouvez voir que les nombres alternent toujours entre le nombre de bits zéro et le nombre de bits '1'. Vous pouvez donc supposer que les nombres impairs représentent 0 bits et les nombres pairs 1 bits. Cela devient (3866343,1,96133654)

Leur problème ne semble pas couvrir les doublons, mais disons qu'ils utilisent "0: 1" pour les doublons.

Gros problème n ° 1: les insertions pour les entiers 1M prendraient des âges .

Problème majeur n ° 2: comme toutes les solutions de codage plain delta, certaines distributions ne peuvent pas être couvertes de cette façon. Par exemple, des nombres entiers de 1 m avec des distances de 0:99 (par exemple +99 chacun). Maintenant, pensez la même chose mais avec distance aléatoire dans le plage de 0:99. (Remarque: 99999999/1000000 = 99.99)

L'approche de Google est à la fois indigne (lente) et incorrecte. Mais pour leur défense, leur problème aurait pu être légèrement différent.

4
alecco

Nous pourrions jouer avec la pile de mise en réseau pour envoyer les numéros dans un ordre trié avant d’avoir tous les numéros. Si vous envoyez 1 M de données, TCP/IP les divisera en paquets de 1500 octets et les transmettra en continu à la cible. Un numéro de séquence sera attribué à chaque paquet.

Nous pouvons le faire à la main. Juste avant de remplir notre RAM, nous pouvons trier ce que nous avons et envoyer la liste à notre cible, tout en laissant des trous dans notre séquence autour de chaque nombre. Ensuite, traitez la 2ème moitié des nombres de la même manière en utilisant ces trous de la séquence.

La pile de mise en réseau à l'extrémité distante assemblera le flux de données résultant en ordre de séquence avant de le transmettre à l'application.

Il utilise le réseau pour effectuer un tri par fusion. C'est un piratage total, mais j'ai été inspiré par l'autre piratage de réseau mentionné précédemment.

4
Kevin Marquette

Pour représenter le tableau trié, il suffit de stocker le premier élément et la différence entre les éléments adjacents. De cette façon, nous nous intéressons au codage de 10 ^ 6 éléments pouvant résumer au plus 10 ^ 8. Appelons cela D. Pour encoder les éléments de D, on peut utiliser un code de Huffman . Le dictionnaire du code de Huffman peut être créé à tout moment et le tableau mis à jour chaque fois qu'un nouvel élément est inséré dans le tableau trié (tri par insertion). Notez que lorsque le dictionnaire change en raison d'un nouvel élément, l'ensemble du tableau doit être mis à jour pour correspondre au nouvel encodage.

Le nombre moyen de bits pour coder chaque élément de D est maximisé si nous avons un nombre égal de chaque élément unique. Dire des éléments d1, d2, ..., dN dans D chacun apparaît F fois. Dans ce cas (dans le pire des cas nous avons 0 et 10 ^ 8 en séquence d’entrée) nous avons

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

somme (1 <= i <= N) F = 10 ^ 6, ou F= 10 ^ 6/N et la fréquence normalisée sera p= F/10 ^ = 1/N

Le nombre moyen de bits sera -log2 (1/P) = log2 (N). Dans ces circonstances, nous devrions trouver un cas qui maximise N. Cela se produit si nous avons des numéros consécutifs pour di à partir de 0, ou di= i - 1, donc

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

c'est à dire.

N <= 201. Et dans ce cas, le nombre moyen de bits est log2 (201) = 7,6511, ce qui signifie que nous aurons besoin d'environ 1 octet par élément d'entrée pour enregistrer le tableau trié. Notez que cela ne signifie pas D en général ne peut pas avoir plus de 201 éléments. Cela sème simplement que si les éléments de D sont uniformément répartis, il ne peut pas avoir plus de 201 valeurs uniques.

3
Mohsen Nosratinia

J'exploiterais le comportement de retransmission de TCP.

  1. Faites en sorte que le composant TCP crée une grande fenêtre de réception.
  2. Recevez une certaine quantité de paquets sans envoyer d’accusé de réception pour eux.
    • Traiter les passes en créant une structure de données compressée (préfixe)
    • Envoyez un accusé de réception en double pour le dernier paquet qui n'est plus nécessaire/attendez le délai de retransmission
    • Goto 2
  3. Tous les paquets ont été acceptés

Cela suppose une sorte d’avantage des compartiments ou des passes multiples.

Probablement en triant les lots/seaux et en les fusionnant. -> arbres radix

Utilisez cette technique pour accepter et trier les 80% premiers, puis lire les 20% restants et vérifier que les 20% restants ne contiennent pas de nombres pouvant atterrir dans les 20% premiers des nombres les plus bas. Envoyez ensuite les 20% des numéros les plus bas, supprimez-les de la mémoire, acceptez les 20% restants des nouveaux numéros et fusionnez. **

3
sleeplessnerd

Si le flux d'entrée pouvait être reçu plusieurs fois, cela serait beaucoup plus facile (aucune information à ce sujet, problème d'idée et de performance temporelle).

Ensuite, nous pourrions compter les valeurs décimales. Avec les valeurs comptées, il serait facile de créer le flux de sortie. Compressez en comptant les valeurs. Cela dépend de ce qu'il y aurait dans le flux d'entrée.

2
Baronth

Si le flux d'entrée pouvait être reçu plusieurs fois, cela serait beaucoup plus facile (pas d'informations à ce sujet, problème d'idée et de performances temporelles). Ensuite, nous pourrions compter les valeurs décimales. Avec les valeurs comptées, il serait facile de créer le flux de sortie. Compressez en comptant les valeurs. Cela dépend de ce qu'il y aurait dans le flux d'entrée.

1
pbies

Il vous suffit de stocker les différences entre les numéros les uns après les autres et d'utiliser un codage pour compresser ces numéros. Nous avons 2 ^ 23 bits. Nous allons le diviser en morceaux de 6 bits et laisser le dernier bit indiquer si le nombre s'étend sur 6 bits supplémentaires (5 bits plus le morceau en extension).

Ainsi, 000010 vaut 1 et 000100 vaut 2. 000001100000 vaut 128. Nous considérons maintenant la pire conversion en représentant les différences dans la séquence d'un nombre allant jusqu'à 10 000 000. Il peut y avoir 10 000 000/2 ^ 5 différences supérieures à 2 ^ 5, 10 000 000/2 ^ 10 différences supérieures à 2 ^ 10, et 10 000 000/2 ^ 15 différences supérieures à 2 ^ 15, etc.

Donc, nous ajoutons combien de bits il faudra pour représenter notre séquence. Nous avons 1 000 000 * 6 + arrondis (10 000 000/2 ^ 5) * 6 + arrondis (10 000 000/2 ^ 10) * 6 + arrondis (10 000 000/2 ^ 15) * 6 + arrondis (10 000 000/2 ^ 20) * 4 = 7935479.

2 ^ 24 = 8388608. Depuis 8388608> 7935479, nous devrions facilement avoir assez de mémoire. Nous aurons probablement besoin d'un peu plus de mémoire pour stocker la somme des emplacements où nous insérons de nouveaux nombres. Nous parcourons ensuite la séquence et trouvons où insérer notre nouveau numéro, diminuons la différence suivante si nécessaire et décalons tout après.

1
gersh

Nous avons 1 Mo - 3 Ko RAM = 2 ^ 23 - 3 * 2 ^ 13 bits = 8388608 - 24576 = 8364032 bits disponibles.

On nous donne 10 ^ 6 numéros dans une plage de 10 ^ 8. Cela donne un écart moyen de ~ 100 <2 ^ 7 = 128

Considérons tout d’abord le problème plus simple des nombres espacés de manière à peu près égale lorsque tous les espaces sont <128. C’est facile. Enregistrez simplement le premier nombre et les espaces de 7 bits:

(27 bits) + 10 ^ 6 numéros d'intervalle de 7 bits = 7000027 bits requis

Notez que les nombres répétés ont des espaces de 0.

Mais que se passe-t-il si nous avons des écarts supérieurs à 127?

OK, supposons qu'une taille d'espace <127 soit représentée directement, mais une taille d'espace de 127 est suivie d'un codage continu de 8 bits pour la longueur réelle de l'espace:

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

etc.

Notez que cette représentation numérique décrit sa propre longueur afin que nous sachions quand le prochain numéro d'intervalle commence.

Avec seulement de petits espaces <127, cela nécessite toujours 7000027 bits.

Il peut y avoir jusqu’à (10 ^ 8)/(2 ^ 7) = 781250 Numéro d’intervalle de 23 bits, ce qui nécessite 16 * 781,250 supplémentaires = 12 500 000 bits, ce qui est trop. Nous avons besoin d’une représentation plus compacte et en augmentation lente des lacunes.

La taille moyenne de l’écart est de 100, donc si nous les réordonnons comme suit: [100, 99, 101, 98, 102, ..., 2, 198, 1, 199, 0, 200, 201, 202, ...] et indexons cette avec un codage de base de Fibonacci binaire dense sans paires de zéros (par exemple, 11011 = 8 + 5 + 2 + 1 = 16) avec des nombres délimités par '00', je pense que nous pouvons garder la représentation de l'écart suffisamment courte, mais elle a besoin plus d'analyse.

1
Toby Kelsey

Le tri est un problème secondaire ici. Comme dit plus haut, il est difficile de stocker les entiers, et ne peut pas fonctionner sur toutes les entrées, car 27 bits par nombre seraient nécessaires.

Mon point de vue est le suivant: stockez uniquement les différences entre les entiers consécutifs (triés), car ils seront vraisemblablement petits. Ensuite, utilisez un schéma de compression, par exemple avec 2 bits supplémentaires par numéro d'entrée, pour coder le nombre de bits sur lesquels le nombre est stocké. Quelque chose comme:

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

Il devrait être possible de stocker un nombre suffisant de listes d'entrées possibles dans une contrainte de mémoire donnée. Les maths sur la manière de choisir le schéma de compression pour le faire fonctionner sur le nombre maximal d'entrées me dépassent.

J'espère que vous pourrez peut-être exploiter connaissance du domaine de votre entrée pour trouver un assez bon schéma de compression entier basé sur ceci.

Oh, et ensuite, vous effectuez un tri par insertion sur cette liste triée lorsque vous recevez des données.

1
Eldritch Conundrum

Si nous ne savons rien de ces chiffres, nous sommes limités par les contraintes suivantes:

  • nous devons charger tous les nombres avant de pouvoir les trier,
  • l'ensemble des nombres n'est pas compressible.

Si ces hypothèses sont vérifiées, il n’ya aucun moyen de mener à bien votre tâche, car vous aurez besoin d’au moins 26 575 425 bits de stockage (3 321 929 octets).

Que pouvez-vous nous dire sur vos données?

1
Yves Daoust

Nous cherchons maintenant une solution concrète couvrant tous les cas possibles d’entrée dans la plage des 8 chiffres avec seulement 1 Mo de RAM. NOTE: travaux en cours, demain continuera. Si vous utilisez le codage arithmétique des deltas des entrées triées, le cas le plus défavorable pour une entrée triée coûterait environ 7 bits par entrée (puisque 99999999/1000000 vaut 99 et log2 (99) presque 7 bits).

Mais vous avez besoin des entiers de 1 m triés pour obtenir 7 ou 8 bits! Les séries plus courtes auraient des deltas plus grands, donc plus de bits par élément.

Je travaille à en prendre le plus possible et à compresser (presque) sur place. Le premier lot de près de 250 000 pouces nécessiterait au mieux environ 9 bits chacun. Donc, le résultat prend environ 275KB. Répétez l'opération avec la mémoire libre restante à quelques reprises. Ensuite, décompressez, fusionnez sur place, comprimez ces fragments compressés. C'est assez difficile, mais possible. Je pense.

Les listes fusionnées se rapprocheraient de plus en plus de la cible de 7 bits par entier. Mais je ne sais pas combien d'itérations il faudrait de la boucle de fusion. Peut-être 3.

Mais l'imprécision de la mise en œuvre du codage arithmétique pourrait le rendre impossible. Si ce problème est possible, il serait extrêmement difficile.

Des volontaires?

1
alecco

L'astuce consiste à représenter l'état de l'algorithme, qui est un entier multiple, en tant que flux compressé de "compteur d'incrément" = "+" et de "compteur de sortie" = "!" personnages. Par exemple, l'ensemble {0,3,3,4} serait représenté par "! +++ !! +!", Suivi d'un nombre quelconque de caractères "+". Pour modifier le multi-jeu, transmettez les caractères en streaming, en ne conservant qu'une quantité constante décompressée à la fois, et apportez les modifications avant de les rediffuser sous forme compressée.

Détails

Nous savons qu'il y a exactement 10 ^ 6 numéros dans le dernier set, donc il y a au plus 10 ^ 6 "!" personnages. Nous savons également que notre plage a une taille de 10 ^ 8, ce qui signifie qu’il ya au plus 10 ^ 8 caractères "+". Le nombre de façons dont nous pouvons organiser 10 ^ 6 "!" S sur 10 ^ 8 "+" s est (10^8 + 10^6) choose 10^6, et donc spécifier un arrangement particulier prend ~ 0,965 Mio `de données. Ce sera un ajustement serré.

Nous pouvons traiter chaque personnage comme indépendant sans dépasser notre quota. Il y a exactement 100 fois plus de "+" caractères que de "!" caractères, ce qui simplifie à 100: 1 chances de chaque caractère étant un "+" si nous oublions qu'ils sont dépendants. Une cote de 100: 101 correspond à ~ 0,08 bits par caractère , pour un total presque identique de ~ 0,965 Mio (ignorer la dépendance a un coût de seulement ~ 12 bits dans ce cas!).

La technique la plus simple pour stocker des caractères indépendants avec une probabilité antérieure connue est codage de Huffman . Notez que nous avons besoin d’un arbre trop grand (un arbre de huffman pour des blocs de 10 caractères a un coût moyen par bloc d’environ 2,4 bits, pour un total de ~ 2,9 Mib. Un arbre d’huffman pour des blocs de 20 caractères a un coût moyen par bloc. d'environ 3 bits, ce qui représente un total d'environ 1,8 Mo. Nous aurons probablement besoin d'un bloc d'une taille de l'ordre de cent, ce qui implique plus de nœuds dans notre arborescence que tout l'équipement informatique existant peut en stocker. ) Cependant, ROM est techniquement "libre" en fonction du problème et les solutions pratiques qui tirent parti de la régularité de l'arbre se ressemblent essentiellement.

Pseudo-code

  • Avoir un arbre de Huffman suffisamment grand (ou des données de compression bloc par bloc similaires) stocké dans la ROM
  • Commencez avec une chaîne compressée de 10 ^ 8 "+" caractères.
  • Pour insérer le nombre N, transmettez la chaîne compressée en continu jusqu'à ce que N "+" caractères soient dépassés, puis insérez un "!". Diffusez la chaîne recomprimée sur la précédente au fur et à mesure, en conservant un nombre constant de blocs mis en mémoire tampon pour éviter les sur/sous-exécutions.
  • Répéter un million de fois: [entrée, décompression de flux> insérer> compresser], puis décompresser en sortie
1
Craig Gidney

Pendant la réception du flux, suivez ces étapes.

1er set une taille de morceau raisonnable

Idée de pseudo-code:

  1. La première étape serait de trouver tous les doublons et de les coller dans un dictionnaire avec son nombre et de les supprimer.
  2. La troisième étape consisterait à placer les numéros existants dans l’ordre de leurs étapes algorithmiques et à les placer dans des compteurs avec des dictionnaires spéciaux avec le premier nombre et leur étape comme n, n + 1 ..., n + 2, 2n, 2n + 1, 2n + 2 ...
  3. Commencez à compresser en morceaux des plages de nombres raisonnables, comme tous les 1000 ou tous les 10 000, les nombres restants qui semblent moins répéter.
  4. Décompressez cette plage si un nombre est trouvé, ajoutez-le à la plage et laissez-le décompressé un peu plus longtemps.
  5. Sinon, il suffit d'ajouter ce numéro à un octet [chunkSize]

Continuez les 4 premières étapes tout en recevant le flux. La dernière étape consisterait à échouer si vous avez dépassé la mémoire ou à générer le résultat une fois que toutes les données ont été collectées. Pour ce faire, commencez à trier les plages et à les décomposer dans l’ordre, puis décompressez celles qui doivent être décompressées. vous arrivez à eux.

0
RetroCoder