51nod2012 字符串的魅力

2012 字符串的魅力

 

先给出一些字符串(只含有小写字母a-z)的定义:

一个长度字符串为LEN的字符串S,表示为S[0],S[1],…,S[LEN-1]

len(S)=LEN

 substr(S,i,j)=S[i],S[i+1],...,S[j],0\leq i\leq j< len(S)

prefix(S,K)=S[0],S[1],...,S[K-1],K\leq len(S)

value(S)=\sum_{i=0}^{len(S)-1}(s[i]-97)\cdot 26^{len(S)-1-i}

charm(S,K)= \left\{\begin{matrix} & value(S)%mod, & len(S)\leq K \\ & (value(prefix(S,K))+len(S)-K)%mod,& len(S)>K\\ &mod=1000000007& \end{matrix}\right.

一位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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值