ACM-字符串完全总结(知识点+模板)

目录

  1. 常用STL方法

  2. manacher算法

  3. 字符串Hash

  4. KMP
    4.1普通KMP
    4.2扩展KMP

  5. Trie(字典树)
    5.1 字典树
    5.1 01字典树

  6. 自动机
    6.1 AC自动机
    6.2 AC自动机上的动态规划
    6.3 回文自动机(回文树)

  7. 后缀数组
    7.1后缀数组的常见用法

  8. 后缀自动机(SAM)



注意

1.对于字符串问题,最好使用char []来存储,不要用string,否则可能会占用大量内存及减低速度
2.strlen(char []),以及相似方法的复杂度均为O(n),千万不要用在循环里!

一.常用STL方法:

任意进制转换:
    itoa(int n,char* s,int r)  //将10进制n转换为r进制并赋给s
 string:
 流:
      #include
      stringstream stream;   //创建名为stream的流
      stream.clear   //重复使用必须清空
      string a,str;
      stream(a);             //将字符串a放入流
      while(a>>str)       //将a流入str中,以空格为拆分进行输出
           cout<< str<< endl;
     迭代器:
       string::iterator it  //创建名为it的迭代器
     反转:
        reverse(s.begin(), s.end());   //原地反转
        s1.assign(s.rbegin(), s.rend());   //反转并赋给s1

 大小写转换:
             transform(s.begin(), s.end(), s.begin(), ::toupper);
             transform(s.begin(), s.end(), s.begin(), ::tolower);

 类型转换:
         string ->int : string s("123");
                        int i = atoi(s.c_str());

         int -> string: int a;
                        stringstream(s) >> a;
 子串:
          string substr(int pos = 0,int n = npos) const;//返回pos开始的n个字符组成的字符串
 插入:s.insert(int p0,string s)    //在p0位置插入字符串s
 更改:
         s.assign(str); //直接
         s.assign(str,1,3);//如果str是”iamangel” 就是把”ama”赋给字符串
         s.assign(str,2,string::npos);//把字符串str从索引值2开始到结尾赋给s
         s.assign(“gaint”); //不说
         s.assign(“nico”,5);//把’n’ ‘I’ ‘c’ ‘o’ ‘\0’赋给字符串
         s.assign(5,’x’);//把五个x赋给字符串
 删除:
      s.erase(13);//从索引13开始往后全删除
      s.erase(7,5);//从索引7开始往后删5个
      iterator erase(iterator it);//删除it指向的字符,返回删除后迭代器的位置  
        iterator erase(iterator first, iterator last);//删除[first,last)之间的所有字符,返回删除后迭代器的位置
 查找:
      int find(char c, int pos = 0) const;//从pos开始查找字符c在当前字符串的位置
      int find(const char *s, int pos = 0) const;//从pos开始查找字符串s在当前串中的位置
      int find(const char *s, int pos, int n) const;//从pos开始查找字符串s中前n个字符在当前串中的位置
      int find(const string &s, int pos = 0) const;//从pos开始查找字符串s在当前串中的位置。
删除所有特定字符:
      str.erase(std::remove(str.begin(), str.end(), 'a'), str.end());
