CCF CSP题解:解压缩(202305-3)

本文介绍了一个针对压缩文件的解码器实现,主要关注将业务需求转化为代码,包括处理literal和recall操作,以及处理大小端问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目背景(OJ链接

西西艾弗岛运营公司是一家负责维护和运营岛上基础设施的大型企业。在公司内,有许多分管不同业务的部门都需要使用到服务器设施。为了便于管理,同时降低公司运行成本, 西西艾弗岛运营公司建设了一套私有云系统。这套私有云系统除了能提供托管的虚拟机服务外,还能提供一些其他的服务。其中,最受好评的当属日志服务。此前,各个业务系统的日志都是分散存放在各自的服务器上的, 这样不仅不方便查看和分析而且也有丢失的风险。而日志服务则能够将各个业务系统的日志统一收集起来,方便查看和管理。

日志服务器收集到的日志都是纯文本,且高度格式化。这意味着日志数据可以被压缩得非常小。但是日志数据量非常大,且对处理效率的要求较高,因此可以牺牲一定的压缩率,使用高效的压缩算法来压缩日志数据。 小 C 被安排来实现解压缩日志的程序,给定一段被压缩的日志数据,他需要将其解压缩。

题目描述

这种压缩算法产生的数据流,可以被视为是一系列的元素。元素分为两种:字面量和回溯引用。字面量包含一系列的字节,对其进行解压缩时,直接将这些字节输出即可。 回溯引用则是将此前已经解压缩得到的数据流的一部分重复输出。回溯引用可以表示为 ⟨ o , l ⟩ \langle o,l \rangle o,l,包含两个数字,分别为偏移量 o o o 和长度 l l l。 偏移量表示从当前位置向前回溯的距离,长度表示需要重复输出的字节数。其中要求 o , l > 0 o,l>0 o,l>0。若已经解压缩了 p p p 字节,当 o ≥ l o≥l ol 时, 表示重复输出自偏移量 ( p − o ) (p−o) (po)(首个字节偏移量为 0)开始的 l l l 个字节。例如,已经解压缩的数据流是 abcde,则回溯引用 ⟨ 3 , 2 ⟩ ⟨3,2⟩ 3,2 表示输出 cd。 当 o < l o<l o<l 时,表示重复输出自偏移量 ( p − o ) (p−o) (po) 开始的的 o o o 个字节,然后继续反复输出这 o o o 个字节,直到共输出 l l l 个字节。例如,已经解压缩的数据流是 abcde, 则回溯引用 ⟨ 2 , 5 ⟩ ⟨2,5⟩ 2,5 表示输出 deded

被压缩的数据格式分为两部分:导引域和数据域。其中导引域保存了原始数据的长度。设原始数据长度为 n n n。则 n n n 可以表示为 ∑ k = 0 d c k × 12 8 k ∑ _{k=0}^d c_k ×128 ^k k=0dck×128k,其中 0 ≤ c k < 128 0≤c_k<128 0ck<128,且 c d ≠ 0 c_d \ne 0 cd=0。导引域的长度为 ( d + 1 ) (d+1) (d+1) 字节,依次保存 c 0 + 128 , c 1 + 128 , ⋯ , c d − 1 + 128 , c d c_0 +128,c_1 +128,⋯,c_{ d−1} +128,c_ d c0+128,c1+128,,cd1+128,cd。即每个字节用低 7 位保存 c k c_k ck,除了最后一个字节的最高位为 0, 其余字节的最高位为 1。例如,原始数据的长度为 1324,那么$
c_k$依次为:44、10,即 16 进制的 0x2C0x0A。因此引导区的长度为 2,字节序列为 0xAC 0x0A
在这里插入图片描述
数据域保存了被压缩后的数据,是连续存储的元素的序列。每个元素的第一个字节的最低两位表示了元素的类型。当最低两位为 0 时,表示这是一个字面量。如果字面量包含的字节个数为 l l l,且 l ≤ 60 l≤60 l60, 那么第一个字节的高 6 位表示 ( l − 1 ) (l−1) (l1)。随后的 l l l 字节即为字面量所包含的原始字节。例如字节 0xE8,其二进制为 1110 1000,低二位是 0,表示这是一个字面量。高六位是 111010,表示数字 58, 即表示该字面量包含 59 个字节。因此,该字节后面的 59 个字节即为字面量所包含的原始字节。如果 l > 60 l>60 l>60,那么则将 ( l − 1 ) (l−1) (l1) 用小端序的 1 至 4 个字节表示,存储于第一个字节的后面。 第一个字节的高六位存储的值为 60、61、62 或 63 时,分别代表 ( l − 1 ) (l−1) (l1) 用 1、2、3 或 4 个字节表示。例如,字节序列 0xF4 0x01 0x0A 中,首字节的二进制为 1111 0100,低二位是 0,表示这是一个字面量。 高 6 位是 111101,表示数字 61,即表示随后有两个字节存储了字面量的长度。随后的两个字节 0x01 0x0A,按小端序组成了十六进制数 0x0A01,即十进制 2561,表示该字面量包含 2562 个字节。 随后的 2562 个字节即为字面量所包含的原始字节。
在这里插入图片描述
在这里插入图片描述
当元素首字节的最低两位是 01 时,表示这是一个回溯引用 ⟨ o , l ⟩ ⟨o,l⟩ o,l,且 4 ≤ l ≤ 11 , 0 < o ≤ 2047 4≤l≤11,0<o≤2047 4l11,0<o2047。此时, o o o 占 11 位,其低 8 位存储于随后的字节中, 高 3 位存储于首字节的高 3 位中。 ( l − 4 ) (l−4) (l4) 占 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−4)=3 (l4)=3,即 l = 7 l=7 l=7。 其高 3 位是 001,与随后的字节 0x1A 组成了十六进制数 0x11A,即十进制 282,表示 o = 282 o=282 o=282。因此,该回溯引用是 ⟨ 282 , 7 ⟩ ⟨282,7⟩ 282,7
在这里插入图片描述
当元素首字节的最低两位是 10 时,表示这是一个回溯引用 ⟨ o , l ⟩ ⟨o,l⟩ o,l,且 1 ≤ l ≤ 64 , 0 < o ≤ 65535 1≤l≤64,0<o≤65535 1l64,0<o65535。此时, o o o 占 16 位,以小端序存储于随后的两个字节中。 ( l − 1 ) (l−1) (l1) 占 6 位,存储于首字节的高 6 位中。例如,字节 0x3E 0x1A 0x01,其首字节的二进制为 0011 1110,其最低两位为 10,表示这是一个回溯引用,其中高 6 位是 001111,表示数字 15, 即 ( l − 1 ) = 15 (l−1)=15 (l1)=15,即 l = 16 l=16 l=16。随后的两个字节 0x1A 0x01,按小端序组成了十六进制数 0x011A,即十进制 282,表示 o = 282 o=282 o=282。因此,该回溯引用是 ⟨ 282 , 16 ⟩ ⟨282,16⟩ 282,16
在这里插入图片描述

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

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

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

