web-dev-qa-db-ja.com

1 MBの中に100万の8桁の数字をソートする RAM

1 MBのRAMを搭載したコンピューターがあり、他にローカルストレージがありません。 TCP接続で100万個の8桁の10進数を受け入れ、それらをソートしてから、ソートされたリストを別のTCP接続で送信する必要があります。

数字のリストには重複が含まれている可能性がありますが、それを破棄してはいけません。コードはROMに格納されるので、1 MBからコードのサイズを差し引く必要はありません。イーサネットポートを駆動し、TCP/IP接続を処理するコードをすでに持っています。コードがデータを読み書きするための1 KBバッファを含め、状態データ用に2 KBが必要です。この問題に対する解決策はありますか?

質問と回答の情報源:

slashdot.org

cleaton.net

634

解決策は1メガバイトと100万バイトの間の違いのためにだけ可能です。重複を許可して重要ではない順序で100万個の8桁の数字を選択するには約2のべき乗があります。したがって、100万バイトのRAMしかないマシンには、すべてを表すのに十分な状態がありません可能性しかし、1M(TCP/IPの場合は2k以下)は1022 * 1024 * 8 = 8372224ビットなので、解決策はあります。

パート1、初期解

このアプローチには1M以上のビットが必要です。後で1Mに収まるように調整します。

私は0から99999999までの範囲の数のコンパクトなソートされたリストを7ビット数のサブリストのシーケンスとして格納します。最初のサブリストには0から127までの数字が、2番目のサブリストには128から255までの数字などが入っています。100000000/128は正確に781250なので、781250のようなサブリストが必要になります。

各サブリストは、2ビットのサブリストヘッダーとそれに続くサブリスト本体で構成されています。サブリスト本体は、サブリストエントリごとに7ビットを占めます。サブリストはすべて一緒に連結されており、そのフォーマットによって、1つのサブリストがどこで終わり、次のサブリストがどこで始まるかを知ることができます。完全に実装されたリストに必要な合計ストレージは2 * 781250 + 7 * 1000000 = 8562500ビットで、これは約1.021 Mバイトです。

4つの可能なサブリストヘッダー値は以下のとおりです。

00 空のサブリスト。何も続かない。

01 シングルトン。サブリストにはエントリが1つしかなく、次の7ビットでそれが保持されます。

10 サブリストは少なくとも2つの異なる数を保持します。最後のエントリが最初のエントリ以下であることを除いて、エントリは降順で格納されます。これにより、サブリストの終わりを識別できます。たとえば、数2,4,6は(4,6,2)として格納されます。数2,2,3,4,4は(2,3,4,4,2)として格納されます。

11 サブリストには、1つの数値を2回以上繰り返します。次の7ビットは数を与えます。次に、値1の0個以上の7ビット項目、その後に値0の7ビット項目が続きます。サブリスト本体の長さによって、繰り返し回数が決まります。たとえば、12、12という数字は(12、0)として格納され、12、12、12という数字は(12、1、0)として格納され、12、12、12、12は(12、1)となります。 、1,0)等々。

私は空のリストから始めて、たくさんの数を読んで32ビット整数としてそれらを保存し、(おそらくヒープソートを使って)その場で新しい数をソートし、それからそれらを新しいコンパクトなソート済みリストにマージします。読み込む数がなくなるまで繰り返してから、もう一度コンパクトリストをたどって出力を生成します。

以下の行は、リストマージ操作の開始直前のメモリを表しています。 "O"は、ソートされた32ビット整数を保持する領域です。 "X"は古いコンパクトリストを保持する領域です。 "="記号はコンパクトリスト用の拡張スペースで、 "O"の各整数に7ビットです。 "Z"は他のランダムなオーバーヘッドです。

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

マージルーチンは、左端の "O"と左端の "X"から読み取りを開始し、左端の "="から書き込みを開始します。書き込みポインタは、新しい整数のすべてがマージされるまでコンパクトリストの読み込みポインタをキャッチしません。両方のポインタが、サブリストごとに2ビット、古いコンパクトリスト内のエントリごとに7ビット進むためです。新しい番号の7ビットエントリ.

パート2、1Mにまとめる

上記のソリューションを1Mに絞り込むには、コンパクトリスト形式をもう少しコンパクトにする必要があります。サブリストタイプの1つを取り除きますので、3つの異なるサブリストヘッダー値が可能になります。次に、サブリストのヘッダー値として "00"、 "01"、 "1"を使用して、数ビット節約することができます。サブリストの種類は次のとおりです。

空のサブリストです。

Bシングルトン。サブリストにはエントリが1つしかなく、次の7ビットがそれを保持します。

Cサブリストには少なくとも2つの異なる番号があります。最後のエントリが最初のエントリ以下であることを除いて、エントリは降順で格納されます。これにより、サブリストの終わりを識別できます。たとえば、数2,4,6は(4,6,2)として格納されます。数2,2,3,4,4は(2,3,4,4,2)として格納されます。

Dサブリストは、1つの数字を2回以上繰り返したものです。

私の3つのサブリストのヘッダ値は "A"、 "B"、 "C"になるので、Dタイプのサブリストを表現する方法が必要です。

「C [17] [101] [58]」のように、Cタイプのサブリストヘッダーとそれに続く3つのエントリがあるとします。 3番目のエントリは2番目のエントリより小さいが最初のエントリより多いため、これは上で説明した有効なCタイプのサブリストの一部にはできません。このタイプの構成要素を使用してDタイプのサブリストを表すことができます。ちなみに、 "C {00 ?????} {1 ??????} {01 ?????}"があるところは、不可能なCタイプのサブリストです。これを使用して、単一の数値を3回以上繰り返したサブリストを表します。最初の2つの7ビットワードは数をエンコードし(以下の "N"ビット)、0個以上の{0100001}ワードが続き、その後に{0100000}ワードが続きます。

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

それはただ一つの数のちょうど2回の繰り返しを保持するリストを残すだけです。私はそれらを別の不可能なCタイプのサブリストパターンで表現するつもりです: "C {0 ??????} {11 ?????} {10 ?????}"。最初の2ワードに7ビットの数字を入れる余地は十分にありますが、このパターンはそれが表すサブリストより長く、事態が少し複雑になります。末尾の5つの疑問符はパターンの一部ではないと見なすことができるので、私のパターンは "C {0NNNNNN} {11N ????} 10"で、繰り返し数を "N"に格納します。 "これは2ビット長すぎます。

私は2ビットを借りて、このパターンの4つの未使用ビットからそれらを返済する必要があります。読み出し時に "C {0NNNNNN} {11N00AB} 10"が発生した場合は、 "N"内の2つのインスタンスを出力し、最後に "10"をビットAとBで上書きし、読み出しポインタを2だけ戻しますビット各コンパクトリストは一度だけ調べられるので、このアルゴリズムでは破壊的な読み込みは問題ありません。

