【JZYZ集训Day2】字符串相关专题 Hash&&KMP&&Trie&&0/1Trie

本文介绍了字符串算法中的哈希、KMP算法和Trie树,包括它们的基本概念、操作方法及应用实例。通过哈希可以高效地处理字符串的前后缀,KMP算法用于字符串匹配,而Trie树则用于快速检索和插入字符串。文章通过具体的例子和题目分析加深了读者的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

提高 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=264ull(如果溢出就直接相当于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=(HsP+valc)  mod  Mvalcval_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+THSPlenT)  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 3TTT24 25 2624~ 25~ 2624 25 26。则

HS=1∗P2+2∗P+3H_S=1*P^2+2*P+3HS=1P2+2P+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=1P3+2P2+3P+4=HSP+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=1P5+2P4+3P3+24P2+25P+26

我们发现这个算式是不是很简单啊!就是一个PPP逐渐递减的过程和权值带入的过程罢了。记忆公式难,但是理解起来不难啊!!!!

SSSPPP进制下左移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(1P2+2P+3)P3=24P2+25P+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 ≤ 10000001length(S),Q1000000
对于每次询问,输出一行表示结果。如果两只兔子完全相同输出 YesYesYes,否则输出 NoNoNo(注意大小写)

【分析】:设我们选取的DNA的序列为SSS,则Fi=Hash1∼iF_i=Hash_{1 \sim i}Fi=Hash1i。则Fi=Fi−1∗131+(Si−′a′+1)F_i=F_{i-1}*131+(S_i-'a'+1)Fi=Fi1131+(Sia+1)
则得到任意区间[l,r][l,r][l,r]hashhashhash值为Fr−Fl−1∗131r−l+1F_r-F_{l-1}*131^{r-l+1}FrFl1131rl+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=1N,看从这个中心位置出发向左右两侧最长可以扩展出最多的回文串。则:

求出一个最大的整数ppp使得S[i−p∼i]=reverse(S[i∼i+p])S[i-p\sim i] = reverse(S[i \sim i+p])S[ipi]=reverse(S[ii+p])则以iii为中心的最长奇回文子串的长度2∗p−12*p-12p1

求出一个最大的整数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[iqi1]=reverse(S[ii+q1]),则以i−1i-1i1iii之间的夹缝为中心的最长偶回文子串的长度就为2∗q2*q2q

我们可以倒着做一遍预处理前缀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.如果PPPccc字符指针指向了一个已经存在的节点QQQ,则令P=QP = QP=Q

2.如果PPPccc字符指针指向空,则新建一个节点QQQ,令PPPccc字符指针指向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)如果PPPccc字符指向空,则SSS没有插入进trie,直接结束查询。

(2)如果PPPccc字符指向一个已经存在的节点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的方式存下来,以达到节省空间的效果。111

它一般用来求解异或最值问题。

为什么说线性基鸡肋呢?因为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-1n1 行,给出 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=34

【分析】:0/1Trie的模板题。

我们对于每一个数到根节点的异或和进行建01trie。

我们知道一个数异或两次还是这一个数,所以说从i∼ji \sim jij上的异或和就是根到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 的长度。

KMP1

我们的问题是:模式串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()ii+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<iA[i−j+1∼i]=A[1∼j]A[i-j+1 \sim i] = A[1\sim j]A[ij+1i]=A[1j]

如果不存在这样的jjj时,令next[i]=0next[i] = 0next[i]=0

2.2.2.AAABBB进行匹配,求出一个数组FFF,其中FiF_iFi表示**“BBB中以iii结尾的子串”与“AAA的前缀能够匹配的最大长度!**

用公式表示就是fi=max(j)f_i = max(j)fi=max(j),其中j≤ij \le iji 并且 B[i−j+1∼i]=A[1∼j]B[i-j+1 \sim i] = A[1 \sim j]B[ij+1i]=A[1j] next1=0next_1 = 0next1=0

