2021牛客暑期多校訓練營10
比賽連結: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; }View Code