Markdown 渲染器
按段落和项目列表分块,利用列计数器模拟折行、缩进和空格删除。
OJ: shumeng
题目 ID: CSP202006C
难度:提高+/省选-
标签:字符串模拟解析
日期: 2026-07-31 16:21
形式化题目
按题目规则渲染简化 Markdown 文本:空白行分隔段落和项目列表,段落各行去首尾空格后用一个空格连接;项目以 * 开头,续行以两个空格开头。只输出最终终端占用的行数。
思路
整个模拟拆成两层:先划分输入块,再按列号逐字符渲染。
块划分
- 空白行:结束当前块,继续扫描;
*开头的行:开始一个项目,其后以两个空格开头的非空白行都是它的续行;- 其它非空白行:组成普通段落。
每个新块在渲染前先与上一个块空一行。
字符输出器
把段落或项目的文本行去首尾空格后,用单个空格连接成一段连续文本。输出器维护当前行已写列数 column 和总行数 row_count:
- 逐字符写入,每写一个字符
column加一; - 当
column到达终端宽度w时,先删除待写文本开头的连续空格,若仍有字符才另起一行; - 普通段落的新行缩进为 0;项目首行与续行都从第 3 列开始,对应
·或三个空格。
代码
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;
// 判断一行是否只由空格组成(空白行分隔段落和项目列表)
bool is_blank(const string &text) {
for (int i = 0; i < (int)text.size(); i++) {
if (text[i] != ' ') return false;
}
return true;
}
// 判断一行是否是项目首行:以 "* " 开头
bool is_item_start(const string &text) {
return text.size() >= 2 && text[0] == '*' && text[1] == ' ';
}
// 判断一行是否是项目续行:以两个空格开头且非空白行
bool is_item_continuation(const string &text) {
return text.size() >= 2 && text[0] == ' ' && text[1] == ' ' && !is_blank(text);
}
// 去掉一行首尾的连续空格
string trim(const string &text) {
int left = 0, right = (int)text.size() - 1;
while (left <= right && text[left] == ' ') left++;
while (left <= right && text[right] == ' ') right--;
return text.substr(left, right - left + 1);
}
// 把一个块的所有文本行去首尾空格后,用单个空格连接成一段文本
string join_lines(const vector<string> &line) {
string text;
for (int i = 0; i < (int)line.size(); i++) {
string part = trim(line[i]);
if (part.empty()) continue;
if (!text.empty()) text += ' ';
text += part;
}
return text;
}
// 终端输出器:维护当前行已写列数和总行数,负责折行与缩进
struct Painter {
int width, row_count = 0, column = 0;
// 另起一行,行首缩进 indent 个列
void start_line(int indent) {
row_count++;
column = indent;
}
// 每个新块与之前的块之间空一行
void start_block() {
if (row_count > 0) row_count++;
}
// 逐字符写入文本,写满宽度时先删除待写开头空格,再折行
void write_text(const string &text, int first_indent, int next_indent) {
int position = 0;
start_line(first_indent);
while (position < (int)text.size()) {
if (column == width) {
// 折行前删除待写文本开头的连续空格
while (position < (int)text.size() && text[position] == ' ') position++;
if (position == (int)text.size()) break;
start_line(next_indent);
}
column++;
position++;
}
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
Painter painter;
cin >> painter.width;
string line;
getline(cin, line); // 读掉宽度数字后的换行
vector<string> source;
while (getline(cin, line)) source.push_back(line);
int position = 0;
while (position < (int)source.size()) {
if (is_blank(source[position])) {
position++;
continue;
}
painter.start_block();
// 普通段落:连续的、非空白、非项目首行的行
if (!is_item_start(source[position])) {
vector<string> paragraph;
while (position < (int)source.size() && !is_blank(source[position])
&& !is_item_start(source[position])) {
paragraph.push_back(source[position++]);
}
painter.write_text(join_lines(paragraph), 0, 0);
continue;
}
// 项目列表:连续的 "* " 开头行,各自跟随两个空格开头的续行
while (position < (int)source.size() && is_item_start(source[position])) {
vector<string> item;
item.push_back(source[position++].substr(2)); // 去掉行首的 "* "
while (position < (int)source.size() && is_item_continuation(source[position])) {
item.push_back(source[position++].substr(2)); // 去掉开头的两个空格
}
painter.write_text(join_lines(item), 3, 3);
}
}
cout << painter.row_count << '\n';
return 0;
}复杂度
设输入字符数为
总结
这类格式模拟的难点是严格区分输入结构和输出规则。先确定块边界,再由统一的字符输出器处理宽度、缩进和折行,能把各种边界条件从解析逻辑中剥离出来。
图示解析
text
原始行
|- 空白行:结束当前块
|- * 空格:读取项目列表
`- 其它行:读取普通段落
`- 连接逻辑文本,按列号逐字符渲染并计数块划分只解释输入属于什么结构;字符输出器只解释如何占用终端列。把这两层分开后,项目的三列缩进只是输出器的一个参数。