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 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> 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';
}