1つの数を2回繰り返すサブリストを書き込むときは、 "C {0NNNNN} 11N00"を書き込み、借用ビットカウンタを2に設定します。カウンタがゼロになると "10"が書き込まれます。したがって、書き込まれた次の2ビットはスロットAとBに入り、その後「10」が最後にドロップされます。

"00"、 "01"、 "1"で表される3つのサブリストヘッダー値で、最も人気のあるサブリストタイプに "1"を割り当てることができます。サブリストのヘッダー値をサブリストの種類にマップするための小さなテーブルが必要になります。また、サブリストのヘッダーの最適なマッピングが何であるかがわかるように、サブリストの種類ごとに出現回数カウンターが必要になります。

完全に入力されたコンパクトリストの最悪の場合の最小限の表現は、すべてのサブリストタイプが同じくらい人気がある場合に発生します。その場合、3つのサブリストヘッダーごとに1ビットを保存するので、リストサイズは2 * 781250 + 7 * 1000000 - 781250/3 = 8302083.3ビットになります。 32ビットのワード境界、つまり8302112ビット、つまり1037764バイトに切り上げます。

1MからTCP/IP状態とバッファの2kを引いた値は1022 * 1024 = 1046528バイトなので、8764バイトを使用できます。

しかし、サブリストのヘッダーマッピングを変更するプロセスはどうでしょうか。以下のメモリマップでは、 "Z"はランダムなオーバーヘッド、 "="は空き領域、 "X"はコンパクトなリストです。

ZZZ=====XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

左端の "X"から読み始め、左端の "="から書き始め、正しく動作します。完了すると、コンパクトリストは少し短くなり、メモリの終わりになります。

ZZZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=======

それでは、それを右にシャントする必要があります。

ZZZ=======XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

ヘッダーマッピング変更プロセスでは、最大1/3のサブリストヘッダーが1ビットから2ビットに変更されます。最悪の場合、これらすべてがリストの先頭になるので、開始する前に少なくとも781250/3ビットの空きストレージが必要になります。これにより、以前のバージョンのコンパクトリストのメモリ要件に戻ります。 (

これを回避するために、781250のサブリストをそれぞれ78125のサブリストの10のサブリストグループに分割します。各グループには、独自の独立したサブリストヘッダーマッピングがあります。グループにAからJの文字を使用します。

ZZZ=====AAAAAABBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ

各サブリストグループは、サブリストヘッダーマッピングの変更中は縮小または変化しません。

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

マッピング変更中のサブリストグループの最悪の場合の一時的な拡張は、4K未満で78125/3 = 26042ビットです。完全に読み込まれたコンパクトリストに4kと1037764バイトを許可すると、メモリマップの "Z"に8764 - 4096 = 4668バイトが残ります。

これは、10個のサブリストヘッダーマッピングテーブル、30個のサブリストヘッダーオカレンスカウント、その他必要な数のカウンター、ポインター、小さなバッファー、そして関数呼び出しのリターンアドレスやスタックスペースのような気づかずに使ったスペースに十分です。ローカル変数.

パート3、実行にはどれくらいかかりますか?

空のコンパクトリストでは、1ビットのリストヘッダーが空のサブリストに使用され、リストの開始サイズは781250ビットになります。最悪の場合、リストは追加された各番号ごとに8ビットずつ増加するため、32ビット番号のそれぞれをリストバッファの先頭に配置して並べ替えてマージするには、32 + 8 = 40ビットの空き容量が必要です。最悪の場合、サブリストのヘッダーマッピングを変更すると、2 * 781250 + 7 *エントリ - 781250/3ビットのスペース使用量が発生します。

リストに少なくとも800000個の数字があると、5回のマージごとにサブリストのヘッダーマッピングを変更するというポリシーでは、最悪の場合、合計約30Mのリストの読み書きアクティビティが実行されます。

出典:

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

169

ここでは触れていない、やや卑劣なトリックがあります。データを保存するための特別な方法はないと想定していますが、それは厳密には当てはまりません。

あなたの問題を回避する一つの方法は、どんな状況下でも誰かが試みるべきではない、以下の恐ろしいことをすることです:データを保存するためにネットワークトラフィックを使ってください。いいえ、NASという意味ではありません。

次のようにして、数バイトのRAMだけで番号をソートできます。

  • 最初に2つの変数を取ります:COUNTERVALUE
  • まずすべてのレジスタを0に設定します。
  • 整数のIを受け取るたびに、COUNTERをインクリメントし、VALUEmax(VALUE, I)に設定します。
  • それからIに設定されるデータが付いているICMPエコー要求パケットをルータに送信して下さい。 Iを消去して繰り返します。
  • 返されたICMPパケットを受信するたびに、整数を抽出して別のエコー要求で再度送信するだけです。これは整数を含んで前後に散らばって膨大な数のICMP要求を生成します。

COUNTER1000000に達すると、すべての値がICMP要求の連続したストリームに格納され、VALUEに最大整数が入ります。いくつかのthreshold T >> 1000000を選んでください。 COUNTERをゼロに設定します。 ICMPパケットを受信するたびに、COUNTERをインクリメントし、含まれている整数Iを別のエコー要求で送信します。ただし、I=VALUEの場合は、ソートされた整数の宛先に送信します。 COUNTER=Tになったら、VALUE1だけデクリメントし、COUNTERをゼロにリセットして繰り返します。 VALUEがゼロになったら、すべての整数を最大値から最小値の順に転送し、2つの永続変数には約47ビットのRAMのみを使用します。値)。

私はこれが恐ろしいことを知っています、そしてあらゆる種類の実用的な問題があり得ることを私は知っています、しかし私はそれがあなたの何人かを笑わせるか少なくともあなたを恐れるかもしれないと思いました。

422
Joe Fitzsimons

これはいくつかの実用的なC++コードです これは問題を解決します。

メモリの制約が満たされていることの証明:

編集者: この記事でも彼のブログでも、作者によって提供される最大メモリ要件の証明はありません。値を符号化するのに必要なビット数は以前に符号化された値に依存するので、そのような証明はおそらく自明ではない。著者は、経験的に遭遇する可能性のある最大のエンコードサイズは1011732であり、バッファーサイズは任意に1013000を選択したと述べています。

typedef unsigned int u32;

namespace WorkArea
{
    static const u32 circularSize = 253250;
    u32 circular[circularSize] = { 0 };         // consumes 1013000 bytes

    static const u32 stageSize = 8000;
    u32 stage[stageSize];                       // consumes 32000 bytes

