【題解】[AHOI2013]差異
阿新 • • 發佈:2021-08-22
\(\text{Solution:}\)
觀察一下原式:
\[\sum_{i=1}^n\sum_{j=i+1}^n \text{len}(i)+\text{len}(j)-2\text{lcp}(i,j) \]我們發現前面那個 \(\text{len}(i)+\text{len}(j)\) 好求。主要是後面的東西:兩個字尾的 \(\text{LCP}\) 怎麼求。
兩個字尾的最長公共字首……這個式子又長得很像樹上的東西……字尾樹似乎可以做這件事。
考慮一下字尾樹的性質:每一個葉子節點表示一個字尾(注意這裡的葉子節點是指每一個字尾都顯式地表現了出來),同時任意一個節點到根的路徑都是對應一個字尾的字首。
那麼兩個字尾的 \(\text{LCP}\) 不就是對應字尾樹上的 \(LCA!\)
考慮一個 \(dp,\) 直接把 \(2\times LCP(i,j)\) 都算出來:設 \(f[i]\) 表示子樹 \(i\) 的答案,若這個點不是字尾,那麼就算出其 \(siz\) 後列舉孩子,計算 \(\sum_{v\in son[x]} siz[v]\times(siz[x]-siz[v])\times len[x]\) 就是對的。因為每一對都算了兩次。
那麼,如果當前點是一個,被某一個字尾所壓縮掉的字尾資訊,也即非顯式表達出來的字尾呢?
那麼注意到,上述 \(dp\) 計算中實際上只計算了每一個葉子和它求 一次 \(LCP\) 的貢獻。
因為你無論枚舉了哪一棵子樹,這個當前的根,必然在這棵子樹的外層,也就是對應每一棵子樹的葉子,只會和它計算一次貢獻。
所以我們需要在最後加上這一層貢獻。
#include<bits/stdc++.h> using namespace std; #define int long long const int N=1e6+10; typedef long long ll; namespace SAM{ int len[N],pa[N],ch[N][26],tot=1,last=1,f[N]; ll siz[N],ans; vector<int>G[N]; void insert(const int &c){ int p=last; int np=++tot; last=tot;siz[np]=1; len[np]=len[p]+1; for(;p&&!ch[p][c];p=pa[p])ch[p][c]=np; if(!p)pa[np]=1; else{ int q=ch[p][c]; if(len[q]==len[p]+1)pa[np]=q; else{ int nq=++tot; memcpy(ch[nq],ch[q],sizeof ch[q]); pa[nq]=pa[q];pa[q]=pa[np]=nq; len[nq]=len[p]+1; for(;p&&ch[p][c]==q;p=pa[p])ch[p][c]=nq; } } } void dfs(int x){ int psiz=siz[x]; for(auto i:G[x]){ dfs(i); siz[x]+=siz[i]; } for(auto i:G[x]){ ans+=siz[i]*(siz[x]-siz[i])*len[x]; printf("%d ::%d %d\n",x,siz[x],siz[i]); } printf("%lld:%lld %lld len: %lld lenfa:%lld pa:%lld\n",x,psiz,siz[x],len[x],len[pa[x]],pa[x]); ans+=1ll*psiz*(siz[x]-psiz)*len[x]; } void BuildTree(){ for(int i=2;i<=tot;++i)G[pa[i]].push_back(i); dfs(1); } } char s[N],t[N]; int slen; signed main(){ scanf("%s",s+1); slen=strlen(s+1); for(int i=1;i<=slen;++i)t[slen-i+1]=s[i]; for(int i=1;i<=slen;++i)putchar(t[i]); puts(""); for(int i=1;i<=slen;++i)SAM::insert(t[i]-'a'); SAM::BuildTree(); ll sum=0; for(int i=1;i<=slen;++i)sum+=(slen-1)*(slen-i+1); printf("%lld\n",sum-SAM::ans); return 0; }