武汉理工大学第三届程序设计竞赛 K-蚝蚝蚝大王的字符串

第一次在正式比赛过了字符串的高阶算法题,感动 ̄▽ ̄,写个题解记录一下
题目链接:题目链接

 首先看到本质不同的子串就想到了后缀自动机,因为后缀自动机的节点压缩的存储的一个字符串的所有子串。后缀自动机的一个模板题也就是求本质不同的子串数量。

 如果这题的询问只限制结尾的地方 R R R那就很简单,离线询问,遍历字符串,边扩展自动机边处理询问即可。

 暂时先抛开起始位置 L L L的限制,“第一次出现"怎么处理?
 回想一下传统"求本质不同的子串数量"的做法是求 ∑ ( l e n [ i ] − l e n [ p a r [ i ] ] ) ∑(len[i]-len[par[i]]) (len[i]len[par[i]]) l e n [ i ] len[i] len[i]是当前节点代表的所有字符串的最大长度。后缀自动机的每个节点 i i i表示结束位置在 e n d p o s [ i ] endpos[i] endpos[i]中的所有子串,换句话说,一个子串可能出现在多个位置,但是我只记录一个,两个不同的子串可能出现的位置的右端点相同(就是endpos相同),我也把它们记录在一个状态(节点)里
 比如说:abcdbcdcd
在这里插入图片描述
对于子串 d d d, c d cd cd,它们的endpos相同,都是{ 4, 7, 9 },但是向左边加个b,那原来出现 c d cd cd 的位置只有两个左边有 b b b, 子串 b c d bcd bcdendpos就为{ 4, 7 }。通俗的说,越长的子串,在原串中出现的次数相对更少,越短的字串,在原串的出现位置相对更多。像什么 “parent tree越"下面"的节点 l e n [ i ] len[i] len[i]越大”,节点(状态) i i i 中所有字符串的长度恰好覆盖区间 [   l e n [ p a r [ i ] ] + 1 ,   l e n [ i ]   ] [ \ len[par[i]]+1,\ len[i]\ ] [ len[par[i]]+1, len[i] ] 中的每一个整数”,这些涉及到 l e n [ i ] len[i] len[i] 的性质我当初都是这么理解的。

  再来看一下每次扩展SAM时涉及到 l e n [ i ] len[i] len[i]的部分:
先贴下自己的SAM板子

void Add(int ch,int pos)
{
    int p = last;
    tot++;
    int np = last = tot;
    len[np] = len[p] + 1;

    while( p>0 && sam[p][ch]==0 ){
        sam[p][ch] = np;
        p = par[p];
    }

    if( p==0 ){
        par[np] = 1;
    }
    else{
        int q = sam[p][ch];
        if( len[q] == len[p]+1 )par[np] = q;
        else{
            tot++;
            int nq = tot;
            len[nq] = len[p]+1;
            par[nq] = par[q];
            for(int i=0;i<26;i++)sam[nq][i] = sam[q][i];
            par[np] = par[q] = nq;

            while( p>0 && sam[p][ch]==q ){
                sam[p][ch] = nq;
                p = par[p];
            }
        }
    }
}
  1. 开始创建一个新节点(状态),设置其(最长的字符串)长度为上一个前缀所在的节点的len+1,因为新增的这个节点也是表示前缀,以某个位置结尾的所有子串中肯定是前缀的长度最长嘛。
    int p = last;
    tot++;
    int np = last = tot;
    len[np] = len[p] + 1;

2.从前一个前缀所在的节点(状态)沿着parent tree往上找,没有找到一个有出边为 c h ch ch的节点(状态),(虽然这个不涉及到 l e n len len哈)。这一步发生在当前扩展的字符 c h ch ch是该字符在字符串中第一次出现,前面都没有出现过

	if( p==0 ){
        par[np] = 1;
    }

