SZUACM集训字符串基础总结: 字符串最小表示 ,KMP, EXKMP, Manracher, Trie树,字符串的hash; 附带一写常见的运用技巧,邝斌大佬的板子和例题[持续更新]

第一部分 字符串的匹配<-------->KMP

模式匹配:子串的定位运算称为串的模式匹配或串匹配。

假设有两个串S,T,设S为主串,也称正文串,T为子串,也称为模式,在主串S中查找与模式T相匹配的子串,如果查找成功,返回匹配的子串第1个字符在主串中的位置。最笨的办法就是穷举所有S的所有子串,判断是否与T匹配。该算法称为BF(Brute Force)算法,Brute Force的意思是蛮力,暴力穷举。

Knuth、Morris和Pratt对 该算法进行了改进,称为KMP算法。

在这里插入图片描述
在这里插入图片描述

i指向的字符前面的两个字符和T串中j指向的字符前面两个字符一-模一 样,因为它们一直相等,才会i++,j++走到当前的位置。只需要在T串本身比较就可以了。就是当不匹配的时候就回退,(回退最长公共前缀的位置如图,此时C与B不匹配,回退就是让蓝色框部分尽可能的长,那么回退的就会少)也就是这部分再次比较了.(pmt该位置(0~i)的子串和t串最长公共前缀(对于aba 末尾的a和第一个a相同))

在这里插入图片描述


回退位置next[]的求解方法:

本质上就是模式串的每一个前缀的后缀和模式串前缀的最长匹配长度

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

举个例子

假设T = ABABABB

(idx) j = 0 1 2 3 4 5 6

人为规定next[0] = -1;

说明:当某个位置不匹配时,我们就要看已经匹配的部分的后缀和前缀的最长公共部分

1.当j=1时发生不匹配,子串为空,next = 0

2.当j=2时发生不匹配,子串为 A,前后缀公共部分是空, next = 1

3.当j=3时发生不匹配,子串为 AB,前后缀公共部分为空, next = 1,

4.当j=4时发生不匹配,子串为 ABA,前后缀公共部分是A, next = 2

5.当j=5时发生不匹配,子串为 ABAB,前后缀公共部分是AB, next = 3

6.当j=6时发生不匹配,子串为 ABABA,前后缀公共部分是ABA, next = 4

7.当j=7时发生不匹配,子串为 ABABAB,前后缀公共部分是ABAB, next = 5

算法复杂性分析:

设S、T串的长度分别为n、m。KMP算法的特点就是,i不回退,当S[j]!=T[j]时, j回退到next[j],重新开始比较。最坏情况下扫描整个S串, 其时间复杂度为0(n)。计算next数组需要扫描整个T串,其时间复杂度为0(m),因此总的时间复杂度为0(n+m)。

下面看代码

void Getnext(int next[],String t)
{
   int j=0,k=-1;
   next[0]=-1;
   while(j<t.length-1)
   {
      if(k == -1 || t[j] == t[k])
      {
         j++;k++;
         next[j] = k;
      }
      else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!
   }
}

int KMP(String s,String t)
{
   int next[MaxSize],i=0;j=0;
   Getnext(t,next);
   while(i<s.length&&j<t.length)
   {
      if(j==-1 || s[i]==t[j])
      {
         i++;
         j++;
      }
      else j=next[j];               //j回退。。。
   }
   if(j>=t.length)
       return (i-t.length);         //匹配成功,返回子串的位置
   else
      return (-1);                  //没找到
}

这篇博客写的比较好

KMP的扩展引用

扩展1. 就是求一个字符串的循环节(next数组的应用)

根据next数组的性质

在这里插入图片描述

那么很明显如果length % eqsX == 0;那么eqsX 就是t串的一个循环节 c = length - next[length - 1];

这里要注意epsX == 0的情况;

举个例子 T = ababab

next[length] = 4 ==> T[0 ~ 3] == T[2 ~ 5]

那么n - next[length] = [ab]一个循环节

模板题传送门

下面看ac代码