    ...

これら2つのアレイを合わせると、1045000バイトのストレージが必要です。残りの変数とスタックスペースのために、1048576 - 1045000 - 2×1024 = 1528バイトが残ります。

私のXeon W3520では約23秒で動作します。プログラム名がsort1mb.exeであれば、次のPythonスクリプトを使用してプログラムが機能することを確認できます。

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!')

アルゴリズムの詳細な説明は、以下の一連の記事にあります。

408
preshing

最初の正解 または 算術符号化による後の答え を参照してください。 以下では、楽しいと思うかもしれませんが、100%防弾の解決策ではありません。

これは非常に興味深い作業であり、これは別の解決策です。誰かがその結果が役に立つ(あるいは少なくとも面白い)とわかることを願っています。

ステージ1:初期データ構造、大まかな圧縮方法、基本結果

簡単な計算をしてみましょう。最初は10 ^ 6 8桁の10進数を格納するための1M(1048576バイト)のRAMがあります。 [0; 99999999]。そのため、1つの数を格納するには27ビットが必要です(符号なしの数が使用されるという仮定のもと)。したがって、生のストリームを保存するには、約3.5MのRAMが必要になります。誰かがすでにそれが実行可能であるように思われないと言いました、しかし私は入力が「十分に良い」であるならばタスクが解決されることができると言います。基本的には、入力データを0.29以上の圧縮率で圧縮し、適切な方法でソートを行うという考え方です。

まず圧縮の問題を解決しましょう。すでに利用可能ないくつかの関連テストがあります。

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

「さまざまな形式の圧縮を使用して、100万個の連続した整数を圧縮するテストを実行しました。結果は次のとおりです。」

None     4000027
Deflate  2006803
Filtered 1391833
BZip2    427067
Lzma     255040

LZMA( Lempel-Ziv-Markov連鎖アルゴリズム )は続行するのに適しています。私は簡単なPoCを用意しましたが、強調すべき詳細がまだいくつかあります。

  1. メモリが限られているので、アイデアは一時的な記憶域として数値を事前ソートして圧縮バケット(動的サイズ)を使用することです
  2. 事前ソートされたデータでより良い圧縮率を達成するのはより簡単です、従って各バケツのための静的バッファがあります(バッファからの数はLZMAの前にソートされることです)
  3. 各バケットは特定の範囲を保持しているため、最終的な並べ替えは各バケットに対して個別に実行できます。
  4. バケットのサイズは正しく設定できるので、保存されているデータを解凍して各バケットごとに別々に最終的なソートを行うのに十分なメモリがあります。

In-memory sorting

注意してください、添付のコードは POC です、それは最終的な解決策として使用することはできません。おそらく圧縮)。 LZMAは最終的な解決策として提案されていません。これは、このPoCに圧縮を導入するための最速の方法として使用されています。

下記のPoCコードを参照してください(コンパイルするには LZMA-Java が必要になるため、単なるデモに注意してください)。

public class MemorySortDemo {

static final int NUM_COUNT = 1000000;
static final int NUM_MAX   = 100000000;

static final int BUCKETS      = 5;
static final int DICT_SIZE    = 16 * 1024; // LZMA dictionary size
static final int BUCKET_SIZE  = 1024;
static final int BUFFER_SIZE  = 10 * 1024;
static final int BUCKET_RANGE = NUM_MAX / BUCKETS;

static class Producer {
    private Random random = new Random();
    public int produce() { return random.nextInt(NUM_MAX); }
}

static class Bucket {
    public int size, pointer;
    public int[] buffer = new int[BUFFER_SIZE];

    public ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
    public DataOutputStream tempDataOut = new DataOutputStream(tempOut);
    public ByteArrayOutputStream compressedOut = new ByteArrayOutputStream();

    public void submitBuffer() throws IOException {
        Arrays.sort(buffer, 0, pointer);

        for (int j = 0; j < pointer; j++) {
            tempDataOut.writeInt(buffer[j]);
            size++;
        }            
        pointer = 0;
    }

    public void write(int value) throws IOException {
        if (isBufferFull()) {
            submitBuffer();
        }
        buffer[pointer++] = value;
    }

    public boolean isBufferFull() {
        return pointer == BUFFER_SIZE;
    }

    public byte[] compressData() throws IOException {
        tempDataOut.close();
        return compress(tempOut.toByteArray());
    }        

    private byte[] compress(byte[] input) throws IOException {
        final BufferedInputStream in = new BufferedInputStream(new ByteArrayInputStream(input));
        final DataOutputStream out = new DataOutputStream(new BufferedOutputStream(compressedOut));

        final Encoder encoder = new Encoder();
        encoder.setEndMarkerMode(true);
        encoder.setNumFastBytes(0x20);
        encoder.setDictionarySize(DICT_SIZE);
        encoder.setMatchFinder(Encoder.EMatchFinderTypeBT4);

        ByteArrayOutputStream encoderPrperties = new ByteArrayOutputStream();
        encoder.writeCoderProperties(encoderPrperties);
        encoderPrperties.flush();
        encoderPrperties.close();

        encoder.code(in, out, -1, -1, null);
        out.flush();
        out.close();
        in.close();

        return encoderPrperties.toByteArray();
    }

    public int[] decompress(byte[] properties) throws IOException {
        InputStream in = new ByteArrayInputStream(compressedOut.toByteArray());
        ByteArrayOutputStream data = new ByteArrayOutputStream(10 * 1024);
        BufferedOutputStream out = new BufferedOutputStream(data);

        Decoder decoder = new Decoder();
        decoder.setDecoderProperties(properties);
        decoder.code(in, out, 4 * size);

        out.flush();
        out.close();
        in.close();

        DataInputStream input = new DataInputStream(new ByteArrayInputStream(data.toByteArray()));
        int[] array = new int[size];
        for (int k = 0; k < size; k++) {
            array[k] = input.readInt();
        }

        return array;
    }
}

static class Sorter {
    private Bucket[] bucket = new Bucket[BUCKETS];

    public void doSort(Producer p, Consumer c) throws IOException {

        for (int i = 0; i < bucket.length; i++) {  // allocate buckets
            bucket[i] = new Bucket();
        }

        for(int i=0; i< NUM_COUNT; i++) {         // produce some data
            int value = p.produce();
            int bucketId = value/BUCKET_RANGE;
            bucket[bucketId].write(value);
            c.register(value);
        }

        for (int i = 0; i < bucket.length; i++) { // submit non-empty buffers
            bucket[i].submitBuffer();
        }

        byte[] compressProperties = null;
        for (int i = 0; i < bucket.length; i++) { // compress the data
            compressProperties = bucket[i].compressData();
        }

        printStatistics();

        for (int i = 0; i < bucket.length; i++) { // decode & sort buckets one by one
            int[] array = bucket[i].decompress(compressProperties);
            Arrays.sort(array);

            for(int v : array) {
                c.consume(v);
            }
        }
        c.finalCheck();
    }

