题目描述
给定一个长度不超过 300,000300,000300,000 的字符串 SSS,以及一个包含 SSS 个单词的字典(1≤S≤40001 \leq S \leq 40001≤S≤4000,每个单词长度不超过 100100100,全部为小写字母,且无重复单词)。要求计算将字符串 SSS 划分成若干段的方式数,其中每一段都必须是字典中的某个单词。由于结果可能很大,输出对 200710272007102720071027 取模后的值。
输入包含多个测试用例,每个测试用例之间用一个空行分隔。对于每个测试用例,首先给出目标字符串,然后是字典大小 SSS,接着是 SSS 个字典单词。
题目分析
问题本质
这是一个字符串划分计数问题,类似于经典的"单词拆分"问题,但需要计算所有可能的划分方式数目。
样例分析
考虑样例:
abcd
4
a
b
cd
ab
可能的划分方式有:
abcdabcd
因此答案为 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(i−len+1,len)==w,则:
dp[i]+=dp[i−len] dp[i] += dp[i - len] dp[i]+=dp[i−len] - 最终答案: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,000n≤300,000,S≤4000S \leq 4000S≤4000,L≤100L \leq 100L≤100,这显然会超时。
优化策略
我们需要优化匹配过程。注意到:
- 字典单词长度不超过 100100100
- 我们可以使用 Trie\texttt{Trie}Trie 树 来高效匹配所有可能的单词
优化后的思路:
- 将字典中所有单词插入 Trie\texttt{Trie}Trie 树
- 在 Trie\texttt{Trie}Trie 节点中记录以该节点结尾的单词长度
- 对于字符串的每个位置 iii,在 Trie\texttt{Trie}Trie 中最多向下匹配 100100100 个字符
- 遇到单词结尾时,根据单词长度进行状态转移
这样时间复杂度降为 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]
算法流程
- 构建 Trie\texttt{Trie}Trie 树,插入所有字典单词
- 初始化 dpdpdp 数组
- 对于每个位置 iii(0≤i<n0 \leq i < n0≤i<n):
- 从根节点开始
- 对于 j=ij = ij=i 到 i+99i+99i+99(不超过 n−1n-1n−1):
- 沿 Trie\texttt{Trie}Trie 树向下移动
- 如果遇到单词结尾,更新 dp[i+len]dp[i + len]dp[i+len]
- 输出 dp[n]dp[n]dp[n]
- 清理 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 的思路在字符串处理问题中非常实用。

458

被折叠的 条评论
为什么被折叠?



