补丁应用

严格解析并校验补丁块,再在行号附近按偏移绝对值优先寻找匹配的原文片段并依次替换。

OJ: shumeng

题目 ID: CSP202409C

难度:提高+/省选-

标签:字符串模拟解析

日期: 2026-07-31 16:21

形式化题目

给定一个原文件(nn 行文本)和一段可能损坏的补丁。补丁由若干块组成,块头形如 @@ -NN,MM +nn,mm @@,其后每行以 -+ 或空格开头,分别表示删除、新增或上下文行。

  • 若补丁非法,输出 Patch is damaged.
  • 若合法,按块顺序应用补丁:对每个块,在原文件当前状态下寻找与块原片段完全匹配的位置,要求实际位置与 NN(加上此前累计偏移)之差的绝对值 <MM< MM;多个候选时先选偏移绝对值最小的,再选偏移较小的。找到后替换为块的新片段,继续处理后续块。最终输出应用后的文件。

思路

本题真正的难点在输入解析与格式校验:应用逻辑本身按题述规则逐块搜索替换即可,但任何一处格式错误都要在改动文件前被发现。

解析与校验补丁

  • 读入原文件后,剩余行中以 # 开头的都是注释,直接丢弃;以 @ 开头的行开始一个新的块。
  • 块头必须严格匹配 @@ -NN,MM +nn,mm @@,四个数都是不带前导零的正整数,且要处理溢出。
  • 块内每行必须以 -+ 或空格开头;- 行与空格行构成原片段,+ 行与空格行构成新片段,两片段行数必须等于 MMmm
  • 校验相邻块的原始 NN:后一块的 NN 不小于前一块的 NN+MM,保证块按行号升序且不重叠。

校验全部在修改文件之前完成,发现错误时文件状态不会被破坏。

寻找匹配位置

设此前所有块造成的累计偏移为 offset,当前块的参考位置为 NN + offset。枚举偏移 δ\deltaMM<δ<MM-MM < \delta < MM),候选起点为 NN + offset + delta,需满足:

  • 候选片段不越界;
  • 除第一块外,候选起点不早于上一个实际替换区间的末尾;
  • 文件中的行与块的原片段逐行相等。

按“δ|\delta| 最小,其次 δ\delta 较小”的规则选择最优候选;找不到候选即判定补丁损坏。

替换与维护偏移

找到起点后,从文件行数组中删除原片段并插入新片段。本块实际偏移为 δ\delta,后续块的行号参考值整体加上 δ\delta,用累计变量 offset 维护。

代码

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:39
 */
#include <bits/stdc++.h>
using namespace std;

// 一个补丁块:参考行号 start,原片段 old_part 与替换片段 new_part
struct Block {
    long long start;
    long long old_count;
    long long new_count;
    vector<string> old_part; // 原片段(- 行和上下文行)
    vector<string> new_part; // 新片段(+ 行和上下文行)
};

// 从 text 的 position 处读一个无前导零的正整数,成功则返回 true
bool read_positive_number(const string &text, int &position, long long &value) {
    if (position >= (int)text.size() || text[position] < '1' || text[position] > '9') return false;
    value = 0;
    while (position < (int)text.size() && '0' <= text[position] && text[position] <= '9') {
        int digit = text[position] - '0';
        if (value > (LLONG_MAX - digit) / 10) return false; // 溢出检查
        value = value * 10 + digit;
        position++;
    }
    return true;
}

// 解析块头 "@@ -start,old_count +new_start,new_count @@",格式非法返回 false
bool read_header(const string &line, Block &block) {
    int position = 0;
    if (line.size() < 12 || line[0] != '@' || line[1] != '@' || line[2] != ' ' || line[3] != '-') {
        return false;
    }
    position = 4;
    if (!read_positive_number(line, position, block.start)) return false;
    if (position >= (int)line.size() || line[position] != ',') return false;
    position++;
    if (!read_positive_number(line, position, block.old_count)) return false;
    if (position >= (int)line.size() || line[position] != ' ') return false;
    position++;
    if (position >= (int)line.size() || line[position] != '+') return false;
    position++;
    long long ignored_new_start;
    if (!read_positive_number(line, position, ignored_new_start)) return false;
    if (position >= (int)line.size() || line[position] != ',') return false;
    position++;
    if (!read_positive_number(line, position, block.new_count)) return false;
    if (position + 3 != (int)line.size()) return false;
    if (line[position] != ' ' || line[position + 1] != '@' || line[position + 2] != '@') {
        return false;
    }
    return true;
}

