2021银川区域赛K题 & 牛客暑假多校训练营10 A题
2021银川区域赛K题 & 牛客暑假多校训练营10 A题
银川赛的K题被牛客加强一波之后又掏出来了
\(\quad\)
题意:
有 \(n\) 个游戏即将发布它们的网址,在 \(i\) 天发布第 \(i\) 个网址,要求每天列出一些索引字符串,使得这些字符串能够表示任一种已发布网址,且不能表示任何没发布的网址。
字符串是一个网址的某个前缀,就说它能表示这个网址。
求出每一天需要的最少字符串数量。
(题意重述了一下,因为原本的太饶舌了,不知道有没有变清楚)
Sample Input
第一行是一个n,接下来第i行即为第i天发布的游戏网址
n < 10^5 串长最大为100
3
ufoipv.ofu
hsbocmvfgboubtz.kq
hfotijo.njipzp.dpn/kb
Sample Output
1
2
2
牛客上的增强版区别在于它给了 32M 的内存限制,卡掉了裸的字典树
\(\quad\)
先考虑原题怎么做:
把 \(n\) 个原串建成字典树,第 \(i\) 个串的终点打上标记 \(i\)
然后一遍dfs,把每个节点的标记更新为它向下能走到的最大标记,即为 \(mx\)
感受一下:如果一个节点标记为5,第4天的时候肯定不能有这个串 ,因为它是5号网址的前缀
而如果这个节点的父节点标记为7,那么第5,6天的时候一定有这个串,因为不能向上了,向下更劣
所以每个节点对 \([mx, mx\_fa]\) 区间上的答案有贡献,差分统计即可
\(\quad\)
再看加强版:
可以发现上面那种做法存在大量没有用的节点,要想办法回收这些节点
能不能统计了一棵子树的贡献之后直接把它删掉?
显然我们是不能完整地插入整棵字典树的,必须一边插入一边删除
那就要求一棵子树删除时,保证后面都不再有节点新增到这个位置
对字符串排序后插入!
具体而言,插入时如果在 \(u\) 节点处要建一个新儿子,那在这之前我们就可以删除并统计 \(u\) 的所有其他儿子的子树
因为字符串被排过序了,这里要拐弯了,后续串再也不会走到 \(u\) 的其他子树了
注意一下,因为 \(u\) 的标记值还未确定,所以 \(u\) 的直接儿子贡献还没法算,需要保留一下
字典树开1e5是够的,实际发现只需要1000就够(一边删一边加,字典树基本上维持了一个链的形状(但不完全)
char s[maxn][101];
int rnk[maxn];
int ans[maxn];
struct Tire{
int ch[133];
int mx;
}t[1005];
stack<int> rub;//回收栈
int tcnt = 0;
bool cmp(int x, int y){
return strcmp(s[x], s[y]) < 0;
}
void cntAndClear(int p, int sav){
for(int i=0;i<130;i++){
if(t[p].ch[i]){
if(sav <= 1){
ans[t[p].mx]--;
ans[t[t[p].ch[i]].mx]++;
}
cntAndClear(t[p].ch[i], sav-1);
if(sav <= 1) t[p].ch[i] = 0;
}
}
if(sav <= 0){
t[p].mx = 0;//清空
rub.push(p);
}
}
void insert(int id){
int p = 0, len = strlen(s[id]);
for(int i=0;i<len;i++){
if(!t[p].ch[s[id][i]]){
cntAndClear(p, 2);
if(rub.empty()) t[p].ch[s[id][i]] = ++tcnt;
else t[p].ch[s[id][i]] = rub.top(), rub.pop();
}
t[p].mx = max(t[p].mx, id);
p = t[p].ch[s[id][i]];
}
t[p].mx = max(t[p].mx,id);
}
void solve(){
int n;
cin >> n;
for(int i=1;i<=n;i++) cin >> s[i], rnk[i] = i;
sort(rnk+1, rnk+n+1, cmp);//借用rnk数组对字符串排序
t[0].mx = n+1;
for(int i=1;i<=n;i++) insert(rnk[i]);
cntAndClear(0, 1);
for(int i=1;i<=n;i++) ans[i] += ans[i-1];
for(int i=1;i<=n;i++) cout << ans[i] << '\n';
}