比赛链接:https://ac.nowcoder.com/acm/contest/11261
F,H,12。
A
题意:
给一系列字符串,对每个字符串,要找到一些前缀,使得当前字符串以及它之前的所有字符串都至少有一个前缀在这些前缀中,而它之后的所有字符串都没有前缀在这些前缀中。对每个字符串,输出找到的前缀的最小个数。空间限制\(32768KB\),\(n \leq 10^5\),每个字符串长度不超过\(100\)。
分析:
首先不考虑空间限制,看这个题怎么做。
涉及到前缀,考虑一下字典树。由于题目说没有字符串是其他字符串的前缀,也没有空字符串,所以每个字符串都终止在字典树的一个叶子上。
从\(1\)到\(n\)枚举当前字符串\(i\),那么所有字符串被分成了两类,第一类是需要有前缀“占领”,第二类是不能被前缀“占领”的。
取一个前缀,就是在字典树上找一个节点。题目的要求实际上就是找的这些节点的子树包含的字符串只能是第一类,不能有第二类,求这些节点可能的最小数目。
为了直观,我们把当前的第一类字符串所对应的叶子成为“白点”,第二类字符串所对应的叶子称为“黑点”,找的前缀对应的点成为“红点”。一开始所有叶子都是黑点。红点的子树里不能有黑点。
我们可以对字典树上每个点记录一个值\(sizb\),表示这个点的子树中有多少黑点;再记录一个值\(sizr\),表示子树中有多少红点。
每次枚举到下一个字符串,有一个黑点变成了白点。相应地,它和它祖先的\(sizb\)都会减少\(1\)。这下会出现一些新的点\(sizb=0\),也就是出现一些新的点可以变成红点。
为了让红点个数最少,我们要让每个红点覆盖到尽量多的白点,也就是让它的深度尽量浅。而一个红点的子树内不再需要别的红点了。
所以我们在被更新的那条祖先链上找深度最浅的一个点,让它变成红点,以前它子树中所有的红点都不要了。这里需要用到\(sizr\),也要再更新一番\(sizr\)。值得注意的是,这个红点的子树内没有黑点了,也就是我们以后再也不会用到这个子树。所以不用再费力修改子树内点的\(sizr\)了。
这样做的时间复杂度是\(O(总字符串长度)\)的,可以。
但是这样我们需要建一个总字符串长度规模的字典树,还要记录这个规模的\(sizb,sizr,fa\),空间太大了。
实际上,字典树还可以压缩。字典树上那些没有旁支的链都可以省略。所以我们递归建树,当一个点需要分叉时再建新点。为了快速判断是否分叉,我们可以把字符串排序以后再建树,这样分到同一个叉里的字符串是一个连续的区间。当只有一个字符串时,结束递归。
这里要注意多个字符串共有的部分也需要新建一个点,因为前缀可以在这里取到!
这样字典树的规模就是\(2*n\)的,因为每增加一个字符串,最多增加两个点(与别人共有的点和自己的叶子点)。空间复杂度也没问题了。
代码如下:
#include<iostream> #include<cstring> #include<algorithm> #include<vector> #define pb push_back using namespace std; int const N=1e5+5,M=(N<<1); int n,cnt,buk[70],sizb[M],sizr[M],pos[N],fa[M]; struct Nd{ string s; int id; }a[N]; vector<int>son[M]; bool cmp(Nd x,Nd y){return x.s<y.s;} int chg(char c) { if(c>='a'&&c<='z')return c-'a'+1; if(c>='A'&&c<='Z')return c-'A'+27; if(c=='.')return 53; if(c=='/')return 54; else return c-'0'+55; } void build(int u,int l,int r,int dep) { //printf("u=%d l=%d r=%d dep=%d fa[u]=%d\n",u,l,r,dep,fa[u]); if(l==r){pos[a[l].id]=u; sizb[u]=1; return;} int pdep=dep; while(1) { /* for(int i=1;i<=65;i++)buk[i]=0; int num=0; for(int i=l;i<=r;i++) { int x=chg(a[i].s[dep]); if(!buk[x])num++; buk[x]++; } if(num==1)dep++; else break; */ if(a[l].s[dep]==a[r].s[dep])dep++; else break; } if(dep>pdep)son[u].pb(++cnt),fa[cnt]=u,u=cnt;//相同部分新建一个点! int L=l,R; while(L<=r) { R=L+1; while(R<=r&&a[R].s[dep]==a[L].s[dep])R++; son[u].pb(++cnt); fa[cnt]=u; build(cnt,L,R-1,dep+1); L=R; } } void dfs(int u) { //if(!son[u].size()){sizb[u]=1; return;} for(int v:son[u]) dfs(v),sizb[u]+=sizb[v]; //printf("sizb[%d]=%d\n",u,sizb[u]); } bool cmp2(Nd x,Nd y){return x.id<y.id;} void work(int u) { int p=u; while(p!=-1)sizb[p]--,p=fa[p]; while(1) { if(fa[u]==0||sizb[fa[u]])break;//红点不能是根节点 u=fa[u]; } int pre=sizr[u]; sizr[u]=1; u=fa[u];//此点以下不再用到了 while(u!=-1)//更新祖先sizr { sizr[u]-=pre; sizr[u]++; u=fa[u]; } } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) cin>>a[i].s,a[i].id=i; sort(a+1,a+n+1,cmp); fa[0]=-1; build(0,1,n,0); dfs(0); sort(a+1,a+n+1,cmp2); for(int i=1;i<=n;i++) { work(pos[i]); printf("%d\n",sizr[0]); } return 0; }