CCF-CSP 実際の質問「202305-3 解凍」アイデア + Python、C++ フルスコアのソリューション

実際の質問と他の質問の解決策を確認したい学生は、CCF-CSP の実際の質問と解決策にアクセスしてください。


質問番号: 202305-3
質問名: 解凍する
制限時間: 5.0秒
メモリ制限: 512.0MB
問題の説明:

トピックの背景

シシ イヴェール アイランド オペレーティング カンパニーは、島のインフラの維持と運用を担当する大企業です。社内には、サーバー設備を使用する必要があるさまざまな業務を担当する部門が数多くあります。管理を容易にし、会社の運営コストを削減するために、
Sisi アイボリー アイランド オペレーティング カンパニーはプライベート クラウド システムを構築しました。このプライベート クラウド システムは、仮想マシン サービスのホストに加えて、他のいくつかのサービスも提供できます。その中でも最も人気があるのがログサービスです。従来、各業務システムのログはそれぞれのサーバーに分散して保存されており、
閲覧や分析に不便なだけでなく、損失の危険性もありました。ログサービスは、さまざまな業務システムのログを一元的に収集でき、閲覧・管理に便利です。

ログ サーバーによって収集されるログはすべてプレーン テキストであり、高度にフォーマットされています。これは、ログ データを非常に小さく圧縮できることを意味します。ただし、ログ データの量は非常に多く、処理効率に対する要求も高いため、一定の圧縮率を犠牲にして、効率的な圧縮アルゴリズムを使用してログ データを圧縮することができます。
Little C は、圧縮されたログ データが与えられた場合にそれを解凍する必要がある場合に、ログを解凍するプログラムを実現するように配置されています。

問題の説明

この圧縮アルゴリズムによって生成されたデータ ストリームは、一連の要素とみなすことができます。要素には、リテラルと後方参照の 2 種類があります。リテラルには一連のバイトが含まれており、解凍すると、これらのバイトを直接出力できます。
逆参照とは、以前に解凍されたデータ ストリームの一部を繰り返し出力することです。後方参照は、オフセット o と長さ l の 2 つの数値で構成される <o,l> として表すことができます。
オフセットは現在位置からの後方距離を示し、長さは繰り返し出力されるバイト数を示します。このうち、o,l>0であることが必要です。p バイトが伸張されている場合、o≧1 のとき、
オフセット (p-o) から l バイトを繰り返し出力することを意味します (最初のバイトのオフセットは 0)。たとえば、解凍されたデータ ストリームが の場合 abcde、後方参照 <3,2> は出力 を示します cd
o<1 の場合は、オフセット (p-o) から o バイトを繰り返し出力し、合計 l バイトが出力されるまで o バイトを繰り返し出力することを意味します。たとえば、解凍されたデータ ストリームが の場合 abcde
後方参照 <2,5> は出力 を示します deded

圧縮データ形式は、ブート フィールドとデータ フィールドの 2 つの部分に分かれています。このうち、ブートフィールドは元のデータの長さを保存します。元のデータ長をnとします。この場合、n は ∑k=0dck×128k (0≤ck<128、
cd≠0) として表すことができます。ブートフィールドの長さは (d+1) バイトで、c0+128,c1+128,⋯,cd−1+128,cd が順に格納されます。つまり、最後のバイトの最上位ビットが 0 で、
他のバイトの最上位ビットが 1 である場合を除き、各バイトは下位 7 ビットを使用して ck を保存します。たとえば、元のデータの長さが 1324 の場合、ck は 44、10、つまり 16 進数、  0x2C..となります0x0Aしたがって、ブート領域の長さは 2 で、バイトのシーケンスは です 0xAC 0x0A


圧縮データ形式