#include <iostream>
#include <cstring>

using namespace std;
const int N = 1e5 + 10;
char p[N];
int ne[N];
int len;

void getnext()
{
    ne[1] = 0;
   for(int i = 2, j = 0; i <= len; ++ i)
   {
       while(j && p[i] != p[j + 1]) j = ne[j];
       if(p[i] == p[j + 1]) j ++ ;
       ne[i] = j;
   } 
    
}

int main()
{
    std::ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
   int T;
    cin >> T;
    while(T --)
    {
        cin >> p + 1;
        len = strlen(p + 1);
        getnext();
        int L = len - ne[len];
        
        if(!ne[len]) cout << len << endl;
        else if(len % L == 0) cout << "0" << endl;
        else cout << L - len % L << endl;
    }
    cout.flush();
    return 0;
}

扩展2.用next数组求解一个字符串的前缀和后缀相同的部分

在这里插入图片描述

通过上图可知,通过next数组的不断的嵌套就可以得出答案

next[g] = next[next[n]];

举个例子就是 T = abbaefgabba

next[n] = 3 T[0 ~ 3] == T[7 ~ 10]

abba == abba

next[next[n]] = 1; ab == ab;

模板题传送门

/* POJ2752 Seek the Name, Seek the Fame */
 
#include <stdio.h>
#include <string.h>
 
char s[400000+1];
int next[400000+1];
int result[400000+1];
 
 
void setnext(char s[], int next[], int len)
{
    next[0] = -1;
 
    int i = 0, j = -1;
    while (i < len)
    {
        if (j == -1 || s[i] == s[j]) {
            ++i;
            ++j;
            next[i] = j;
        } else
            j = next[j];
    }
}
 
int main(void)
{
    int count, t, i;
 
    while(scanf("%s", s) != EOF) {
        int len = strlen(s);
 
        // 计算next[]数组值
        setnext(s, next, len);
 
        // 计算结果:从字符串的尾部开始,下一个只能在与后缀相同的前缀字符串中
        count = 0;
        t = next[len - 1];
        while(t != -1) {
            if(s[t] == s[len - 1])
                result[count++] = t + 1;
            t = next[t];
        }
 
        // 输出结果
        for(i=count-1; i>=0; i--)
            printf("%d ", result[i]);
        printf("%d\n", len);
    }
 
    return 0;
}

扩展3,next数组求三个位置(前中后)的匹配

HDU4763

题意:给定一个字符串,长度在10^6之内,让这个字符串去匹配EAEBE形式的串,其中AB是任意长度的串(可为0),求E串最长是多少。

假如说我们去除中间的部分就是扩展2,加上中间之后就是从在中间找一个位置这个next[mid] = next[next[next[…]]]

#include<stdio.h>
#include<string.h>
#include<string>
#include<algorithm>
#include<math.h>
#include<map>
#include<queue>
#include<vector>
#include<stack>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
const int N=1000005;
 
char s[N];
int Next[N],l;
 
void get_Next()
{
    int i=0,j=-1;
    Next[0]=-1;
    while(i<l)
    {
        if(j==-1||s[i]==s[j])
            Next[++i]=++j;
        else
            j=Next[j];
    }
}
 
int KMP()
{
    int i,j;
    for(i=Next[l]; i; i=Next[i])
        for(j=2*i; j<=l-i; j++)
            if(Next[j]==i)
                return i;
    return 0;
}
 
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%s",s);
        int ans=0,k,i,j;
        l=strlen(s);
        get_Next();
//        for(i=1; i<=l; i++)
//            printf("%d ",Next[i]);
//        printf("\n");
        printf("%d\n",KMP());
    }
    return 0;
}

--------------------------------------------------分割线-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Manacher算法

我们先看两个字符串:

ABCCBA

ABCDCBA

显然这两字符串是回文的 然而两个串的对称中心的特性不同,第一个串,它的对称中心在两个C中间,然而第二个串,它的对称中心就是D。这样我们如果要记录回文串的对称中心,就显得复杂了。

