「UOJ700」可爱多的字符串
题目
有一次机灵鬼和学长可爱多打比赛, 可爱多不会做一道字符串题,机灵鬼做了很久终于做出来了,这是机灵鬼第一次做出可爱多不会的题。
可爱多觉得很丢人,于是准备研究字符串。可爱多精通 \(\mathrm{kmp}\) 算法。\(\mathrm{kmp}\) 算法的输入是一个字符串 \(S\),该算法的核心是对每个 \(i\in [1,n]\) 求出 \(\mathrm{next}_i\),定义为:
其中 \(S[l, r]\) 表示字符串 \(S\) 中第 \(l\) 个到第 \(r\) 个字符组成的子串,如果 \(r < l\) 则为空串。
他发现,如果 \(i\) 向 \(\mathrm{next}_i\) 连边,最后会形成一棵以 0 为根的树。他还注意到,如果一个串的“循环度”比较高的话,这颗树所有点的深度和就会比较大,比如 \(\mathrm{aaaa}\) 的树的深度和是 \(1+2+3+4\), \(\mathrm{abab}\) 的深度和为 \(1+1+2+2\),而 \(\mathrm{abcd}\) 的深度和只有 \(1+1+1+1\),他给这个树的深度和取了一个名字,叫做 \(\mathrm{next}\) 树深度和。所以,可爱多遇到一个串就想算出他的 \(\mathrm{next_i}\) 树深度和。
可爱多觉得仅仅算出一个串的 \(\mathrm{next_i}\) 树深度和并不能体现出他高超的水平。于是,他给每个位置设置了一个喜爱度 \(w_i\),现在他想计算 \(\sum_{i=1}^n w_i\times \mathrm{dep}_i\),我们称之为带权 \(\mathrm{next}_i\) 树深度和。可爱多经过很多练习过后,对带权 \(\mathrm{next_i}\) 树深度和了如指掌,如果你告诉他一个长为 \(n\) 的字符串 \(S\),以及一个长为 \(n\) 的数组 \(W=\{w_1,w_2,\dots,w_n\}\),他可以立马告诉你这个串的带权 \(\mathrm{next_i}\) 树深度。
现在机灵鬼给了可爱多一个长为 \(n\) 的字符串 \(S\) 供他研究,可爱多也设置好了 \(n\) 个位置喜爱度。机灵鬼会多次取出字符串的一部分,即多次给出 \(l,r\) ,他想让可爱多告诉他,当 \(S'=S[l,r]\), \(W'=\{w_l,w_{l+1},\dots,w_r\}\) 时,带权 \(\mathrm{next}\) 树深度和。为了避免答案过大,答案对 \(2^{32}\) 取模。
机灵鬼不想太为难可爱多,于是他给出的字符串字符集为 \(\{0,1\}\)。可爱多算不出来就会觉得很丢脸,于是请你帮帮他。
记询问次数为 \(m\)。
所有数据满足 \(1\le n,m\le 2\times 10^5,1\le w_i\le 10^9\)。
分析
这个题和 「CF1098F」Ж-function 有关系(就是 \(w_i=1\) 的 case),所以我们可以从中得到启发,考虑计算后缀 \(S[l,n]\) 和 \(S[j,n]\)(\(l\le j\le r\))之间产生的贡献。
具体来说,设 \(\operatorname{LCP}(i,j)\) 为 \(S[i,n]\) 和 \(S[j,n]\) 的最长公共前缀长度,\(s_i=\sum_{j=1}^iw_j\),则可以得到一个询问的答案为:
\(\operatorname{LCP}\) 可以在后缀树上用 LCA 的相关信息刻画,故可以考虑在后缀树上做重链剖分,并对于每一条重链,计算 LCA 在该重链上时的答案。为了方便,之后我们仅考虑 \(\sum_{l<j\le r}s_{\min\{r,j+\operatorname{LCP}(l,j)-1\}}\) 这一块非平凡的贡献。
设 \(S[i,n]\) 在后缀树上对应的结点为 \(p_i\)。枚举一条重链,设结点 \(u\) 的祖先中第一个重链上的结点为 \(c_u\),\(c_u\) 所包含的最长字符串长度为 \(d_u\)。暴力一点,先枚举一下 \(c_{p_l}\) 和 \(c_{p_j}\) 的深度关系:
-
若 \(c_{p_j}\) 更浅,则相当于计算 \(\sum_{l<j\le r}s_{\min\{r,j+d_j-1\}}\),此时 \(\min\) 可以通过比大小拆开。
总的来说变成了一个三维偏序,但是因为总共有 \(O(n\log n)\) 个点和 \(O(m\log n)\) 次询问,所以复杂度是 \(O((n+m)\log^3n)\),有点夸张。
-
若 \(c_{p_l}\) 更浅,则相当于计算 \(\sum_{l<j\le r}s_{\min\{r,j+d_l-1\}}\)。欸,这个时候再来拆 \(\min\) 的话,则下标里会出现 \(j+d_l-1\),没法分开,怎么办?
难道要做动态卷积?那就说明这个方法行不通。注意到此时我们可以将 \(c_j\) 限制在 \(c_l\) 的重儿子子树内,而根据后缀树的含义,这样的 \(j\) 并不平凡——它们标定了一些“出现位置”。我们进一步想到,可以将 \(s_{j+d_l-1}\) 和字符串 \(S[j,j+d_l]\) 结合起来,也即是 \(S[j,j+d_l]\) 的出现位置的右端点下标减一的 \(s\) 之和。这个问题恰好可以放在正串的 parent tree 上做,只需要定位并子树数点即可,复杂度为 \(O((n+q)\log^2n)\)。
Note.
注意不是 \(S[j,j+d_l-1]\),因为它的出现位置不太能和重儿子中的后缀对应起来。而 \(S[j,j+d_l]\) 本身就是重儿子结点中包含的点。
Remark.
这里其实是对于“左端点统计”和“右端点统计”进行了一个替换。下标的平移及固定的平移长度促使我们将 \(s_{j+d_l-1}\) 看作是一个和右端点 \(j+d_l\) 有关的信息。
这样就得到了一个 \(O((n+q)\log^3n)\) 的做法,可以通过 80pts。
考虑进一步优化上面的算法,它慢就慢在第一个部分的处理中。
进一步打开 \(\min\{r,j+\operatorname{LCP}(l,j)-1\}\),可以发现其为 \(\min\{r,j+d_j-1,j+d_l-1\}\)。刚刚我们就是先讨论 \(d_j\) 和 \(d_l\) 的大小,但注意到 \(r\) 和 \(j+d_l-1\) 关联更加紧密,我们不妨就先将它们讨论掉。
如果 \(r\le j+d_l-1\),也即是 \(r-d_l<j\),我们要求的就是 \(\sum_{\max\{r-d_l,l\}<j\le r}s_{\min\{r,j+d_j-1\}}\)。此时因为偏序的合并,问题就只剩下了二维偏序,可以以 \(O((n+q)\log^2n)\) 的总复杂度解决;而如果 \(r-d_l\ge j\),我们要求的就是 \(\sum_{l<j\le r-d_l}s_{j-1+\min\{d_l,d_j\}}\),就可以用上面的第二部分的讨论解决了。
这里需要注意减去 \(l\) 所在子树的额外贡献(主要出现在 \(r-d_l<j\) 的贡献中)。
这样,复杂度就被优化到了 \(O((n+q)\log^2n)\)。
几个小细节:
-
在正串 parent tree 上做统计的时候,用离线扫描线配合 BIT 的方法会远比线段树合并快,值得注意。
-
字符集为 \(\{0,1\}\),说明后缀树上一个结点只有至多两个儿子。这说明我们不用考虑一个结点的多个轻儿子之间的交叉贡献。
代码
不出意料地达到了 10 KB 之长。
#include <bits/stdc++.h>
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )
const int MAXN = 4e5 + 5, MAXLOG = 20;
template<typename _T>
inline void Read( _T &x ) {
x = 0; char s = getchar(); bool f = false;
while( s < '0' || '9' < s ) { f = s == '-', s = getchar(); }
while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
if( f ) x = -x;
}
template<typename _T>
inline void Write( _T x ) {
if( x < 0 ) putchar( '-' ), x = -x;
if( 9 < x ) Write( x / 10 );
putchar( x % 10 + '0' );
}
struct SAM {
int ance[MAXN][MAXLOG];
int seq[MAXN], buc[MAXN];
int corr[MAXN], edp[MAXN];
int ch[MAXN][2], fa[MAXN], mx[MAXN];
int rt, lst, ntot, lg2;
SAM(): ch{{}}, fa{}, mx{}, rt( 0 ), lst( 0 ), ntot( 0 ) {}
inline void Copy( const int &a, const int &b ) {
fa[a] = fa[b], mx[a] = mx[b];
memcpy( ch[a], ch[b], sizeof( ch[b] ) );
}
inline void Expand( const char &c ) {
int x = c ^ '0', p = lst, cur = ++ ntot;
mx[cur] = mx[lst] + 1, lst = cur;
while( p && ! ch[p][x] ) ch[p][x] = cur, p = fa[p];
if( ! p ) { fa[cur] = rt; return ; }
int q = ch[p][x];
if( mx[q] == mx[p] + 1 ) { fa[cur] = q; return ; }
int nq = ++ ntot; Copy( nq, q );
mx[nq] = mx[p] + 1, fa[cur] = fa[q] = nq;
while( p && ch[p][x] == q ) ch[p][x] = nq, p = fa[p];
}
inline void Build( const char *str ) {
rt = lst = ntot = 1;
int n = strlen( str + 1 );
rep( i, 1, n ) Expand( str[i] ), edp[corr[i] = lst] = i;
rep( i, 1, ntot ) buc[mx[i]] ++;
rep( i, 2, n ) buc[i] += buc[i - 1];
per( i, ntot, 1 ) seq[buc[mx[i]] --] = i;
rep( i, 1, ntot ) ance[i][0] = fa[i];
lg2 = log2( ntot );
rep( j, 1, lg2 ) rep( i, 1, ntot )
ance[i][j] = ance[ance[i][j - 1]][j - 1];
}
inline int Locate( const int &l, const int &r ) const {
int p = corr[r], len = r - l + 1;
for( int k = lg2 ; ~ k ; k -- )
if( ance[p][k] && mx[ance[p][k]] >= len )
p = ance[p][k];
return p;
}
};
struct BIT {
unsigned su[MAXN]; int n;
BIT(): su{}, n( 0 ) {}
inline void Init( const int &N ) {
n = N, memset( su, 0, ( n + 1 ) << 2 );
}
inline void Down( int &x ) const { x &= x - 1; }
inline void Up( int &x ) const { x += x & ( - x ); }
inline void Update( int x, unsigned v ) { for( ; x <= n ; Up( x ) ) su[x] += v; }
inline unsigned Query( int x ) const { unsigned ret = 0; for( ; x ; Down( x ) ) ret += su[x]; return ret; }
inline unsigned Query( const int &l, const int &r ) const { return Query( r ) - Query( l - 1 ); }
};
BIT su0, su1;
int qL[MAXN], qR[MAXN];
unsigned ans[MAXN];
unsigned wei[MAXN], pref[MAXN], ppref[MAXN];
char str[MAXN];
int N, Q;
namespace Forward {
struct Question {
int l, r, id;
unsigned sgn;
Question(): l( 0 ), r( 0 ), id( 0 ), sgn( 0 ) {}
Question( int L, int R, int I, unsigned S ): l( L ), r( R ), id( I ), sgn( S ) {}
};
std :: vector<Question> qst[MAXN];
std :: vector<int> son[MAXN];
int siz[MAXN], ord[MAXN], seq[MAXN], tot = 0;
SAM forw;
int ntot;
void DFS( const int &u ) {
siz[u] = 1, seq[ord[u] = ++ tot] = u;
for( const int &v : son[u] )
DFS( v ), siz[u] += siz[v];
}
inline void Init() {
forw.Build( str ), ntot = forw.ntot;
rep( i, 2, ntot ) son[forw.fa[i]].push_back( i );
DFS( 1 );
}
inline void AddQuestion( const int &u, const int &l, const int &r, const int &id ) {
qst[ord[u]].emplace_back( l, r, id, 1u );
qst[ord[u] + siz[u]].emplace_back( l, r, id, -1u );
}
inline void Work() {
su1.Init( ntot );
per( i, ntot, 1 ) {
int u = seq[i];
if( forw.edp[u] )
su1.Update( forw.edp[u], pref[forw.edp[u] - 1] );
for( const Question &q : qst[i] )
ans[q.id] += q.sgn * su1.Query( q.l, q.r );
}
}
}
namespace Backward {
std :: vector<int> assoEdp[MAXN], assoQry[MAXN], glbEdp, glbQry;
std :: vector<int> son[MAXN], qry[MAXN];
std :: vector<int> chn[MAXN];
int app[MAXN], its[MAXN], itsQ[MAXN];
int siz[MAXN], heavy[MAXN];
int tot = 0;
int mx[MAXN], edp[MAXN];
SAM bacw;
int ntot;
void DFS1( const int &u ) {
siz[u] = 1, heavy[u] = 0;
for( const int &v : son[u] ) {
DFS1( v ), siz[u] += siz[v];
if( siz[heavy[u]] < siz[v] ) heavy[u] = v;
}
app[u] = edp[u] ? edp[u] : app[son[u][0]];
}
void DFS2( const int &u ) {
for( const int &v : son[u] ) DFS2( v );
if( heavy[bacw.fa[u]] ^ u ) {
tot ++;
for( int x = u ; x ; x = heavy[x] )
chn[tot].push_back( x );
}
}
inline void Init() {
std :: reverse( str + 1, str + 1 + N );
bacw.Build( str ), ntot = bacw.ntot;
std :: reverse( str + 1, str + 1 + N );
rep( i, 2, ntot ) son[bacw.fa[i]].push_back( i );
memcpy( mx, bacw. mx, ( ntot + 1 ) << 2 );
rep( i, 1, ntot )
edp[i] = bacw.edp[i] ? N - bacw.edp[i] + 1 : 0;
DFS1( 1 ), DFS2( 1 );
}
void Collect( const int &u, std :: vector<int> &retEdp, std :: vector<int> &retQry ) {
if( edp[u] ) {
retEdp.push_back( edp[u] );
for( const int &q : qry[u] ) retQry.push_back( q );
}
}
void CollectSub( const int &u, std :: vector<int> &retEdp, std :: vector<int> &retQry ) {
Collect( u, retEdp, retQry );
for( const int &v : son[u] ) CollectSub( v, retEdp, retQry );
}
inline void Work() {
su0.Init( ntot );
su1.Init( ntot );
rep( i, 1, Q )
qry[bacw.corr[N - qL[i] + 1]].push_back( i );
rep( i, 1, tot ) {
int n = chn[i].size();
glbEdp.clear(), glbQry.clear();
rep( j, 0, n - 1 ) {
int u = chn[i][j];
std :: vector<int> &curQry = assoQry[j],
&curEdp = assoEdp[j];
curQry.clear(), curEdp.clear();
Collect( u, curEdp, curQry );
for( const int &v : son[u] )
if( v ^ heavy[u] ) {
int oQ = curQry.size(), oE = curEdp.size();
CollectSub( v, curEdp, curQry );
int nQ = curQry.size(), nE = curEdp.size();
if( oQ < nQ ) {
rep( k, oE, nE - 1 ) su0.Update( curEdp[k], 1u );
rep( k, oQ, nQ - 1 ) {
int q = curQry[k];
ans[q] -= su0.Query( std :: max( qL[q], qR[q] - bacw.mx[u] ) + 1, qR[q] ) * pref[qR[q]];
}
rep( k, oE, nE - 1 ) su0.Update( curEdp[k], -1u );
}
}
for( const int &e : curEdp ) its[e] = mx[u];
for( const int &q : curQry ) {
itsQ[q] = mx[u]; int w = heavy[u];
if( qL[q] < qR[q] - mx[u] )
Forward :: AddQuestion( Forward :: forw.Locate( app[w], app[w] + mx[u] ), qL[q] + mx[u] + 1, qR[q], q );
}
if( edp[u] ) {
for( const int &q : curQry )
if( qL[q] < edp[u] && edp[u] <= qR[q] - mx[u] )
ans[q] += pref[std :: min( qR[q], edp[u] + mx[u] - 1 )];
}
glbEdp.insert( glbEdp.end(), curEdp.begin(), curEdp.end() );
glbQry.insert( glbQry.end(), curQry.begin(), curQry.end() );
}
if( glbQry.empty() ) continue;
std :: sort( glbQry.begin(), glbQry.end(),
[] ( const int &a, const int &b ) -> bool {
return qR[a] < qR[b];
} );
std :: sort( glbEdp.begin(), glbEdp.end(),
[] ( const int &a, const int &b ) -> bool {
return a + its[a] < b + its[b];
} );
for( int m = glbEdp.size(), q = glbQry.size(), j = 0, k = 0 ; j < m || k < q ; ) {
int u = j < m ? glbEdp[j] : -1, v = k < q ? glbQry[k] : -1;
if( j < m && ( k >= q || u + its[u] <= qR[v] ) )
su0.Update( u, 1u ), su1.Update( u, pref[u + its[u] - 1] ), j ++;
else
ans[v] += su1.Query( std :: max( qL[v], qR[v] - itsQ[v] ) + 1, qR[v] ), k ++;
}
for( int m = glbEdp.size(), q = glbQry.size(), j = 0, k = 0 ; j < m || k < q ; ) {
int u = j < m ? glbEdp[j] : -1, v = k < q ? glbQry[k] : -1;
if( j < m && ( k >= q || u + its[u] <= qR[v] ) )
su0.Update( u, -1u ), su1.Update( u, -pref[u + its[u] - 1] ), j ++;
else
ans[v] += su0.Query( std :: max( qL[v], qR[v] - itsQ[v] ) + 1, qR[v] ) * pref[qR[v]], k ++;
}
// note that one node can own at most one light son only.
// thus no contribution across light subtrees needs considering.
rep( j, 0, n - 1 ) {
int u = chn[i][j];
for( const int &q : assoQry[j] )
if( qL[q] < qR[q] - mx[u] )
ans[q] += su1.Query( qL[q] + 1, qR[q] - mx[u] );
for( const int &e : assoEdp[j] )
su1.Update( e, pref[e + its[e] - 1] );
}
for( const int &e : glbEdp )
su1.Update( e, - pref[e + its[e] - 1] );
}
}
}
int main() {
Read( N ), Read( Q );
scanf( "%s", str + 1 );
rep( i, 1, N ) {
Read( wei[i] );
pref[i] = pref[i - 1] + wei[i];
ppref[i] = ppref[i - 1] + pref[i];
}
rep( i, 1, Q ) {
Read( qL[i] ), Read( qR[i] );
ans[i] = pref[qR[i]] - ( ppref[qR[i] - 1] - ( qL[i] > 1 ? ppref[qL[i] - 2] : 0 ) );
}
Forward :: Init();
Backward :: Init();
Backward :: Work();
Forward :: Work();
rep( i, 1, Q ) Write( ans[i] ), putchar( '\n' );
return 0;
}