题目信息
题目来源:CCF 天津省选 2010;
在线评测地址:Luogu#3879;
运行限制:时间 (2.00 extrm{s}),空间限制 (128 extrm{MiB})。
题目描述
英语老师留了 (N) 篇阅读理解作业,但是每篇英文短文都有很多生词需要查字典,为了节约时间,现在要做个统计,算一算某些生词都在哪几篇短文中出现过。
输入格式
第一行为整数 (N),表示短文篇数,其中每篇短文只含空格和小写字母。
按下来的 (N) 行,每行描述一篇短文。每行的开头是一个整数 (L),表示这篇短文由 (L) 个单词组成。接下来是 (L) 个单词,单词之间用一个空格分隔。
然后为一个整数 (M),表示要做几次询问。后面有 (M) 行,每行表示一个要统计的生词。
输出格式
对于每个生词输出一行,统计其在哪几篇短文中出现过,并按从小到大输出短文的序号,序号不应有重复,序号之间用一个空格隔开(注意第一个序号的前面和最后一个序号的后面不应有空格)。如果该单词一直没出现过,则输出一个空行。
数据规模与约定
- 对于 (30\%) 的数据,(1le Mle 10^3)。
- 对于 (100\%) 的数据,(1le Mle 10^4),(1le Nle 10^3)。
每篇短文长度(含相邻单词之间的空格)(le 5 imes 10^3) 字符,每个单词长度 (le 20) 字符。
分析
这道题的一个特点就是要在时间与空间取平衡,从而最大化效率。
(30)- ( exttt{pts})
暴力思路,对于每一个询问去扫前面所有的文章。复杂度 (mathcal{O}(Msum |S|)),估计连部分分都拿不到。
(30 exttt{pts})
用一个 set 记录每一篇文章有的字符串。每一次询问时,分别去每一个 set 里查询是否存在即可。
复杂度是 (mathcal{O}(NM|s|log L)),相比于前面的做法优秀很多,如果复杂度优秀的话可以勉强 AC,但并不是最好的做法。
(100 exttt{pts})
Solution 1
如果有比 set 更好的维护字符串的数据结构,不难想到 Trie。如果我们用 Trie 维护这些字符串呢?单次复杂度就能省去一个 (log),只有 (mathcal{O}(NM|s|))。吸了氧气以后能跑进 (100 extrm{ms}),是一种很好的做法。
Solution 2
其实这道题有很多时间复杂度很优秀的做法,但是大多都由于空间限制被枪毙了。
比如说,维护一个 Trie,每个节点连接着一个数组,记录着以这个节点结束的字符串所在文章的集合。单次询问复杂度只有询问字符串长度。
但是,一个 Trie 内有巨量的节点,而且放了所有的字符串,爆空间是在所难免的事。
这时,我们可以退而求其次,转而用 map 这种空间常数较小的数据结构来维护。每一个 string 指向一个集合,用 vector 维护,复杂度是 (mathcal{O}left((N+M)|s|log sum L ight))。实际运行常数较大。
Code
这里使用了第一种做法。
#include <cstdio>
#include <vector>
using namespace std;
constexpr int max_n = 1000, max_l = 20;
char tmp[max_l+1]; // 临时存储,用来给函数传参
struct node // 字典树节点
{
bool is_end;
int son[26];
node() : is_end(false)
{
for (int i = 0; i < 26; i++)
son[i] = -1;
}
};
struct trie // 一棵字典树
{
vector<node> tr; // 树
trie() { }
void clear(int len)
{
tr.assign(len, node());
}
void insert() // 插入
{
int ptr = 0;
for (int i = 0; tmp[i]; i++) // 寻路
{
if (tr[ptr].son[tmp[i]-'a'] != -1) // 有子节点
ptr = tr[ptr].son[tmp[i]-'a']; // 继续走
else // 没有子节点
{
ptr = tr[ptr].son[tmp[i]-'a'] = tr.size(); // 新建一个,并继续走。
tr.push_back(node());
}
}
tr[ptr].is_end = true; // 打上标记
}
bool askif() // 询问是否存在
{
int ptr = 0;
for (int i = 0; tmp[i]; i++) // 寻路
{
if (tr[ptr].son[tmp[i]-'a'] == -1) // 没有子节点
return false; // 字符串不存在
else
ptr = tr[ptr].son[tmp[i]-'a']; // 有就继续走
}
return tr[ptr].is_end; // 是结尾就存在
}
};
trie passages[max_n]; // 一篇文章建一棵树
int main()
{
int n, m, cnt;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%d", &cnt);
passages[i].clear(1);
for (int j = 0; j < cnt; j++)
{
scanf("%s", tmp);
passages[i].insert(); // 输入并插入
}
}
scanf("%d", &m);
for (int i = 0; i < m; i++) // 处理询问
{
scanf("%s", tmp);
for (int j = 0; j < n; j++)
if (passages[j].askif())
printf("%d ", j + 1); // 存在就输出,默认升序
putchar('
'); // 不需要加上标记,没有就留空行
}
return 0; // 然后就 AC 了、
}