1 条题解

  • 0
    @ 2025-12-5 15:08:54

    ROI 2019 Day2 T4. 寻找思路 题解

    问题描述

    给定模式串 pp(长度 mm)和经过压缩的文本串 tt,求 pp 在解压后的 tt 中出现的次数。
    压缩格式由 nn 个块组成:

    • 1 w:将字符串 ww 追加到 tt 末尾
    • 2 pos len:从 ttpos\mathrm{pos} 位置开始,连续复制 len\mathrm{len} 个字符到末尾

    限制条件

    • wi2×105\sum |w_i| \le 2\times 10^5
    • m2×105m \le 2\times 10^5
    • n104n \le 10^4
    • Ln1015L_n \le 10^{15}(解压后文本可能极大)

    核心难点

    1. 文本可能极大LnL_n 可达 101510^{15},无法显式展开
    2. 复制操作可能交叉:复制区间 [pos,pos+len1][\mathrm{pos}, \mathrm{pos}+\mathrm{len}-1] 可能包含正在生成的部分
    3. 高效计数:需要在 O((n+m)logL)O((n+m)\log L) 或类似复杂度内完成

    关键观察

    1. 模式串匹配的特征

    设模式串 pp 的长度为 mm。当文本 tt 扩展时:

    • 新的匹配可能出现在:
      • 新添加的 ww 内部
      • 复制区域内部
      • 跨越新旧文本的边界

    2. 复制操作的数学性质

    ii 个操作后,tt 的长度为 LiL_i。 对于复制操作 2 pos len

    • 相当于从 tt 中取子串 t[pos:pos+len1]t[\mathrm{pos} : \mathrm{pos}+\mathrm{len}-1]
    • pos+len1Li1\mathrm{pos}+\mathrm{len}-1 \le L_{i-1},则是纯复制(不重叠)
    • 否则可能产生重叠复制,形成类周期结构

    3. KMP/自动机方法

    可使用 KMP 或 AC 自动机跟踪匹配状态:

    • 在扩展文本时,维护当前后缀与模式串的匹配长度
    • 每次添加字符时更新状态
    • 但需要处理批量复制操作的高效更新

    算法设计

    总体思路:状态转移 + 倍增

    1. 预处理

      • 对模式串 pp 构建 KMP 的 π\pi 数组(失配函数)
      • 构建转移自动机:next_state[state][c] 表示当前匹配长度为 statestate,遇到字符 cc 后的新匹配长度
    2. 操作处理

      • 对每个操作,不显式生成文本,而是计算:
        • 操作后文本的总长度 LiL_i
        • 初始空状态(匹配长度 0)处理该操作后得到的结束状态
        • 该操作内部产生的匹配次数
    3. 复制操作的高效处理: 对于 2 pos len

      • 如果复制区间不重叠且长度不太大,可逐字符模拟(使用转移自动机)
      • 对于大长度,利用倍增/矩阵幂加速:
        • 定义 fpos(state,k)f_{pos}(state, k):从位置 pospos 开始复制 2k2^k 个字符后,状态的转移结果和产生的匹配数
        • 通过二进制分解 lenlen 快速计算
    4. 边界匹配: 跨操作边界的匹配:

      • 记录每个操作结束时的 KMP 状态(匹配长度)
      • 当下一个操作开始时,继续从该状态匹配

    数据结构

    struct BlockResult {
        long long new_len;          // 操作后文本长度
        int end_state;              // 操作结束时的KMP状态
        long long match_count;      // 本操作内部新增的匹配数
        vector<int> prefix_state;   // 文本前缀的状态(用于查询区间)
    };
    

    算法步骤

    1. 读取模式串,构建 KMP 自动机
    2. 初始化:当前文本 tt 为空,状态为 0,总匹配数为 0
    3. 依次处理每个操作:
      • 类型 1:添加字符串 ww,逐字符更新状态
      • 类型 2:复制操作,使用倍增方法处理
    4. 输出总匹配数

    倍增方法详解

    对于复制操作 2 pos len

    预处理倍增表

    dp[k][state]dp[k][state] 表示:从 KMP 状态 statestate 开始,处理从某个固定位置开始的 2k2^k 个字符后的结果。

    但由于复制起点 pospos 可能不同,我们需要按位置查询字符。

    替代方案:在线段树上预处理

    1. 构建文本的"虚拟"线段树,每个节点存储:

      • 该区间对应的字符串
      • 从各个初始状态处理该区间后的结果
    2. 对于复制区间 [pos,pos+len1][pos, pos+len-1]

      • 在线段树中分解为 O(logL)O(\log L) 个区间
      • 按顺序处理这些区间,传递状态

    实际实现中的简化

    考虑到 wi2×105\sum |w_i| \le 2\times 10^5,实际展开的"基础文本"并不大。
    我们可以:

    1. 维护前 M=2×105M = 2\times 10^5 个字符(或 2m2m 个字符,足够处理边界)
    2. 对于超长的复制,利用周期性:
      • 如果复制区间完全在已展开文本内,直接复制
      • 否则,重复复制直到填满需求或发现周期

    复杂度分析

    • KMP 预处理:O(mΣ)O(m|\Sigma|)O(m)O(m)
    • 类型 1 操作:总 O(wi)O(\sum |w_i|)
    • 类型 2 操作:
      • 短复制:O(len)O(\mathrm{len})
      • 长复制:利用周期性,O(mlogL)O(m \log L)
    • 总复杂度:O((n+m)logL+wi)O((n+m)\log L + \sum |w_i|)

    C++ 实现框架

    #include <bits/stdc++.h>
    using namespace std;
    
    typedef long long ll;
    
    const int MAXM = 200005;
    const int MAXL = 1000005; // 维护的文本最大长度
    
    int m, n;
    string pattern;
    int pi[MAXM];
    int transition[MAXM][26]; // 自动机转移
    
    struct Operation {
        int type;
        string s; // type=1时使用
        ll pos, len; // type=2时使用
    };
    
    // 维护一段实际文本
    string maintained_text;
    ll total_len = 0;
    ll total_matches = 0;
    int current_state = 0;
    
    // KMP预处理
    void build_kmp(const string& p) {
        pi[0] = 0;
        for (int i = 1; i < m; i++) {
            int j = pi[i-1];
            while (j > 0 && p[i] != p[j]) j = pi[j-1];
            if (p[i] == p[j]) j++;
            pi[i] = j;
        }
        
        // 构建转移表
        for (int state = 0; state <= m; state++) {
            for (char c = 'a'; c <= 'z'; c++) {
                if (state < m && c == pattern[state]) {
                    transition[state][c-'a'] = state + 1;
                } else if (state > 0) {
                    transition[state][c-'a'] = transition[pi[state-1]][c-'a'];
                } else {
                    transition[state][c-'a'] = 0;
                }
            }
        }
    }
    
    // 处理一个字符,更新状态和匹配数
    void process_char(char c) {
        current_state = transition[current_state][c-'a'];
        if (current_state == m) {
            total_matches++;
            current_state = pi[m-1]; // 可重叠匹配
        }
    }
    
    // 处理类型1操作
    void process_type1(const string& s) {
        for (char c : s) {
            maintained_text.push_back(c);
            if (maintained_text.size() > MAXL) {
                maintained_text.erase(maintained_text.begin());
            }
            process_char(c);
        }
        total_len += s.length();
    }
    
    // 处理类型2操作
    void process_type2(ll pos, ll len) {
        // 确保pos在有效范围内
        pos--; // 转换为0-based
        
        // 策略:如果复制的全部内容都在maintained_text中,直接复制
        // 否则,分段处理
        
        ll copied = 0;
        while (copied < len) {
            // 计算本次能复制的长度
            ll available = min(len - copied, total_len - pos);
            
            // 如果当前位置在维护的文本中
            if (pos < maintained_text.size()) {
                ll start_idx = pos;
                for (ll i = 0; i < available; i++) {
                    char c = maintained_text[(start_idx + i) % maintained_text.size()];
                    process_char(c);
                    maintained_text.push_back(c);
                    if (maintained_text.size() > MAXL) {
                        maintained_text.erase(maintained_text.begin());
                    }
                }
            } else {
                // 需要模拟复制
                // 简化:直接扩展maintained_text
                // 实际应使用更高效的方法
                for (ll i = 0; i < available; i++) {
                    // 这里简化处理,实际需要从虚拟文本获取字符
                    // 使用周期性:文本可能由之前的复制生成
                    process_char('a'); // 占位
                }
            }
            
            copied += available;
            pos = 0; // 后续从文本开头复制
        }
        
        total_len += len;
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
        
        cin >> m >> n;
        cin >> pattern;
        
        build_kmp(pattern);
        
        vector<Operation> ops(n);
        for (int i = 0; i < n; i++) {
            int type;
            cin >> type;
            if (type == 1) {
                string s;
                cin >> s;
                ops[i] = {type, s, 0, 0};
            } else {
                ll pos, len;
                cin >> pos >> len;
                ops[i] = {type, "", pos, len};
            }
        }
        
        // 处理操作
        for (int i = 0; i < n; i++) {
            if (ops[i].type == 1) {
                process_type1(ops[i].s);
            } else {
                process_type2(ops[i].pos, ops[i].len);
            }
        }
        
        cout << total_matches << endl;
        
        return 0;
    }
    

    注意事项

    1. 大整数处理:使用 long long,注意 Ln1015L_n \le 10^{15}
    2. 内存管理:只维护必要的文本前缀
    3. 边界情况
      • 模式串长度为 1
      • 复制操作产生重叠
      • 文本全为同一字符
    4. 取模:本题不需要取模,直接输出匹配数

    进一步优化

    对于大数据,需要实现完整的倍增方法:

    1. 构建块级别的转移:将每个操作视为一个"块",预处理从任意状态处理该块后的结果
    2. 倍增处理长复制:对于复制操作,使用快速幂思想组合转移
    3. 查询优化:使用前缀和记录文本字符,支持 O(1)O(1) 查询任意位置的字符

    总结

    本题是字符串匹配与动态文本生成的结合,主要考察:

    • KMP/自动机的应用
    • 对压缩格式的高效处理
    • 倍增思想在字符串操作中的应用
    • 大数据的处理能力

    关键突破点在于将复制操作视为状态转移,并使用倍增加速,避免显式展开极大文本。

    • 1

    信息

    ID
    5795
    时间
    5000ms
    内存
    1024MiB
    难度
    10
    标签
    递交数
    5
    已通过
    1
    上传者