解压缩

顺序解析变长字面量和两类回溯引用,直接维护已解压字节流并按偏移逐字节复制。

OJ: shumeng

题目 ID: CSP202305C

难度:普及+/提高-

标签:字符串模拟编码解码双指针

日期: 2026-07-31 16:21

形式化题目

输入一段合法的十六进制压缩数据:导引域以 7 位分组编码给出原始数据长度;数据域由字面量与回溯引用组成,回溯引用 o,l\langle o,l\rangle 表示从当前输出位置向前偏移 oo 处复制 ll 个字节(允许 o<lo<l 的重叠复制)。要求输出完整原始数据,每行 8 个字节。

思路

这是一道按格式逐字节解析的模拟题,关键是区分“描述长度”和“实际复制长度”,并按字节追加回溯结果。

解析导引域

输入的每两个十六进制字符表示一个字节。导引域采用小端序的 7 位分组编码:低 7 位是当前长度部分,最高位为 1 表示后面还有字节。顺序读取并累加即可得到原始长度。

解析数据域

根据元素首字节最低两位区分类型:

  • 00:字面量。高 6 位直接给出 l1l-1;当高 6 位为 60 至 63 时,再读取对应数量(1 至 4 个)的小端字节得到 l1l-1,随后复制 ll 个字面量字节。
  • 01:回溯引用,4l114\le l\le 11。长度为首字节第 2 至 4 位加 4;偏移共 11 位,低 8 位在下一个字节,高 3 位在首字节高 3 位。
  • 10:回溯引用,1l641\le l\le 64。长度为首字节高 6 位加 1;偏移为随后两个小端字节。

压缩数据保证合法,因此不需要处理类型 11 或越界引用。

处理重叠回溯

设当前已输出长度为 pp,偏移为 oo,回溯起点是 output.size()-o。每输出一个字节都从当前输出缓冲区的回溯位置取值并追加,这样 o<lo<l 时后一个字节能读到刚刚复制的内容,自然支持重叠复制,不需要额外的周期串逻辑。

由于原始数据长度最多为 2 MiB,完整保存解压结果是可行的;解析指针和输出指针都只向前移动一次。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:40
 */
#include <bits/stdc++.h>
using namespace std;

int compressed_size;                 // 压缩数据的字节数 s
string encoded;                      // 拼接后的十六进制字符串
vector<unsigned char> bytes;          // 解码后的压缩数据字节
vector<unsigned char> output;        // 解压输出的原始数据字节
int original_size;                   // 导引域解析出的原始数据长度

// 十六进制字符转成数值 0..15
int hex_value(char ch) {
    if ('0' <= ch && ch <= '9') return ch - '0';
    return ch - 'a' + 10;
}

// 解析导引域:小端序 7 位分组,最高位为 1 表示后面还有字节。
// 通过引用回传解析结束后的位置,供数据域继续读取。
int parse_original_size(int &position) {
    int size = 0;
    int shift = 0;
    while (true) {
        int byte = bytes[position++];
        size |= (byte & 127) << shift; // 低 7 位是当前长度部分
        if ((byte & 128) == 0) break;  // 最高位为 0 表示导引域结束
        shift += 7;
    }
    return size;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> compressed_size;
    string line;
    for (int i = 0; i < (compressed_size + 7) / 8; i++) {
        cin >> line;
        encoded += line;
    }

    // 每两个十六进制字符还原成一个字节
    bytes.resize(compressed_size);
    for (int i = 0; i < compressed_size; i++) {
        bytes[i] = (hex_value(encoded[2 * i]) << 4) | hex_value(encoded[2 * i + 1]);
    }

    int position = 0; // 压缩数据的读取位置,先解析导引域,再继续解析数据域
    original_size = parse_original_size(position);
    output.reserve(original_size);

    // 逐个元素解析,直到输出长度达到原始长度
    while ((int)output.size() < original_size) {
        int first = bytes[position++];
        int type = first & 3; // 最低两位决定元素类型

        if (type == 0) {
            // 字面量:高 6 位给出 l-1;为 60..63 时再读 1..4 个字节的小端长度
            int length_code = first >> 2;
            int length;
            if (length_code < 60) {
                length = length_code + 1;
            } else {
                int extra = length_code - 59;
                int encoded_length = 0;
                for (int i = 0; i < extra; i++) {
                    encoded_length |= bytes[position++] << (8 * i);
                }
                length = encoded_length + 1;
            }
            // 直接复制接下来的 length 个字节
            for (int i = 0; i < length; i++) output.push_back(bytes[position++]);
        } else if (type == 1) {
            // 回溯引用:长度 4..11,偏移占 11 位(首字节高 3 位 + 后 1 字节低 8 位)
            int length = ((first >> 2) & 7) + 4;
            int offset = ((first >> 5) << 8) | bytes[position++];
            // 逐字节从 output.size()-offset 处复制,天然支持 o < l 的重叠情况
            for (int i = 0; i < length; i++) output.push_back(output[output.size() - offset]);
        } else {
            // 回溯引用:长度 1..64,偏移为后两个小端字节
            int length = (first >> 2) + 1;
            int offset = bytes[position] | (bytes[position + 1] << 8);
            position += 2;
            int start = (int)output.size() - offset;
            // 重叠时按周期重复复制 offset 个字节
            for (int i = 0; i < length; i++) output.push_back(output[start + i % offset]);
        }
    }

    // 输出解压结果,每行 8 个字节,十六进制小写
    cout << hex << setfill('0');
    for (int i = 0; i < original_size; i++) {
        cout << setw(2) << (int)output[i];
        if ((i + 1) % 8 == 0) cout << '\n';
    }
    if (original_size % 8 != 0) cout << '\n';

    return 0;
}

复杂度

设压缩输入字节数为 ss,原始数据长度为 nn。解析和输出均为线性过程,时间复杂度为 O(s+n)O(s+n),空间复杂度为 O(s+n)O(s+n)

总结

这类 LZ 风格编码的关键是区分“描述长度”与“实际复制长度”,并按字节追加回溯结果。只要每次都从当前输出缓冲区的回溯位置读取,重叠复制就能被自然处理。