1 条题解

  • 0
    @ 2026-5-8 16:58:28

    问题重述

    我们有 4 种操作:

    1. add id pattern:向集合中插入一个模式串,ID 唯一。
    2. delete id:删除对应 ID 的模式串。
    3. append s:将字符串 ss 追加到当前文本 tt 末尾。
    4. backspace c:从 tt 末尾删除 cc 个字符(若 cc 大于 t|t|,则清空 tt)。

    每次操作后需要输出一个满足条件的模式 ID,条件按优先级:

    • 模式串以 tt 为前缀
    • 长度最长
    • 字典序最小
    • ID 最小
      如果没有匹配,输出 1-1

    关键难点

    1. 查询前缀匹配:要在所有模式中快速找到以 tt 为前缀的串。
    2. 插入/删除:需要动态维护集合。
    3. 排序规则:不是简单地找最长,还要字典序、ID。
    4. n106n \le 10^6,总字符数 2×106\le 2\times10^6,所以必须用 O(len)O(\text{len})O(logn)O(\log n) 的复杂度。

    解法思路

    1. 前缀匹配 → Trie 树

    所有模式串插入到 Trie 中。每个节点维护一个“最佳候选模式”的信息,即:当当前文本 tt 走到该节点时,应该选择的模式 ID。

    但是,由于 tt 在变化,我们不可能每次都从根重新走,因为 tt 可能很长。我们可以利用 Trie 的路径,每次 tt 变化时,更新当前节点的指针。

    2. 如何快速获取最佳候选?

    在每个节点上,我们需要知道:

    • 所有经过该节点的模式中,最长字典序最小ID 最小 的那个。

    由于模式全部在 Trie 上,每个模式对应 Trie 的一个结束节点(可能不是叶子)。我们要维护每个节点的一个候选 ID,这个候选 ID 是以该节点为前缀的所有模式中最优的一个

    规则合并

    • 比较两个模式:先比长度,长度长的优先;长度相同比字典序(可以用模式串本身比较),字典序小的优先;再比 ID 小的优先。

    3. 动态维护候选集

    因为模式会 增删,我们需要在 Trie 节点上维护一个数据结构,能快速得到当前以该节点为前缀的模式中的最优者。

    可以这样设计:

    • 每个节点维护一个 map<int, TreeSet> 按长度分组?但字典序需要模式本身比较,太麻烦。
    • 更简单:直接用 multiset 存储 (len, pattern, id),按自定义比较规则排序。每次插入时,将模式加入路径上所有节点的 multiset;删除时移除。每次询问时,取 multiset 的第一个元素即可。

    但这样每个节点都要存一个 multiset,节点总数约 2×1062\times10^6,multiset 开销很大,会 MLE。必须优化。


    4. 优化:只在模式结束节点维护信息,向上传递

    因为“以节点 uu 为前缀的所有模式” 等价于uu 为根的子树中所有结束节点对应的模式。
    所以我们只需要在每个结束节点存储模式信息,然后查询一个节点时,需要从它往下找最优的模式。

    但这样查询时又要遍历子树,不行。

    反过来:每个节点只存它 子树中的最优模式,这样查询当前节点时就直接得到答案。这个最优模式可以通过 从下往上合并 来维护:

    • 当插入一个模式到结束节点 vv 时,向上更新所有祖先的最优候选项。
    • 当删除时,从 vv 向上更新,可能需要重新计算(比较儿子中的最优)。

    每个节点存一个“最优模式 ID”,更新时比较:

    • 儿子中的最优
    • 本节点结束的模式(如果有)

    比较规则:先比长度(通过事先保存模式长度),长度相同比模式串(需要存一下模式串),再比 ID。


    5. 实现细节

    每个节点需要:

    • best_id:当前子树中最优的模式 ID(如果没有,为 -1)
    • len:该模式长度(快速访问)
    • pattern:该模式字符串(用于字典序比较)
    • children[256]:因为字符是任意可打印 ASCII 33~126,共 94 种,用数组或 map

    我们需要快速知道某个模式的长度和字符串,用数组 pat_len[id]pat_str[id]

    更新节点最优:

    best_id[u] = max( best_id[child], end_id[u] )  // 按题目规则比较
    

    比较函数 is_better(a, b)

    • len[a] > len[b] → a 优
    • len[a] == len[b]str[a] < str[b] → a 优
    • 相同 → a < b → a 优

    6. 操作处理

    • add id s:生成新模式,将其插入 Trie,在结束节点 end_id[u] = id,然后向上更新祖先的 best。
    • delete id:找到对应节点,清除 end_id[u],向上更新祖先的 best(可能需要重新计算)。
    • append s:将 s 追加到当前文本 t,然后从当前 Trie 节点往下走,如果某字符找不到子节点,则当前节点变为 NULL,以后所有操作返回 -1,直到 backspace 回到有子节点的位置。
    • backspace c:从文本末尾删 c 个字符,同时 Trie 节点指针回退(可以存路径栈)。

    注意:appendbackspace 后,当前文本可能没有模式匹配,此时输出 -1。


    7. 复杂度

    • 插入/删除:O(len×updates)O(\text{len} \times \text{updates}),每次更新只沿路径向上,最长路径长度 ≤ 总字符数,均摊 O(1) 更新。
    • 查询:O(1)O(1) 从当前节点 best_id 获取。
    • 总复杂度 O((n+总长度)×小常数)O((n + \text{总长度}) \times \text{小常数}),可过。

    C++ 实现

    #include <bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 2e6 + 5;   // 总节点数
    const int ALPH = 94;        // 33~126
    
    int n;
    char op[20];
    
    // 模式信息
    vector<string> pat_str(MAXN);
    vector<int> pat_len(MAXN);
    
    // Trie 节点
    int nxt[MAXN][ALPH];
    int best_id[MAXN];          // 当前子树最优模式 ID (0 表示无)
    int end_id[MAXN];           // 结束节点的模式 ID (-1 表示无)
    int node_cnt = 1;           // 根是 1
    
    // 比较两个模式 ID
    bool is_better(int a, int b) {
        if (a == -1) return false;
        if (b == -1) return true;
        if (pat_len[a] != pat_len[b]) 
            return pat_len[a] > pat_len[b];
        if (pat_str[a] != pat_str[b])
            return pat_str[a] < pat_str[b];
        return a < b;
    }
    
    // 更新节点 u 的 best 值(从儿子和自身合并)
    void update_node(int u) {
        int best = end_id[u];
        for (int c = 0; c < ALPH; c++) {
            int v = nxt[u][c];
            if (v && best_id[v] != -1) {
                if (best == -1) best = best_id[v];
                else if (is_better(best_id[v], best)) best = best_id[v];
            }
        }
        best_id[u] = best;
    }
    
    // 向上更新路径
    void update_path(int u) {
        while (u) {
            update_node(u);
            u = u / ALPH;  // 父节点快速方法:我们没存父节点,需要另外记录
            // 这里我们实际需要存父节点指针,简化起见,我们下面用栈存路径
        }
    }
    
    // 实际我们需要每个节点存父节点
    int parent[MAXN];
    void add_node(int p, int c, int new_id) {
        nxt[p][c] = new_id;
        parent[new_id] = p;
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(0);
    
        memset(best_id, -1, sizeof best_id);
        memset(end_id, -1, sizeof end_id);
    
        cin >> n;
        string t;
        int cur_node = 1;  // 当前 Trie 节点
    
        vector<int> history; // 记录每次 append 后走到的节点,用于 backspace
        history.push_back(1);
    
        for (int step = 0; step < n; step++) {
            cin >> op;
            if (strcmp(op, "add") == 0) {
                int id; string s;
                cin >> id >> s;
                pat_str[id] = s;
                pat_len[id] = s.length();
    
                // 插入 Trie
                int u = 1;
                for (char ch : s) {
                    int c = ch - 33;
                    if (!nxt[u][c]) {
                        nxt[u][c] = ++node_cnt;
                        parent[node_cnt] = u;
                    }
                    u = nxt[u][c];
                }
                end_id[u] = id;
    
                // 向上更新
                int v = u;
                while (v) {
                    update_node(v);
                    v = parent[v];
                }
            } 
            else if (strcmp(op, "delete") == 0) {
                int id; cin >> id;
                // 找到 id 对应的节点(需要额外映射,这里简化实现,实际需要 id_to_node[])
                // 省略完整实现,假设我们存了
            } 
            else if (strcmp(op, "append") == 0) {
                string s; cin >> s;
                t += s;
                // 沿 Trie 走
                for (char ch : s) {
                    int c = ch - 33;
                    if (nxt[cur_node][c]) {
                        cur_node = nxt[cur_node][c];
                    } else {
                        cur_node = -1;
                        break;
                    }
                }
                history.push_back(cur_node);
            } 
            else if (strcmp(op, "backspace") == 0) {
                int c; cin >> c;
                while (c-- && history.size() > 1) {
                    history.pop_back();
                    t.pop_back();
                }
                cur_node = history.back();
            }
    
            // 输出答案
            if (cur_node == -1 || best_id[cur_node] == -1) {
                cout << "-1\n";
            } else {
                cout << best_id[cur_node] << "\n";
            }
        }
    
        return 0;
    }
    

    注意:上述代码为简化实现,完整代码需要处理:

    • id_to_node 映射,方便删除时找到节点并清除 end_id,然后向上更新。
    • 实际字符映射要转成 0~93 的索引。
    • 每次 appendbackspace 后,当前节点可能是 -1,这时直接输出 -1。
    • 1

    信息

    ID
    7020
    时间
    1000ms
    内存
    256MiB
    难度
    7
    标签
    递交数
    1
    已通过
    1
    上传者