3.找到了,判断是否满足 l e n [ q ] = = l e n [ p ] + 1 len[q] == len[p]+1 len[q]==len[p]+1

    else{
        int q = sam[p][ch];
        if( len[q] == len[p]+1 )par[np] = q;
        else{
            tot++;
            int nq = tot;
            len[nq] = len[p]+1;
            par[nq] = par[q];
            for(int i=0;i<26;i++)sam[nq][i] = sam[q][i];
            par[np] = par[q] = nq;

            while( p>0 && sam[p][ch]==q ){
                sam[p][ch] = nq;
                p = par[p];
            }
        }
    }

  首先这个 “从前一个前缀所在的节点(状态)沿着parent tree往上找” 大概是这样的 :假设当前字符串:cde cd bcd abcd(空格仅为了隔开),接下来的字符是 ‘e’ 。现在我要扩展 ‘e’,那么首先cde cd bcd abcd所在的节点(状态)肯定没有 ‘e’ 的出边,沿着parent tree往上会遇到 abcd,显然没有;bcd,还是没有; cd,诶,状态节点{ 2,5,8,12 }(endpos集) 沿着 该节点sam上的 ‘e’ 边可以到达{ 3 }。这一步其实就是不断把左边的字符去掉,越短的串出现的位置更多,更有可能碰到一个后边跟着 ‘e’ 的位置,如果全都去掉了,那就是空串(跑到根节点1号)如果1号节点都没有 ‘e’ 的出边的话,就说明当前我插入的 ‘e’ 是第一次出现。
  (接下来我用字符串表示那个子串所在的节点)好的接下来由于 l e n [ ′ c d e ′ ] = = l e n [ ′ c d ′ ] + 1 len['cde'] == len['cd']+1 len[cde]==len[cd]+1,走上面那个if,那么现在SAM的parent tree中有一部分子树大概长这样:
在这里插入图片描述

  好的,现在观察一下扩展了 **‘e’**之后出现了多少个新的子串?
  ——10个,也就是 l e n [ 3 ] − l e n [ 2 ] = 13 − 3 = 10 len[3] - len[2] = 13 - 3 = 10 len[3]len[2]=133=10
