• CF1037H Security (SAM+二维偏序)


    题目链接 CF1037H Security

    做法:(mathrm{SAM}) 后缀自动机,线段树(不用合并)

    题意简述

      给出一个文本串 (S) ,有 (Q) 次询问,每次询问给出模式串 (T) ,问在 (S) 串中 ([l,r]) 区间上是否存在比 (T) 的字典序大的子串,如果存在输出其中字典序最小的那个子串,否则输出 (-1)(1le lle r le|S|le10^5,1le Q,sum|T|le 2 imes 10^5)

    贪心分析

      首先考虑怎么求最小的比 (T) 字典序大的串,不考虑区间限制。设当前 (T)(S) 上最大匹配长度为 (len)

    • (len = |T|) ,则在 (T) 串后拼上一个字符 (c) 肯定比 (T) 字典序要大,我们只需在 (S) 串中找到一个串为 (T+c) ,并使 (c) 最小即可。
    • 否则,(T[1,len]) 上与 (S) 匹配,但是 (T[1,len+1]) 不匹配了,我们只需找到一个最小字符 (c),满足 (c>T[len+1])(T[1,len]+c)(S) 的子串,那么 (T[1,len]+c) 就是答案了。如果没找到这样的 (c) ,将匹配长度减一,重复进行这个操作。
    • (len=0) 也找不到这样的 (c) ,只能输出 (-1)

      可以用反证法证明这种构造方法得到的一定是最小的比 (T) 字典序大的串。而要想知道 (+c) 后是否是 (S) 的子串(即是否存在到 (c) 的转移),用 (mathrm{SAM}) 就好啦。

    线段树维护二维偏序

      考虑怎么实现区间限制。我们想知道 (S[l,r]) 上是否能匹配子串,自然不能每个区间都建立一个 (mathrm{SAM})。我们要在原 (S)(mathrm{SAM}) 中判断当前匹配到的状态,是否能被 ([l,r]) 完整包含。一个状态是一些子串的结束位置的集合,如果存在一个结束位置 (maxPos) ,满足 (l le maxPos-len+1,maxPosle r),((len) 为子串长度),则说明在 ([l,r]) 区间上有该子串。

      观察到如果 (r) 固定,使得所有结束位置都小于 (r) ,那么我们只需考虑 (l le maxPos-len+1) 部分的限制即可。 (maxPos) 越大,(maxPos-len+1) 越大,满足单调性,我们只需把一个状态的最大的结束位置找出来作判断即可。这就是经典的二维偏序问题了。我们先将询问按照右端点从小到大排序,将 (S[1,r]) 的位置值进线段树,这样找最大结束位置时保证一定是满足 (maxPosle r) 的。

        //按照右端点排序,小的在前
        sort(ques + 1, ques + 1 + n);
        for(int i = 1,r = 1;i <= n;i++){
            //偏序:让线段树只维护S串中的[1,ques[i].r],这样在check查询时只需判断是否大于左端点ques[i].l即可
            for(; r <= ques[i].r; r ++)
                update(1, 1, dfs_cnt, dfn[id[r]], r);
            match_t(1, i, 1, ques[i].id);
        }
    

      这里线段树是建立在 (mathrm{SAM})(mathrm{parent}) 树的 (mathrm{dfs}) 序上的,查询一个状态所有结束位置的最大值 即 查询它的子树上所有状态的结束位置的最大值。把子树转化为一段区间,就可以用线段树友好地进行区间操作了。(id[i]) 是一个序列下标到 (mathrm{SAM}) 上状态的映射。然后就是匹配过程了,看完整代码吧~

    #include<bits/stdc++.h>
    using namespace std;
    #define ls (now<<1)
    #define rs (now<<1)+1
    #define mid (l+r)/2
    typedef long long ll;
    const int N = 1e6+6;
    const int M = 2e5+6;
    int n,dfn[N],high[N],dfs_cnt,mx[N << 1],id[N];
    int nxt[N][26],link[N],len[N],cnt = 1,las = 1;
    bool flag[N] = {0};
    string ans[M];
    char s[N],t[N],*inputT = t;
    vector<int>a[N];
    
    struct node{
        int l,r,id,len;
        char *t;
        bool operator<(const node &y )const{return r<y.r;}
    }ques[N];
    
    void insert(int c,int o){
        int cur = ++cnt,p = las;
        len[cur] = len[las] + 1;
        las = cur;
        id[o] = cur;
        for(; p && !nxt[p][c] ; p = link[p])
            nxt[p][c] = cur;
        if(!p) link[cur] = 1;
        else{
            int q = nxt[p][c];
            if(len[q] == len[p] + 1) link[cur] = q;
            else{
                int clone = ++cnt;
                len[clone] = len[p] + 1;
                link[clone] = link[q];
                link[q] = link[cur] = clone;
                memcpy(nxt[clone],nxt[q],sizeof(nxt[q]));
                for(; nxt[p][c] == q;p = link[p])
                    nxt[p][c] = clone;
            }
        }
    }
    
    //求dfs序,now子树范围[dfn[now],high[now]]
    void dfs(int now){
        dfn[now] = ++dfs_cnt;
        for(auto to:a[now]) dfs(to);
        high[now] = dfs_cnt;
    }
    
    //普通线段树求区间最大值
    void update(int now,int l,int r,int x,int v){
        if(l == r) {mx[now] = max(mx[now],v);return;}
        if(x <= mid) update(ls,l,mid,x,v);
        else update(rs,mid+1,r,x,v);
        mx[now] = max(mx[ls],mx[rs]);
    }
    
    int query(int now,int l,int r,int x,int y){
        if(x <= l && r <= y) return mx[now];
        int an = 0;
        if(x <= mid) an = query(ls,l,mid,x,y);
        if(y > mid) an = max(an,query(rs,mid+1,r,x,y));
        return an;
    }
    
    //查询在now节点在SAM上的endpos集合中是否存在一点maxPos被[l,r]完整包含
    //因为线段树上节点都是小于r的,所以只要满足maxPos-match_len+1 >= l,即说明以maxPos结尾,长度为match_len的串在S串的[l,r]中完整出现
    //利用线段树O(lgn)查询now子树上(在dfs序中是一段区间)的S串位置的最大值maxPos
    bool check(int l,int now,int match_len){
        int maxPos = query(1, 1, dfs_cnt, dfn[now], high[now]);
        if(maxPos - match_len + 1 >= l) return true;
        else return false;
    }
    
    //now:当前t串已经在SAM匹配的节点,o:查询序号,index:当前要尝试匹配的t串的下标,ans_id离线的答案编号
    bool match_t(int now,int o,int index,int ans_id){
        //如果已经匹配到t串末尾,尝试在其后放一个字符,如果成功就能与t串拼成一个比t串字典序大的串
        if(ques[o].len < index){
            for(int i = 0;i < 26;i++)
                if(nxt[now][i] && check(ques[o].l, nxt[now][i], index)){
                    flag[ans_id] = 1;         //flag标记时候已找到一个比t串字典序大的串
                    ans[ans_id] += 'a'+ i;
                    return true;
                }
            return false;       //没拼成
        }
        int c = ques[o].t[index] - 'a';
        int next = nxt[now][c];
        //如果匹配上了,并且也被[l,r]区间包含了,在后面的匹配中也找到了比t串字典序大的串,这里就拼上与t串匹配的串即可
        if(next && check(ques[o].l, next, index) && match_t(next,o,index + 1,ans_id)){
            ans[ans_id] += 'a' + c;
            return true;
        }
        //否则,如果能拼上一个比c大的字符,一定也可以得到一个比t串字典序大的串
        else{
            for(int i = c + 1;i < 26;i++)
                if(nxt[now][i] && check(ques[o].l, nxt[now][i], index)){
                    flag[ans_id] = 1;
                    ans[ans_id] += 'a'+ i;
                    return true;
                }
            return false;
        }
    }
    
    int main(){
        scanf("%s",s+1);
        int lens = strlen(s+1);
        //建SAM
        for(int i = 1;i <= lens;i++) insert(s[i]-'a',i);
    
        //parent树连边,建立dfs序
        for(int i = 1;i <= cnt;i++) a[link[i]].push_back(i);
        dfs(1);
    
        scanf("%d",&n);
        //用inputT指针串联起字符串,防止开多个字符串爆内存
        for(int i = 1;i <= n;i++){
            scanf("%d %d %s", &ques[i].l, &ques[i].r, inputT + 1);
            ques[i].t = inputT;
            ques[i].id = i;
            ques[i].len = strlen(ques[i].t + 1);
            inputT += ques[i].len + 1;
        }
        //按照右端点排序,小的在前
        sort(ques + 1, ques + 1 + n);
        for(int i = 1,r = 1;i <= n;i++){
            //偏序:让线段树只维护S串中的[1,ques[i].r],这样在check查询时只需判断是否大于左端点ques[i].l即可
            for(; r <= ques[i].r; r ++)
                update(1, 1, dfs_cnt, dfn[id[r]], r);
            match_t(1, i, 1, ques[i].id);
        }
    
        for(int i = 1;i <= n;i++) {
            if(!flag[i]) printf("-1
    ");
            else{
                //倒序输出字符串
                int l = ans[i].size();
                for(int j = l - 1;j >= 0;j-- )
                    printf("%c",ans[i][j]);
                printf("
    ");
            }
        }
        return 0;
    }
    
    

      这题和 P4770 [NOI2018] 你的名字 做法其实是差不多的,但是由于我不会线段树合并,都只会用一颗线段树解决(°ー°〃),那时我是看 这个题解 做的题,在这里又用上了,做完这题可以去挑战《你的名字》啦。

  • 相关阅读:
    几种不同风格的Toast
    [置顶] 如何访问web文件夹之外的文件
    30天自制操作系统笔记(九十)
    tomcat install on Linux
    共享内存使用的基本思路和接口
    30天自制操作系统笔记(九十)——源码
    storm单机版安装配置
    新安装XAMPP,phpMyAdmin错误:#1045
    TI-Davinci开发系列之二使用CCS5.2TI Simulator模拟环境调试DSP程序
    ffmpeg的logo, delogo滤镜参数设置
  • 原文地址:https://www.cnblogs.com/ailanxier/p/14216115.html
Copyright © 2020-2023  润新知