UVa 1401 Remember the Word

题目描述

给定一个长度不超过 300,000300,000300,000 的字符串 SSS,以及一个包含 SSS 个单词的字典(1≤S≤40001 \leq S \leq 40001S4000,每个单词长度不超过 100100100,全部为小写字母,且无重复单词)。要求计算将字符串 SSS 划分成若干段的方式数,其中每一段都必须是字典中的某个单词。由于结果可能很大,输出对 200710272007102720071027 取模后的值。

输入包含多个测试用例,每个测试用例之间用一个空行分隔。对于每个测试用例,首先给出目标字符串,然后是字典大小 SSS,接着是 SSS 个字典单词。


题目分析

问题本质

这是一个字符串划分计数问题,类似于经典的"单词拆分"问题,但需要计算所有可能的划分方式数目。

样例分析

考虑样例:

abcd
4
a
b
cd
ab

可能的划分方式有:

  • a b cd
  • ab cd

因此答案为 222

直接思路与复杂度分析

我们可以使用动态规划来解决这个问题:

  • 定义 dp[i]dp[i]dp[i] 表示字符串前 iii 个字符的划分方式数。
  • 初始状态:dp[0]=1dp[0] = 1dp[0]=1(空串有一种划分方式)。
  • 状态转移:对于每个位置 iii,枚举字典中的每个单词 www,如果 substr(i−len+1,len)==wsubstr(i - len + 1, len) == wsubstr(ilen+1,len)==w,则:
    dp[i]+=dp[i−len] dp[i] += dp[i - len] dp[i]+=dp[ilen]
  • 最终答案:dp[n]dp[n]dp[n],其中 nnn 是字符串长度。

然而,直接实现的时间复杂度为 O(n×S×L)O(n \times S \times L)O(n×S×L),其中 n≤300,000n \leq 300,000n300,000S≤4000S \leq 4000S4000L≤100L \leq 100L100,这显然会超时。

优化策略

我们需要优化匹配过程。注意到:

  • 字典单词长度不超过 100100100
  • 我们可以使用 Trie\texttt{Trie}Trie 来高效匹配所有可能的单词

优化后的思路

  1. 将字典中所有单词插入 Trie\texttt{Trie}Trie
  2. Trie\texttt{Trie}Trie 节点中记录以该节点结尾的单词长度
  3. 对于字符串的每个位置 iii,在 Trie\texttt{Trie}Trie 中最多向下匹配 100100100 个字符
  4. 遇到单词结尾时,根据单词长度进行状态转移

这样时间复杂度降为 O(n×100)O(n \times 100)O(n×100),可以接受。


算法设计

数据结构

  • Trie\texttt{Trie}Trie 节点
    • 子节点指针数组(大小为 262626,对应小写字母)
    • 存储以该节点结尾的单词长度列表

动态规划状态

  • dp[i]dp[i]dp[i]:前 iii 个字符的划分方式数
  • 初始状态:dp[0]=1dp[0] = 1dp[0]=1
  • 最终答案:dp[n]dp[n]dp[n]

算法流程

  1. 构建 Trie\texttt{Trie}Trie 树,插入所有字典单词
  2. 初始化 dpdpdp 数组
  3. 对于每个位置 iii0≤i<n0 \leq i < n0i<n):
    • 从根节点开始
    • 对于 j=ij = ij=ii+99i+99i+99(不超过 n−1n-1n1):
      • 沿 Trie\texttt{Trie}Trie 树向下移动
      • 如果遇到单词结尾,更新 dp[i+len]dp[i + len]dp[i+len]
  4. 输出 dp[n]dp[n]dp[n]
  5. 清理 Trie\texttt{Trie}Trie 树内存

代码实现

// Remember the Word
// UVa ID: 1401
// Verdict: Accepted
// Submission Date: 2025-10-16
// UVa Run Time: 0.100s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <iostream>
#include <cstring>
#include <vector>
#include <string>
using namespace std;

const int MOD = 20071027; // 取模值
const int MAXN = 300010; // 最大字符串长度
const int ALPHABET = 26; // 字母表大小

int dp[MAXN]; // dp数组

// Trie树节点结构
struct TrieNode {
    TrieNode* children[ALPHABET]; // 子节点指针数组
    vector<int> wordLengths; // 存储以此节点结尾的单词长度
    TrieNode() {
        memset(children, 0, sizeof(children)); // 初始化子节点为空
    }
};

// 向Trie树中插入单词
void insert(TrieNode* root, const string& word) {
    TrieNode* node = root;
    for (char ch : word) {
        int idx = ch - 'a'; // 计算字母索引
        if (!node->children[idx]) {
            node->children[idx] = new TrieNode(); // 创建新节点
        }
        node = node->children[idx];
    }
    node->wordLengths.push_back(word.length()); // 记录单词长度
}

// 递归清理Trie树内存
void clearTrie(TrieNode* root) {
    if (!root) return;
    for (int i = 0; i < ALPHABET; i++) {
        clearTrie(root->children[i]); // 递归清理子节点
    }
    delete root; // 删除当前节点
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    string text;
    int caseNum = 1;
    while (cin >> text) { // 读取每个测试用例的目标字符串
        int S;
        cin >> S; // 读取字典大小
        TrieNode* root = new TrieNode(); // 创建Trie根节点
        
        // 插入所有字典单词
        for (int i = 0; i < S; i++) {
            string word;
            cin >> word;
            insert(root, word);
        }
        
        int n = text.length();
        memset(dp, 0, sizeof(dp)); // 初始化dp数组
        dp[0] = 1; // 空串有1种划分方式
        
        // 动态规划过程
        for (int i = 0; i < n; i++) {
            if (dp[i] == 0) continue; // 如果当前位置不可达,跳过
            TrieNode* node = root;
            // 从位置i开始,在Trie中最多匹配100个字符
            for (int j = i; j < n && j - i < 100; j++) {
                int idx = text[j] - 'a';
                if (!node->children[idx]) break; // 无匹配,退出
                node = node->children[idx];
                // 遍历所有以当前节点结尾的单词
                for (int len : node->wordLengths) {
                    if (i + len <= n) {
                        dp[i + len] = (dp[i + len] + dp[i]) % MOD; // 状态转移
                    }
                }
            }
        }
        
        cout << "Case " << caseNum++ << ": " << dp[n] << "\n"; // 输出结果
        clearTrie(root); // 清理Trie树内存
    }
    
    return 0;
}

复杂度分析

  • 时间复杂度O(n×100)O(n \times 100)O(n×100),其中 nnn 是目标字符串长度。每个位置最多匹配 100100100 个字符。
  • 空间复杂度O(字典总字符数+n)O(\text{字典总字符数} + n)O(字典总字符数+n),主要用于存储 Trie\texttt{Trie}Trie 树和 dp\texttt{dp}dp 数组。

总结

本题通过结合 Trie\texttt{Trie}Trie动态规划,高效地解决了大规模字符串划分计数问题。关键点在于利用 Trie 树优化单词匹配过程,将复杂度从不可接受的 O(n×S×L)O(n \times S \times L)O(n×S×L) 降低到可行的 O(n×100)O(n \times 100)O(n×100)。这种 Trie\texttt{Trie}Trie + DP\texttt{DP}DP 的思路在字符串处理问题中非常实用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值