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;
}复杂度
- 时间:每个字符只在所在的区块和行内结构中被处理常数次,时间复杂度为
,其中 为文档总字符数。 - 空间:递归与中间字符串占用
额外空间。
总结
这类格式转换题要先区分区块规则和行内规则:空行只负责切分区块,不能直接输出;行内结构应先把内部文本转换完毕,再包上外层标签,才能正确处理不同类型的嵌套。