为了解决这个问题,把两种情况统一起来,我们就在字母之间插入隔板,这样两个问题就统一了,因为所有的对称中心都会有个字符与之对应。像这样

​ $#A#B#C#C#B#A#

这样我们串中所有的回文串的长度就可以变成奇数个

manacher为什么有如此优秀的复杂度呢?让我用文字说明一下

1.对于一个回文串,有且仅有一个对称中心。且叫它回文对称中心。

2.在一个回文串内,任选一段区间 X ,一定存在关于"回文对称中心"对称的一个区间,且把这个区间叫做关于区间_X_的对称区间。

3.区间和对称区间一定全等。(你都是对称的怎么可能不全等)

4.若一个区间的对称区间是回文串,这个区间必定是一个回文串。在大的回文串内,它们回文半径相等。

5.然而我们通过确定关系预先得到的回文半径,它的数值,必定小于等于这个位置真实的回文串半径。

6.因此,我们若记录以每个位置为中心的回文串半径,当我们通过另一个回文中心将这个原先的中心点对称过去时,就可以确定对称过去的那个点的回文半径了。

7.考虑"另一个回文中心"如何确定,就是那个极大回文串的回文中心,也就是边界顶着右边我们已知的最远位置的,最长的回文串。

8.然而,考虑到,我们只能确认我们已知的回文串内的对称关系和回文半径等量关系,关于这个极大回文串右侧的世界,我们一无所知。

9.记录这些数据到p数组。同时记录一个mid,一个r,分别代表 已经确定的右侧最靠右的回文串的对称中心和右边界。

10.那么,当我们扫描到一个新的字符时,怎么先确定它的部分回文半径呢?

11.若当前扫描到的位置为i,若mid<=i<=r,则我们可以找到它的一个对称点。这个点的位置是多少?是 mid×2−i

所以,拓展一个新点时,我们不必从这个点左右两边第一个位置开始向两边拓展,可以预先确定一部分回文串。就是因为这个,manacher的复杂度是线性的。

若扩展一个新的关于该字符的回文半径,可以先确定一部分P[i]。

且我们知道,我们能确定的范围,其右侧不得大于r,即:p[i]+i-1<=r 移项得:p[i] <= r-i+1

下面看代码

mx是右边界,id目前最大回文半径的中心

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <map>
#include <cstring>
#include <iomanip>
#include <unordered_map>
#define INF 0x3f3f3f3f
#define f first
#define s second
#define IOS std:ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
using namespace std;
const int maxn = 2e5 + 10;
typedef long long LL;
typedef pair <int,int> PII;
int a,b,n,t;
char s[maxn];
char Ma[maxn];
int Mp[maxn];
int main()
{
    IOS;
    while(cin >> s)
    {
        int l = 0;
        int len = strlen(s);
        Ma[l ++] = '$';
        Ma[l ++] = '#';
        for(int i = 0; i < len; ++ i)
        {
            Ma[l ++] = s[i];
            Ma[l ++] = '#';
        }
        Ma[l] = 0;
        int mx = 0, id = 0;//mx是右边界,目前最大回文半径的中心
        for(int i = 0; i < l; ++ i)
        {
            Mp[i] = mx > i ? min(Mp[2 * id - i],mx - i) : 1;
            while(Ma[i + Mp[i]] == Ma[i - Mp[i]]) Mp[i] ++;
            if(i + Mp[i] > mx)
            {
                mx = i + Mp[i];
                id = i;
            }
        }
        int ans = 0;
        for(int i = 0; i < l; ++ i)
          ans = max(ans,Mp[i]);
          cout << ans - 1<< endl;
    }
    return 0;
}

下面看1个例子

S2 = abaaba

T2 = $ # a # b # a # a # b # a

P2 = 1 0 2 1 2 1 2 7 2 1 2 1 2 1

我们可以得到一个结论就是Mp[i] - 1就是原字符串回文长度

Manacher算法的扩展引用

