Markdown

先按空行划分区块,再递归扫描行内强调和超级链接并输出对应 HTML。

OJ: shumeng

题目 ID: CSP201703C

难度:普及+/提高-

标签:字符串模拟解析

日期: 2026-07-31 16:21

形式化题目

给定一段简化的 Markdown 文档,将其转换为 HTML。文档由标题、无序列表、段落三类区块组成,区块内可能出现强调 _Text_ 与超级链接 [Text](Link),两类行内结构可以互相嵌套,同类结构不嵌套。输入保证语法合法。

思路

转换分两层:先按区块组织输出标签,再对每段文字做行内结构转换。

区块处理

先看一个先按空行显式拆分区块、再逐块转换的朴素写法:

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:48
 */
// brute.cpp:先按空行显式拆分区块,再按语法逐块转换。
#include <bits/stdc++.h>
using namespace std;

string convert_inline(const string &text) {
    string result;
    for (int i = 0; i < (int)text.size();) {
        if (text[i] == '_') {
            int end = i + 1;
            while (text[end] != '_') {
                end++;
            }
            result += "<em>" + convert_inline(text.substr(i + 1, end - i - 1)) + "</em>";
            i = end + 1;
        } else if (text[i] == '[') {
            int text_end = i + 1;
            while (text[text_end] != ']') {
                text_end++;
            }
            int link_end = text_end + 2;
            while (text[link_end] != ')') {
                link_end++;
            }
            string content = convert_inline(text.substr(i + 1, text_end - i - 1));
            string url = text.substr(text_end + 2, link_end - text_end - 2);
            result += "<a href=\"" + url + "\">" + content + "</a>";
            i = link_end + 1;
        } else {
            result += text[i++];
        }
    }
    return result;
}

bool is_heading(const string &line) {
    return !line.empty() && line[0] == '#';
}

bool is_list_item(const string &line) {
    return !line.empty() && line[0] == '*';
}

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

    vector<vector<string> > blocks;
    vector<string> current_block;
    string line;
    while (getline(cin, line)) {
        if (line.empty()) {
            if (!current_block.empty()) {
                blocks.push_back(current_block);
                current_block.clear();
            }
        } else {
            current_block.push_back(line);
        }
    }
    if (!current_block.empty()) {
        blocks.push_back(current_block);
    }

    for (int i = 0; i < (int)blocks.size(); i++) {
        vector<string> &block = blocks[i];
        if (is_heading(block[0])) {
            int level = 0;
            while (block[0][level] == '#') {
                level++;
            }
            int content_start = level;
            while (block[0][content_start] == ' ') {
                content_start++;
            }
            cout << "<h" << level << ">" << convert_inline(block[0].substr(content_start));
            cout << "</h" << level << ">\n";
        } else if (is_list_item(block[0])) {
            cout << "<ul>\n";
            for (int j = 0; j < (int)block.size(); j++) {
                int content_start = 1;
                while (block[j][content_start] == ' ') {
                    content_start++;
                }
                cout << "<li>" << convert_inline(block[j].substr(content_start)) << "</li>\n";
            }
            cout << "</ul>\n";
        } else {
            cout << "<p>";
            for (int j = 0; j < (int)block.size(); j++) {
                if (j > 0) {
                    cout << '\n';
                }
                cout << convert_inline(block[j]);
            }
            cout << "</p>\n";
        }
    }

    return 0;
}

正式做法可以一趟处理输入行。跳过空行后:

  • # 开头的是标题,# 的个数决定等级,输出 <hN>...</hN>
  • 连续以 * 开头的行构成一个无序列表,输出一对 <ul>...</ul>,每行包 <li>...</li>
  • 其余连续非空行构成一个段落,内部换行原样保留,只在首行前加 <p>、末行后加 </p>

空行只负责切分区块,本身不输出任何内容。

行内转换

