构建每个元素的父指针,候选元素沿祖先链从后向前贪心匹配后代选择器。
OJ: shumeng
题目 ID: CSP201809C
难度:普及+/提高-
标签:树字符串模拟栈
日期: 2026-07-31 16:21
形式化题目
给定一棵由 .. 缩进表示层级,行号即节点编号),每个节点有标签(大小写不敏感)与可选的 id 属性(大小写敏感)。给出 p)或 id(如 #main),多段之间用空格分隔表示后代关系。对每个选择器,按行号从小到大输出所有被选中的节点行号。
思路
朴素做法
为每个元素显式收集从根到它的祖先链,再从根向下顺序匹配选择器前面的各段,最后检查元素自身是否匹配最后一段。逻辑直观但每次都要重建一条链。
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:41
*/
// brute.cpp:小数据暴力解,为每个元素显式收集祖先链后,按选择器从根向下匹配。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n, m;
string tag[MAXN], id[MAXN];
int parent[MAXN];
string to_lower_string(string value) {
for (int i = 0; i < (int)value.size(); i++) {
value[i] = (char)tolower((unsigned char)value[i]);
}
return value;
}
bool match_part(int node, const string &part) {
if (part[0] == '#') {
return id[node] == part.substr(1);
}
return tag[node] == part;
}
// 把当前元素的祖先链(根到它)显式收集出来,再从根开始顺序匹配选择器各段。
bool match_selector(int node, const vector<string> &parts) {
vector<int> chain;
for (int current = node; current != -1; current = parent[current]) {
chain.push_back(current);
}
reverse(chain.begin(), chain.end());
// 从根向下尽量匹配前面的各段,最后一段要求当前节点自己匹配。
int need = 0;
for (int i = 0; i < (int)chain.size() - 1 && need + 1 < (int)parts.size(); i++) {
if (match_part(chain[i], parts[need])) {
need++;
}
}
return need + 1 == (int)parts.size() && match_part(node, parts.back());
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
string line;
getline(cin, line);
int last_at_depth[MAXN];
fill(last_at_depth, last_at_depth + MAXN, -1);
for (int i = 1; i <= n; i++) {
getline(cin, line);
int depth = 0;
while (line[2 * depth] == '.') {
depth++;
}
int start = 2 * depth;
int space = (int)line.find(' ', start);
if (space == -1) {
tag[i] = to_lower_string(line.substr(start));
} else {
tag[i] = to_lower_string(line.substr(start, space - start));
id[i] = line.substr(space + 2);
}
parent[i] = depth == 0 ? -1 : last_at_depth[depth - 1];
last_at_depth[depth] = i;
}
for (int query = 1; query <= m; query++) {
getline(cin, line);
stringstream input(line);
vector<string> parts;
string part;
while (input >> part) {
if (part[0] != '#') {
part = to_lower_string(part);
}
parts.push_back(part);
}
vector<int> answer;
for (int node = 1; node <= n; node++) {
if (match_selector(node, parts)) {
answer.push_back(node);
}
}
cout << answer.size();
for (int i = 0; i < (int)answer.size(); i++) {
cout << ' ' << answer[i];
}
cout << '\n';
}
return 0;
}构建父指针
读入文档时维护每个深度最后出现的元素。当前行深度为 parent 指针。标签与标签选择器全部转为小写;id 保留原样以满足大小写敏感规则。
沿祖先链从后向前贪心匹配
对选择器 A B C,枚举每个元素作为候选 C。候选先匹配 C,然后沿父指针向上找最近的 B,再继续向上找 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:41
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n, m;
string tag[MAXN], id[MAXN]; // 每个元素的小写标签与 id 属性
int parent[MAXN]; // 每个元素的父元素行号,根元素为 -1
// 标签统一转小写,id 保持原样。
string to_lower_string(string value) {
for (int i = 0; i < (int)value.size(); i++) {
value[i] = (char)tolower((unsigned char)value[i]);
}
return value;
}
// 判断节点 node 是否匹配选择器的一段:'#' 开头是 id 选择器,否则是标签选择器。
bool match_part(int node, const string &part) {
if (part[0] == '#') {
return id[node] == part.substr(1);
}
return tag[node] == part;
}
// 判断节点 node 是否被整个后代选择器选中,从最后一段向根贪心匹配。
bool match_selector(int node, const vector<string> &parts) {
int last = (int)parts.size() - 1;
if (!match_part(node, parts[last])) {
return false;
}
node = parent[node];
for (int index = last - 1; index >= 0; index--) {
// 沿祖先链向上找最近一段能匹配的祖先。
while (node != -1 && !match_part(node, parts[index])) {
node = parent[node];
}
if (node == -1) {
return false;
}
node = parent[node];
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
string line;
getline(cin, line);
int last_at_depth[MAXN]; // 每个缩进深度最后出现的元素行号
fill(last_at_depth, last_at_depth + MAXN, -1);
// 读入文档:缩进层级决定父子关系。
for (int i = 1; i <= n; i++) {
getline(cin, line);
int depth = 0;
while (line[2 * depth] == '.') {
depth++;
}
int start = 2 * depth;
int space = (int)line.find(' ', start);
if (space == -1) {
tag[i] = to_lower_string(line.substr(start));
} else {
tag[i] = to_lower_string(line.substr(start, space - start));
id[i] = line.substr(space + 2);
}
parent[i] = depth == 0 ? -1 : last_at_depth[depth - 1];
last_at_depth[depth] = i;
}
for (int query = 1; query <= m; query++) {
getline(cin, line);
stringstream input(line);
vector<string> parts;
string part;
while (input >> part) {
if (part[0] != '#') {
part = to_lower_string(part);
}
parts.push_back(part);
}
// 枚举每个元素作为匹配的最后一段。
vector<int> answer;
for (int node = 1; node <= n; node++) {
if (match_selector(node, parts)) {
answer.push_back(node);
}
}
cout << answer.size();
for (int i = 0; i < (int)answer.size(); i++) {
cout << ' ' << answer[i];
}
cout << '\n';
}
return 0;
}复杂度
设选择器段数为
总结
后代选择器不要求相邻父子关系,只要求祖先关系。把缩进文档转成父指针后,查询就变成在一条祖先链上匹配有序子序列;标签和 id 的大小写规则必须分别处理。