删除所有重复字符:
      要求对象有序O(n+n),如果先排序O(nlogn+n+n)
      str.erase(unique(str.begin(),str.end(),str.end());

二.manacher算法

能求出以任一点为中点的回文半径(回文串长度),O(n)

#include
#include
#include 
#include
using namespace std;
const int maxl=1100005;
int p[2*maxl+5];   //p[i]-1表示以i为中点的回文串长度
int Manacher(string s)
{
    string now;
    int len=s.size();
    for(int i=0;i<len;i++)      //将原串处理成%a%b%c%形式,保证长度为奇数
    {
       now+='%';
       now+=s[i];
    }
    now+='%';
    int len=now.size();
    int pos=0,R=0;
    for (int i=0;i<len;i++)
    {
        if (i<R) p[i]=min(p[2*pos-i],R-i); else p[i]=1;
        while (0<=i-p[i]&&i+p[i]<len&&now[i-p[i]]==now[i+p[i]]) p[i]++;
        if (i+p[i]>R) {pos=i;R=i+p[i];}
    }
    int MAX=0;
    for (int i=0;i<len;i++)
    {
        cout<<i<<" : "<<p[i]-1<<endl;            //p[i]-1为now串中以i为中点的回文半径,即是s中最长回文串的长度
        cout<<now.substr(i-p[i]+1,2*p[i]-1)<<endl;
        MAX=max(MAX,p[i]-1);
    }
    return MAX;           //最长回文子串长度
}
string a;
int main()
{
    std::ios::sync_with_stdio(false);
    while(cin>>a)
    cout<<Manacher(a)<<endl;
    return 0;
}

三.字符串Hash

hash[i]=(hash[i-1]*seed+str[i])%MOD
将字符串通过hash函数映射成一个数字,并由此判断是否重复等,近似字符串匹配

#include 
#include 
#include 
#include 
using namespace std;
const int mod=1e6+13;
unordered_set<long long>hash_table;
int eid=0,p[mod];
struct node
{
    long long next;
    string s;
}e[100005];
void insert(int u,string str)
{
    e[eid].s=str;
    e[eid].next=p[u];
    p[u]=eid++;
}
string str;
unsigned int BKDRHash(string str)   //unsigned int 在溢出时会自动取模2^32??
{
    unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
    unsigned int hash = 0;
    for(int i=0;i<str.size();i++)
        hash = (hash * seed + (str[i]))%mod;
    return (hash & 0x7FFFFFFF)%mod;
}
// AP Hash Function
unsigned int APHash(string str)
{
    unsigned int hash = 0;
    int i;
    for (i=0;i<str.size(); i++)
    {
        if ((i & 1) == 0)
            hash ^= ((hash << 7) ^ (str[i]) ^ (hash >> 3));
        else
            hash ^= (~((hash << 11) ^ (str[i]) ^ (hash >> 5)));
    }
    return (hash & 0x7FFFFFFF);
}
int hash_in(string s)
{
    long long u=BKDRHash(s);
    for(int i=p[u];i!=-1;i=e[i].next)    //使用链式前向星模拟链表进行冲突判断
    {
        string v=e[i].s;
        if(v==s) return 0;
    }
    insert(u,s);
    return 1;
}
int main()
{
    memset(p,-1,sizeof(p));
    while(cin>>str)
    {
        cout<<str<<endl;
        cout<<BKDRHash(str)<<endl;
        cout<<APHash(str)<<endl;
        cout<<hash_in(str)<<endl;
    }
    return 0;
}


四.KMP

字符串匹配,并求出匹配位置,O(n+m)

#include 
#include 
using namespace std;
int Next[100005];    //next[i]表示,在t[0,i)中,所有匹配真前缀和真后缀的长度
int n,m;
string s,t;
void GetNext()   //原始版本,原始next[i]表示,在t[0,i)中,所有匹配真前缀和真后缀的长度值右移一位,然后初值赋为-1而得
{
    Next[0] = -1;
    int k = -1;
    int j = 0;
    int len=t.size();
    while (j < len - 1)
    {
        //p[k]表示前缀,p[j]表示后缀
        if (k == -1 || t[j] == t[k])
        {
            ++k;
            ++j;
            Next[j] = k;
        }
        else
        {
            k = Next[k];
        }
    }
}
void getnext()   //优化版,对于重复部分效果更好
{
    int k=Next[0]=-1;  //模式串指针,将next[0]=-1 表示一个哨兵,即是一个通配符,可以和任何字符匹配
    int j=0;     //主串指针
    int len=t.size();
    while(j<len-1)
    {
        if(k==-1 || t[j]==t[k])    //匹配
        {
            k++;
            j++;
            if(t[j]!=t[k])
                Next[j]=k;
            else
                Next[j]=Next[k];
        }
        else          //失配
            k=Next[k];
    }
}
int KMP()     //串全部从0开始
{
    getnext();
    int i=0;
    int j=0;
    int lens=s.size();
    int lent=t.size();
    while(i<lens && j<lent)
    {
        if(j==-1 || s[i]==t[j])
        {
            i++;
            j++;
        }
        else
            j=Next[j];
    }
    if(j==lent)
        return i-j;
    else
        return -1;
}
int main()
{
    while(cin>>s>>t)
        cout<<KMP()<<endl;
    return 0;
}

扩展KMP

求S的所有后缀与T的最长公共前缀,O(n+m)

#include
using namespace std;
const int maxn=10086;   //字符串长度最大值
int Next[maxn],exnext[maxn]; //ex数组即为extend数组
string s,t;
//预处理计算Next数组
void getnext(string str)           //next[i]表示,t[i...m]与t[1...m]中的最长公共前缀
{
    int i=0,j,po,len=str.size();
    Next[0]=len;//初始化Next[0]
    while(str[i]==str[i+1]&&i+1<len)//计算Next[1]
        i++;
    Next[1]=i;
    po=1;//初始化po的位置
    for(i=2; i<len; i++)
    {
        if(Next[i-po]+i<Next[po]+po)//第一种情况,可以直接得到Next[i]的值
            Next[i]=Next[i-po];
        else//第二种情况,要继续匹配才能得到Next[i]的值
        {
            j=Next[po]+po-i;
            if(j<0)j=0;//如果i>po+Next[po],则要从头开始匹配
            while(i+j<len&&str[j]==str[j+i])//计算Next[i]
                j++;
            Next[i]=j;
            po=i;//更新po的位置
        }
    }
}
//计算extend数组
void exkmp(string s,string t)                    //exnext[i],表示s[i...n]与t[1...m]中的最长公共前缀
{
    int i=0,j,po,len=s.size(),l2=t.size();
    getnext(t);//计算子串的Next数组
    while(s[i]==t[i] && i<l2&&i<len)//计算ex[0]
        i++;
    exnext[0]=i;
    po=0;//初始化po的位置
    for(i=1; i<len; i++)
    {
        if(Next[i-po]+i<exnext[po]+po)//第一种情况,直接可以得到ex[i]的值
            exnext[i]=Next[i-po];
        else//第二种情况,要继续匹配才能得到ex[i]的值
        {
            j=exnext[po]+po-i;
            if(j<0)j=0;//如果i>ex[po]+po则要从头开始匹配
            while(i+j<len&&j<l2&& s[j+i]==t[j])//计算ex[i]
                j++;
            exnext[i]=j;
            po=i;//更新po的位置
        }
    }
}
int main()
{
    while(cin>>s>>t)
    {
        exkmp(s,t);
        for(int i=0;i<t.size();i++)
            cout<<Next[i]<<endl;
        for(int i=0;i<s.size();i++)
            cout<<exnext[i]<<endl;
    }
    return 0;
}

五.字典树(Trie)

一个节点和其所有孩子都有相同的前缀,即该节点对应的字符串,或者说从根节点出发到该节点的路径,组成的子串
各种操作均为O(n)

常见用法:
1.字符串匹配
2.前缀查询
3.求连续异或和

#include
using namespace std;
const int N=100005;
int tot=0;   //tot表示下个结点的编号
int trie[N][26];   //存贮字典树,tire[rt][x],表示其下一个结点的编号,其中rt表示父节点编号,x表示字母
int sum[N];    //保存前缀出现次数
void insert(string ch) {                        //类似链式前向星的思想
    int rt = 0;
    int len=ch.size();
    for (int i = 0; i < len; i++) {
        if (trie[rt][ch[i] - 'a'] == 0)         //当前字母未出现过
            trie[rt][ch[i] - 'a'] = ++tot;
        sum[trie[rt][ch[i]-'a']]++;
        rt = trie[rt][ch[i] - 'a'];
    }
}
bool find(string s)      //查询s是否存在
{
    int rt=0;
    int len=s.size();
    for(int i=0;i<len;i++)
    {
        if(trie[rt][s[i]-'a']==0) return false;     //在rt后没有以s[i]开头的字符串
        rt=trie[rt][s[i]-'a'];    //若有,则继续向下
    }
    return true;
}
int search(string s)        //查询以s为前缀的串个个数
{
    int rt=0;
    int len=s.size();
    for(int i=0;i<len;i++)
    {
        if(trie[rt][s[i]-'a']==0) return 0;
        rt=trie[rt][s[i]-'a'];
    }
    return sum[rt];
}
int main()
{
    string a;
    while(cin>>a)
    {
        insert(a);
        cout<<find(a)<<endl;
        cout<<search(a)<<endl;
    }
    return 0;
}

01字典树

用字典树保存一个数的二进制形式(只由01构成),若想要异或值大,一定要越靠前的数异或后为1,即尽可能使权值较大的数异或后为1
常见用法:
求各种与异或相关的操作

求区间最大异或值

#include
#define maxn 1005
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
int a[maxn];
int num[32*maxn][2];
int node[32*maxn][2];
int val[32*maxn];
int sum,ans,l,r,anss,s;
void init(){
    sum=1;
    ans=-inf;
    memset(num,0,sizeof num);
    memset(node,0,sizeof node);
    memset(val,0,sizeof val);
}
void change1(int m,int x){
    int pos=0;
    for(int i=30;i>=0;i--){
        int j=x>>i&1;
        num[pos][j]+=m;
        if(node[pos][j]) pos=node[pos][j];
        else{
            memset(node[sum],0,sizeof node[sum]);
            node[pos][j]=sum++;
            pos=node[pos][j];
        }
    }
    val[pos]=x;
}
int search1(int L,int R,int x){
    int pos=0;
    int w=0;
    
    for(int i=30;i>=0;i--){
        int j=x>>i&1;
        if(num[pos][!j]){
            w+=1<<i;
            pos=node[pos][!j];
        }
        else    pos=node[pos][j];
    }
    if(w>ans) ans=w,l=L,r=R,anss=val[pos];
}
int main(){
    int t,n;
    cin>>t;
    while(t--){
        init();
        cin>>n;
        for(int i=1;i<=n;i++) cin>>a[i],change1(1,a[i]);
        for(int i=1;i<=n;i++){
            s=0;
            for(int j=i;j<=n;j++){
                s+=a[j];
                change1(-1,a[j]);
                int w=search1(i,j,s);
            }
            
            for(int j=i;j<=n;j++) change1(1,a[j]);
        }
        cout<<l<<" "<<r<<" "<<anss<<" "<<ans<<endl;
    }
}

六.AC自动机:

结合KMP与trie,相当于在trie上构建了一个next数组,保存每个位置的失配后调整位置
用于多字符串匹配,如在匹配cat时失配,但存在另一个单词cart,则失配指针指向前缀ca,避免重复计算

#include 
using namespace std;

const int MAXN=1000005;
const int MAXC=26;

struct AC_Automaton {
    int trie[MAXN][MAXC], fail[MAXN], sta[MAXN],Q[MAXN];
    int tot;

    void init() {
        memset(trie, 255, sizeof(trie));
        memset(fail, 0, sizeof(fail));
        tot = 0;
        memset(sta, 0, sizeof(sta));
    }

    void insert(string ch) {              //插入字符串到字典树中
        int rt = 0, l = ch.size();
        for (int i = 0; i < l; i++)
        {
            if (trie[rt][ch[i] - 'a'] == -1) trie[rt][ch[i] - 'a']= ++tot;
            rt = trie[rt][ch[i] - 'a'];
        }
            sta[rt]++;
    }
    
    void build()           //构建fail指针数组,相当于next数组,利用bfs求
    {
        queue<int>Q;
        for(int i=0;i<MAXC;i++)       //初始化
            if(trie[0][i]==-1)
                trie[0][i]=0;         //将不存在的点指向根节点
            else
                Q.push(trie[0][i]);    //将与根节点直接相连的点入队

        while(!Q.empty())
        {
            int rt=Q.front();
            Q.pop();
            for(int i=0;i<MAXC;i++)
            {
                if(trie[rt][i]==-1)           //某一点无后续节点,将其连向失配指针所在位置
                    trie[rt][i]=trie[fail[rt]][i];
                else                            //有后续节点
                {
                    fail[trie[rt][i]]=trie[fail[rt]][i];     //其失配指针是从其父亲失配指针指向位置向后搜索i,若有则连接,若无连向根
                    Q.push(trie[rt][i]);
                }
            }
        }
    }
    
    int solve(string ch) {                         //求字典树中有多少字符串出现在ch中
        int ret = 0, rt = 0, l = ch.size();
        for (int i = 0; i < l; i++) {
            rt = trie[rt][ch[i] - 'a'];
            int tmrt = rt;
            while (tmrt) {
                ret += sta[tmrt];
                sta[tmrt] = 0;
                tmrt = fail[tmrt];
            }
        }
        return ret;
    }
}T;

int main()
{
    std::ios::sync_with_stdio(false);
    int n;
    string a;
    cin>>n;
    T.init();
    while(n--)
    {
        cin>>a;
        T.insert(a);
    }
    T.build();
    string t;
    cin>>t;
    cout<<T.solve(t)<<endl;
    return 0;
}

AC自动机上的动态规划

Trie图上每个点都是一个状态,在AC自动机的状态相互装化,可以实现动态规划

1.求模式串在原串中出现的次数:(所有模式串不同)
构建Trie树时,在每个串结尾节点进行标记,在AC自动机上匹配时,每遇到一次结尾节点即表示成功匹配,ans++

2.求模式串在原串中出现次数,若模式串B是模式串A的子串,则只记录A:
构建Trie树时,在每个串结尾节点进行标记,在AC自动机上匹配时,对经过的子串模式串消除标记,其余与之前相同

3.求原串中不包含任何模式串的串的种类数:
对所有模式串构建AC自动机,模式串的终止的都是非法的,不可经过
dp[i][j]表示长度为i,状态为j的字符串的种类数,枚举所有字符进行状态匹配,答案即为sum(dp[m][i])
若m较小,n较大,可以考虑用矩阵乘法加速dp

4.求一个长度最短的串使得它包含所有模式串:
对所有模式串建立AC自动机,若n很小,可用状态压缩dp
二进制状态j&2^k表示从根节点到该结点的路径上有第k个模式串 dp[i][j]表示状态i,二进制状态j的最短长度,初始化dp[i][j]=0
在Trie上求(0,0)到(i,2n-1)点的最短路,答案即是dp[i][2-1]

回文自动机(回文树)

以最长回文为前缀构建的字典树,用回文串代替字典树中的前缀

常用方法:

1.求串S前缀0~i内本质不同回文串的个数(两个串长度不同或者长度相同且至少有一个字符不同便是本质不同)
2.求串S内每一个本质不同回文串出现的次数
3.求串S内回文串的个数(其实就是1和2结合起来)
4.求以下标i结尾的回文串的个数

const int MAXN = 100005 ;  
const int N = 26 ;  

struct Palindromic_Tree {  
    int next[MAXN][N] ;//next指针,next指针和字典树类似,指向的串为当前串两端加上同一个字符构成  
    int fail[MAXN] ;//fail指针,失配后跳转到fail指针指向的节点  
    int cnt[MAXN] ; //表示节点i表示的本质不同的串的个数(建树时求出的不是完全的,最后count()函数跑一遍以后才是正确的)
    int num[MAXN] ; //表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数
    int len[MAXN] ;//len[i]表示节点i表示的回文串的长度(一个节点表示一个回文串)
    int S[MAXN] ;//存放添加的字符  
    int last ;//指向新添加一个字母后所形成的最长回文串表示的节点。
    int n ;//表示添加的字符个数。
    int p ;//表示添加的节点个数。遍历所有结点要从[2~p-1]

    int newnode ( int l ) {//新建节点  
        for ( int i = 0 ; i < N ; ++ i ) next[p][i] = 0 ;  
        cnt[p] = 0 ;  
        num[p] = 0 ;  
        len[p] = l ;  
        return p ++ ;  
    }  

    void init () {//初始化  
        p = 0 ;  
        newnode (  0 ) ;  
        newnode ( -1 ) ;  
        last = 0 ;  
        n = 0 ;  
        S[n] = -1 ;//开头放一个字符集中没有的字符,减少特判  
        fail[0] = 1 ;  
    }  

    int get_fail ( int x ) {//和KMP一样,失配后找一个尽量最长的  
        while ( S[n - len[x] - 1] != S[n] ) x = fail[x] ;  
        return x ;  
    }  

    void add ( int c ) {  
        c -= 'a' ;  
        S[++ n] = c ;  
        int cur = get_fail ( last ) ;//通过上一个回文串找这个回文串的匹配位置  
        if ( !next[cur][c] ) {//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串  
            int now = newnode ( len[cur] + 2 ) ;//新建节点  
            fail[now] = next[get_fail ( fail[cur] )][c] ;//和AC自动机一样建立fail指针,以便失配后跳转  
            next[cur][c] = now ;  
            num[now] = num[fail[now]] + 1 ;  
        }  
        last = next[cur][c] ;  
        cnt[last] ++ ;  
    }  

    void count () {  
        for ( int i = p - 1 ; i >= 0 ; -- i ) cnt[fail[i]] += cnt[i] ;  
        //父亲累加儿子的cnt,因为如果fail[v]=u,则u一定是v的子回文串!  
    }  
} ;  

七.后缀数组:

计算一个字符串所有后缀经过字典排序后的起始下标结果

sa数组(后缀数组): 将S的n个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入SA
rk数组(名次数组): 名次数组Rank[i]保存的是Suffix(i)在所有后缀中从小到大排列的“名次”
height数组:定义height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。那么对于j和k,不妨设rank[j]< rank[k],则有以下性质:
suffix(j)和suffix(k)的最长公共前缀为height[rank[j]+1],height[rank[j]+2],height[rank[j]+3],……,height[rank[k]]中的最小值。

常用方法:

单串问题:

1.可重叠最长重复子串:给定一个字符串,求最长重复子串,这两个子串可重叠
重复子串即是两后缀的公共前缀,最长重复子串,等价于两后缀的最长公共前缀的最大值
只需计算height数组,再比较最大值,就是最长重复子串的长度

2.不可重叠最长重复子串:
直接用height数组不能保证两后缀不重叠,可二分答案,进行判定
问题转化为:原串中是否有长度为k的重复子串,按height数组把后缀分组,每组height值都>=k。
若存在至少一组,SAmax - SAmin >=k,则存在长度为k的重复子串

3.最长k次重复子串:求至少出现k次的最长重复子串,且k个子串可以重叠
先二分答案,将后缀分成如果组,判断有没有一个组的后缀个数不小于k,若有,则存在k个相同的子串满足条件
反之,不存在

4.不同子串的个数:
即对n-sa[i]+1-height[i]求和

双串问题:

1.最长公共子串:给定两个串,求这两个串的最长公共子串
先将两个串拼接在一起,形成A&B的形式,$为分隔符,小于所有字母。再求出新串的后缀数组及height
求排名相邻,原来不在同一个字符串的height值的最大值

2.长度小于k的公共子串个数:求两个串中,长度不小于k的公共子串个数,可以相同
先将两个串拼接,按height值分组后,统计每组中后缀之间的最长公共前缀和
扫描一遍,没遇到一个b的后缀就统计与前面a的后缀能产生多少个长度不小于k的公共子串
a的后缀用一个单调栈维护,再对a的后缀与前面b的后缀也做同样的计算

多串问题:

1.其它串没有的子串:问a串中有多少种字符串集合B中没有的连续子串
先将a串和其它子串拼接,求一遍总的子串个数,再减去a串与集合B相连的子串个数

2.多串的最长公共子串:
将n个字符串拼接,二分答案,将后缀按height值分成若干组,判断是否存在一组后缀属于n个字符串

3.不少于k个串的最长子串
将n个字符串拼接,求后缀数组,二分答案,将后缀按height值分成若干组
判断每组的后缀是否出现在不小于k个的原串中

4.每个串中至少出现2次且不重叠的子串个数:
将所有串拼接,二分答案,按height值分组,若某一组存在每个串至少出现2次,则满足

5.出现或反转后出现在每个字符串的最长子串:
先将每个字符串反转,在将反转后的与原来的一起全部拼接,求后缀数组。
二分答案,将后缀分组,判断时看是否有一组后缀在每个原来的串或反转后的串中出现

构建后缀数组和height,倍增法O(nlogn),求出现次数大于等于k次的最长子串长度

#include
using namespace std;

const int MAXN = 200010;
int s[MAXN];  // s 数组保存了字符串中的每个元素值,除最后一个元素外,每个元素的值在 1..m 之间,最后一个元素的值为 0
int wa[MAXN], wb[MAXN], wc[MAXN], wd[MAXN];  // 这 4 个数组是后缀数组计算时的临时变量,无实际意义
int sa[MAXN]; //  sa[i] 保存第 i 小的后缀在字符串中的开始下标,i 取值范围为 0..n-1
int cmp(int *r, int a, int b, int l) {
    return r[a] == r[b] && r[a + l] == r[b + l];
}
void getSA(int *r, int *sa, int n, int m) {  // n 为字符串的长度,m 为字符最大值
    int i, j, p, *x = wa, *y = wb;
    for (i = 0; i < m; ++i) wd[i] = 0;
    for (i = 0; i < n; ++i) wd[x[i] = r[i]]++;
    for (i = 1; i < m; ++i) wd[i] += wd[i - 1];
    for (i = n - 1; i >= 0; --i) sa[--wd[x[i]]] = i;
    for (j = 1, p = 1; p < n; j *= 2, m = p) {
        for (p = 0, i = n - j; i < n; ++i) y[p++] = i;
        for (i = 0; i < n; ++i) if (sa[i] >= j) y[p++] = sa[i] - j;
        for (i = 0; i < n; ++i) wc[i] = x[y[i]];
        for (i = 0; i < m; ++i) wd[i] = 0;
        for (i = 0; i < n; ++i) wd[wc[i]]++;
        for (i = 1; i < m; ++i) wd[i] += wd[i - 1];
        for (i = n - 1; i >= 0; --i) sa[--wd[wc[i]]] = y[i];
        for (swap(x, y), p = 1, x[sa[0]] = 0, i = 1; i < n; ++i)
            x[sa[i]] = cmp(y, sa[i - 1], sa[i], j) ? p - 1 : p++;
    }
    return;
}

int n;            //字符串长度
int Rank[MAXN];  // Rank[i] 表示从下标 i 开始的后缀的排名,值为 1..n
int height[MAXN]; // 下标范围为 1..n,height[1] = 0,表示suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,即排名相邻的两个后缀的最长公共前缀
void getHeight(int *r,int *sa,int n) {
    int i, j, k = 0;
    for (i = 1; i <= n; ++i) Rank[sa[i]] = i;
    for (i = 0; i < n; i++) {
        if (k) k--;
        int j = sa[Rank[i] - 1];
        while (r[i + k] == r[j + k]) k++;
        height[Rank[i]] = k;
    }
    return;
}
int lcp[MAXN][30];                      //存储lcp
void init_RMQ(int n)                   //初始化rmq
{
    for(int i=0;i<n;i++) lcp[i][0]=height[i];
    for(int j=1;(1<<j)<=n;j++)
        for(int i=0;i+(1<<j)<=n;i++)
            lcp[i][j]=min(lcp[i][j-1],lcp[i+(1<<(j-1))][j-1]);
}
int RMQ(int l,int r)                 //查询l~r的lcp
{
    int k=0;
    while((1<<(k+1))<=r-l+1) k++;
    int ans=min(lcp[l][k],lcp[r-(1<<k)+1][k]);
    return ans;
}
int ask(int l,int r)                //询问l~r的lcp,含边界处理
{
    if(l==r) return n-sa[r];
    return RMQ(l+1,r);
}
bool check(int nk,int K)         //将height分组判断
{
    int cnt=1;
    for(int i=1;i<=n;i++)
    {
        if(height[i]>=nk)         //统计长度大于nk的子串个数
        {
            cnt++;
            if(cnt>=K) return true;      //出现次数大于等于K,找到答案
        }
        else
            cnt=1;
    }
    return false;
}
int main()
{
    int K;
    cin>>n>>K;
    for(int i=0;i<n;i++)
        cin>>s[i];
    s[n]=0;                //必须要加!!,将s最后一位置为一个最小值

    getSA(s,sa,n+1,20000);      //!!!必须是n+1!!!
    getHeight(s,sa,n);
    init_RMQ(n+1);        //注意是n+1!!!!
    /*                       //求出现次数大于等于k的最长子串长度
    int l=0,r=n;
    int ans=0;
    while(l<=r)
    {
        int mid=(l+r)>>1;      //二分枚举子串长度
        if(check(mid,K))
        {
            ans=mid;
            l=mid+1;
        }
        else r=mid-1;
    }
    cout<
    /*                                          //求出现次数在[l,r]区间中的子串个数
    for(int i=1;i+l-1<=n;i++)
    {
        ans+=ask(i,i+l-1);                    //求出所有长度为l的区间的贡献
        if(i-1>0) ans-=ask(i-1,i+l-1);         //减去相邻区间重复贡献值
        if(i+r<=n) ans-=ask(i,i+r);            //减去长度为r+1区间的贡献值
        if(i-1>0 && i+r<=n) ans+=ask(i-1,i+r);       //容斥处理,加上多减去的部分
    }
    printf("%lld\n",ans);
    */
    return 0;
}


八.后缀自动机

求出现k次(或大于k次,或k1~k2次)的子串个数

#include
using namespace std;
typedef long long ll;
const int maxn = 2e6+3;

int root,last;
int cnt;
int l;

int sv[maxn*2];
struct query
{
    int a;
    ll ans;
}qu[maxn];

struct sam_node
{
    int fa,son[26];
    int len;
    void init(int _len)
    {
        len = _len;
        fa = -1;
        memset(son,-1,sizeof(son));
    }
}t[maxn*2];

void init()
{
    cnt = 0;
    root = last = 0;
    t[cnt].init(0);
}

void extend(int w)
{
    int p = last;
    int np = ++cnt;
    t[cnt].init(t[p].len+1);
    sv[l] = np;
    int q, nq;
    while(p != -1 && t[p].son[w] == -1)
    {
        t[p].son[w] = np;
        p = t[p].fa;
    }
    if(p == -1) t[np].fa = root;
    else
    {
        q = t[p].son[w];
        if (t[p].len+1 == t[q].len) t[np].fa=q;
        else
        {
            nq = ++cnt;
            t[nq].init(0);
            t[nq] = t[q];
            t[nq].len = t[p].len+1;
            t[q].fa = nq;
            t[np].fa = nq;
            while(p!=-1&&t[p].son[w]==q)
            {
                t[p].son[w] = nq;
                p = t[p].fa;
            }
        }
    }
    last = np;
}

int w[maxn], r[maxn*2];

void topo()
{
    for(int i = 0; i <= l; ++i) w[i] = 0;
    for(int i = 1; i <= cnt; ++i) w[t[i].len]++;
    for(int i = 1; i <= l; ++i) w[i] += w[i-1];
    for(int i = cnt; i >= 1; --i) r[w[t[i].len]--] = i;
    r[0] = 0;
}

int dp[maxn*2];
char s[maxn];


int main()
{
    int n, k, p;
    while(~scanf("%s",&s))
    {
        int L,R;
        scanf("%d%d",&L,&R);
        int tl = strlen(s);
        l = 0;
        init();
        for(int i = 0; i < tl; ++i)
        {
            ++l;
            extend(s[i]-'A');
        }
        for(int i = 0; i <= cnt; ++i) dp[i] = 0;
        topo();
        p = root;
        for(int i = 0; i < l; ++i)
        {
            p = t[p].son[s[i]-'A'];
            dp[p]++;
        }
        for(int i = cnt; i >= 1; --i)
        {
            p = r[i];
            if(t[p].fa != -1) dp[t[p].fa] += dp[p];
        }
        ll ans1 = 0, ans2 = 0;
        for(int i = 1; i <= cnt; ++i)
            if(dp[i] >= L)                        //L表示下界
                ans1 += t[i].len-t[t[i].fa].len;
        for(int i = 1; i <= cnt; ++i)
            if(dp[i] >= R+1)                      //R表示上界
                ans2 += t[i].len-t[t[i].fa].len;

        printf("%lld\n", ans1-ans2);           //求出现次数为L~R之间的子串的个数
    }
    return 0;
}

你可能感兴趣的:(ACM常用模板,算法完全解析)