Markdown 渲染器

按段落和项目列表分块,利用列计数器模拟折行、缩进和空格删除。

OJ: shumeng

题目 ID: CSP202006C

难度:提高+/省选-

标签:字符串模拟解析

日期: 2026-07-31 16:21

形式化题目

按题目规则渲染简化 Markdown 文本:空白行分隔段落和项目列表,段落各行去首尾空格后用一个空格连接;项目以 * 开头,续行以两个空格开头。只输出最终终端占用的行数。

思路

整个模拟拆成两层:先划分输入块,再按列号逐字符渲染。

块划分

  • 空白行:结束当前块,继续扫描;
  • * 开头的行:开始一个项目,其后以两个空格开头的非空白行都是它的续行;
  • 其它非空白行:组成普通段落。

每个新块在渲染前先与上一个块空一行。

字符输出器

把段落或项目的文本行去首尾空格后,用单个空格连接成一段连续文本。输出器维护当前行已写列数 column 和总行数 row_count

  1. 逐字符写入,每写一个字符 column 加一;
  2. column 到达终端宽度 w 时,先删除待写文本开头的连续空格,若仍有字符才另起一行;
  3. 普通段落的新行缩进为 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;
}

复杂度

设输入字符数为 LL。解析和渲染都是线性扫描,时间复杂度为 O(L)O(L),空间复杂度为 O(L)O(L)

总结

这类格式模拟的难点是严格区分输入结构和输出规则。先确定块边界,再由统一的字符输出器处理宽度、缩进和折行,能把各种边界条件从解析逻辑中剥离出来。

图示解析

text
原始行
|- 空白行:结束当前块
|- * 空格:读取项目列表
`- 其它行:读取普通段落
   `- 连接逻辑文本,按列号逐字符渲染并计数

块划分只解释输入属于什么结构;字符输出器只解释如何占用终端列。把这两层分开后,项目的三列缩进只是输出器的一个参数。