データ フィールドには、連続して格納される一連の要素である圧縮データが格納されます。各要素の最初のバイトの下位 2 ビットは、要素のタイプを示します。最下位 2 ビットが 0 の場合、これはリテラル値であることを意味します。リテラルに含まれるバイト数が l で、l ≤60 の場合、
最初のバイトの上位 6 ビットは (l−1) を表します。次の l バイトは、リテラルに含まれる元のバイトです。たとえば 0xE8、バイトのバイナリ値 1110 1000は 0 で、下位 2 ビットは 0 であり、これがリテラル値であることを示します。上位 6 ビットは で 111010、数値 58 を示します。
これは、リテラルに 59 バイトが含まれていることを意味します。したがって、このバイトに続く 59 バイトは、リテラルに含まれる元のバイトです。l>60 の場合、(l-1) はリトルエンディアン順に 1 ~ 4 バイトで表され、最初のバイトの後に格納されます。
最初のバイトの上位 6 ビットに格納されている値が 60、61、62、または 63 の場合、(l-1) はそれぞれ 1、2、3、または 4 バイトで表されることを意味します。たとえば、 0xF4 0x01 0x0A バイト シーケンスでは、最初のバイトのバイナリ値は で 1111 0100、下位 2 ビットは 0 であり、これがリテラル値であることを示します。
上位 6 ビットは 111101数値 61 を表し、リテラル値の長さが次の 2 バイトに格納されることを意味します。次の 2 バイトは、 0x01 0x0Aリトル エンディアン順の 16 進数 0x0A01、つまり 10 進数の 2561 を形成し、リテラルに 2562 バイトが含まれていることを示します。
次の 2562 バイトは、リテラルに含まれる元のバイトです。


リテラル値、長さは 60 バイトを超えない 


リテラル、60 バイトを超える 

要素の最初のバイトの下位 2 ビットが である場合 01 、これは後方参照 <o,l>、および 4≤l≤11,0<o≤2047 であることを意味します。このとき、o は 11 ビットを占め、その下位 8 ビットが次のバイトに格納され、上位
3 ビットが最初のバイトの上位 3 ビットに格納されます。(l-4) は 3 ビットを占め、最初のバイトのビット 2 ~ 4 に格納されます。以下に示すように:

 7 6 5 4 3 2 1 0   7 6 5 4 3 2 1 0
+-----+-----+-+-+ +----------------+
|o(h3)| l-4 |0|1| |o (lower 8 bits)|
+-----+-----+-+-+ +----------------+

例如,字节 0x2D 0x1A,其首字节的二进制为 001 011 01,其最低两位为 01,表示这是一个回溯引用,其中 2 至 4 位是 011,表示数字 3,意味着 (l−4)=3,即 l=7。
其高 3 位是 001,与随后的字节 0x1A 组成了十六进制数 0x11A,即十进制 282,表示 o=282。因此,该回溯引用是 〈282,7〉。


回溯引用,形式 1 

当元素首字节的最低两位是 10 时,表示这是一个回溯引用 〈o,l〉,且 1≤l≤64,0<o≤65535。此时,o 占 16 位,以小端序存储于随后的两个字节中。
(l−1) 占 6 位,存储于首字节的高 6 位中。例如,字节 0x3E 0x1A 0x01,其首字节的二进制为 0011 1110,其最低两位为 10,表示这是一个回溯引用,其中高 6 位是 001111,表示数字 15,
即 (l−1)=15,即 l=16。随后的两个字节 0x1A 0x01,按小端序组成了十六进制数 0x011A,即十进制 282,表示 o=282。因此,该回溯引用是 〈282,16〉。


回溯引用,形式 2 

我们规定,元素的首字节的最低两位不允许是 11。如果出现了这种情况,那么这个数据域就是非法的。

压缩后的数据为合法的,当且仅当以下条件都满足:

  1. 引导区的长度不超过 4 字节;
  2. 引导区能被正确恢复为原始数据的长度;
  3. 每个元素的首字节的最低两位不是 11
  4. 每个元素都能按照规则被恢复为原始数据;
  5. 得到的原始数据长度恰好等于引导区中编码的原始数据长度。

输入格式

从标准输入读入数据。

输入包含有若干行,第一行是一个正整数 s,表示输入被解压缩数据的字节数。

接下来有 ⌈s8⌉ 行,表示输入的被解压缩的数据。每行只含有数字或字母 a 至 f
每两个字符组成一个十六进制数字,表示一个字节。除了最后一行,每行都恰有 8 个字节。输入数据保证是合法的。

输出格式

输出到标准输出中。

输出解压缩后的数据,每行连续输出 8 个字节,每个字节由两位十六进制数字(数字或字母 a 至 f)表示;但最后一行可以不满 8 个字节。

样例输入

81
8001240102030405
060708090af03c00
0102030405060708
090a0b0c0d0e0f01
0203040506070809
0a0b0c0d0e0f0102
030405060708090a
0b0c0d0e0f010203
0405060708090a0b
0c0d0e0fc603000d
78

样例输出