扩展应用1.可以判段某个位置前面到起始位置以及某个位置到字符串的末尾是是否为回文串(某个前缀或者后缀是否回文)

模板传送hdu3613

题目大意:给你一个由26个小写字母组成的字符串,每个字母对应有一个权值,题目要求你将这个字符串切成两条,如果两条的字符串是回文的就是所有字母权值之和,否则为0,问你最大的切割价值是多少?

我们可以用Manacher算法预处理一下得到MP[]回文半径数组

(1)假如说S[0~i]是回文的那么Mp[i + 2] 就是它的回文中心所以我们只要判断一下Mp[i + 2] == i + 2

(2)假如说S[i~length-1]是回文的那么i + length + 2就是回文中心,那么Mp[i + len + 2] == len - i;那么其就回文

这是核心代码

for(int i = 0; i < len - 1; ++ i)
        {
            int tmp = 0;
            int num = Mp[i + 2] - 1;
            if(num == i + 1)tmp += hash[i];
            num = Mp[i + len + 2] - 1;
            if(num == len - i - 1) tmp += hash[len - 1] - hash[i];
            if(tmp > ans) ans = tmp;
        }

完整代码如下

#include <iostream>
#include <cstdio>
#include <stack>
#include <vector>
#include <map>
#include <cstring>
#include <deque>
#include <queue>
#include <algorithm>
#define MEM(a,al) memset(a,al,sizeof(a))
#define sfx(x) scanf("%lf",&x)
#define sfxy(x,y) scanf("%lf%lf",&x,&y)
#define sdx(x) scanf("%d",&x)
#define sdxy(x,y) scanf("%d%d",&x,&y)
#define pfx(x) printf("%.0f\n",x)
#define pfxy(x,y) printf("%.6f %.6f\n",x,y)
#define pdx(x) printf("%d\n",x)
#define pdxy(x,y) printf("%d %d\n",x,y)
#define getArray(a,len) for(int ia = 0; ia < len; ia++) scanf("%d",&a[ia])
#define printArray(a,len) for(int ia = 0; ia < len; ia++) printf("%d%c",a[ia],(ia==len-1)?'\n':' ')
#define fora(i,n) for(i = 0; i < n; i++)
#define fora1(i,n) for(i = 1; i <= n; i++)
#define foraf(i,n) for(int i = 0; i < n; i++)
#define foraf1(i,n) for(int i = 1; i <= n; i++)
#define ford(i,n) for(i = n-1; i >= 0; i--)
#define ford1(i,n) for(i = n; i > 0; i--)
#define fordf(i,n) for(int i = n-1; i >= 0; i--)
#define fordf1(i,n) for(int i = n; i > 0; i--)
#define IOS std::ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define INF 0x3f3f3f3f
#define hash Hash
#define f first
#define s second
using namespace std;
const int N = 5e5 + 10;
typedef pair<int,int> PII;
typedef long long LL;
char a[N], Ma[N * 2];
int T, hash[N], ne[26], Mp[N * 2];
inline void Manarcher(char a[],int len)
{
    int l = 0;
    Ma[l ++] = '$';
    Ma[l ++] = '#';
    for(int i = 0; i < len; ++ i)
    {
        Ma[l ++] = a[i];
        Ma[l ++] = '#';
    }
    Ma[l] = 0;
    int mx = 0, idx = 0;
    for(int i = 0; i < l; ++ i)
    {
        Mp[i] = mx > i ? min(Mp[2 * idx - i],mx - i) : 1;
        while(Ma[i + Mp[i]] == Ma[i - Mp[i]]) Mp[i] ++;
        if(i + Mp[i] > mx)
        {
            mx = i + Mp[i];
            idx = i;
        }
        
    }
}
int main()
{
    IOS;
    cin >> T;
    while(T --)
    {
        for(int i = 0; i < 26; ++ i)
          cin >> ne[i];
          cin >> a;
        int len = strlen(a);
        hash[0] = ne[a[0] - 'a'];
        for(int i = 1; i < len; ++ i)
          hash[i] = ne[a[i] - 'a'] + hash[i - 1]; 
        Manarcher(a,len);
         int ans = 0;
        for(int i = 0; i < len - 1; ++ i)
        {
            int tmp = 0;
            int num = Mp[i + 2] - 1;
            if(num == i + 1)tmp += hash[i];
            num = Mp[i + len + 2] - 1;
            if(num == len - i - 1) tmp += hash[len - 1] - hash[i];
            if(tmp > ans) ans = tmp;
        }
        cout << ans << endl;
    }
    return 0;
}