// 判断文件中从 start 行开始的若干行是否等于 part 片段
bool same_lines(const vector<string> &file, long long start, const vector<string> &part) {
    for (int i = 0; i < (int)part.size(); i++) {
        if (file[(int)start - 1 + i] != part[i]) return false;
    }
    return true;
}

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

    int n;
    cin >> n;
    string line;
    getline(cin, line);
    vector<string> file(n);
    for (int i = 0; i < n; i++) getline(cin, file[i]);

    // 读取补丁:以 # 开头的行是注释,以 @ 开头的行开始一个新的块
    vector<vector<string> > raw_blocks;
    while (getline(cin, line)) {
        if (!line.empty() && line[0] == '#') continue;
        if (!line.empty() && line[0] == '@') {
            raw_blocks.push_back(vector<string>());
            raw_blocks.back().push_back(line);
        } else if (!raw_blocks.empty()) {
            raw_blocks.back().push_back(line);
        }
    }

    // 解析并校验每个块:头格式、行内容前缀、片段行数、相邻块不重叠
    bool damaged = raw_blocks.empty();
    vector<Block> blocks;
    if (!damaged) {
        for (int i = 0; i < (int)raw_blocks.size(); i++) {
            Block block;
            if (!read_header(raw_blocks[i][0], block)) {
                damaged = true;
                break;
            }
            for (int j = 1; j < (int)raw_blocks[i].size(); j++) {
                const string &content = raw_blocks[i][j];
                if (content.empty() ||
                    (content[0] != '-' && content[0] != '+' && content[0] != ' ')) {
                    damaged = true;
                    break;
                }
                if (content[0] != '+') block.old_part.push_back(content.substr(1));
                if (content[0] != '-') block.new_part.push_back(content.substr(1));
            }
            if (damaged) break;
            if (block.old_count != (long long)block.old_part.size() ||
                block.new_count != (long long)block.new_part.size()) {
                damaged = true;
                break;
            }
            if (!blocks.empty()) {
                if (blocks.back().start > LLONG_MAX - blocks.back().old_count ||
                    block.start < blocks.back().start + blocks.back().old_count) {
                    damaged = true;
                    break;
                }
            }
            blocks.push_back(block);
        }
    }

    // 依次应用每个块:在参考行号附近按规则寻找匹配位置并替换
    long long offset = 0;       // 此前所有块造成的累计行号偏移
    long long previous_end = 0; // 上一个块实际替换区间的结束行
    for (int i = 0; i < (int)blocks.size() && !damaged; i++) {
        Block &block = blocks[i];
        if (block.start > LLONG_MAX - offset) {
            damaged = true;
            break;
        }
        long long requested_start = block.start + offset;
        long long radius = block.old_count - 1;

        // 枚举偏移 delta,寻找匹配原片段的最优位置
        bool found = false;
        long long best_delta = 0;
        long long best_start = 0;
        for (long long delta = -radius; delta <= radius; delta++) {
            if ((delta > 0 && requested_start > LLONG_MAX - delta) ||
                (delta < 0 && requested_start < LLONG_MIN - delta)) {
                continue;
            }
            long long candidate = requested_start + delta;
            if (candidate < 1 || candidate > (long long)file.size() - (long long)block.old_part.size() + 1) {
                continue;
            }
            if (i > 0 && candidate < previous_end) continue; // 不能与上一个替换区间重叠
            if (!same_lines(file, candidate, block.old_part)) continue;
            // 选 |delta| 最小者,相同时选 delta 较小者
            if (!found || llabs(delta) < llabs(best_delta) ||
                (llabs(delta) == llabs(best_delta) && delta < best_delta)) {
                found = true;
                best_delta = delta;
                best_start = candidate;
            }
        }
        if (!found) {
            damaged = true;
            break;
        }

        // 用新片段替换原片段
        int erase_begin = (int)best_start - 1;
        file.erase(file.begin() + erase_begin,
                   file.begin() + erase_begin + (int)block.old_part.size());
        file.insert(file.begin() + erase_begin, block.new_part.begin(), block.new_part.end());
        if (offset > LLONG_MAX - best_delta) {
            damaged = true;
            break;
        }
        offset += best_delta;
        previous_end = best_start + (long long)block.old_part.size();
    }

    if (damaged) {
        cout << "Patch is damaged.\n";
        return 0;
    }
    for (int i = 0; i < (int)file.size(); i++) cout << file[i] << '\n';
    return 0;
}

复杂度

设原文件 nn 行、补丁块数 BB、块内最大原片段长度 LL

  • 时间:每块最多尝试 2MM12MM-1 个偏移,每次比较 MMMM 行,总复杂度 O(MM2+nB)O(\sum MM^2 + nB),在 n2000n \le 2000、块数 25\le 25 的范围内很小。
  • 空间:文件行数组与所有块片段,O(n+补丁总长度)O(n + \text{补丁总长度})

总结

补丁应用不是按头部行号直接替换:必须先把语法、计数和块顺序全部校验通过,再在允许的偏移范围内按规则寻找上下文匹配。把每个块保存为“原片段—新片段—参考行号”,用累计偏移顺序模拟,就能完整实现题目规定的宽松 patch 行为。