严格解析并校验补丁块,再在行号附近按偏移绝对值优先寻找匹配的原文片段并依次替换。
OJ: shumeng
题目 ID: CSP202409C
难度:提高+/省选-
标签:字符串模拟解析
日期: 2026-07-31 16:21
形式化题目
给定一个原文件(@@ -NN,MM +nn,mm @@,其后每行以 -、+ 或空格开头,分别表示删除、新增或上下文行。
- 若补丁非法,输出
Patch is damaged.; - 若合法,按块顺序应用补丁:对每个块,在原文件当前状态下寻找与块原片段完全匹配的位置,要求实际位置与
NN(加上此前累计偏移)之差的绝对值;多个候选时先选偏移绝对值最小的,再选偏移较小的。找到后替换为块的新片段,继续处理后续块。最终输出应用后的文件。
思路
本题真正的难点在输入解析与格式校验:应用逻辑本身按题述规则逐块搜索替换即可,但任何一处格式错误都要在改动文件前被发现。
解析与校验补丁
- 读入原文件后,剩余行中以
#开头的都是注释,直接丢弃;以@开头的行开始一个新的块。 - 块头必须严格匹配
@@ -NN,MM +nn,mm @@,四个数都是不带前导零的正整数,且要处理溢出。 - 块内每行必须以
-、+或空格开头;-行与空格行构成原片段,+行与空格行构成新片段,两片段行数必须等于MM与mm。 - 校验相邻块的原始
NN:后一块的NN不小于前一块的NN+MM,保证块按行号升序且不重叠。
校验全部在修改文件之前完成,发现错误时文件状态不会被破坏。
寻找匹配位置
设此前所有块造成的累计偏移为 offset,当前块的参考位置为 NN + offset。枚举偏移 NN + offset + 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;
}复杂度
设原文件
- 时间:每块最多尝试
个偏移,每次比较 行,总复杂度 ,在 、块数 的范围内很小。 - 空间:文件行数组与所有块片段,
。
总结
补丁应用不是按头部行号直接替换:必须先把语法、计数和块顺序全部校验通过,再在允许的偏移范围内按规则寻找上下文匹配。把每个块保存为“原片段—新片段—参考行号”,用累计偏移顺序模拟,就能完整实现题目规定的宽松 patch 行为。