扩展应用2:Manacher求解所有回文串的左端点和右端点Manacher + 差分

从对称中心到回文串的末尾都是一个回文串,那么假如说我们要给区间标记,统计一下某个位置有多少个回文串以其为(左右端点)那么我们就要对用Manacher算法求出所有的回文半径

根据Manacher的性质原串的中心为 i / 2 - 1

Manacher的右端点为(i + Mp[i] - 1 - (i & 1)) / 2

下面看代码

        int j = i + (i & 1);
        if(j / 2 - 1 >= 0)    Prefix[j / 2 - 1] ++;
        if((i + Mp[i] - 1 - (i & 1)) / 2 >= 0)    Prefix[(i + Mp[i] - 1) / 2] --;

上面利用了差分的思想

for(int i = 1; i < len; ++ i)
      Prefix[i] += Prefix[i - 1]; 

查字典的Trie

Trie,又叫字典树,字典你现在可以想一想你手上的牛津第八版词典,假设你是这本词典的主编,我们要去查找一个单词这么查,怎么添加一个热点词汇?
我们一般查找单词都是按26个字母排序去检索的,而添加也是一样的,那为什么我们可以怎么轻松去完成这些步骤?那是因为词典特有的存储方式。我们来看一下实体图:

在这里插入图片描述
这个就是词典和Tire的存储方式。我们还是给他一个官方的定义:
Trie树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高
其实它可以看成一个二维数组,保存一些字符串->值的对应关系。基本上,它跟Map的 HashMap 功能相同,都是 key-value 映射,是一个键值对!只不过 Trie 的 key 只能是字符串。

至于Trie树的实现,可以用数组,也可以用指针动态分配,我做题时为了方便就用了数组,静态分配空间。当然我们后面会提到动态如何去实现。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。当然后面会详细解释它。

如果在字符串hash成数字用maptle了我们可以用trie树去hash

Trie特点及方法
Trie树的基本性质可以归纳为:
(1)根节点不包含字母,除根节点意外每个节点只包含一个字母。
(2)从根节点到某一个节点,路径上经过的字符连接起来,为该节点存储的单词。
(3)每个节点的所有子节点包含的单词不相同。
Trie树有一些特性:
其实就是基本性质扩展到实际问题!
1)根节点不包含字母,除根节点外每一个节点都只包含一个字母。
2)从根节点到某一节点,路径上经过的字母连接起来,为该节点对应的单词。
3)每个节点的所有子节点包含的字母都不相同。
4)如果字母的种数为n,则每个结点的出度为n,这也是空间换时间的体现,浪费了很多的空间。
5)插入查找的复杂度为O(n),n为单词长度。
插入,查找基本思想(以查单词为例):
1、插入过程
对于一个单词,从根开始,沿着单词的各个字母所对应的树中的节点分支向下走,直到单词遍历完,将最后的节点标记为红色,表示该单词已插入Trie树。
2、查询过程
同样的,从根开始按照单词的字母顺序向下遍历trie树,一旦发现某个节点标记不存在或者单词遍历完成而最后的节点未标记为红色,则表示该单词不存在,若最后的节点标记为红色,表示该单词存在。

模板
好了有了一些基本点知识点,我们来用他们去解决实际问题:
我们就想怎么去向词典中加东西,和怎么去查询一个单词是否存在?
插入
代码:

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
我们还是以画图的方式去理解:


// 插入一个字符串
void insert(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p] ++ ;
}

查询
当然查询与插入思想一致,我们在这里不做过多的强调与解释!!

