题目大意:
有
n
≤
1
e
5
n\le1e5
n≤1e5个字符串,每天依次发表一个字符串,要求找出最少的前缀使得已经发表的字符串的前缀都能可以被表示,而那些“未发表”的字符串不能够被表示(题目保证了不会出现某个在前面的字符串是另一个在后面的字符串的前缀)
空间要求不超过32M
解题思路:
- 这题如果没有空间要求就是一道非常裸的字典树,将每个串对应字典
树上的节点标为黑点,每次操作会将一个黑点变为白点,目标是将尽可能少的非根节点标红使得任意一个白点到根的路径上都有被标红的节点,且任意一个黑点到根的路径上都没有被标红的节点。我们记录下每个点的子树里有多少个黑点,每次有一个黑点变为白点的时候,首先标红这个白点,然后我们将所有的红色标记尽可能往上推就能得到最少被标红的节点数。(源自:牛客官方题解) - 但是因为空间需求我们不可能建立一颗完全的字典树,所以我们需要压缩字典树,对于字典树上某个结点u的子结点v只有一个(非单词结尾结点),那么很明显当前u结点可以省略。
- 最后以上面的策略递归构造字典树。
AC代码:
#include <bits/stdc++.h>
#define ft first
#define sd second
#define pb push_back
#define IOS ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define seteps(N) fixed << setprecision(N)
#define endl "\n"
const int maxn = 1e5 + 10;
using namespace std;
typedef long long ll;
typedef double db;
typedef pair<int, int> pii;
const ll mod = 1e9 + 7;
int n, tot = 1, ans = 0;
int mx[maxn << 1], tp[maxn], cnt[maxn]; //tp[i]记录字典树中第i个字符串可以被替代是哪一个
char ch[maxn][105];
vector <int> id[maxn << 1];
map <int, int> mp[105];
void insert(int o, int dep) {
if (id[o].size() == 1) {mx[o] = id[o][0]; id[o].clear(); return;}
if (o != 1) { //判断当前字典树结点中存储的每个字符串的第dep位是否相同,如果相同则到下一层插入,该层不插入
bool ok = false;
for (auto np : id[o])
if (ch[np][dep] != ch[id[o][0]][dep]) {
ok = true;
break;
}
if (!ok) {insert(o, dep + 1); id[o].clear(); return;}
}
for (auto np : id[o]) { //遍历当前字典树结点中存储的字符串
mx[o] = max(mx[o], np); //记录经过当前结点的字符串最大序号
int tmp = ch[np][dep];//类似普通字典树的写法
if (!mp[dep].count(tmp)) //当前结点没有的话就新建
mp[dep][tmp] = ++tot;
id[mp[dep][tmp]].pb(np); //下一个节点要存当前遍历到的点
}
id[o].clear(); //清空为了省内存
for (auto np : mp[dep]) { //当前结点要分叉了
insert(np.sd, dep + 1);
if (mx[np.sd] != mx[o] && o != 1) //对于相等的情况以及根节点不考虑
tp[mx[np.sd]] = mx[o]; //下一个结点可以被mx[o]代替
}
mp[dep].clear(); //清空回溯
return;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%s", ch[i]);
id[1].pb(i);
}
insert(1, 0);
for (int i = 1; i <= n; i++) cnt[tp[i]]++;
for (int i = 1; i <= n; i++) {
ans += 1 - cnt[i];
printf("%d\n", ans);
}
}