i=2∼Ni = 2 \sim Ni=2N的情况下,假设next[1∼i−1]next[1 \sim i-1]next[1i1]已经计算完毕,计算nextinext_inexti时,需要找出j<i且A[i−j+1∼i]=A[1−j]j<i且A[i-j+1 \sim i]=A[1-j]j<iA[ij+1i]=A[1j]的整数jjj取最大值。

朴素方法求next数组

枚举j∈[1,i−1]j ∈ [1,i-1]j[1,i1],并检查A[i−j+1∼i]A[i-j+1 \sim i]A[ij+1i]A[1−j]A[1-j]A[1j]是否相等。

该算法对每个iii枚举i−1i-1i1个非前缀子串,并检查与对应前缀的匹配情况,是O(n2)O(n^2)O(n2)的,真的好拉胯。

不朴素的方法求next数组

怎么样不朴素呢

如果j0j_0j0nextinext_inexti的一个候选项,则小于j0j_0j0最大的nextinext_inexti的候选项nextj0next_{j0}nextj0。那么nextj0+1∼j0−1next_{j0}+1 \sim j_0 -1nextj0+1j01之间的数都是不是nextinext_inexti的候选项。

证明见算阶。

根据引理,当next[i−1]next[i-1]next[i1]计算完毕时,我们可得知next[i−1]next[i-1]next[i1]的所有候选项从大到小依次是next[next[i−1]],next[next[next[i−1]]]...next[next[i-1]],next[next[next[i-1]]]...next[next[i1]],next[next[next[i1]]]...

而如果jjjnextinext_inexti的候选项的话,那么j−1j-1j1显也必须是next[i−1]+1...next[next[next[i−1]]]...next[i-1]+1...next[next[next[i-1]]]...next[i1]+1...next[next[next[i1]]]...等等作为jjj的选项即可。

next数组的求法

1.初始化next[1]=j=0next[1] = j = 0next[1]=j=0,假设next[1∼i−1]next[1 \sim i-1]next[1i1]已经求出,现在求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 个前缀,分别是 aababaabaaabaab

我们希望知道一个 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[1next[i]]=s[inext[i]+1i] 一定相等且最大! 循环节长度就是:i/(i−next[i])i / (i - next[i])i/(inext[i])
s[1∼n]s[1 \sim n]s[1n]具有t < i长度循环节的充要条件是:前缀等于后缀, 即 s[1∼n−t]=s[t+1∼n]s[1 \sim n-t] = s[t+1 \sim n]s[1nt]=s[t+1n]
如果存在,请找出最短的循环节对应的 KKK 值(也就是这个前缀串的所有可能重复节中,最大的 KKK 值)。

本题求最短的循环节ttt:即ttt最小,也就是n−tn - tnt最大, 也就是前缀和后缀相等且最大,这就是next数组的含义!
n−next[n]n - next[n]nnext[n]就是循环节t最短时的最大长度!

i−next[i]i - next[i]inext[i]能够整除i时候,那么s[1 i−next[i]]s[1 ~ i-next[i]]s[1 inext[i]]就是s[1 i]s[1 ~ i]s[1 i]的最小循环元, 次数是:i/(i−next[i])i / (i - next[i])i/(inext[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;
    }
数据库连接失败spring: datasource: type: com.alibaba.druid.pool.DruidDataSource # driverClassName: org.mariadb.jdbc.Driver driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 master: #test # url: jdbc:mysql://59.110.60.119:3306/WaterQuality # username: mydb # password: 123456 #production # url: jdbc:mysql://114.116.210.210:9527/water_jzyz # username: waterjzyz # password: waterjzyz@890 #使用本地的 # url: jdbc:mysql://8.142.164.112:3306/water_jzyz # username: root # password: root url: jdbc:mysql://localhost:3307/waterquality username: root password: root # # 从库数据源 slave: # 从数据源开关/默认关闭 enabled: false url: username: password: # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /druid/* # 控制台管理用户名和密码 login-username: dllcsoft login-password: dllcsoft filter: stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
07-21
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值