UVALive - 8086 Substring Sorting (后缀数组+线段树上二分)

题意:

  给一个串S, 多次询问k和m,求S的所有长度为k的不同子串中,字典序为排第m的串的最早出现位置

简化问题:

  如果没有长度k的限制,并且没有不同子串的限制要怎么做。要字典序第m大,容易想到用后缀数组,因为它就是将n个后缀按字典序排好的,设f(i) = 排名<=i的所有后缀的所有前缀的个数和,假设答案的串是排名i的后缀的前缀,那么有f(i) >= k 且 f(i-1) < k,则满足二分性,可以二分后缀排名解决。

扩展:

  有不同子串的限制,则类似求一个串有多少不同子串那样,对于每个排名为i的后缀的所有前缀,去掉height(i)个前缀即可,同样可以二分。

  此时有一个问题,相同的串如何求出最早出现位置?假设找到的答案长度为len,排名为i,如果这个串多次出现,那么只能在排名i...j中连续地出现,其中i<=j且对于所有k属于(i, j],height[k] >= len,那么最早出现位置就是min(sa[k]),k属于[i, j],如何得到对应区间[i, j]?二分j,用rmq取区间最小的height判断即可。

回到本题:

  有了长度k的限制后,对于每个排名为i的后缀,它的长度必须大于等于k,则n - sa(i) >= k,由子问题知道,如果后缀i可以提供一个不同子串,必须满足height(i) < k。

  那么,问题转化为,对于所有的排名i,在满足n - sa(i) >= k 且 height(i) < k 的i中找到一个第m大的i。

  有两个限制,容易想到排序,我们按询问k排序,同时对于所有后缀,按height值排序,则可去掉一个条件限制,接下来,我们需要一个数据结构,可以查询第m大的i,且数据可以是动态插入和删除的。

  考虑在线段树上二分,线段树叶子节点维护下标i(即排名i),值为0或1,非叶子节点维护区间和,要找到第m大的i,则从根结点开始跑,设左区间和为sum(l),则当sum(l) <= k时,答案在左区间里,否则,在右区间里找到第m - sum(l)的数,递归下去即可。另外,对于已经插入到线段树的排名i, 用优先队列维护sa(i), 不满足n - sa(i) >= k时将其删除。

#include<bits/stdc++.h>

#define rep(i,e) for(int i=0;i<(e);i++)
#define rep1(i,e) for(int i=1;i<=(e);i++)
#define repx(i,x,e) for(int i=(x);i<=(e);i++)
#define pii pair<int,int>
#define X first
#define Y second
#define PB push_back
#define MP make_pair
#define mset(var,val) memset(var,val,sizeof(var))
#define scd(a) scanf("%d",&a)
#define scdd(a,b) scanf("%d%d",&a,&b)
#define scddd(a,b,c) scanf("%d%d%d",&a,&b,&c)
#define IOS ios::sync_with_stdio(false);cin.tie(0)

using namespace std;

typedef long long ll;
template <class T>
void test(T a){cout<<a<<endl;}
template <class T,class T2>
void test(T a,T2 b){cout<<a<<" "<<b<<endl;}
template <class T,class T2,class T3>
void test(T a,T2 b,T3 c){cout<<a<<" "<<b<<" "<<c<<endl;}
const int N = 2e6+10;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1e9+7;
struct Node{
    int lc,rc;
    int val;
    void init(){
        lc=rc=val=0;
    }
}tree[N];
int tot;
void init(){
    tot=1;
    tree[0].init();
}
int update(int i, int l,int r,int pos){
    //原本想用区间第k大解决,发现做不了...
    //所以这个线段树是可持续化的写法(其实只是懒得改)
    int rt = tot++;
    tree[rt].init();
    tree[rt].val = 1;
    if(l==r){
        return rt;
    }
    int mid = l+r>>1;
    if(pos<=mid){
        tree[rt].rc = tree[i].rc;
        tree[rt].lc = update(tree[i].lc, l,mid,pos);
    }else {
        tree[rt].lc = tree[i].lc;
        tree[rt].rc = update(tree[i].rc, mid+1,r,pos);
    }
    tree[rt].val = tree[tree[rt].lc].val + tree[tree[rt].rc].val;
    return rt;
}
void del(int i,int l,int r,int pos){
    tree[i].val = 0;
    if(l==r){
        return;
    }
    int mid = l+r>>1;
    if(pos<=mid) del(tree[i].lc, l,mid,pos);
    else del(tree[i].rc,mid+1,r,pos);
    tree[i].val = tree[tree[i].lc].val + tree[tree[i].rc].val;
}
int n;