    public void printStatistics() {
        int size = 0;
        int sizeCompressed = 0;

        for (int i = 0; i < BUCKETS; i++) {
            int bucketSize = 4*bucket[i].size;
            size += bucketSize;
            sizeCompressed += bucket[i].compressedOut.size();

            System.out.println("  bucket[" + i
                    + "] contains: " + bucket[i].size
                    + " numbers, compressed size: " + bucket[i].compressedOut.size()
                    + String.format(" compression factor: %.2f", ((double)bucket[i].compressedOut.size())/bucketSize));
        }

        System.out.println(String.format("Data size: %.2fM",(double)size/(1014*1024))
                + String.format(" compressed %.2fM",(double)sizeCompressed/(1014*1024))
                + String.format(" compression factor %.2f",(double)sizeCompressed/size));
    }
}

static class Consumer {
    private Set<Integer> values = new HashSet<>();

    int v = -1;
    public void consume(int value) {
        if(v < 0) v = value;

        if(v > value) {
            throw new IllegalArgumentException("Current value is greater than previous: " + v + " > " + value);
        }else{
            v = value;
            values.remove(value);
        }
    }

    public void register(int value) {
        values.add(value);
    }

    public void finalCheck() {
        System.out.println(values.size() > 0 ? "NOT OK: " + values.size() : "OK!");
    }
}

public static void main(String[] args) throws IOException {
    Producer p = new Producer();
    Consumer c = new Consumer();
    Sorter sorter = new Sorter();

    sorter.doSort(p, c);
}
}

乱数を使用すると、次のようになります。

bucket[0] contains: 200357 numbers, compressed size: 353679 compression factor: 0.44
bucket[1] contains: 199465 numbers, compressed size: 352127 compression factor: 0.44
bucket[2] contains: 199682 numbers, compressed size: 352464 compression factor: 0.44
bucket[3] contains: 199949 numbers, compressed size: 352947 compression factor: 0.44
bucket[4] contains: 200547 numbers, compressed size: 353914 compression factor: 0.44
Data size: 3.85M compressed 1.70M compression factor 0.44

単純な昇順(1つのバケットが使用される)の場合は、次のようになります。

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

_編集_

結論:

  1. Nature をだまそうとしないでください
  2. より少ないメモリ使用量でより単純な圧縮を使用する
  3. いくつかの追加の手がかりが本当に必要です。一般的な防弾対策は実行可能ではないようです。

ステージ2:強化された圧縮、最終的な結論

前のセクションで既に述べたように、任意の適切な圧縮技術を使用することができる。それでは、LZMAを取り除き、より簡単で(可能であれば)より良いアプローチを選択しましょう。 算術符号化基数木 など、良い解決策はたくさんあります。

とにかく、シンプルだが便利なエンコーディング方式は、他の外部ライブラリよりも実例となり、気の利いたアルゴリズムをいくつか提供します。実際の解決策は非常に簡単です。部分的にソートされたデータを含むバケットがあるので、数値の代わりにデルタを使用できます。

encoding scheme

ランダム入力テストは、わずかに良い結果を示します。

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

サンプルコード

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

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

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

        int len = bits;

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

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

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

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

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

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

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

   return length;
}

注意してください、このアプローチ:

  1. 多くのメモリを消費しません
  2. ストリームを扱う
  3. それほど悪くない結果を提供する

完全なコードを見つけることができます here 、BinaryInputおよびBinaryOutputの実装を見つけることができます here

最終的な結論

最終的な結論はありません:)時には、1つ上のレベルに移動し、 メタレベル の観点からタスクを確認することが本当に良い考えです。

この作業に時間を費やすのは楽しかったです。ところで、下記の興味深い答えがたくさんあります。ご清聴ありがとうございました。

362
Renat Gilmanov

ギルマノフの答えは、その仮定が非常に間違っています。 100万連続整数のpointlessメジャーに基づいて推測を開始します。それはギャップがないことを意味します。これらのランダムなギャップは、どんなに小さくても、本当に悪い考えになります。

自分で試してみてください。 100万個のランダムな27ビット整数を取得し、並べ替え、 7-Zip 、xzなど、必要なLZMAで圧縮します。結果は1.5MB以上です。一番の前提は、連番の圧縮です。偶数delta encoding1.1-MB以上です。そして、これが100MB以上のRAMを圧縮に使用していることを気にしないでください。そのため、圧縮された整数でさえ問題に適合せず、ランタイムRAMの使用法は気にしません

人々がきれいなグラフィックスと合理化を支持しているのは悲しいことです。

#include <stdint.h>
#include <stdlib.h>
#include <time.h>

int32_t ints[1000000]; // Random 27-bit integers

int cmpi32(const void *a, const void *b) {
    return ( *(int32_t *)a - *(int32_t *)b );
}

int main() {
    int32_t *pi = ints; // Pointer to input ints (REPLACE W/ read from net)

    // Fill pseudo-random integers of 27 bits
    srand(time(NULL));
    for (int i = 0; i < 1000000; i++)
        ints[i] = Rand() & ((1<<27) - 1); // Random 32 bits masked to 27 bits

    qsort(ints, 1000000, sizeof (ints[0]), cmpi32); // Sort 1000000 int32s

    // Now delta encode, optional, store differences to previous int
    for (int i = 1, prev = ints[0]; i < 1000000; i++) {
        ints[i] -= prev;
        prev    += ints[i];
    }

    FILE *f = fopen("ints.bin", "w");
    fwrite(ints, 4, 1000000, f);
    fclose(f);
    exit(0);

}

LTSMAでints.binを圧縮します...

$ xz -f --keep ints.bin       # 100 MB RAM
$ 7z a ints.bin.7z ints.bin   # 130 MB RAM
$ ls -lh ints.bin*
    3.8M ints.bin
    1.1M ints.bin.7z
    1.2M ints.bin.xz
55
alecco

これについて考える1つの方法は、組み合わせ論の観点からだと思います。ソートされた番号順の可能な組み合わせはいくつあるのでしょうか。 0、0、0、...、0というコードに0、0、0、0、...、1というコード、1と99999999、99999999、... 99999999というコードの組み合わせを指定すると、 Nは何ですか?言い換えれば、結果スペースはどのくらい大きいのでしょうか。

まあ、これについて考える1つの方法は、これがN = Mグリッド(N = 1,000,000とM = 100,000,000)の単調経路の数を見つける問題の全単射であることに気づいていることです。つまり、幅が1,000,000、高さが100,000,000のグリッドがある場合、左下から右上までの最短パスはいくつありますか。もちろん、最短経路では、右または上に移動するだけで済みます(下または左に移動する場合は、以前に達成した進行を元に戻すことになります)。これが数値ソート問題の全単射であることを確認するには、次の点に注意してください。

