目录
- 常用STL方法
- manacher算法
- 字符串Hash
KMP
4.1普通KMP
4.2扩展KMPTrie(字典树)
5.1 字典树
5.1 01字典树自动机
6.1 AC自动机
6.2 AC自动机上的动态规划
6.3 回文自动机(回文树)后缀数组
7.1后缀数组的常见用法
注意
1.对于字符串问题,最好使用char []来存储,不要用string,否则可能会占用大量内存及减低速度
2.strlen(char []),以及相似方法的复杂度均为O(n),千万不要用在循环里!
一.常用STL方法:
任意进制转换:
itoa(int n,char* s,int r) //将10进制n转换为r进制并赋给s
string:
流:
#include<sstream>
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());
二.manacher算法
能求出以任一点为中点的回文半径(回文串长度),O(n)
#include<iostream>
#include<cstring>
#include <string>
#include<algorithm>
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 <iostream>
#include <string>
#include <cstring>
#include <unordered_set>
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 <iostream>
#include <string>
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<bits/stdc++.h>
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<bits/stdc++.h>
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<bits/stdc++.h>
#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 <bits/stdc++.h>
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() { //构建失配指针表,相当于next数组
int l = 0, r = 0;
for (int i = 0; i < MAXC; i++)
if (trie[0][i] == -1)
trie[0][i] = 0;
else
Q[++ r] = trie[0][i];
while (l < r) {
int rt = Q[++l];
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];
Q[++ r] = 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,2^n-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 ;//表示添加的节点个数。
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.出现或反转后出现在每个字符串的最长子串:
先将每个字符串反转,在将反转后的与原来的一起全部拼接,求后缀数组。
二分答案,将后缀分组,判断时看是否有一组后缀在每个原来的串或反转后的串中出现
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
const int BufferSize = 1180000;
char buffer[BufferSize];
char *out_tail = buffer;
const int N = 100005;
int n;
char s[N];
namespace SA {
int sa[N], rk[N], ht[N], s[N<<1], t[N<<1], p[N], cnt[N], cur[N];
#define pushS(x) sa[cur[s[x]]--] = x
#define pushL(x) sa[cur[s[x]]++] = x
#define inducedSort(v) fill_n(sa, n, -1); fill_n(cnt, m, 0); \
for (int i = 0; i < n; i++) cnt[s[i]]++; \
for (int i = 1; i < m; i++) cnt[i] += cnt[i-1]; \
for (int i = 0; i < m; i++) cur[i] = cnt[i]-1; \
for (int i = n1-1; ~i; i--) pushS(v[i]); \
for (int i = 1; i < m; i++) cur[i] = cnt[i-1]; \
for (int i = 0; i < n; i++) if (sa[i] > 0 && t[sa[i]-1]) pushL(sa[i]-1); \
for (int i = 0; i < m; i++) cur[i] = cnt[i]-1; \
for (int i = n-1; ~i; i--) if (sa[i] > 0 && !t[sa[i]-1]) pushS(sa[i]-1)
void sais(int n, int m, int *s, int *t, int *p) {
int n1 = t[n-1] = 0, ch = rk[0] = -1, *s1 = s+n;
for (int i = n-2; ~i; i--) t[i] = s[i] == s[i+1] ? t[i+1] : s[i] > s[i+1];
for (int i = 1; i < n; i++) rk[i] = t[i-1] && !t[i] ? (p[n1] = i, n1++) : -1;
inducedSort(p);
for (int i = 0, x, y; i < n; i++) if (~(x = rk[sa[i]])) {
if (ch < 1 || p[x+1] - p[x] != p[y+1] - p[y]) ch++;
else for (int j = p[x], k = p[y]; j <= p[x+1]; j++, k++)
if ((s[j]<<1|t[j]) != (s[k]<<1|t[k])) {ch++; break;}
s1[y = x] = ch;
}
if (ch+1 < n1) sais(n1, ch+1, s1, t+n, p+n1);
else for (int i = 0; i < n1; i++) sa[s1[i]] = i;
for (int i = 0; i < n1; i++) s1[i] = p[sa[i]];
inducedSort(s1);
}
template<typename T>
int mapCharToInt(int n, const T *str) {
int m = *max_element(str, str+n);
fill_n(rk, m+1, 0);
for (int i = 0; i < n; i++) rk[str[i]] = 1;
for (int i = 0; i < m; i++) rk[i+1] += rk[i];
for (int i = 0; i < n; i++) s[i] = rk[str[i]] - 1;
return rk[m];
}
// Ensure that str[n] is the unique lexicographically smallest character in str.
template<typename T>
void suffixArray(int n, const T *str) {
int m = mapCharToInt(++n, str);
sais(n, m, s, t, p);
for (int i = 0; i < n; i++) rk[sa[i]] = i;
for (int i = 0, h = ht[0] = 0; i < n-1; i++) {
int j = sa[rk[i]-1];
while (i+h < n && j+h < n && s[i+h] == s[j+h]) h++;
if (ht[rk[i]] = h) h--;
}
}
};
int main()
{
scanf("%s", s);
n = strlen(s);
s[n] = 'a' - 1;
SA::suffixArray(n, s);
for (int i = 1; i <= n; ++i)
cout<<SA::sa[i]+1<<" ";
cout<<endl;
*out_tail++ = '\n';
for (int i = 2; i <= n; ++i)
cout<<SA::ht[i]<<" ";
cout<<endl;
return 0;
}