每段真正的文字交给 convert_inline 处理。从左到右扫描:

  • 遇到 _:向后找配对的 _,递归转换中间文字并包上 <em>
  • 遇到 [:向后找 ]( 和右括号 ),递归转换链接文字,地址保持不变,输出 <a href="Link">Text</a>
  • 其余字符原样保留。

递归自然处理“链接在强调内”或“强调在链接文字内”的嵌套;题目保证语法合法、同类结构不嵌套,因此这种配对扫描没有歧义。

代码

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

// 转换一行文字里的行内结构:_Text_ -> <em>Text</em>,
// [Text](Link) -> <a href="Link">Text</a>。两种结构可以互相嵌套。
// 递归处理内部文字,保证嵌套(链接在强调内、强调在链接文字内)也能正确转换。
string convert_inline(const string &text) {
    string result;
    int length = (int)text.size();
    for (int i = 0; i < length;) {
        if (text[i] == '_') {
            int end = i + 1;
            while (text[end] != '_') {
                end++;
            }
            result += "<em>" + convert_inline(text.substr(i + 1, end - i - 1)) + "</em>";
            i = end + 1;
        } else if (text[i] == '[') {
            int text_end = i + 1;
            while (text[text_end] != ']') {
                text_end++;
            }
            int link_end = text_end + 2;
            while (text[link_end] != ')') {
                link_end++;
            }
            string link_text = convert_inline(text.substr(i + 1, text_end - i - 1));
            string link = text.substr(text_end + 2, link_end - text_end - 2);
            result += "<a href=\"" + link + "\">" + link_text + "</a>";
            i = link_end + 1;
        } else {
            result += text[i];
            i++;
        }
    }
    return result;
}

bool is_heading(const string &line) {
    return !line.empty() && line[0] == '#';
}

bool is_list_item(const string &line) {
    return !line.empty() && line[0] == '*';
}

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

    vector<string> lines;
    string line;
    while (getline(cin, line)) {
        lines.push_back(line);
    }

    int total_lines = (int)lines.size();
    for (int i = 0; i < total_lines;) {
        if (lines[i].empty()) {
            i++; // 空行只分隔区块,不输出任何内容
            continue;
        }

        if (is_heading(lines[i])) {
            // 标题:统计开头的 # 个数得到等级,跳过后面的空格得到标题内容
            int level = 0;
            while (lines[i][level] == '#') {
                level++;
            }
            int content_start = level;
            while (lines[i][content_start] == ' ') {
                content_start++;
            }
            string content = convert_inline(lines[i].substr(content_start));
            cout << "<h" << level << ">" << content << "</h" << level << ">\n";
            i++;
        } else if (is_list_item(lines[i])) {
            // 列表:连续以 * 开头的行属于同一个列表
            cout << "<ul>\n";
            while (i < total_lines && is_list_item(lines[i])) {
                int content_start = 1;
                while (lines[i][content_start] == ' ') {
                    content_start++;
                }
                cout << "<li>" << convert_inline(lines[i].substr(content_start)) << "</li>\n";
                i++;
            }
            cout << "</ul>\n";
        } else {
            // 段落:连续非空行构成一个段落,内部换行原样保留
            cout << "<p>";
            bool first_line = true;
            while (i < total_lines && !lines[i].empty()) {
                if (!first_line) {
                    cout << '\n';
                }
                cout << convert_inline(lines[i]);
                first_line = false;
                i++;
            }
            cout << "</p>\n";
        }
    }

    return 0;
}

复杂度

  • 时间:每个字符只在所在的区块和行内结构中被处理常数次,时间复杂度为 O(L)O(L),其中 LL 为文档总字符数。
  • 空间:递归与中间字符串占用 O(L)O(L) 额外空间。

总结

这类格式转换题要先区分区块规则和行内规则:空行只负责切分区块,不能直接输出;行内结构应先把内部文本转换完毕,再包上外层标签,才能正确处理不同类型的嵌套。