あなたは私達の順序の数として私達の道の水平な足を想像することができる、足のY位置は価値を表す。

enter image description here

そのため、パスが単に最後まで最後まで右に移動すると、一番上までジャンプします。これは、0,0,0、...、0の順序付けと同じです。代わりに、一番上までジャンプしてから右に1,000,000回移動すると、99999999、99999999、...、99999999と同じになります。右に1回移動し、次に右に移動するパス最後に最後まで上がると(それから必然的に一番上までジャンプします)、0,1,2,3、...、999999と同じです。

幸いなことに、この問題はすでに解決されています。このようなグリッドには(N + M)(M)個のパスを選択できます。

(1,000,000 + 100,000,000)(100,000,000)〜= 2.27 * 10 ^ 2436455を選択

したがって、Nは2.27 * 10 ^ 2436455に等しいので、コード0は0、0、0、...、0を表し、コード2.27 * 10 ^ 2436455、および何らかの変更は99999999、99999999、...、99999999を表します。

0から2.27 * 10 ^ 2436455までのすべての数値を格納するには、lg2(2.27 * 10 ^ 2436455)= 8.0937 * 10 ^ 6ビットが必要です。

1メガバイト= 8388608ビット> 8093700ビット

ですから、少なくとも実際には結果を保存するのに十分なスペースがあるようです。もちろん、ここで興味深いことに、数字が流れ込むときにソートが行われます。これに対する最善の方法がわからない場合、残りの294908ビットが得られます。それぞれの時点でそれが全体の順序付けであると仮定し、その順序付けのためのコードを見つけてから、新しい番号が戻ってきて前のコードを更新するという面白いテクニックがあると思います。手の波手の波.

ここでの私の提案は、 Danの解決策 - に大きく依存しています

まず最初に、私は解法が all 可能な入力リストを扱わなければならないと思います。私は一般的な答えはこの仮定をしていないと思います(これはIMOが大きな間違いです)。

いかなる形式の可逆圧縮もすべての入力のサイズを縮小しないことが知られています。

すべての一般的な回答は、それらがそれらに余分なスペースを許可するのに十分効果的に圧縮を適用できるだろうと仮定します。実際、それらの部分的に完成したリストの一部を非圧縮形式で保持し、それらがソート操作を実行できるようにするのに十分な大きさの余分なスペースの塊。これは単なる悪い仮定です。

そのような解決策のために、彼らがどのように彼らが彼らの圧縮をするかについての知識を持っている誰でもこの計画のためにうまく圧縮しないいくつかの入力データを設計することができるでしょう。

代わりに私は数学的なアプローチを取ります。可能な出力は、範囲0..MAXの要素からなる長さLENのすべてのリストです。ここではLENは1,000,000で、MAXは100,000,000です。

任意のLENおよびMAXの場合、この状態をエンコードするのに必要なビット数は次のとおりです。

Log2(MAXマルチチュースLEN)

したがって、数値の場合、受信と並べ替えが完了したら、すべての可能な出力を一意に区別できるように結果を格納するために、少なくともLog2(100,000,000 MC 1,000,000)ビットが必要になります。

これは〜= 988kb です。だから私たちは実際に私たちの結果を保持するのに十分なスペースがあります。この観点から、それは可能です。

[より良い例があるので、無意味なランブルを削除しました...]

ベストアンサー はこちら .

もう一つの良い答えは はこちら そして基本的にリストを1要素ずつ拡張する機能として挿入ソートを使う(一度に複数の要素を挿入できるように、いくつかの要素と事前ソートをバッファする) ) 7ビットデルタのバケットでも、Niceのコンパクトな状態エンコーディングを使用します。

20
davec

この作業が可能であるとします。出力の直前に、百万のソートされた数のインメモリ表現があるでしょう。そのような表現はいくつありますか。繰り返しの数があるかもしれないのでnCr(choose)は使えませんが、 multisets で動作するmultichooseと呼ばれる操作があります。

  • 0.99,999,999の範囲で100万の数を選択するには、 2.2e2436455 の方法があります。
  • そのためには、すべての可能な組み合わせを表すために 8,093,730 ビット、つまり1,011,717バイトが必要です。

理論的には、ソートされた数のリストの正しい(十分な)表現を思い付くことができれば、それは可能かもしれません。たとえば、非常識な表現では、10MBのルックアップテーブルまたは数千行のコードが必要になることがあります。

ただし、「1M RAM」が100万バイトを意味する場合、明らかに十分なスペースがありません。理論的に5%メモリを増やすことが理論的に可能になるという事実は、表現が非常に効率的でなければならず、おそらく正気ではないことを私に示唆しています。

18
Dan

(私の最初の答えは間違っていました。悪い数学を残念に思います。休憩の下を見てください。)

これはどう?

最初の27ビットには、あなたが見た中で最も小さい番号が格納され、次に見られる番号との差が格納されます。差の格納に使用されるビット数を格納するには5ビット、次に差が格納されます。その番号をもう一度見たことを示すには、00000を使用します。

これは、より多くの数が挿入されるにつれて、数の間の平均差が小さくなるので、より多くの数を追加するにつれて、より少ないビットを使用して差を格納するために機能するためです。これはデルタリストと呼ばれると思います。

私が考えることができる最悪の場合は、すべての数字が等間隔(100)であることです。最初の数字を0とします。

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

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

救助にReddit!

並べ替えるだけでいいのなら、この問題は簡単です。どの数字を見たかを記憶するのに122k(100万ビット)かかります(0が見られたら0番目のビット、2300が見られたら2300番目のビットなど)。

数字を読み、それをビットフィールドに格納してから、カウントを維持しながらビットをシフトアウトします。

しかし、あなたはあなたが何個見たか覚えておく必要があります。私はこのスキームを考え出すために上記のサブリストの答えに触発されました:

1ビットを使用する代わりに、2または27ビットを使用してください。

  • 00は番号が見えなかったことを意味します。
  • 01は一度見たことを意味します
  • 1はあなたがそれを見たことを意味し、そして次の26ビットは何回のカウントです。

私はこれがうまくいくと思います:重複がなければ、あなたは244kのリストを持っています。最悪の場合、各番号が2回表示されます(1つの番号が3回表示されると、残りのリストが短くなります)。つまり、50,000回以上表示され、950,000項目が0回または1回表示されます。

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

次のエンコーディングを使用すれば、さらに改善することができます。

0は数字を見なかったことを意味します10はあなたがそれを見たことを意味します11はあなたが数え続ける方法です

これは、平均して280.7kのストレージになります。

編集:私の日曜日の朝の数学は間違っていた。

最悪の場合、500,000の数字が2回表示されるので、数学は次のようになります。

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