int query(int i,int l,int r, int k){
    if(tree[i].val < k) return -1;
    if(l==r) return l;
    int mid = l+r>>1;
    int lc = tree[i].lc;
    if(tree[lc].val >= k){
        return query(lc, l,mid,k);
    }else return query(tree[i].rc, mid+1, r, k-tree[lc].val);
}

int rnk[N],sa[N], height[N], tmp[N], cnt[N];char s[N];
void suf(int n,int m){
    int i,j,k;
    n++;
    for(i=0;i<n*2+5;i++)rnk[i]=sa[i]=height[i]=tmp[i]=0;
    for(i=0;i<m;i++)cnt[i]=0;
    for(i=0;i<n;i++)cnt[rnk[i]=s[i]]++;
    for(i=1;i<m;i++)cnt[i]+=cnt[i-1];
    for(i=0;i<n;i++)sa[--cnt[rnk[i]]] = i;
    for(int k = 1;k<=n;k<<=1){
        for(i=0;i<n;i++){
            int j = sa[i]-k;
            if(j<0) j+=n;
            tmp[cnt[rnk[j]]++]=j;
        }
        sa[tmp[cnt[0]=0]]=j=0;
        for(i=1;i<n;i++){
            if(rnk[tmp[i]]!=rnk[tmp[i-1]] || rnk[tmp[i]+k]!=rnk[tmp[i-1]+k])cnt[++j]=i;
            sa[tmp[i]] = j;
        }
        memcpy(rnk, sa, n*sizeof(int));
        memcpy(sa, tmp, n*sizeof(int));
        if(j>=n-1) break;
    }
    for(j = rnk[height[i=k=0]=0];i<n-1;i++,k++){
        while(~k&&s[i]!=s[sa[j-1]+k]) height[j]=k--,j=rnk[sa[j]+1];
    }
}
struct Query{
    int len,k,i;
    bool operator < (const Query &b)const{
        return len<b.len;
    }
}p[N];
pii pos[N];
int ans[N];
int mi[2][N][20];
void rmqinit(){
    rep1(i,n){
        mi[0][i][0] = height[i];
        mi[1][i][0] = sa[i];
    }
    rep(d,2)
    for(int k = 1;(1<<k)<=n;k++){
        for(int i = 1;i+(1<<k)-1<=n;i++){
            mi[d][i][k] = min(mi[d][i][k-1], mi[d][i+(1<<k-1)][k-1]);
        }
    }
}
int rmqquery(int l,int r,int d){
    int k = log2(r-l+1);
    return min(mi[d][l][k], mi[d][r-(1<<k)+1][k]);
}
int rmqquery(int x, int len){
    int A = x+1, B = n;
    int r=x;
    while(A<=B){
        int mid = A+B>>1;
        if(rmqquery(x+1,mid,0) >= len){
            r = mid;
            A = mid+1;
        }else B = mid-1;
    }
    int l = x;
    return rmqquery(l,r,1);
}

void work(){
    init();
    scanf("%s",s);
    n = strlen(s);
    suf(n,128);
    int q;scd(q);
    rep(i,q){
        int len,k;scdd(len,k);
        p[i].i = i;
        p[i].len = len;
        p[i].k = k;
    }
    sort(p,p+q);
    rep1(i,n){
        pos[i] = MP(height[i], i);
    }
    sort(pos+1,pos+1+n);
    int idx = 1;
    int rt = 0;
    rmqinit();
    priority_queue<int> que;
    for(int i = 0;i<q;){
        int len = p[i].len;
        while(idx<=n && pos[idx].X < len){
            int r = pos[idx].Y;
            rt = update(rt, 0,n,r);
            que.push(sa[r]);
            idx++;
        }
        while(!que.empty() && n - que.top() < len){
            int top = que.top();que.pop();
            del(rt, 0, n, rnk[top]);
        }
        while(i<q && p[i].len == len){
            int r = query(rt, 0,n,p[i].k);
            if(r==-1) ans[p[i].i] = -1;
            else {
                ans[p[i].i] = rmqquery(r, len);
            }
            i++;
        }
    }
    rep(i,q){
        if(ans[i]==-1){
            puts("Not found");
        }else printf("%d\n", ans[i]);
    }
}
int main() {
    #ifdef local
    freopen("in.txt","r",stdin);
    #endif // local
//    IOS;
    int t;scd(t);while(t--)
        work();
}

代码

如有写的不好或错误的地方,还请指出。如果哪里解释不清楚,欢迎交流和讨论

原文地址:https://www.cnblogs.com/Toshi/p/9521333.html

时间: 2024-10-07 18:42:09

UVALive - 8086 Substring Sorting (后缀数组+线段树上二分)的相关文章

HDU 1403 Longest Common Substring(后缀数组,最长公共子串)

