元素选择器

构建每个元素的父指针,候选元素沿祖先链从后向前贪心匹配后代选择器。

OJ: shumeng

题目 ID: CSP201809C

难度:普及+/提高-

标签:字符串模拟

日期: 2026-07-31 16:21

形式化题目

给定一棵由 nn 行文本描述的树(.. 缩进表示层级,行号即节点编号),每个节点有标签(大小写不敏感)与可选的 id 属性(大小写敏感)。给出 mm 个选择器,每个选择器由若干段组成:一段是标签(如 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;
}

构建父指针

读入文档时维护每个深度最后出现的元素。当前行深度为 dd,它的父亲就是深度 d1d-1 的最后元素,由此得到每个元素的 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;
}

复杂度

设选择器段数为 kk、树高为 HH。每个查询枚举 nn 个候选元素并在祖先链上扫描,时间复杂度 O(n(H+k))O(n(H+k)),空间复杂度 O(n)O(n)。本题 n100n \le 100,足够直接。

总结

后代选择器不要求相邻父子关系,只要求祖先关系。把缩进文档转成父指针后,查询就变成在一条祖先链上匹配有序子序列;标签和 id 的大小写规则必须分别处理。