代替の符号化は、の平均ストレージをもたらす。

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

:(

12
jfernand

考えられるすべての入力にわたってこの問題を解決する方法が1つあります。カンニング.

  1. TCP上でm個の値を読み取ります。ここで、mは、メモリー内でソート可能な最大値、おそらくn/4です。
  2. 250,000程度の数字を並べ替えて出力します。
  3. 他の4分の3についても繰り返します。
  4. 受信機は、受信した4つの番号リストを処理しながらマージします。 (単一のリストを使うよりも遅くはありません。)
10
xpda

どのようなコンピュータを使っていますか?他の「通常の」ローカルストレージはありませんが、たとえばビデオRAMはありますか。 1メガピクセルx 32ビット/ピクセル(たとえば)は、必要なデータ入力サイズにかなり近いです。

(私は主に古い Acorn RISC PC のメモリを頼みます。低解像度または低色深度のスクリーンモードを選択した場合、利用可能なシステムRAMを拡張するためにVRAMを「借りる」ことができます!)これは、通常のRAMが数MBしかないマシンではかなり役に立ちました。

7
DNA

基数ツリー を試してみます。データをツリーに格納できる場合は、データを転送するために順序どおりのトラバースを実行できます。

これが1MBに収まるかどうかはわかりませんが、試してみる価値があると思います。

7

基数ツリーは「プレフィックス圧縮」を利用するので、基数ツリー表現はこの問題を処理することに近づくでしょう。しかし、1バイトで1つのノードを表すことができる基数木表現を想像するのは難しいです - 2はおそらく限界についてです。

しかし、データの表現方法に関係なく、一度ソートされると、プレフィックス圧縮形式で保存できます。ここで、10、11、および12は、001b、001b、001bで表され、1の増分を示します。前の番号から。おそらく、その場合、10101bは5の増分、1101001bは9の増分などを表します。

6
Hot Licks

10 ^ 8の範囲には10 ^ 6の値があるため、平均で100コードポイントごとに1つの値があります。 N番目の点から(N + 1)番目までの距離を格納します。重複値のスキップは0です。つまり、スキップには平均7ビット未満を格納する必要があるため、そのうちの100万ビットは、800万ビットのストレージにうまく収まります。

これらのスキップは、ハフマンエンコーディングなどによってビットストリームにエンコードする必要があります。挿入は、ビットストリームを反復処理して新しい値の後に書き換えることです。暗黙の値を繰り返し入力して書き出して出力します。実用的には、おそらく、それぞれ10 ^ 4のコードポイント(および平均100個の値)をカバーする10 ^ 4のリストとして実行することをお勧めします。

スキップの長さに関するポアソン分布(平均=分散= 100)を仮定することによって、ランダムデータのための良いハフマン木を先験的に構築することができますが、実際の統計量を入力に保持し、対処する最適木を生成するために使用できます。病理学的症例。

6
Russ Williams

1MのRAMがあり、他にローカルストレージがないコンピュータがあります

チートする別の方法:代わりに非ローカル(ネットワーク)ストレージを使用して(あなたの質問でこれを排除することはできません)、単純なディスクベースのマージソート(またはソートに十分なRAM)を使用できるネットワークサービスを呼び出します。すでに与えられている(明らかに非常に独創的な)解決策を必要とせずに、あなたはただ1Mの数字を受け入れる必要があるので、メモリ内で)。

これは不正行為かもしれませんが、現実世界の問題に対する解決策を探しているのか、それともルールの曲げを招くパズルを探しているのかは明確ではありません。しかし、「本物の」解決策(他の人が指摘したように、圧縮可能な入力に対してのみ有効です)。

5
DNA

私はその解決策はビデオ符号化からの技術、すなわち離散コサイン変換を組み合わせることであると思います。デジタルビデオでは、ビデオの明るさや色の変化を110 112 115 115などの通常の値として記録するのではなく、それぞれ最後から減算します(ランレングスエンコーディングと同様)。 110 112 115 116は110 2 3 1になります。23 1という値は、オリジナルより少ないビット数で済みます。

それでは、ソケットに到着したときに入力値のリストを作成するとしましょう。値ではなく、その前の要素のオフセットを各要素に格納します。私達は私達が行くように私達を分類する、従ってオフセットは肯定的になるだけであるだろう。しかし、オフセットは8桁の10進数で、これは3バイトに収まります。各要素を3バイトにすることはできないので、これらをパックする必要があります。各バイトの最上位ビットを「継続ビット」として使用できます。これは、次のバイトが数値の一部であり、各バイトの下位7ビットを組み合わせる必要があることを示しています。重複はゼロです。

リストがいっぱいになると、数値は近づくはずです。つまり、次の値までの距離を決定するために平均1バイトだけが使用されます。便利であれば7ビットの値と1ビットのオフセットが必要ですが、「継続」値には8ビット未満しか必要としないスイートスポットがあるかもしれません。

とにかく、私はいくつかの実験をしました。私は乱数発生器を使い、私は百万のソートされた8桁の10進数を約1279000バイトに収めることができます。各番号の間の平均スペースは、一貫して99です...

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

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

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

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

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

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

Google の(悪い)アプローチ、HNスレッドから。 RLEスタイルのカウントを保管する。

あなたの初期のデータ構造は '99999999:0'(すべてゼロ、数字は見ていません)です、そしてあなたがあなたのデータ構造が '3866343:0,1:1,96133654:0'になるようにあなたが数3,866,344を見るとしましょう。数字は常にゼロのビット数と '1'のビット数の間で交互に変わるので、奇数は0ビット、偶数は1ビットを表すと仮定することができます。これは(3866343,1,96133654)になります

彼らの問題は重複をカバーするようには見えませんが、彼らが重複のために "0:1"を使用するとしましょう。

大きな問題#1:1M整数の挿入 は年齢 をとるでしょう。

大きな問題#2:すべてのプレーンデルタエンコーディングソリューションのように、いくつかのディストリビューションはこの方法ではカバーできません。例えば、距離が0:99の整数1m(例えば、それぞれ+99)。今度は同じだが ランダムな距離 0/99の の範囲で と考える。 (注:99999999/1000000 = 99.99)

グーグルのアプローチは価値がない(遅い)そして正しくない。しかし彼らの弁護では、彼らの問題は多少異なっていたかもしれません。

4
alecco

すべての番号を取得する前に、ネットワーキングスタックを使用して番号をソート順に送信することができます。 1Mのデータを送信すると、TCP/IPはそれを1500バイトのパケットに分割し、それらをターゲットに送信します。各パケットにはシーケンス番号が与えられます。

これは手作業でできます。 RAMを埋める直前に、持っているものをソートしてターゲットにリストを送信することができますが、各番号の周りに順番に穴を空けることができます。その後、順番にそれらの穴を使用して同じ方法で数字の2番目の1/2を処理します。