// 查询字符串出现的次数

int query(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

动态实现 当然动态实现它,要注意指针的移动与及时的更新! 与静态一样在Trie树中主要有2个操作,插入、查找。一般情况下Trie树中很少存在删除单独某个结点的情况,因此只虑删除整棵树。

1、插入
假设存在字符串str,Trie树的根结点为root。i=0,p=root。
1)取str[i],判断p->next[str[i]-‘a’]是否为空,若为空,则建立结点temp,并将p->next[str[i]-‘a’]指向temp,然后p指向temp;若不为空,则p=p->next[str[i]-‘a’];
2)i++,继续取str[i],循环1)中的操作,直到遇到结束符’\0’,此时将当前结点p中的 exist置为true。

2、查找
假设要查找的字符串为str,Trie树的根结点为root,i=0,p=root
1)取str[i],判断判断p->next[str[i]-‘a’]是否为空,若为空,则返回false;若不为空,则p=p->next[str[i]-‘a’],继续取字符。
2)重复1)中的操作直到遇到结束符’\0’,若当前结点p不为空并且st为true,则返回true,否则返回false。

3、删除
删除可以以添加的思想进行删除,它是是一个递归思想(先去查找单词,当他的父节点只有他一个儿子节点那么p->next[i] = NULL;);

(与下面ac代码相比这个要复杂一点,考虑的情况更加多, 为了你理解我建议你直接去看AC的动态代码)

typedef  256 TREE_WIDTH 
typedef  128 WORDLENMAX 
struct trie_node_st {
        int idx;
        int pass;  
        struct trie_node_st *next[TREE_WIDTH];
};
struct trie_node_st root={0, 0, {NULL}};
//清空
void myfree(struct trie_node_st * rt)
{
    for(int i=0; i<TREE_WIDTH; i++){
        if(rt->next[i]!=NULL){
            myfree(rt->next[i]);
            rt->next[i] = NULL;
        }
    }
    free(rt);
    return;
}
void insert (char *word)
{
        int i;
        struct trie_node_st *curr, *newnode;

        if (word[0]=='\0')  return;
        curr = &root;
        for (i=0; ; ++i) {
                if (word[i] == '\0') {
                        break;
                }
                curr->pass++;
                if (curr->next[word[i]] == NULL)//不存在
                {
                        newnode = (struct trie_node_st*)malloc(sizeof(struct trie_node_st));//开辟空间
                        memset (newnode, 0, sizeof (struct trie_node_st));//清空
                        curr->next[word[i]] = newnode;//插入
                } 
                curr = curr->next[word[i]];//存在
        }
        curr->idx ++;
        return 0;
}
 int query (struct trie_node_st *rootp)
{
        char worddump[WORDLENMAX+1];
         int pos=0;
        int i;

        if (rootp == NULL) return 0;
        if (rootp->idx) {
                worddump[pos]='\0';
               return  rootp->idx+rootp->pass;//返回
        }
        for (i=0;i<TREE_WIDTH;++i) {
                worddump[pos++]=i;
                query(rootp->next[i]);
                pos--;
        }
        return 0;
}

下面就是个人比较喜欢的静态代码:

#include<iostream>

using namespace std;

const int N=200010;
int son[N][26],cnt[N],idx;
char str[N];
bool st[N];

int n;

void insert(char str[])
{
    int p=0;
    for(int i=0;str[i];i++){
        int u=str[i]-'a';
        if(!son[p][u])son[p][u]=++idx;
        p=son[p][u];
    }
    cnt[p]++;
}
int query(char str[]){
    int p=0;
    for(int i=0;str[i];i++){
        int u=str[i]-'a';
        if(!son[p][u])return 0;
        p=son[p][u];
    }
    return cnt[p];
}
int main(){
    cin>>n;
    while(n--){
        string op;
        cin>>op>>str;
        if(op=="I")insert(str);
        else 
        cout<<query(str)<<endl;

    }
    return 0;
}

字符串的最小表示法

相关推荐
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页