输入格式

从标准输入读入数据。

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

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

输出格式

输出到标准输出。

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

样例

样例1输入

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

样例1输出

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

样例1解释

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

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

首先读入第一字节 80,其最高位为 1,于是继续读入第二字节 01,其最高位为 0,因此读入引导区结束。得到 c 0 = 0 , c 1 = 1 c_0 =0, c_1=1 c0=0,c1=1, 原始数据长度为: 0 × 12 8 0 + 1 × 12 8 1 = 128 0×128^0 + 1 × 128^1 =128 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 l l 是 50。 随后继续读入两个字节 0300,按小端序组成十六进制数 0x0003,即十进制 3,表示回溯引用的偏移量 o o o 是 3。因此,这个回溯引用是 ⟨ 3 , 50 ⟩ ⟨3,50⟩ 3,50。 由于 50 = 16 × 3 + 2 50=16×3+2 50=16×3+2,将此时缓冲区中最后三个字节 0d 0e 0f 重复输出 16 次,然后继续输出 0d 0e,补足共 50 个字节。

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

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

子任务

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

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

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

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

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

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

题解

文件压缩涉及到的编解码,贯穿了计算机计算、存储和通信的方方面面。本题要为2MB以内的压缩文件写解码器,给出的需求很明确,不存在太多的复杂度优化的空间,主要的难度是将业务需求仔细实现。由于位操作并非经常涉及,所以可能因大小端问题或粗心而出错。