hdu题目 poj题目 参考了 罗穗骞的论文<后缀数组——处理字符串的有力工具> 题意:求两个序列的最长公共子串 思路:后缀数组经典题目之一(模版题) //后缀数组sa:将s的n个后缀从小到大排序后将 排序后的后缀的开头位置 顺次放入sa中,则sa[i]储存的是排第i大的后缀的开头位置.简单的记忆就是“排第几的是谁”. //名次数组rank:rank[i]保存的是suffix(i){后缀}在所有后缀中从小到大排列的名次.则 若 sa[i]=j,则 rank[j]=i.简单的记忆就是“你排第几”

POJ 3693 Maximum repetition substring (后缀数组)

题目大意: 求出字典序最小,重复次数最多,的子串. 思路分析: RMQ + height 数组可以求出任意两个后缀的lcp 我们枚举答案字符串的重复的长度. 如果这个字符串的长度为 l ,而且这个字符串出现过两次或两次以上 那么你会发现在原串中  str[0] str[l] str[2*l] ....肯定有相邻的两个被包含在重复的串中. 我们求出这两个相邻的后缀的lcp 我们上面仅仅说的是被包含在重复的串中,但并不一定就是以 str[0], str[l],str[2*l]....为起点的. 那我

【BZOJ】4293: [PA2015]Siano 线段树上二分

[题意]给定n棵高度初始为0的草,每天每棵草会长高a[i],m次收割,每次在d[i]天将所有>b[i]的草收割到b[i],求每次收割量.n<=500000. [算法]线段树上二分 [题解]按照生长速度a[]排序后,容易发现数列永远单调. 在线段树上的区间维护以下值: 1.最后一棵草的高度a 2.上次收割日期b 3.总的草高和c 4.总的生长速度和d 5.收割标记D和B 上传的时候注意右区间收割晚于左区间时强制合并. 下传的时候注意标记D和B直接覆盖. 线段树上二分: 1.判断当前区间是否符合(

CF1083C Max Mex(线段树上二分)

这题卡倍增害我T了一发= = 显然Mex是可以二分的,于是就可以考虑二分一个Mex然后check一下 然后怎么check呢?可以对点权建一棵线段树,节点\([l,r]\)表示,链上点权的集合包括\([l,r]\)时,最短的链的端点 合并两个区间就是在四个端点间选两个作为新链的端点,判断另外两个端点在不在这条链上,在的话这就是一条合法的链.判断方法就是判断一下两段的距离是否等于一整条链的距离. 这样时间复杂度是\(O(nlog^2n)\),感觉可过的样子?然而还可以在线段树上二分把时间复杂度优化到

BZOJ 1396: 识别子串( 后缀数组 + 线段树 )

这道题各位大神好像都是用后缀自动机做的?.....蒟蒻就秀秀智商写一写后缀数组解法..... 求出Height数组后, 我们枚举每一位当做子串的开头. 如上图(x, y是height值), Heights数组中相邻的3个后缀, 假如我们枚举s2的第一个字符为开头, 那我们发现, 长度至少为len = max(x, y)+1, 才能满足题意(仅出现一次). 这个很好脑补...因为s2和其他串的LCP是RMQ, 肯定会<=LCP(s1,s2)或<=LCP(s2,s3). 然后就用len去更新s2中

HDU 1403 Longest Common Substring(后缀数组啊 求最长公共子串 模板题)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1403 Problem Description Given two strings, you have to tell the length of the Longest Common Substring of them. For example: str1 = banana str2 = cianaic So the Longest Common Substring is "ana", a

2016暑假多校联合---Substring(后缀数组)

2016暑假多校联合---Substring Problem Description ?? is practicing his program skill, and now he is given a string, he has to calculate the total number of its distinct substrings. But ?? thinks that is too easy, he wants to make this problem more interesti

hdu1403---Longest Common Substring(后缀数组求2个字符串的最长公共子串)

Problem Description Given two strings, you have to tell the length of the Longest Common Substring of them. For example: str1 = banana str2 = cianaic So the Longest Common Substring is "ana", and the length is 3. Input The input contains several

【XSY1551】往事 广义后缀数组 线段树合并

题目大意 给你一颗trie树,令\(s_i\)为点\(i\)到根的路径上的字符组成的字符串.求\(max_{u\neq v}(LCP(s_u,s_v)+LCS(s_u,s_v))\) \(LCP=\)最长公共前缀,\(LCS=\)最长公共后缀 \(1\leq n\leq 200000\),字符集为\(\{0\ldots 300\}\) 题解 我们先看看这个\(LCP(s_u,s_v)\)怎么求 广义后缀自动机不行.广义后缀树可能可以,但我不会.广义后缀数组可以.然后我就开始手推广义后缀数组 广义