0102030405060708
090a000102030405
060708090a0b0c0d
0e0f010203040506
0708090a0b0c0d0e
0f01020304050607
08090a0b0c0d0e0f
0102030405060708
090a0b0c0d0e0f0d
0e0f0d0e0f0d0e0f
0d0e0f0d0e0f0d0e
0f0d0e0f0d0e0f0d
0e0f0d0e0f0d0e0f
0d0e0f0d0e0f0d0e
0f0d0e0f0d0e0f0d
0e02030405060708

样例说明

上述输入数据可以整理为:

80 01
24 0102030405060708090a
f0 3c
    000102030405060708090a0b0c0d0e0f
      0102030405060708090a0b0c0d0e0f
      0102030405060708090a0b0c0d0e0f
      0102030405060708090a0b0c0d0e0f
c6 0300
0d 78

首先读入第一字节 80,其最高位为 1,于是继续读入第二字节 01,其最高位为 0,因此读入引导区结束。得到 c0=0,c1=1,
原始数据长度为:0×1280+1×1281=128。

然后继续读入字节 24,其二进制是 0010 0100,最低两位为 00,表示这是一个字面量,取其高六位,是十进制数字 9,
表示这个字面量的长度为 10。接下来读入 10 个字节,得到字面量 0102030405060708090a

然后继续读入字节 f0,其二进制是 1111 0000,最低两位为 00,表示这是一个字面量,取其高六位,是十进制数字 60,表示此后的一个字节是字面量的长度减 1。
继续读入字节 3c,得到数字 60,表示这个字面量的长度是 61,接下来读入 61 个字节。

然后继续读入字节 c6,其二进制是 1100 0110,最低两位为 10,表示这是一个回溯引用,取其高六位,是十进制数字 49,表示回溯引用的长度 l 是 50。
随后继续读入两个字节 0300,按小端序组成十六进制数 0x0003,即十进制 3,表示回溯引用的偏移量 o 是 3。因此,这个回溯引用是 〈3,50〉。
由于 50=16×3+2,将此时缓冲区中最后三个字节 0d 0e 0f 重复输出 16 次,然后继续输出 0d 0e,补足共 50 个字节。

然后继续读入字节 0d,其二进制是 0000 1101,最低两位为 01,表示这是一个回溯引用,取其位 2 至 4,是 011,是十进制数字 3,表示回溯引用的长度 l 是 7。
随后读入一个字节 78,其二进制是 0111 1000,与本元素首字节 0d 的最高三位 000 拼合得到 000 0111 1000,是十进制数字 120,表示回溯引用的偏移量 o 是 120。
因此,这个回溯引用是 〈120,7〉。此前已经输出了 121 字节,此时从缓冲区开始的偏移量 121−120=1 的位置开始输出 7 个字节,即 02030405060708

此时,输入已经处理完成,共输出了 10+61+50+7=128 字节,与从引导区中读入的原始数据长度一致,因此解压缩成功。

子任务

对于 10% 的输入,解压缩后的数据长度不超过 127 字节,且仅含有字面量,每个字面量元素所含数据的长度不超过 60 字节;

对于 20% 的输入,解压缩后的数据长度不超过 1024 字节,且仅含有字面量,每个字面量元素所含数据的长度不超过 60 字节;

对于 40% 的输入,解压缩后的数据长度不超过 1024 字节,且仅含有字面量;

对于 60% 的输入,解压缩后的数据长度不超过 1024 字节,且包含的回溯引用的首字节的最低两位都是 01

对于 80% 的输入,解压缩后的数据长度不超过 4096 字节;

对于 100% 的输入,解压缩后的数据长度不超过 2MiB(2×220 字节),且 s≤2×106,且保证是合法的压缩数据。

真题来源:解压缩

感兴趣的同学可以如此编码进去进行练习提交

题目理解:

        ブート フィールドとデータ フィールドに分割できる圧縮コードを用意します。ブート フィールドは解凍されたデータの長さを決定し、データ フィールドもセグメント化できます。各セグメントは下位 2 ビットによって決定されます。最初のバイトの 、00 の場合はリテラル値、01 または 10 の場合は後方参照です。解凍されたデータを 1 行 8 バイトで出力します。最後の行は 8 バイト未満を許可します。

アイデア分析:

        バイトは何度も読み取られるため、 バイトを読み取って 現在の読み取り位置を記録する関数をカプセル化するのが最善です。文字列はリトル エンディアン順に調整する必要があるため、文字列をリトル エンディアン順に調整する関数をカプセル化することを検討できます  。01 と 10 は両方とも後方参照で終わるため、関数をカプセル化して 文字列を埋めることもできます 。バイトを扱うのは面倒なので、  stoi() - 符号付き整数 または stoul - 符号なし整数を使用して 16 進数変換を実行できます。

 C++ フルスコア ソリューション:

#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;
int n, idx, p; // 当前已经解压缩了 p 字节,下一个读的是第 idx 下标的字符
string res; // 解压后的数据

string readBytes(int num)
{
    char byte[2 * num];
    for (int i = 0; i < 2 * num; i ++) cin >> byte[i];
    idx += num * 2;
    return string(byte, 2 * num);
}

void trackBack(int o, int l)
{
    int start = res.length() - o * 2;
    int len = o * 2;
    string back_track_string = res.substr(start, len);
    int cnt = 0;
    while (cnt < l * 2 - l * 2 % len)
    {
        res += back_track_string;
        cnt += len;
    }
    res += back_track_string.substr(0, l * 2 % len);
}
int main()
{
    cin >> n;
    string bts;
    vector<int> c;
    int v_c;
    // 读入字节 直到最高位为0
    while ((bts = readBytes(1)) >= "80")
    {
        v_c = stoi(bts, nullptr, 16);
        v_c -= 128;
        c.push_back(v_c);
    }
    // 最高位为0时,直接保存到c里
    v_c = stoi(bts, nullptr, 16);
    c.push_back(v_c);
    // 引导区结束,计算原始数据长度
    int length = 0;
    for (int i = 0; i < c.size(); i ++) length += c[i] * pow(128, i);

    while (idx < n * 2)
    {
        // 接下来是数据域
        // 读入一个字节
        bts = readBytes(1);
        string string_to_binary = bitset<8>(stoi(bts, nullptr, 16)).to_string();
        string lowest_two_digits = string_to_binary.substr(6, 2);
        if (lowest_two_digits == "00")
        {
            string high_six_digits = string_to_binary.substr(0, 6);
            int ll = stoi(high_six_digits, nullptr, 2);
            // l <= 60,高六位 ll 表示 l - 1
            if (ll <= 59)
                res += readBytes(ll + 1);
            else
            {
                // 第一个字节的高六位存储的值为 60、61、62 或 63 时,分别代表 l - 1 用 1、2、3 或 4 个字节表示
                int literal_length = ll - 59;
                // 按照小端序重组字符串 0x01 0x0A => 0x0A01
                string string1 = readBytes(literal_length);
                string string2;
                // 字符串每两位反转
                for (int i = string1.length() - 2; i >= 0; i -= 2)
                    string2 += string1.substr(i, 2);
                int l = 1 + stoi(string2, nullptr, 16); // 字面量长度
                res += readBytes(l);
            }
        }
        else if (lowest_two_digits == "01")
        {
            // 第 2 ~ 4 位即 从下标 3 开始的三位 001 011 01
            string two_to_four_digits = string_to_binary.substr(3, 3);
            // l - 4 占 3 位,存储于首字节的 2 至 4 位中
            int l = stoi(two_to_four_digits, nullptr, 2) + 4;
            // o 占 11 位,其低 8 位存储于随后的字节中,高 3 位存储于首字节的高 3 位中
            string high_three_digits = string_to_binary.substr(0, 3);
            string next_byte_binary = bitset<8>(stoi(readBytes(1), nullptr, 16)).to_string();
            int o = stoi(high_three_digits + next_byte_binary, nullptr, 2);
            // 回溯引用
            trackBack(o, l);
        }
        else if (lowest_two_digits == "10")
        {
            string high_six_digits = string_to_binary.substr(0, 6);
            // l 占 6 位,存储于首字节的高 6 位中
            int l = stoi(high_six_digits, nullptr, 2) + 1;
            // o 占 16 位,以小端序存储于随后的两个字节中
            string string1 = readBytes(2);
            string string2;
            // 字符串每两位反转
            for (int i = string1.length() - 2; i >= 0; i -= 2)
                string2 += string1.substr(i, 2);
            int o = stoi(string2, nullptr, 16);
            // 回溯引用
            trackBack(o, l);
        }
    }
    for (int i = 0; i < res.length(); i ++)
    {
        cout << res[i];
        // 输出,每16个字符加一个换行
        if ((i + 1) % 16 == 0) cout << endl;
    }
    // 若最后一行不能凑8个,则补一个换行
    if (res.length() % 16) cout << endl;
    return 0;
}

 操作結果:

おすすめ

転載: blog.csdn.net/weixin_53919192/article/details/131565864