AC代码

本文提供满分代码。使用CPP14即可编译。
在这里插入图片描述

#include <iostream>
#include <vector>
#include <cmath>

using namespace std;

vector<unsigned char> bin_code;
string input;
unsigned int cur_idx = 0;
string output;

void print_output() {
    for (int i = 0; i < ceil(output.length() / 16.0); i++) {
        cout << output.substr(16 * i, 16) << endl;
    }
}

void init() {
    int s;
    cin >> s;
    bin_code.resize(s);
    for (int i = 0; i < (ceil(s / 8.0)); ++i) {
        string line;
        cin >> line;
        input.append(line);
    }
    for (int i = 0; i < input.length(); i += 2) {
        char tmp[3];
        tmp[0] = input[i];
        tmp[1] = input[i + 1];
        tmp[2] = 0;
        unsigned v;
        sscanf(tmp, "%x", &v);
        bin_code[i / 2] = v;
    }
}

void lead_in() {
    while (bin_code[cur_idx++] & 0x80);
//     cout << cur_idx << endl;
}

void literal_handle() {
//    cout << "start literal handle: cur_idx is " << cur_idx << endl;
    unsigned l = bin_code[cur_idx++] >> 2;
    if (l < 60) {
        l++;
    } else {
        int tmp = (int) l - 59;
        l = 0;
        for (int i = 0; i < tmp; i++) {
            l = l | (bin_code[cur_idx++] << (8 * i));
        }
        l++;
    }
    output.append(input.substr(cur_idx * 2, l * 2));
    cur_idx += l;
//    print_output();
//    cout << "---------" << endl;
//    cout << "end literal handle: cur_idx is " << cur_idx << endl;
}

void recall_handle() {
    unsigned type = bin_code[cur_idx] & 0x3;
//    cout << "start recall handle, type: " << type << endl;
    unsigned l = 0, o = 0;
    if (0x1 == type) {
        l = ((bin_code[cur_idx] >> 2) & 0x7) + 4;
        o = bin_code[cur_idx] & 0xe0;
        o = o << 3;
        o |= bin_code[++cur_idx];
    } else {
        l = (bin_code[cur_idx] >> 2) + 1;
        o |= bin_code[++cur_idx];
        o |= (bin_code[++cur_idx] << 8);
    }
    unsigned cur_len = output.length();
    for (unsigned i = cur_len - o * 2; l--;) {
        output.push_back(output[i]);
        output.push_back(output[i + 1]);
        i = i + 2;
        if (i == cur_len)
            i = cur_len - o * 2;
    }
    cur_idx++;
//    print_output();
//    cout << "---------" << endl;
}


void decode_data() {
    while (cur_idx < bin_code.size()) {
        unsigned type = bin_code[cur_idx] & 0x3;
        switch (type) {
            case 0x0:
                literal_handle();
                break;
            case 0x1:
            case 0x2:
                recall_handle();
                break;
            default:
                cout << "?????";
                exit(EXIT_FAILURE);
                break;
        }
    }
}

int main() {
    init();
    lead_in();
    decode_data();
    print_output();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

曹无悔

请支持我的梦想!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值