遠端のネットワーキングスタックは、結果のデータストリームをシーケンスの順序で組み立ててから、アプリケーションに渡します。

ネットワークを使用してマージソートを実行しています。これは完全なハックですが、私は前述の他のネットワーキングハックに触発されました。

4
Kevin Marquette

ソートされた配列を表現するために、最初の要素と隣接する要素間の差を格納するだけです。このようにして、最大で10 ^ 8になる可能性のある10 ^ 6個の要素をエンコードすることに関心があります。これを _ d _ としましょう。 _ d _ の要素をエンコードするには、 ハフマンコード を使用できます。ハフマンコードの辞書は外出先で作成することができ、新しい項目がソートされた配列に挿入されるたびに配列が更新されます(挿入ソート)。新しい項目が原因で辞書が変更された場合は、新しいエンコーディングに合わせて配列全体を更新する必要があります。

_ d _ の各要素を符号化するための平均ビット数は、各一意の要素の数が等しい場合に最大になります。 _ d _ の要素d1d2、...、dNはそれぞれFと現れる回。その場合(最悪の場合、入力シーケンスに0と10 ^ 8の両方があります)

sum(1 <= i <= NFdi = 10 ^ 8

どこで

sum(1 <= i <= NF = 10 ^ 6、またはF) = 10 ^ 6/Nそして正規化された頻度はp = F/10 ^ = 1/N

平均ビット数は-log2(1/P)= log2(N)になります。このような状況下では、Nを最大化するケースを見つけるべきです。これは、0から始まるdiの連続番号がある場合、またはdi = i- 1の場合に発生します。

10 ^ 8 = sum(1 <= i <= NFdi = sum(1 <= i <= N(10 ^ 6/N)(i-1)=(10 ^ 6/NNN- 1)/ 2

すなわち.

N <=201。この場合、平均ビット数はlog2(201)= 7.6511です。つまり、ソートされた配列を保存するには、入力要素あたり約1バイトが必要になります。これは _ d _ が一般に201個を超える要素を持つことができないという意味ではないことに注意してください。 _ d _ の要素が一様に分布している場合、201個を超える一意の値を持つことはできないことがわかります。

3

TCPの再送信動作を悪用します。

  1. TCPコンポーネントに大きな受信ウィンドウを作成させる。
  2. ACKを送信せずに、ある程度の量のパケットを受信します。
    • それらをパスで処理して、(接頭辞)圧縮データ構造を作成する
    • 不要になった最後のパケットに対して重複ACKを送信/再送信タイムアウトを待つ
    • 後藤2
  3. すべてのパケットが受け入れられました

これはバケツまたは複数パスのある種の利点を前提としています。

おそらくバッチ/バケットをソートしてそれらをマージすることによって。 - >基数木

このテクニックを使用して最初の80%を受け入れてソートしてから最後の20%を読み取り、最後の20%に最低の数字の最初の20%に入る数字が含まれていないことを確認します。それから20%最も低い数を送り、記憶から取除き、新しい数の残りの20%を受け入れそして併合しなさい**

3
sleeplessnerd

入力ストリームを数回受け取ることができれば、これははるかに簡単になります(それに関する情報がない、アイデアおよび時間パフォーマンスの問題)。

それから、10進数を数えることができます。カウント値を使用すると、出力ストリームを簡単に作成できます。値を数えて圧縮します。入力ストリームの内容によって異なります。

2
Baronth

入力ストリームを数回受信できた場合、これははるかに簡単になります(それに関する情報がない、アイデアや時間パフォーマンスの問題)。それから、10進数を数えることができます。カウント値を使用すると、出力ストリームを簡単に作成できます。値を数えて圧縮します。入力ストリームの内容によって異なります。

1
pbies

あなたはただ順番に数の間の違いを保存し、これらの数を圧縮するためにエンコーディングを使う必要があります。 2 ^ 23ビットあります。それを6ビットのチャンクに分割し、最後のビットに、その数が別の6ビット(5ビットに拡張チャンクを加えたもの)まで及ぶかどうかを示させます。

したがって、000010は1、000100は2です。000001100000は128です。ここで、最大10,000,000,000までの数の順序で差異を表す場合の最悪のキャストを考えます。 2 ^ 5より大きい10,000,000/2 ^ 5の差、2 ^ 10より大きい10,000,000/2 ^ 10の差、および2 ^ 15より大きい10,000,000/2 ^ 15の差などがあります。

そのため、シーケンスを表すのに必要なビット数を追加します。 1,000,000 * 6 +切り上げ(10,000,000/2 ^ 5)* 6 +切り上げ(10,000,000/2 ^ 10)* 6 +切り上げ(10,000,000/2 ^ 15)* 6 +切り上げ(10,000,000/2 ^ 20)* 4 = 7935479.

2 ^ 24 =8388608。8388608> 7935479なので、十分なメモリが簡単にあるはずです。新しい数字を挿入したときの場所の合計を格納するために、もう少しメモリが必要になるでしょう。その後、シーケンスをたどり、新しい番号を挿入する場所を見つけ、必要に応じて次の差を減らし、それ以降のすべてを右にシフトします。

1
gersh

1 MB - 3 KB RAM = 2 ^ 23 - 3 * 2 ^ 13ビット= 8388608 - 24576 = 8364032ビットが利用可能です。

10 ^ 8の範囲で10 ^ 6の数値が与えられます。これにより、平均ギャップは約100 <2 ^ 7 = 128になります。

最初に、すべてのギャップが128より小さい場合の、かなり等間隔の数字の単純な問題を考えてみましょう。これは簡単です。最初の数と7ビットのギャップを格納するだけです。

(27ビット)+ 10 ^ 6 7ビットのギャップ番号= 7000027ビットが必要

繰り返し数には0のギャップがあることに注意してください。

しかし、127より大きいギャップがあるとどうなりますか。

さて、127以下のギャップサイズが直接表現されるとしましょう、しかし127のギャップサイズの後に実際のギャップ長のための連続的な8ビットエンコーディングが続きます:

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

等.

この数値表現はそれ自身の長さを記述しているので、次のギャップ番号がいつ始まるかがわかります。

127未満の小さなギャップでも、これにはまだ7000027ビットが必要です。

最大(10 ^ 8)/(2 ^ 7)= 781250 23ビットのギャップ番号がある可能性があり、余分な16 * 781,250 = 12,500,000ビットが必要になります。よりコンパクトで徐々に増加するギャップの表現が必要です。

平均ギャップサイズは100なので、それらを[100、99、101、98、102、...、2、198、1、199、0、200、201、202、...]のように並べ替えると、これにインデックスが付けられます。 '00'で区切られた数字で、ゼロのペアがない(例えば、11011 = 8 + 5 + 2 + 1 = 16)、密なバイナリフィボナッチベースエンコーディングでは、ギャップ表現を十分短くすることができると思いますが、必要です。より多くの分析。

1
Toby Kelsey

並べ替えは、ここで2番目の問題です。他の人が言ったように、整数を格納するだけでは困難で、数あたり27ビットが必要になるので、すべての入力に対して機能するわけではありませんです。

私の考えでは、これは次のとおりです。連続した(ソートされた)整数の差のみを格納します。それらはおそらく小さいからです。それから、圧縮スキームを使います。入力番号あたり2ビットを追加して、その番号が格納されるビット数をエンコードします。何かのようなもの:

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

与えられたメモリの制約内で、かなりの数の可能な入力リストを格納することが可能であるべきです。どのようにして圧縮方式を選択して最大入力数で機能させるかという数学は、私を超えています。

これに基づいて整数圧縮方式を見つけるためにあなたの入力に関するドメイン固有の知識を利用できるかもしれません。

ああそしてそれから、あなたはデータを受け取ると同時にそのソートされたリストに挿入ソートをする。

1

これらの数値について何も知らない場合は、以下の制約によって制限されます。

  • 並べ替える前に、すべての数字を読み込む必要があります。
  • 数字の集合は圧縮できません。

これらの仮定が成り立つなら、あなたは少なくとも26,575,425ビットの記憶域(3,321,929バイト)を必要とするので、あなたのタスクを実行する方法はありません。

あなたはあなたのデータについて何を私たちに言うことができますか?

1
Yves Daoust

今、実際の解決策を目指して、わずか1MBのRAMで8桁の範囲で入力のすべての可能なケースをカバーします。注:進行中の作業、明日も続きます。ソートされた整数値のデルタの算術符号化を使用すると、1Mのソートされた整数値の最悪の場合、エントリごとに約7ビットのコストがかかります(99999999/1000000は99で、log2(99)は約7ビットです)。

しかし、7または8ビットになるようにソートされた1mの整数が必要です。より短い級数はより大きなデルタを持つことになり、それゆえ要素あたりのビット数は増えます。

私は可能な限り多くのものを取り、(ほぼ)その場で圧縮することに取り組んでいます。 250Kに近い最初のバッチは、せいぜいそれぞれ9ビットを必要とするでしょう。だから結果は約275KBかかります。残りの空きメモリを数回繰り返します。それから、それらの圧縮されたチャンクを解凍して適切な場所に圧縮します。これは 非常に難しい ですが、可能です。おもう。

マージされたリストは、整数ターゲットあたり7ビットにますます近づきます。しかし、マージループの反復回数がわかりません。おそらく3。

しかし、算術符号化の実装の不正確さはそれを不可能にするかもしれません。この問題がまったく可能であるならば、それは非常にきついでしょう。

ボランティア?

1
alecco

トリックは、「インクリメントカウンタ」=「+」と「出力カウンタ」=「!」の圧縮ストリームとして、整数のマルチセットであるアルゴリズムの状態を表すことです。キャラクターたとえば、集合{0,3,3,4}は "!+++ !! +!"のように表現され、その後に任意の数の "+"文字が続きます。マルチセットを変更するには、一度に一定量だけ解凍した状態で文字をストリームアウトし、それらを圧縮形式でストリームバックする前に変更をインプレースで行います。

詳細

ファイナルセットにはちょうど10 ^ 6個の数字があることを知っているので、せいぜい10 ^ 6 "!"しかありません。キャラクターまた、範囲のサイズは10 ^ 8、つまり最大で10 ^ 8 "+"の文字があることを知っています。 10 ^ 8 "+"の中で10 ^ 6 "!"を整理できる方法の数は(10^8 + 10^6) choose 10^6なので、特定の整理を指定すると 〜0.965 MiB `のデータになります。それはきついでしょう。

割り当て量を超えずに、各文字を独立したものとして扱うことができます。 "!"よりも正確に100倍多い "+"文字があります。依存していることを忘れると、各文字が "+"になる確率は100:1になります。 100:101のオッズは、 〜0.08ビット/文字 に相当します。ほぼ同じ合計の 〜0.965 MiB に対して(依存関係を無視すると、コストは 〜12ビット)のみです この場合!)。

既知の事前確率で独立した文字を格納するための最も簡単な手法は、 ハフマンコーディング です。非現実的に大きな木が必要であることに注意してください(10文字のブロックのハフマン木は、ブロックあたりの平均コストが約2.4ビット、合計約2.9 Mibです。20文字のブロックのハフマン木は、ブロックあたりの平均コストを持ちます私たちはおそらく100のオーダーのサイズのブロックを必要とするでしょう、これは今まで存在した全てのコンピュータ機器が格納できるよりも多くのノードがツリーの中にあることを意味します。 )ただし、問題に応じてROMは技術的に「フリー」であり、ツリー内の規則性を利用する実用的な解決策は基本的に同じに見えます。

疑似コード

  • ROMに格納されている十分に大きいハフマンツリー(または同様のブロックごとの圧縮データ)を持っている
  • 10 ^ 8 "+"文字の圧縮文字列で始めます。
  • 数字Nを挿入するには、N "+"文字が過ぎるまで圧縮文字列をストリームアウトしてから "!"を挿入します。オーバーランやアンダーランを避けるために、一定量のバッファブロックを保持しながら、再圧縮した文字列を前の文字列に戻します。
  • 100万回繰り返します:[入力、ストリームの解凍>挿入>圧縮]、そして解凍して出力
1
Craig Gidney

ストリームを受信して​​いる間これらのステップをして下さい。

1セット目の手頃なチャンクサイズ

擬似コードのアイデア:

  1. 最初のステップは、すべての重複を見つけ、それらをその数とともに辞書に入れてそれらを削除することです。
  2. 3番目のステップは、アルゴリズムのステップの順序で存在する番号を配置し、最初の番号とn、n + 1 ...、n + 2、2n、2n + 1のようにそれらのステップを持つカウンター特殊辞書に配置することです。 2n + 2 ...
  3. 繰り返し回数が少なくなる残りの数を1000個または10000個ごとのように、いくつかの合理的な数の範囲で圧縮し始めます。
  4. 数値が見つかった場合はその範囲を解凍し、それを範囲に追加してしばらく圧縮しないでください。
  5. それ以外の場合は、単にその数をバイトに追加してください[chunkSize]

ストリームを受信しながら最初の4つのステップを続けます。最後のステップは、メモリを超えた場合は失敗するか、範囲のソートを開始して結果を順番に吐き出し、結果を圧縮解除してソートする必要がある場合は結果をソートすることによってすべてのデータが収集されたらあなたはそれらに着きます。

0
RetroCoder