用 Trie 存所有号码并在末尾补哨兵,插入途中发现更早号码提前终止或当前号码结尾仍有后继即输出 NO。
OJ: roj
题目 ID: 3055
难度:普及
标签:Trie字符串
创建: 2026-10-01 12:28
更新: 2026-10-06 11:42
题目描述
给定 NO,否则输出 YES。输入:第一行整数 911、97625999、91125426,输出 NO(911 是 91125426 的前缀);样例二:输入 113、12340、123440、12345、98346,输出 YES。
思路
把每个号码逐字符插入 Trie,并在末尾额外走一个哨兵字符(数字里不存在的编号),让“某号码在此终止”也变成一条边;插入途中若发现下一个节点已挂着哨兵(更早的号码在此终止),或走到末尾时节点仍有后继(更早的号码从这里延伸下去),就说明存在前缀关系,输出 NO。每个字符只处理一次,单组复杂度为号码总长
参考代码
Python 版:
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-10-01 12:28
# update_at: 2026-10-01 12:28
import sys
END = "\x00" # 哨兵字符:拼在号码末尾,表示“某个号码在此终止”
def is_prefix_free(numbers: list[str]) -> bool:
"""号码集合里是否两两互不为前缀(一个都不出现在另一个开头)。"""
sons: list[dict[str, int]] = [{}] # sons[i]:第 i 个节点的子节点表,字符 → 节点编号
for number in numbers:
node = 0
for ch in number + END: # 补一个终止哨兵,让“前缀”也拥有自己的终点
if ch == END and sons[node]: # 最后一位仍有后继 → 当前号码是更早号码的前缀
return False
son = sons[node].get(ch)
if son is None: # 惰性建点:先在父节点登记,再追加节点本身,编号才不会越过列表末尾
son = sons[node][ch] = len(sons)
sons.append({})
elif END in sons[son]: # 已有号码在这位终止 → 它是当前号码的前缀
return False
node = son
return True
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
out: list[str] = []
t = int(next(data))
for _ in range(t):
n = int(next(data))
numbers = [next(data).decode() for _ in range(n)]
verdict = "YES" if is_prefix_free(numbers) else "NO" # 兼容 = 无前缀关系
out.append(verdict)
print("\n".join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* 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-06 11:40
* update_at: 2026-10-06 11:42
*/
#include <iostream>
#include <cstring>
#include <string>
using namespace std;
typedef long long ll;
// Trie:sons[node][c] 表示节点 node 沿字符 c 走到的子节点编号,0 表示不存在
const int MAXNODE = 400005; // 每组最多 n*11 个字符,节点数不超过总字符数
int sons[MAXNODE][11]; // 子节点表:0~9 为数字,10 为哨兵 '$'
int node_cnt; // 当前已用节点数(含根 0)
// 每组数据开始前清空 Trie(只清实际用到的部分)
void trie_init() {
node_cnt = 1; // 根节点编号 0
memset(sons[0], 0, sizeof(sons[0]));
}
// 插入号码 s(末尾隐含哨兵 '$'),途中检测前缀冲突;有冲突返回 false
bool trie_insert(const string &s) {
int node = 0;
int len = s.size(); // 先存成 int,避免和有符号下标比较出问题
for (int i = 0; i <= len; i++) {
int c = (i == len) ? 10 : s[i] - '0'; // 末尾多走一步:哨兵 '$' 用编号 10
bool has_son = false; // 节点 node 是否已有任何子节点
for (int k = 0; k < 11; k++)
if (sons[node][k]) has_son = true;
if (c == 10 && has_son) return false; // 结尾仍有后继:当前号码是更早号码的前缀
if (sons[node][c] == 0) { // 惰性建点(含哨兵位)
sons[node][c] = node_cnt;
memset(sons[node_cnt], 0, sizeof(sons[node_cnt]));
node_cnt++;
} else if (sons[sons[node][c]][10] != 0) {
// 下一个节点已有哨兵:有号码在这位终止,它是当前号码的前缀
return false;
}
node = sons[node][c];
}
return true;
}
int t = 0, n = 0;
string s;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
cin >> n;
trie_init();
bool ok = true; // 兼容 = 任意两个号码互不为前缀
for (int i = 1; i <= n; i++) {
cin >> s;
if (ok && !trie_insert(s)) ok = false; // 已判 NO 也要读完剩余输入
}
cout << (ok ? "YES" : "NO") << "\n";
}
return 0;
}