提高 Union 普转提 字符串算法(By—littlefools Garlic)
字符串算法是一种比较难以理解的算法。我们分常用的和用处不大和完全没啥用的三种。
常用的:字符串哈希(其实这个可以用map实现)、Trie树、0/1Trie(提高)
用处不大的:KMP算法
完全没啥用的:Z函数(EXKMP)、Manacher算法、可持久化trie树
字符串Hash
Hash可以看成一种数组的统计和映射的思想。我们在这里不多提Hash的思想。
字符串hash是把一个任意长度的字符串映射成一个非负整数,并且冲突概率几乎为0
(记住这个字符串->非负整数的关系)
我们取一个固定值PPP,把字符串看成PPP进制数,并且分配一个大于000的数值,代表某种字符。
如:对于小写字母构成的字符串,a=1,b=2...z=26a=1,b=2...z=26a=1,b=2...z=26。
取一个固定值MMM,求出该PPP进制数对于MMM的余数,就是该字符串的HashHashHash值。(记住这句话,是字符串Hash思想的关键)
一般来说,P=131P=131P=131或者P=13331P=13331P=13331等等是最佳的取值。且M=264(ull)M=2^{64}(ull)M=264(ull)(如果溢出就直接相当于mod Mmod~ Mmod M。在此种情况下基本不会出冲突。
在我们比赛的时候,面对大数据,我们要一直尝试PPP的取值,或者多跑几遍Hash。直到跑出最优的结果,我们也可以构几组数据进行检验,除了一些非常恶心毒瘤的数据之外,一般都可以通过。
对于各种字符串的操作都可以通过PPP进制数直接映射到Hash值上。
在字符串S后添加一个字符c
假设我们做的字符串SSS的hash值为HSH_SHS,则如果在SSS后添加一个字符ccc构成的Hash值就为:
HS+C=(Hs∗P+valc) mod MH_{S+C}=(H_s*P+val_c)~~mod~~MHS+C=(Hs∗P+valc) mod M(valcval_cvalc是我们选定ccc的代表值,乘PPP就相当于在进制PPP左移运算)
在字符串S后添加一个字符串T
字符串TTT的hash值应该是HT=(HS+T−HS∗PlenT) mod MH_T=(H_{S+T}-H_S*P^{len_T})~~mod~~ MHT=(HS+T−HS∗PlenT) mod M。
等价于通过PPP进制下载SSS后面补000的方式,把SSS移到S+TS+TS+T的左端对齐,两式相减得到HTH_THT。
我们举个栗子吧
例如 $S = ”abc“,”abc“,”abc“, c =$ ‘d’,T=T=T=“xyz”,SSS表示PPP进制数为1 2 31~2~31 2 3,TTT为24 25 2624~ 25~ 2624 25 26。则
HS=1∗P2+2∗P+3H_S=1*P^2+2*P+3HS=1∗P2+2∗P+3 HS+C=1∗P3+2∗P2+3∗P+4=HS∗P+4H_{S+C}=1*P^3+2*P^2+3*P+4= H_S *P +4HS+C=1∗P3+2∗P2+3∗P+4=HS∗P+4
HS+T=1∗P5+2∗P4+3∗P3+24∗P2+25∗P+26H_{S+T}=1*P^5+2*P^4+3*P^3+24*P^2+25*P+26HS+T=1∗P5+2∗P4+3∗P3+24∗P2+25∗P+26
我们发现这个算式是不是很简单啊!就是一个PPP逐渐递减的过程和权值带入的过程罢了。记忆公式难,但是理解起来不难啊!!!!
SSS在PPP进制下左移lenTlen_TlenT位,为1 2 3 0 0 01~2~3~0~0~01 2 3 0 0 0,则两式相减为TTT表示为PPP进制数,24 25 2624~25~2624 25 26。
HT=HS+T−(1∗P2+2∗P+3)∗P3=24∗P2+25∗P+26H_T=H_{S+T}-(1*P^2+2*P+3)*P^3=24*P^2+25*P+26HT=HS+T−(1∗P2+2∗P+3)∗P3=24∗P2+25∗P+26
这样看来,就是O(N)O(N)O(N)的时间处理字符串的前缀Hash值。O(1)O(1)O(1)的时间查询,效率嗯高。
【例题】 请教神牛(JZYZOJ 241)
张文军是个非常牛B的人,每天都有人来向他请教问题.但是他有原则.同一个人不能在一个学期内请教他两次,并且他每天只见一个请教者, 无论他以前是否请教过,否则他就没时间去干其他事情了,嘿嘿(坏笑…就是不见王普立).
于是,现在的问题就是,神牛并不是总记得每一个人.所以,你需要写一个程序帮助他判断每天接见的那个人是否请教过.
【分析】:字符串哈希的板子题。
#include<bits/stdc++.h>
using namespace std;
const unsigned long long Mod = 998244353;
const unsigned long long Mod2 = 1004535809;
int base1 = 233,base2 = 4099;
map<pair<long long ,long long>,bool> maap;
long long n ;
string str;
int main(){
cin>>n;
for(int i = 1 ;i <= n;i++){
cin>>str;
long long Hash1 = 0,Hash2 = 0;
for(int j = 0,len = str.size(); j < len ; j++){
Hash1 = (Hash1 * base1 + str[j])%Mod;
Hash2 = (Hash2 * base2 + str[j])%Mod2;
}
if(maap[make_pair(Hash1,Hash1)]++) printf("%d\n",i);
}
return 0;
}
【例题】 兔子与兔子
很久很久以前,森林里住着一群兔子。有一天,兔子们想要研究自己的 DNADNADNA 序列。我们首先选取一个好长好长的 DNADNADNA 序列(小兔子是外星生物,DNADNADNA 序列可能包含 262626 个小写英文字母),然后我们每次选择两个区间,询问如果用两个区间里的 DNADNADNA 序列分别生产出来两只兔子,这两个兔子是否一模一样。注意两个兔子一模一样只可能是他们的 DNADNADNA 序列一模一样。
第一行一个 DNADNADNA 字符串 SSS。 接下来一个数字 mmm,表示 mmm 次询问。 接下来 mmm 行,每行四个数字 l1,r1,l2,r2l1, r1, l2, r2l1,r1,l2,r2,分别表示此次询问的两个区间,注意字符串的位置从111开始编号。 其中 1≤length(S),Q≤10000001 ≤ length(S), Q ≤ 10000001≤length(S),Q≤1000000
对于每次询问,输出一行表示结果。如果两只兔子完全相同输出 YesYesYes,否则输出 NoNoNo(注意大小写)
【分析】:设我们选取的DNA的序列为SSS,则Fi=Hash1∼iF_i=Hash_{1 \sim i}Fi=Hash1∼i。则Fi=Fi−1∗131+(Si−′a′+1)F_i=F_{i-1}*131+(S_i-'a'+1)Fi=Fi−1∗131+(Si−′a′+1)
则得到任意区间[l,r][l,r][l,r]的hashhashhash值为Fr−Fl−1∗131r−l+1F_r-F_{l-1}*131^{r-l+1}Fr−Fl−1∗131r−l+1,则当两个区间的HashHashHash相等时,则两个子串相等。
时间复杂度为O(∣S∣+Q)O(|S|+Q)O(∣S∣+Q),线性滴。
#include<bits/stdc++.h>
#define ULL unsigned long long
using namespace std;
const int N=1e6+10;
char s[N];
int m;
ULL p[N],b=131,f[N];
int main(){
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
scanf("%s",s+1);
int len=strlen(s+1);
p[0]=1;
for(int i=1;i<=len;i++){
f[i]=f[i-1]*b+(s[i]-'a'+1);
p[i]=p[i-1]*b;
}
scanf("%d",&m);
while(m--){
int l,r,ll,rr,ans,anss;
scanf("%d%d%d%d",&l,&r,&ll,&rr);
ans=f[r]-f[l-1]*p[r-l+1];
anss=f[rr]-f[ll-1]*p[rr-ll+1];
if(ans==anss) puts("Yes");
else puts("No");
}
return 0;
}
求一个字符串的最长回文子串长度
这个算法只能解决一个问题:给定一个字符串,求它的最长回文子串长度。所以这个算法就显得十分拉胯,因为条件太苛刻了,一般上来说不会单独出一个解决这种问题的题目。
暴力:找出所有子串,遍历所有子串,找出最长的回文串,时间复杂度为O(n3)O(n^3)O(n3)
优化暴力(最常用的方法):因为回文串是对称的,根据这个性质,枚举每个位置,找在这个位置上能扩展到的最长回文串。复杂度是O(n2)O(n^2)O(n2)的
Manacher算法:可以将这个问题优化到O(n)O(n)O(n),但是显然这个算法难以理解且少有题目这么搞,如果大家想要了解,这里推荐一个blog(https://www.cnblogs.com/lykkk/p/10460087.html)
【例题】Palindrome
给定一个长度为NNN的字符串SSS,求它的最长回文子串。
【分析】我们可以发现回文串可以分为两类。
【第一类】:奇回文串,$A[1 \sim M] ,,,M为奇数。且为奇数。且为奇数。且A[1 \sim \frac{M}{2}+1]=reverse(A[\frac{M}{2}+1 \sim M])$
【第二类】:偶回文串,$B[1 \sim M] ,,,M为偶数。且为偶数。且为偶数。且B[1 \sim \frac{M}{2}]=reverse(B[\frac{M}{2}+1 \sim M])$
我们可以枚举SSS的回文子串的中心位置i=1∼Ni=1 \sim Ni=1∼N,看从这个中心位置出发向左右两侧最长可以扩展出最多的回文串。则:
求出一个最大的整数ppp使得S[i−p∼i]=reverse(S[i∼i+p])S[i-p\sim i] = reverse(S[i \sim i+p])S[i−p∼i]=reverse(S[i∼i+p])则以iii为中心的最长奇回文子串的长度2∗p−12*p-12∗p−1
求出一个最大的整数qqq使得S[i−q∼i−1]=reverse(S[i∼i+q−1])S[i-q \sim i-1] = reverse(S[i \sim i+q-1])S[i−q∼i−1]=reverse(S[i∼i+q−1]),则以i−1i-1i−1和iii之间的夹缝为中心的最长偶回文子串的长度就为2∗q2*q2∗q。
我们可以倒着做一遍预处理前缀HashHashHash值,可以O(1)O(1)O(1)的计算任意子串的HashHashHash值。则可以对p,qp,qp,q二分答案,用HashHashHash值比较一个正读和倒读的子串是否相等,则可以在O(logN)O(logN)O(logN)的时间内求出最大的p,qp,qp,q,在枚举过的所有中心位置对应的奇偶回文子串长度中取maxmaxmax就可以求出答案,复杂度为O(NlogN)O(NlogN)O(NlogN)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
char ch[N];
unsigned long long f1[N], f2[N], p[N];
bool check1(int x, int y){//x是中心,y是延伸出去的长度
return f1[x] - f1[x - y - 1] * p[y + 1] == f2[x] - f2[x + y + 1] * p[y + 1];
}
bool check2(int x, int y){//x和x+1中间的空隙是中心,y是延伸出去的长度
return f1[x] - f1[x - y] * p[y] == f2[x + 1] - f2[x + y + 1] * p[y];
}
int main(){
int T = 0;
while(1){
scanf("%s", ch + 1);//从1位开始存储
if(ch[1] == 'E')break;
int len = strlen(ch + 1);//从1位开始的长度
f1[0] = f2[len + 1] = 0;//初始化
p[0] = 1;//131^0
for(int i = 1, j = len; i <= len ; i++, j--){
f1[i] = f1[i-1] * 131 + ch[i]-'a'+1;//hash of 1-->i
f2[j] = f2[j+1] * 131 + ch[j]-'a'+1;//hash of i<--len
p[i] = p[i-1] * 131;//131^i,每个位置的权值
}
int ans = 0;
for(int i = 1; i <=len ; i++){ //以i为中心,二分左右延伸的距离
int ll = 0, rr = min(i, len + 1 - i);//[ll,rr)
while(ll + 1 < rr){
int mid = (ll + rr) / 2;
if(check1(i, mid)) ll = mid;
else rr = mid;
}
ans = max(ans, ll * 2 + 1);
//以i和i+1之间的空隙为中心,二分左右延伸的距离
ll = 0, rr = min(i + 1, len + 1 - i);//[ll,rr)
while(ll + 1 < rr){
int mid = (ll + rr) >> 1;
if(check2(i, mid)) ll = mid;
else rr = mid;
}
ans = max(ans, ll * 2);
}
T++;
printf("Case %d: %d\n", T, ans);
}
return 0;
}
Trie树(字典树)
Trie树又称为字典树,是一种用于实现字符串快速检索的多叉树结构。
Trie树的每个节点都有若干个字符指针,若插入(insert)或查找(query)时扫描到一个字符ccc,则沿着ccc的指针走向该指针指向的节点。时间复杂度为O(NC)O(NC)O(NC)(NNN为节点个数,CCC为字符集的大小)
建树
先建立一棵空Trie,只包含一个根节点(root),该点的字符指针均指向空。
int trie[MaxN][26],tot = 1;
插入(insert)
当需要插入一个字符串SSS的时候,我们令一个指针PPP起初指向根节点,然后依次扫描SSS中的每个字符ccc
1.如果PPP的ccc字符指针指向了一个已经存在的节点QQQ,则令P=QP = QP=Q
2.如果PPP的ccc字符指针指向空,则新建一个节点QQQ,令PPP的ccc字符指针指向QQQ,然后令P=QP=QP=Q
当SSS中的字符扫描完毕时,在当前节点PPP上标记它是不是一个字符串的末尾。
可能不是很好理解,其实就是一个去掉重字符的过程。可以画图理解。
void insert(char* str){
int len = strlen(str),p = 1;
for(int k = 0 ; k < len ; k++){
int ch = str[k] - 'a';
if(trie[p][ch] == 0) trie[p][ch] = ++tot;
p = trie[p][ch];
}
end[p] = true;
}
查询(query)
主要是查询一个字符串在trie里是否存在。
令一个指针PPP起初指向根节点,然后依次扫描SSS里的每个节点ccc。
(1)如果PPP的ccc字符指向空,则SSS没有插入进trie,直接结束查询。
(2)如果PPP的ccc字符指向一个已经存在的节点QQQ,则令P=QP = QP=Q
当SSS中的字符扫描完毕时,若当前节点PPP被标记为一个字符串的末尾,则说明SSS在trie树中存在,否则SSS没有被加入进trie树。
bool query(char* str){
int len = strlen(str),p = 1;
for(int k = 0; k < len ;k ++){
p = trie[p][str[k]-'a'];
if(p == 0) return false;
}
return end[p];
}
这个比较简单,就不放例题理解啦~
顺带一提2022统一省选的D1T1就可以用trie树做。
0/1 Trie
名字很大气,其实和普通的Trie并无多大区别,只是插入的是二进制的0/1串。01trie其实就是按位插入数字(将数字转化为2进制串),所谓“按位”即将该数字按2进制拆分为每一位是0或1的数字。只需要trie[][]trie[][]trie[][]数组和val[]val[]val[]数组(valvalval是建树插入xxx时记录该条数值的最后一个节点为xxx,记录后就可以直接取出)。把数拆成二进制的形式,然后每一位都只有两个字符:0或1,然后按照trie的方式存下来,以达到节省空间的效果。
它一般用来求解异或最值问题。
为什么说线性基鸡肋呢?因为0/1 Trie能解决线性基解决不了的东西!
建树:
int p = 0 , tot = 1;
for(int i = 32; i >= 0; i--){
int t = (x >> i) & 1;
if(!trie[p][t]) trie[p][t] = tot++;
p = trie[p][t];
}
val[p] = x;
查询
int p = 0;
for(int i = 32; i >= 0 ;i--){
int t = (x >> i) & 1;
if(!trie[p][t]) p = trie[p][t];
else p = trie[p][t ^ 1];
}
return val[p];
P4551 最长异或路径
给定一棵 nnn 个点的带权树,结点下标从 111 开始到 nnn。寻找树中找两个结点,求最长的异或路径.异或路径指的是指两个结点之间唯一路径上的所有边权的异或。
第一行一个整数 nnn,表示点数。
接下来 n−1n-1n−1 行,给出 u,v,wu,v,wu,v,w ,分别表示树上的 uuu 点和 vvv 点有连边,边的权值是 www。
样例 #1
4
1 2 3
2 3 4
2 4 6
输出 #1
7
最长异或序列是 1,2,31,2,31,2,3,答案是 7=3⊕47=3\oplus 47=3⊕4。
【分析】:0/1Trie的模板题。
我们对于每一个数到根节点的异或和进行建01trie。
我们知道一个数异或两次还是这一个数,所以说从i∼ji \sim ji∼j上的异或和就是根到iii上的异或和 异或 根到jjj上的异或和
对于每一位上进行贪心,如果这一位有一个与它不同的,即异或 后是1,那我们就顺着这条路径往下,否则就顺着原路往下走,因为当前这一位上面的权值比后面的所有位数加起来还要高(10000和01111谁大?)
预处理:
vector<edge> p[MAXX];
void dfs(int x, int fa) //计算每个结点到根的异或路径,x为当前的结点,fa为父结点
{
int len = p[x].size();
for (int i = 0; i < len; ++i){
if (p[x][i].v != fa){
s[p[x][i].v] = s[x] ^ p[x][i].w; //与父结点异或就是当前结点到根的异或路径
dfs(p[x][i].v, x); //把当前结点变成父结点,子结点为当前结点的子结点
}
}
}
完成0/1 Trie的操作:
void insert(int x){
int u = 0; //根
for (int i = (1 << 30); i; i >>= 1)
{
int a = bool(x & i); // a说明这一位的值为0或1
if (!trie[u][a]) //插入结点
trie[u][a] = ++cnt;
u = trie[u][a];
}
}
int Query(int x){
int res = 0, u = 0;
for (int i = (1 << 30); i; i >>= 1){
int a = bool(x & i);
if (trie[u][!a]){
res += i;
u = trie[u][!a];
}
else u = trie[u][a];
}
return res;
}
主程序:
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
for (int i = 1; i < n; ++i){
int u, v, w;
cin >> u >> v >> w;
p[u].push_back((edge){v, w});
p[v].push_back((edge){u, w});
}
dfs(1, -1); //每个结点到根的异或路径
for (int i = 1; i <= n; ++i) insert(s[i]);
int ans = 0;
for (int i = 1; i <= n; ++i) ans = max(ans, Query(s[i]));
cout << ans;
return 0;
}
KMP算法
KMP算法是一种字符串匹配算法,可以在 O(n+m)O(n+m)O(n+m)的时间复杂度内实现两个字符串的匹配。n=∣S∣n = |S|n=∣S∣ 为串 SSS 的长度,m=∣P∣m = |P|m=∣P∣ 为串 PPP 的长度。
我们的问题是:模式串PPP是否为主串SSS的子串。
朴素做法(必须先打这个来验证答案的正确性再考虑KMP)
先判断两个字符串是否相等从前往后逐字符比较,一旦遇到不相同的字符,就直接return 0;
如果两个字符串都结束了,仍然没有出现不对应的字符,则return 1。
bool Work(string S,string P){
if S.length() != P.length() return 0;
for(int i = 1 ; i <= S.length() ; i++)
if (S[i] != P[i]) return 0;
return 1;
}
然后我们可以知道两个字符串是否相等了。那么我们再考虑 i=0,1,2...,S.length()−P.length()i = 0, 1, 2 ... , S.length()-P.length()i=0,1,2...,S.length()−P.length()
再枚举i∼i+P.length()i \sim i+P.length()i∼i+P.length(),与PPP作比较,如果一致的话就是找到一个匹配。
void Work(char *S,char *P){
int Len_S = S.length() , Len_P = P.length();
int delta = Len_S - Len_P;
for(int i = 0 ; i <= delta ; i++){
bool flag = 1;
for(int j = 0; P[j] != '\0' ; j++)
if(S[i+j] != P[j]) flag = 0,break;
if(flag) cout<<i<<endl; //i是现在的位置
}
}
总时间复杂度是O(mn)O(mn)O(mn)的,真的拉胯呀!!!!!
但是我们可以通过朴素KMP算法和Hash结合一样,与KMP时间花费差不多。
KMP做法
1.1.1.对字符串AAA进行“自我匹配”,求出一个数组nextinext_inexti(这个数组将是我们理解KMP算法的重中之重!!!)其中nextinext_inexti表示**AAA中以iii结尾的非前缀子串与AAA的前缀能够匹配的最大长度**!
如果用公式表示的话就是nexti=max(j)next_i = max(j)nexti=max(j),其中j<ij <ij<i且A[i−j+1∼i]=A[1∼j]A[i-j+1 \sim i] = A[1\sim j]A[i−j+1∼i]=A[1∼j]
如果不存在这样的jjj时,令next[i]=0next[i] = 0next[i]=0。
2.2.2.对AAA和BBB进行匹配,求出一个数组FFF,其中FiF_iFi表示**“BBB中以iii结尾的子串”与“AAA的前缀能够匹配的最大长度!**
用公式表示就是fi=max(j)f_i = max(j)fi=max(j),其中j≤ij \le ij≤i 并且 B[i−j+1∼i]=A[1∼j]B[i-j+1 \sim i] = A[1 \sim j]B[i−j+1∼i]=A[1∼j] next1=0next_1 = 0next1=0
在i=2∼Ni = 2 \sim Ni=2∼N的情况下,假设next[1∼i−1]next[1 \sim i-1]next[1∼i−1]已经计算完毕,计算nextinext_inexti时,需要找出j<i且A[i−j+1∼i]=A[1−j]j<i且A[i-j+1 \sim i]=A[1-j]j<i且A[i−j+1∼i]=A[1−j]的整数jjj取最大值。
朴素方法求next数组
枚举j∈[1,i−1]j ∈ [1,i-1]j∈[1,i−1],并检查A[i−j+1∼i]A[i-j+1 \sim i]A[i−j+1∼i]与A[1−j]A[1-j]A[1−j]是否相等。
该算法对每个iii枚举i−1i-1i−1个非前缀子串,并检查与对应前缀的匹配情况,是O(n2)O(n^2)O(n2)的,真的好拉胯。
不朴素的方法求next数组
怎么样不朴素呢
如果j0j_0j0是nextinext_inexti的一个候选项,则小于j0j_0j0最大的nextinext_inexti的候选项nextj0next_{j0}nextj0。那么nextj0+1∼j0−1next_{j0}+1 \sim j_0 -1nextj0+1∼j0−1之间的数都是不是nextinext_inexti的候选项。
证明见算阶。
根据引理,当next[i−1]next[i-1]next[i−1]计算完毕时,我们可得知next[i−1]next[i-1]next[i−1]的所有候选项从大到小依次是next[next[i−1]],next[next[next[i−1]]]...next[next[i-1]],next[next[next[i-1]]]...next[next[i−1]],next[next[next[i−1]]]...
而如果jjj是nextinext_inexti的候选项的话,那么j−1j-1j−1显也必须是next[i−1]+1...next[next[next[i−1]]]...next[i-1]+1...next[next[next[i-1]]]...next[i−1]+1...next[next[next[i−1]]]...等等作为jjj的选项即可。
next数组的求法
1.初始化next[1]=j=0next[1] = j = 0next[1]=j=0,假设next[1∼i−1]next[1 \sim i-1]next[1∼i−1]已经求出,现在求next[i]next[i]next[i]
2.不断尝试扩展长度jjj,如果扩展失败(下一个字符不相等),则j=next[j]j=next[j]j=next[j]直到j=0j=0j=0(之后从头匹配)
3.如果能扩展成功,则扩展长度jjj++,next[i]=jnext[i]=jnext[i]=j
Next[1] = 0;
for(int i = 2 , j = 0; i <= n ; i++){
while(j > 0 && a[i] != a[j+1]) j = Next[j];
if(a[i] == a[j+1]) j++;
Next[i] = j;
}
求F数组和求next差不多:
for(int i = 1, j = 0; i <= m; i++){
while(j > 0 && (j == n || b[i] != a[j+1])) j = Next[j];
if(b[i] == a[j+1]) j++;
f[i] = j;
//if(f[i] == n) 则A在B中的某一次出现。
}
[例] KMP板子
给定一个字符串 A 和一个字符串 B,求 B 在 A 中的出现次数。
A 中不同位置出现的 B 可重叠
input
RachelAhhhh
h
output
5
没什么好说的。。
题:period
一个字符串的前缀是从第一个字符开始的连续若干个字符,例如 abaab
共有 55 个前缀,分别是 a
,ab
,aba
,abaa
,abaab
。
我们希望知道一个 N 位字符串 S 的前缀是否具有循环节。
换言之,对于每一个从头开始的长度为 i(i>1)的前缀,是否由重复出现的子串 A 组成,即 AAA…A (A 重复出现 K 次,K>1)。
【分析】:
对于具有循环节性质的字符串,它的nextnextnext数组有一个性质:
s[1∼next[i]]=s[i−next[i]+1∼i]s[1 \sim next[i]] = s[i-next[i]+1 \sim i]s[1∼next[i]]=s[i−next[i]+1∼i] 一定相等且最大! 循环节长度就是:i/(i−next[i])i / (i - next[i])i/(i−next[i])
s[1∼n]s[1 \sim n]s[1∼n]具有t < i长度循环节的充要条件是:前缀等于后缀, 即 s[1∼n−t]=s[t+1∼n]s[1 \sim n-t] = s[t+1 \sim n]s[1∼n−t]=s[t+1∼n]
如果存在,请找出最短的循环节对应的 KKK 值(也就是这个前缀串的所有可能重复节中,最大的 KKK 值)。
本题求最短的循环节ttt:即ttt最小,也就是n−tn - tn−t最大, 也就是前缀和后缀相等且最大,这就是next数组的含义!
则n−next[n]n - next[n]n−next[n]就是循环节t最短时的最大长度!
当i−next[i]i - next[i]i−next[i]能够整除i时候,那么s[1 i−next[i]]s[1 ~ i-next[i]]s[1 i−next[i]]就是s[1 i]s[1 ~ i]s[1 i]的最小循环元, 次数是:i/(i−next[i])i / (i - next[i])i/(i−next[i]).
核心代码:
int T = 1;
while(cin >> n, n){
cin >> s + 1;
for(int i = 2, j = 0; i <= n; i ++){
while(j && s[i] != s[j + 1]) j = ne[j];
if(s[i] == s[j + 1]) j ++;
ne[i] = j;
}
cout << "Test case #" << T ++ << endl;
for(int i = 1; i <= n; i ++){
int t = i - ne[i];
if(i % t == 0 && i / t > 1) cout << i << " " << i / t << endl;
}
cout << endl;
}