2012 字符串的魅力
先给出一些字符串(只含有小写字母a-z)的定义:
一个长度字符串为LEN的字符串S,表示为S[0],S[1],…,S[LEN-1]
一位51nod选手得到一些字符串,他想考察这些字符串所有不重复的子串的charm值,这个任务非常艰巨,他想请求聪明的你来帮助他解决这个问题。子串的定义为上述的substr。
提示:
例如样例n = 2, k = 1, l = 1, r = 2
2个字符串为ba, bb,则所有不重复子串为a, b, ba, bb
charm(a, 1) = 0, charm(b, 1) = 1, charm(ba, 1) = 2, charm(bb, 1) = 2
则charm(,1)属于区间[1,2]的不重复的子串的个数为3,分别为b, ba, bb
输入
第一行输入四个整数,n,k,l,r, 1<=n<=1000, 1<=k<=10, 0<=l<=r<mod, 其中n表示有n
个字符串,k为charm的第二个参数,l,r表示一个区间,接下来n行,每行一个字符串(只含
小写字母)。
保证n个字符串的字符个数之和不超过100000。
输出
输出一行,一行一个整数,表示这n个字符串的所有不重复的子串的charm(,k)属于区间
[l,r]的这样的子串的个数。注意: 重复的子串只能计算一次
输入样例
2 1 1 2
ba
bb
输出样例
3
解析:
这道题是用后缀自动机做的,不过需要知道后缀自动机的一些性质。
以ACADD为例建立后缀自动机:
列出各个状态表示的字符串集合:
1: ∅
2: A
3: AC, C
4: ACA, CA
5: ACAD, CAD, AD
6: ACADD, CADD, ADD, DD
7: D
可以发现每个区间表示的字符串集合有很多特点。
性质1:长度是连续的,如果用mx[i]表示状态i所表示的字符串集合中的最长的字符串的长度, pre[i]表示结点i后缀链接所指向的节点,后缀链接为上图蓝色的虚线,那么我们可以精确地表示状态i所表示的连续字符串的长度区间为[mx[pre[i]]+1, mx[i]]
性质2:每个状态所表示的状态中,所有长度较短的字符串都为较长的字符串的后缀。
其实我们还可以更精确地确定每个状态所表示的字符串具体是什么。根据以上两条性质,我们只要知道每个状态所有字符串的最后一个字母在原串中的位置,那么我们就知道所有的字符串了。例如状态5,ACAD,CAD,AD的D在原串ACADD左数第4个位置,然后根据长度,我们就知道了状态5在原串上覆盖了从[0,3], [1,3], [2,3] (下标从0开始)
如何知道每个状态所有字符串的最后一个字母在原串中的位置?这个很简单,我们只要在建立后缀自动机的过程中,当每个结点被创建出来时,判断当前正在加入的字符在原串中的index,将index记为pos[i],表示结点i所有字符串的最后一个字母在原串中的位置。从而可以确定状态i在原串中覆盖的区间[pos[i]-mx[i]+1, pos[i]], ……, [pos[i]-mx[pre[i]], pos[i]]
对于这道题目我们首先考虑简单情况,即只含有一个字符串,那么这个串所有不重复的子串都在后缀自动机里了,由于自动机的结点数最多O(n), 我们按照结点来计算所有子串的魅力值。根据魅力值的定义,长度小于K的部分,只需当成26进制数即可,大于K的部分的魅力值则是长度减去K,然而对于每个结点,根据性质2,短字符串是长字符串的后缀,这样每个结点还是无法一起计算,所以最简单的做法就是把原串反转一下,这样后缀的性质变成原串的前缀了,这样每个状态表示的所有字符串在原串中有公共的前缀,于是就可以一起计算了,因为K<=10,长度小于K的部分直接暴力算,长度大于K的部分只与长度有关,根据性质1,长度是连续的,所以大于K的部分,这些字符串的魅力值实际上是一段连续的区间。
再考虑多个串的情况,思想差不多,首先将每个字符串反转,然后不同串之间用#分割,建立多个串的自动机,同时维护pos,不过这里的pos是指在各个串中的index,而不是连接后的整个串的index。由于多串建立的自动机的状态可能含有#,因为我们在计算每个状态时不能将#部分也算进去。
例如某个自动机在原串中所覆盖的最长部分为AAA#BB#CCC#DDDY,A,B,C,D和Y表示小写字母,这里特别用Y来区别这是这个结点所有字符串的最后一个字符。根据上面精确确定覆盖区间的方法,我们需要确定最短的字符串在哪个位置,可能是A,B,C,D,Y中的任一位置,也可能是#的位置,但是我们只需关心,是否在D和Y的位置,因为如果在A,B,C那么这个最短的串必然包含#,这样这个子串不是每个串的子串,而是加入#的整个串的子串。
考虑几种情况,例如对于结点i,最短字符串长度为mx[pre[i]]+1,根据pos[i]我们可以确定最短字符串的起始位置,pos[i]-mx[pre[i]], 这里可以说明为什么要将pos保留各个串的index,因为当pos[i]是对应在某个原串的下标,那么pos[i]-mx[pre[i]]会出现小于0,也就是出现跨过#的情况,对于pos[i]-mx[pre[i]]>=0, 由于这个位置还在那个串中,所以没有跨过#,知道最短串的左端点的位置,我们可以一直向左扫,直到扫到最长串的左端点,在扫描过程中很可能出现跨过#的情况,一旦跨过#,我们就不需要统计了。但是如果通过扫描的方式,那么复杂度就上升为子串的数目的量级,因此我们只能假设在扫描了,实际上需要通过各种条件判断,分类讨论。
这里给出标程处理的一种做法,经供参考。
对于每个结点计算覆盖区间,就是计算最短串的左端点的位置R=max(pos[i]-mx[pre[i]],-1)这里与-1取最大值,因为小于等于-1代表跨#了,同理计算最长串的左端点的位置L=max(pos[i]+1-mx[i],-1),很显然最短串的左端点的位置要大于等于最长串的做短点的位置,即L<=R。接下来开始暴力计算小于等于K的部分
for(int j=0;j<k&&j<=pos[i];j++){
char c=s[id][pos[i]-j];
v=((ll)v*26+c-'a')%mod;
if(pos[i]-j<=R&&pos[i]-j>=L&&l<=v&&v<=r) ans++;
}
从右端点pos[i]开始往左扫K个,并且如果j>pos[i]了,也就是扫出第id个字符串了,扫到#了。获取pos[i]-j对应位置上的字符c,计算当前的26进制数,接下来就是统计了,pos[i]-j<=R&&pos[i]-j>=L说明当前扫的左端点到pos[i]这个串在这个结点包含的字符串集合里,l<=v&&v<=r表示左端点到pos[i]这个串的魅力值在我们询问的区间里,所以贡献+1.
接下来处理大于K的部分,考虑长度为lj的串,右端点为pos[i],则左端点为pos[i]-lj+1, 这个左端点应该要小于等于最短串的左端点R,即lj>= pos[i]-R+1, 同时lj应该要大于等于K+1,所以lj=max(K, pos[i]-R)+1,也就是至少长lj的字符串才有可能有大于K的部分。同理考虑长度为rj的串,左端点pos[i]-rj+1要大于等于最长串的左端点L,并且左端点要大于等于0,否则就跨#,这样rj<=pos[i]-max(0,L)+1,也就是只有满足lj<=rj,这样才有合法的大于K的部分,lj-=K, rj-=K,最终我们得到大于K的部分的长度区间[lj, rj], 以及前K个字符组成的26进制数取模后v,那么就是v+lj,v+lj+1,…,v+rj这么一个等差序列取模。现在我们还要统计这个等差序列取模后有多少个数字在区间[l,r]中。
由于v<mod, lj, rj<mod,所以这个序列所有数字都小于2*mod, 分三种情况讨论:
第1种: v+rj<mod,这种就是[v+lj, v+rj]有多少个数字在[l,r]中,只需要求这两个区间的交集就可以了
第2种: v+rj>=mod,v+lj<mod,等差序列分成两个区间[v+lj,mod-1]和[0,v+rj-mod],对这两个区间分别和[l,r]求交集即可
第3种: v+rj>=mod,v+lj>=mod,对[v+lj-mod, v+rj-mod]和[l,r]求交集即可
注意1个trick,很可能某个节点是XXXX#XXX#的形式,这个会对上述处理产生影响,然而这种情况,我们知道所有串都不是合法的子串。需要在建立自动机时, 用ind标记下每个结点表示的字符串的最后字符在哪一个原字符串,ind=-1代表#
总之,这道题目细节很多,需要对后缀自动机的性质有深入理解,代码写起来还是很短的,总复杂度为O((26+K)*n)。
ps: 这题竟然过了这么多人, 不太科学啊, 突然发现后缀数组可以水过, 出题时没考虑后缀数组解法, 应该出道后缀数组没法水的, ^_^ !
放代码:
#include <bits/stdc++.h>
using std::pair;
using std::vector;
using std::string;
typedef long long ll;
typedef pair<int, int> pii;
#define fst first
#define snd second
#define pb(a) push_back(a)
#define mp(a, b) std::make_pair(a, b)
#define debug(...) fprintf(stderr, __VA_ARGS__)
template <typename T> bool chkmax(T& a, T b) { return a < b ? a = b, 1 : 0; }
template <typename T> bool chkmin(T& a, T b) { return a > b ? a = b, 1 : 0; }
const int oo = 0x3f3f3f3f;
string procStatus() {
std::ifstream t("/proc/self/status");
return string(std::istreambuf_iterator<char>(t), std::istreambuf_iterator<char>());
}
template <typename T> T read(T& x) {
int f = 1; x = 0;
char ch = getchar();
for(;!isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) x = x * 10 + ch - 48;
return x *= f;
}
const int N = 200000;
namespace SAM {
int lst, cnt;
int fail[N + 5];
int ch[N + 5][30];
int len[N + 5], pos[N + 5];
inline void clear() { lst = cnt = 1; }
inline int newnode(int _l, int _p) {
++ cnt;
len[cnt] = _l, pos[cnt] = _p;
return cnt;
}
void extend(int c, int _p) {
int p = lst;
if(ch[p][c]) {
int q = ch[p][c];
if(len[q] == len[p] + 1) {
lst = q;
} else {
int nq = newnode(len[p] + 1, _p);
memcpy(ch[nq], ch[q], sizeof ch[q]);
fail[nq] = fail[q], fail[q] = nq;
for(; p && ch[p][c] == q; p = fail[p]) ch[p][c] = nq;
lst = nq;
}
} else {
lst = newnode(len[p] + 1, _p);
for(; p && !ch[p][c]; p = fail[p]) ch[p][c] = lst;
if(!p)
fail[lst] = 1;
else {
int q = ch[p][c];
if(len[q] == len[p] + 1) {
fail[lst] = q;
} else {
int nq = newnode(len[p] + 1, _p);
memcpy(ch[nq], ch[q], sizeof ch[q]);
fail[nq] = fail[q], fail[lst] = fail[q] = nq;
for(; p && ch[p][c] == q; p = fail[p]) ch[p][c] = nq;
}
}
}
}
}
const int mo = 1e9 + 7;
int n, k, l, r;
inline ll count(int x, int y) {
chkmax(x, l);
chkmin(y, r);
return std::max(y - x + 1, 0);
}
void solve() {
int len0 = 0, len1 = 0;
static char st[N + 5], tmp[N + 5];
read(n), read(k), read(l), read(r);
SAM::clear();
for(int i = 1; i <= n; ++i) {
scanf("%s", tmp);
len1 = strlen(tmp);
std::reverse(tmp, tmp + len1);
for(int j = 0; j < len1; ++j) {
SAM::extend(tmp[j]-'a', len0), st[len0 ++] = tmp[j];
}
SAM::lst = 1;
}
using namespace SAM;
ll ans = 0;
for(int i = 2; i <= cnt; ++i) {
int hsh = 0;
int _l = len[fail[i]] + 1, _r = len[i], _p = pos[i];
for(int j = 1; j <= k && j <= _r; ++j) {
hsh = (hsh * 26ll + st[_p] - 'a') % mo;
if(j >= _l && j <= _r) {
ans += (l <= hsh && hsh <= r);
}
_p --;
}
if(k < _r) {
if(k >= _l) {
_l = hsh + 1;
} else {
_l = hsh + (_l - k);
}
_r = (hsh + (_r - k)) % mo;
if(_l <= _r) {
ans = (ans + count(_l, _r));
} else {
ans = (ans + count(_l, mo - 1) + count(0, _r));
}
}
}
printf("%lld\n", ans);
}
int main() {
solve();
return 0;
}