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

可能的划分方式有:

  • abcd
  • abcd

因此答案为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>usingnamespacestd;constintMOD=20071027;// 取模值constintMAXN=300010;// 最大字符串长度constintALPHABET=26;// 字母表大小intdp[MAXN];// dp数组// Trie树节点结构structTrieNode{TrieNode*children[ALPHABET];// 子节点指针数组vector<int>wordLengths;// 存储以此节点结尾的单词长度TrieNode(){memset(children,0,sizeof(children));// 初始化子节点为空}};// 向Trie树中插入单词voidinsert(TrieNode*root,conststring&word){TrieNode*node=root;for(charch:word){intidx=ch-'a';// 计算字母索引if(!node->children[idx]){node->children[idx]=newTrieNode();// 创建新节点}node=node->children[idx];}node->wordLengths.push_back(word.length());// 记录单词长度}// 递归清理Trie树内存voidclearTrie(TrieNode*root){if(!root)return;for(inti=0;i<ALPHABET;i++){clearTrie(root->children[i]);// 递归清理子节点}deleteroot;// 删除当前节点}intmain(){ios::sync_with_stdio(false);cin.tie(nullptr);string text;intcaseNum=1;while(cin>>text){// 读取每个测试用例的目标字符串intS;cin>>S;// 读取字典大小TrieNode*root=newTrieNode();// 创建Trie根节点// 插入所有字典单词for(inti=0;i<S;i++){string word;cin>>word;insert(root,word);}intn=text.length();memset(dp,0,sizeof(dp));// 初始化dp数组dp[0]=1;// 空串有1种划分方式// 动态规划过程for(inti=0;i<n;i++){if(dp[i]==0)continue;// 如果当前位置不可达,跳过TrieNode*node=root;// 从位置i开始,在Trie中最多匹配100个字符for(intj=i;j<n&&j-i<100;j++){intidx=text[j]-'a';if(!node->children[idx])break;// 无匹配,退出node=node->children[idx];// 遍历所有以当前节点结尾的单词for(intlen:node->wordLengths){if(i+len<=n){dp[i+len]=(dp[i+len]+dp[i])%MOD;// 状态转移}}}}cout<<"Case "<<caseNum++<<": "<<dp[n]<<"\n";// 输出结果clearTrie(root);// 清理Trie树内存}return0;}

复杂度分析

  • 时间复杂度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\texttt{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的思路在字符串处理问题中非常实用。