好的相信看到这里的你已经知道怎么做了,本篇文章到此结束。
  写着写着发现了更多的对后缀自动机不了解的地方了╭(╯^╰)╮,想了半天 l e n [ q ] = = l e n [ p ] + 1 len[q] == len[p]+1 len[q]==len[p]+1不满足的情况是怎样的想着有点头痛。(感觉是不是可能后面遇到 …bcde,往上找,最后 p = 2 , q = 3 p=2,q=3 p=2,q=3,然后 l e n [ 3 ] ! = l e n [ 2 ] + 1 len[3]!=len[2]+1 len[3]!=len[2]+1,于是就新建一个节点插到中间??额不对 p 、 q p、q pq是sam边上的关系不是 parent tree 的父子)

  回到这个题上来,可以发现每扩展一个字符,新增的、也就是前面从未出现过的的子串有 l e n [ i ] − l e n [ p a r [ i ] ] len[i]-len[par[i]] len[i]len[par[i]] 个。关于这个性质可以感性的认知一下:扩展第 i i i 个字符,首先至少会出现一个新的子串——那就是长度为 i i i 的前缀,前面我说了“越长的子串出现的次数相对更少,越短的出现次数相对更对多”,那么前缀 i i i 的所有后缀中,随着长度减小,它在之前已经出现过的概率越大。比如:abcdecde,刚刚扩展了 **‘e’**之后,abcdecde肯定是新增的,短一个的 bcdecde也是,再短一个的 cdecde还是,直到 cde 就不是了。
  所以扩展第 i i i 个字符时,创建一个新节点,可以理解为是因为产生了新的、前面没有出现过的子串,这些子串肯定都是前缀 i i i 的某个后缀,其中最长的肯定就是前缀 i i i 本身,而最短的那个后缀长度,就是 前面已经出现过的后缀 中,最长的那一个的长度+1,而前面已经出现过的后缀中最长的那个在哪个节点?其实SAM每次扩展就是找这么一个节点,然后把新节点作为那个节点的儿子。
 通过这点,用第 i i i 个字符扩展SAM我们可以知道一些区间 [ x, i ],其中 x ∈ [   i − l e n [ c u r ] ,   i − ( l e n [ p a r [ c u r ] + 1 )   ] x∈[\ i-len[cur],\ i-(len[par[cur]+1) \ ] x[ ilen[cur], i(len[par[cur]+1) ],这些区间位置指示的是字符串中第一次出现的子串的位置。

 那么就先对所有查询按右端点排序,然后逐步扩展SAM,也就是处理查询 [L, R]时,当前SAM中的所有子串(的结束位置)都是在 R 左边的(包括R)。那我现在就只需要考虑有多少 “第一次出现的子串”的起始位置 在 L 右边(包括L)。每次扩展后,用线段树对起始位置的区间 [   i − l e n [ c u r ] ,   i − ( l e n [ p a r [ c u r ] + 1 )   ] [\ i-len[cur],\ i-(len[par[cur]+1) \ ] [ ilen[cur], i(len[par[cur]+1) ] 加1,然后查询 [ L, R ]之间的和即可。

个人代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int MAX_N = 100005;

struct Query{
    int x,y,no;
    Query(int a=0,int b=0,int c=0){ x=a; y=b; no=c; }
};
bool comp(const Query& a, const Query& b){
    return a.y < b.y;
}
// 这个是自己开始验证思路用的qwq
struct FakeSegmentTree{
    int s[200010];
    void modify(int x,int y,int d){
        for(int i=x;i<=y;i++)s[i] += d;
    }
    int query(int x,int y){
        int res = 0;
        for(int i=x;i<=y;i++)res += s[i];
        return res;
    }
};

struct SegmentTree {
    struct node {
        long long sum;
        int len, tag;
    } st[MAX_N << 2];
    void push_up(int x) {
        st[x].sum = st[x << 1].sum + st[x << 1 | 1].sum;
        st[x].len = st[x << 1].len + st[x << 1 | 1].len;
    }
    void push_down(int x) {
        if (!st[x].tag) return;
        st[x << 1].tag += st[x].tag;
        st[x << 1].sum += st[x << 1].len * st[x].tag;
        st[x << 1 | 1].tag += st[x].tag;
        st[x << 1 | 1].sum += st[x << 1 | 1].len * st[x].tag;
        st[x].tag = 0;
    }
    void build(int x, int l ,int r) {
        if (l == r) {
            st[x] = {0, 1, 0};
            return;
        }
        int mid = (l + r) >> 1;
        build(x << 1, l, mid);
        build(x << 1 | 1, mid + 1, r);
        push_up(x);
    }
    void modify(int x, int l, int r, int ql, int qr) {
        if (ql > qr || ql > r || qr < l) return;
        if (ql <= l && r <= qr) {
            st[x].tag += 1;
            st[x].sum += st[x].len;
            return;
        }
        push_down(x);
        int mid = (l + r) >> 1;
        modify(x << 1, l, mid, ql, qr);
        modify(x << 1 | 1, mid + 1, r, ql, qr);
        push_up(x);
    }
    long long query(int x, int l, int r, int ql, int qr) {
        if (ql > qr || ql > r || qr < l) return 0;
        if (ql <= l && r <= qr) return st[x].sum;
        push_down(x);
        int mid = (l + r) >> 1;
        return query(x << 1, l, mid, ql, qr) + query(x << 1 | 1, mid + 1, r, ql, qr);
    }
};

SegmentTree startpos;
vector<Query>query;
char s[MAX_N];
int par[MAX_N*2], sam[MAX_N*2][26], len[MAX_N*2];
int last,tot;
long long ans[100010];
int n;

void Add(int ch,int pos)
{
    int p = last;
    tot++;
    int np = last = tot;
    len[np] = len[p] + 1;
    // Size[np] = 1;

    while( p>0 && sam[p][ch]==0 ){
        sam[p][ch] = np;
        p = par[p];
    }

    if( p==0 ){
        par[np] = 1;
    }
    else{
        int q = sam[p][ch];
        if( len[q] == len[p]+1 )par[np] = q;
        else{
            tot++;
            int nq = tot;
            len[nq] = len[p]+1;
            par[nq] = par[q];
            for(int i=0;i<26;i++)sam[nq][i] = sam[q][i];
            par[np] = par[q] = nq;

            while( p>0 && sam[p][ch]==q ){
                sam[p][ch] = nq;
                p = par[p];
            }
        }
    }

    startpos.modify( 1,1,n, pos-len[np]+1, pos-len[par[np]] );
}


int main()
{
    // int n;
    int Q,x,y;
    scanf("%d",&n);
    scanf("%s",s+1);
    scanf("%d",&Q);
    for(int i=1;i<=Q;i++){
        scanf("%d %d",&x,&y);
        query.push_back( Query(x,y,i) );
    }

    sort( query.begin(), query.end(), comp );

    int pos = 0;
    startpos.build(1,1,n);
    last = 1; tot = 1;
    for(auto& [x,y,no] : query){
        while( pos < y ){
            pos++;
            Add(s[pos] - 'a',pos);
        }
        ans[no] = startpos.query( 1,1,n, x,pos );
    }
   
    for(int i=1;i<=Q;i++)printf("%lld\n",ans[i]);


}

  本来是心血来潮想写下题解加深一下对SAM的理解的结果越写越乱,磨磨蹭蹭写了一个半小时,还是太菜了555。
  不过这题能"一次过"还是多亏了数据结构队友能手写各种"100%正确"的线段树,要我写还真写不来
  不过写这题的时候中途看了眼提交看下队友有没有开别的题,然后发现队友K题交了6发,我一看K是哪题啊,结果就是我写的这道题!你们又不懂SAM交个毛线啊,演我吧。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值