构建字典树配合动态规划判定文章前缀可达性,利用单词长度极小的特点常数级转移与剪枝。
OJ: roj
题目 ID: 1475
难度:提高
标签:Trie动态规划字符串
创建: 2026-09-30 13:08
更新: 2026-10-07 11:01
形式化题目
给定一个包含
现给出
约束
- 单词长度不超过
- 文章长度至多
(约 字符)
正解
思路
这是一道经典的字符串可达性动态规划问题。
1. 状态定义
设文章长度为
表示长度为 的前缀 能够被字典中的单词完全拼接拆分。 - 初始时,
(长度为 0 的空前缀天然可被理解)。
2. 状态转移
若前缀
因为字典中单词长度满足
- 沿着 Trie 转移,每次到达一个单词的终止节点(即存在单词以该字符结尾),便将对应的后继前缀位置置为
; - 若遇到 Trie 中没有的字符分支,则说明无法继续匹配更长的单词,立刻退出本次字符延伸。
3. 核心剪枝与提前终止
由于单词长度不超过
最终能够被理解的最长前缀长度即为所有满足
4. 样例推演与状态转移示例
以样例字典 whatisyouname 为例:
| 前缀长度 |
前缀子串 | 是否可达 |
匹配单词及推进 |
|---|---|---|---|
| 0 | 空串 | 1 | 匹配 what,标记 |
| 1~3 | w, wh, wha |
0 | 不可达,跳过 |
| 4 | what |
1 | 匹配 is,标记 |
| 5 | whati |
0 | 不可达,跳过 |
| 6 | whatis |
1 | 从位置 6 (you...) 匹配,字典无对应单词,无法推进 |
| 7~10 | … | 0 | 连续不可达,超过最长单词长度 4,提前终止 |
最长可理解的前缀长度为 6(对应前缀 whatis)。
flowchart LR
A["0: 空串 (f[0]=1)"] -->|"what (长 4)"| B["4: what (f[4]=1)"]
B -->|"is (长 2)"| C["6: whatis (f[6]=1)"]
C -. "you 未在字典中" .-> D["失配,最大可达位置为 6"]
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 13:15
# update_at: 2026-09-30 13:15
import sys
TERMINAL = "#" # 字典树单词终止标记
def build_trie(words: list[str]) -> dict:
"""构建字典树,节点包含子字符映射及终止标记。"""
trie: dict = {}
for word in words:
node = trie
for ch in word:
node = node.setdefault(ch, {})
node[TERMINAL] = True
return trie
def longest_prefix(text: str, trie: dict, max_len: int) -> int:
"""计算单个文本在字典树下能被完全拆分理解的最长前缀长度。"""
length = len(text)
reach = bytearray(length + 1)
reach[0] = 1
last_reach = ans = 0
for i in range(length + 1):
if not reach[i]:
if i - last_reach > max_len: # 连续空隙超过最长单词,后续前缀必不可达
break
continue
last_reach = ans = i
node = trie
for j in range(i, min(i + max_len, length)):
ch = text[j]
if ch not in node:
break
node = node[ch]
if TERMINAL in node:
reach[j + 1] = 1
return ans
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
n = int(next(data))
m = int(next(data))
words = [next(data).decode() for _ in range(n)]
texts = [next(data).decode() for _ in range(m)]
trie = build_trie(words)
max_len = max(len(w) for w in words) if words else 0
ans = [longest_prefix(text, trie, max_len) for text in texts]
print("\n".join(map(str, ans)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 03:21
* update_at: 2026-10-05 03:21
*/
#include <cstring>
#include <iostream>
using namespace std;
typedef long long ll;
const int MAXNODE = 205; // 字典树节点上限:20 个单词,每个单词长度不超过 10
const int MAXLEN = 1000005; // 单篇文章长度上限 1 MB
int trie_next[MAXNODE][26]; // trie_next[u][c]:节点 u 沿字符 c 走到的子节点,0 表示没有该分支
int is_end[MAXNODE]; // is_end[u] = 1 表示节点 u 是某个单词的结尾
char reach[MAXLEN]; // reach[i] = 1 表示文章长度为 i 的前缀可以被字典完整拆分
char text[MAXLEN]; // 当前处理的文章
ll n, m;
int node_cnt = 0; // 0 号节点是字典树的根
int max_word_len = 0; // 字典中最长单词的长度,用来限制匹配步数和剪枝
// 把单词 w 插入字典树。
void insert_word(const char *w) {
int u = 0;
for (int i = 0; w[i] != '\0'; i++) {
int c = w[i] - 'a';
if (trie_next[u][c] == 0) {
node_cnt++;
trie_next[u][c] = node_cnt;
}
u = trie_next[u][c];
}
is_end[u] = 1;
}
// 计算当前文章能被字典理解的最长前缀长度。
int longest_prefix() {
int len = strlen(text);
for (int i = 0; i <= len; i++) {
reach[i] = 0;
}
reach[0] = 1; // 空前缀天然可以被理解
int last_reach = 0; // 最近一个可达前缀的位置
int ans = 0;
for (int i = 0; i <= len; i++) {
if (reach[i] == 0) {
// 连续超过最长单词长度都没有可达位置,后面任何位置都不可能可达,直接结束
if (i - last_reach > max_word_len) {
break;
}
continue;
}
last_reach = i;
ans = i;
// 从位置 i 出发沿字典树匹配,最多前进 max_word_len 步
int u = 0;
for (int j = i; j < len && j < i + max_word_len; j++) {
int c = text[j] - 'a';
if (trie_next[u][c] == 0) {
break;
}
u = trie_next[u][c];
if (is_end[u]) {
reach[j + 1] = 1;
}
}
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
char word[16]; // 单词长度不超过 10
for (ll i = 1; i <= n; i++) {
cin >> word;
int wlen = strlen(word);
if (wlen > max_word_len) {
max_word_len = wlen;
}
insert_word(word);
}
for (ll i = 1; i <= m; i++) {
cin >> text; // text 已按 1 MB 上限开足
cout << longest_prefix() << "\n";
}
return 0;
}复杂度
- 时间复杂度:构建 Trie 树耗时
,由于 ,建树几乎瞬时完成。对长度为 的文章,只有在 时才沿 Trie 向后扫描至多 步,每次仅需一步常数转移;配合不可达间隙剪枝,整体匹配时间为 。即使在 级别数据下,也能在 1 秒左右完成。 - 空间复杂度:Trie 树规模不超过 200 个节点;采用
bytearray(L + 1)记录前缀可达性,占用内存仅约,空间复杂度为 ,远低于 的限制。
总结
本题利用“单词数量少、单词长度极短(