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

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

提高 Union 普转提 字符串算法(By—littlefools Garlic)

字符串算法是一种比较难以理解的算法。我们分常用的和用处不大完全没啥用的三种。

常用的:字符串哈希(其实这个可以用map实现)Trie树0/1Trie(提高)

用处不大的:KMP算法

完全没啥用的:Z函数(EXKMP)Manacher算法可持久化trie树

字符串Hash

Hash可以看成一种数组的统计和映射的思想。我们在这里不多提Hash的思想。

字符串hash是把一个任意长度的字符串映射成一个非负整数,并且冲突概率几乎为0

(记住这个字符串->非负整数的关系)

我们取一个固定值 P P P,把字符串看成 P P P进制数,并且分配一个大于 0 0 0的数值,代表某种字符。

如:对于小写字母构成的字符串, a = 1 , b = 2... z = 26 a=1,b=2...z=26 a=1,b=2...z=26

取一个固定值 M M M,求出该 P P P进制数对于 M M M的余数,就是该字符串的 H a s h Hash Hash值。(记住这句话,是字符串Hash思想的关键)

一般来说, P = 131 P=131 P=131或者 P = 13331 P=13331 P=13331等等是最佳的取值。且 M = 2 64 ( u l l ) M=2^{64}(ull) M=264ull(如果溢出就直接相当于 m o d   M mod~ M mod M。在此种情况下基本不会出冲突。

在我们比赛的时候,面对大数据,我们要一直尝试 P P P的取值,或者多跑几遍Hash。直到跑出最优的结果,我们也可以构几组数据进行检验,除了一些非常恶心毒瘤的数据之外,一般都可以通过。

对于各种字符串的操作都可以通过 P P P进制数直接映射到Hash值上。

在字符串S后添加一个字符c

假设我们做的字符串 S S S的hash值为 H S H_S HS,则如果在 S S S后添加一个字符 c c c构成的Hash值就为:

H S + C = ( H s ∗ P + v a l c )    m o d    M H_{S+C}=(H_s*P+val_c)~~mod~~M HS+C=(HsP+valc)  mod  M v a l c val_c valc是我们选定 c c c的代表值,乘 P P P就相当于在进制 P P P左移运算)

在字符串S后添加一个字符串T

字符串 T T T的hash值应该是 H T = ( H S + T − H S ∗ P l e n T )    m o d    M H_T=(H_{S+T}-H_S*P^{len_T})~~mod~~ M HT=(HS+THSPlenT)  mod  M

等价于通过 P P P进制下载 S S S后面补 0 0 0的方式,把 S S S移到 S + T S+T S+T的左端对齐,两式相减得到 H T H_T HT

我们举个栗子吧

例如 $S = ” a b c “, ”abc“, abc c =$ ‘d’, T = T= T=“xyz”, S S S表示 P P P进制数为 1   2   3 1~2~3 1 2 3 T T T 24   25   26 24~ 25~ 26 24 25 26。则

H S = 1 ∗ P 2 + 2 ∗ P + 3 H_S=1*P^2+2*P+3 HS=1P2+2P+3 H S + C = 1 ∗ P 3 + 2 ∗ P 2 + 3 ∗ P + 4 = H S ∗ P + 4 H_{S+C}=1*P^3+2*P^2+3*P+4= H_S *P +4 HS+C=1P3+2P2+3P+4=HSP+4

H S + T = 1 ∗ P 5 + 2 ∗ P 4 + 3 ∗ P 3 + 24 ∗ P 2 + 25 ∗ P + 26 H_{S+T}=1*P^5+2*P^4+3*P^3+24*P^2+25*P+26 HS+T=1P5+2P4+3P3+24P2+25P+26

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

S S S P P P进制下左移 l e n T len_T lenT位,为 1   2   3   0   0   0 1~2~3~0~0~0 1 2 3 0 0 0,则两式相减为 T T T表示为 P P P进制数, 24   25   26 24~25~26 24 25 26

H T = H S + T − ( 1 ∗ P 2 + 2 ∗ P + 3 ) ∗ P 3 = 24 ∗ P 2 + 25 ∗ P + 26 H_T=H_{S+T}-(1*P^2+2*P+3)*P^3=24*P^2+25*P+26 HT=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;
}
【例题】 兔子与兔子

很久很久以前,森林里住着一群兔子。有一天,兔子们想要研究自己的 D N A DNA DNA 序列。我们首先选取一个好长好长的 D N A DNA DNA 序列(小兔子是外星生物, D N A DNA DNA 序列可能包含 26 26 26 个小写英文字母),然后我们每次选择两个区间,询问如果用两个区间里的 D N A DNA DNA 序列分别生产出来两只兔子,这两个兔子是否一模一样。注意两个兔子一模一样只可能是他们的 D N A DNA DNA 序列一模一样。
第一行一个 D N A DNA DNA 字符串 S S S。 接下来一个数字 m m m,表示 m m m 次询问。 接下来 m m m 行,每行四个数字 l 1 , r 1 , l 2 , r 2 l1, r1, l2, r2 l1,r1,l2,r2,分别表示此次询问的两个区间,注意字符串的位置从 1 1 1开始编号。 其中 1 ≤ l e n g t h ( S ) , Q ≤ 1000000 1 ≤ length(S), Q ≤ 1000000 1length(S),Q1000000
对于每次询问,输出一行表示结果。如果两只兔子完全相同输出 Y e s Yes Yes,否则输出 N o No No(注意大小写)

【分析】:设我们选取的DNA的序列为 S S S,则 F i = H a s h 1 ∼ i F_i=Hash_{1 \sim i} Fi=Hash1i。则 F i = F i − 1 ∗ 131 + ( S i − ′ a ′ + 1 ) F_i=F_{i-1}*131+(S_i-'a'+1) Fi=Fi1131+(Sia+1)
则得到任意区间 [ l , r ] [l,r] [l,r] h a s h hash hash值为 F r − F l − 1 ∗ 13 1 r − l + 1 F_r-F_{l-1}*131^{r-l+1} FrFl1131rl+1,则当两个区间的 H a s h Hash Hash相等时,则两个子串相等。
时间复杂度为 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 ( n 3 ) O(n^3) O(n3)
优化暴力(最常用的方法):因为回文串是对称的,根据这个性质,枚举每个位置,找在这个位置上能扩展到的最长回文串。复杂度是 O ( n 2 ) O(n^2) O(n2)

Manacher算法:可以将这个问题优化到 O ( n ) O(n) O(n),但是显然这个算法难以理解且少有题目这么搞,如果大家想要了解,这里推荐一个blog(https://www.cnblogs.com/lykkk/p/10460087.html)

【例题】Palindrome

给定一个长度为 N N N的字符串 S S S,求它的最长回文子串。

【分析】我们可以发现回文串可以分为两类。

【第一类】:奇回文串,$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])$

我们可以枚举 S S S的回文子串的中心位置 i = 1 ∼ N i=1 \sim N i=1N,看从这个中心位置出发向左右两侧最长可以扩展出最多的回文串。则:

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

求出一个最大的整数 q q q使得 S [ i − q ∼ i − 1 ] = r e v e r s e ( 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 − 1 i-1 i1 i i i之间的夹缝为中心的最长偶回文子串的长度就为 2 ∗ q 2*q 2q

我们可以倒着做一遍预处理前缀 H a s h Hash Hash值,可以 O ( 1 ) O(1) O(1)的计算任意子串的 H a s h Hash Hash值。则可以对 p , q p,q p,q二分答案,用 H a s h Hash Hash值比较一个正读和倒读的子串是否相等,则可以在 O ( l o g N ) O(logN) O(logN)的时间内求出最大的 p , q p,q p,q,在枚举过的所有中心位置对应的奇偶回文子串长度中取 m a x max max就可以求出答案,复杂度为 O ( N l o g N ) 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)时扫描到一个字符 c c c,则沿着 c c c的指针走向该指针指向的节点。时间复杂度为 O ( N C ) O(NC) O(NC)( N N N为节点个数, C C C为字符集的大小)

建树

先建立一棵空Trie,只包含一个根节点(root),该点的字符指针均指向空。

int trie[MaxN][26],tot = 1;
插入(insert)

当需要插入一个字符串 S S S的时候,我们令一个指针 P P P起初指向根节点,然后依次扫描 S S S中的每个字符 c c c

1.如果 P P P c c c字符指针指向了一个已经存在的节点 Q Q Q,则令 P = Q P = Q P=Q

2.如果 P P P c c c字符指针指向空,则新建一个节点 Q Q Q,令 P P P c c c字符指针指向 Q Q Q,然后令 P = Q P=Q P=Q

S S S中的字符扫描完毕时,在当前节点 P P P标记它是不是一个字符串的末尾。

可能不是很好理解,其实就是一个去掉重字符的过程。可以画图理解。

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里是否存在

令一个指针 P P P起初指向根节点,然后依次扫描 S S S里的每个节点 c c c

(1)如果 P P P c c c字符指向空,则 S S S没有插入进trie,直接结束查询。

(2)如果 P P P c c c字符指向一个已经存在的节点 Q Q Q,则令 P = Q P = Q P=Q

S S S中的字符扫描完毕时,若当前节点 P P P被标记为一个字符串的末尾,则说明 S S S在trie树中存在,否则 S S S没有被加入进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的数字。只需要 t r i e [ ] [ ] trie[][] trie[][]数组和 v a l [ ] val[] val[]数组( v a l val val是建树插入 x x x时记录该条数值的最后一个节点为 x x x,记录后就可以直接取出)。把数拆成二进制的形式,然后每一位都只有两个字符: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 最长异或路径

给定一棵 n n n 个点的带权树,结点下标从 1 1 1 开始到 n n n。寻找树中找两个结点,求最长的异或路径.异或路径指的是指两个结点之间唯一路径上的所有边权的异或。
第一行一个整数 n n n,表示点数。
接下来 n − 1 n-1 n1 行,给出 u , v , w u,v,w u,v,w ,分别表示树上的 u u u 点和 v v v 点有连边,边的权值是 w w w

样例 #1
4
1 2 3
2 3 4
2 4 6
输出 #1
7

最长异或序列是 1 , 2 , 3 1,2,3 1,2,3,答案是 7 = 3 ⊕ 4 7=3\oplus 4 7=34

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

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

我们知道一个数异或两次还是这一个数,所以说从 i ∼ j i \sim j ij上的异或和就是根到 i i i上的异或和 异或 根到 j j j上的异或和

对于每一位上进行贪心,如果这一位有一个与它不同的,即异或 后是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 为串 S S S 的长度, m = ∣ P ∣ m = |P| m=P 为串 P P P 的长度。

KMP1

我们的问题是:模式串 P P P是否为主串 S S S的子串

朴素做法(必须先打这个来验证答案的正确性再考虑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 . l e n g t h ( ) − P . l e n g t h ( ) i = 0, 1, 2 ... , S.length()-P.length() i=0,1,2...,S.length()P.length()

再枚举 i ∼ i + P . l e n g t h ( ) i \sim i+P.length() ii+P.length(),与 P P P作比较,如果一致的话就是找到一个匹配。

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 ( m n ) O(mn) O(mn)的,真的拉胯呀!!!!!

但是我们可以通过朴素KMP算法和Hash结合一样,与KMP时间花费差不多。

KMP做法

1. 1. 1.对字符串 A A A进行“自我匹配”,求出一个数组 n e x t i next_i nexti(这个数组将是我们理解KMP算法的重中之重!!!)其中 n e x t i next_i nexti表示** A A A中以 i i i结尾的非前缀子串 A A A的前缀能够匹配的最大长度**!

如果用公式表示的话就是 n e x t i = m a x ( j ) next_i = max(j) nexti=max(j),其中 j < i j <i j<i A [ i − j + 1 ∼ i ] = A [ 1 ∼ j ] A[i-j+1 \sim i] = A[1\sim j] A[ij+1i]=A[1j]

如果不存在这样的 j j j时,令 n e x t [ i ] = 0 next[i] = 0 next[i]=0

2. 2. 2. A A A B B B进行匹配,求出一个数组 F F F,其中 F i F_i Fi表示**“ B B B中以 i i i结尾的子串”与“ A A A的前缀能够匹配的最大长度!**

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

i = 2 ∼ N i = 2 \sim N i=2N的情况下,假设 n e x t [ 1 ∼ i − 1 ] next[1 \sim i-1] next[1i1]已经计算完毕,计算 n e x t i next_i nexti时,需要找出 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]的整数 j j j取最大值。

朴素方法求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]是否相等。

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

不朴素的方法求next数组

怎么样不朴素呢

如果 j 0 j_0 j0 n e x t i next_i nexti的一个候选项,则小于 j 0 j_0 j0最大的 n e x t i next_i nexti的候选项 n e x t j 0 next_{j0} nextj0。那么 n e x t j 0 + 1 ∼ j 0 − 1 next_{j0}+1 \sim j_0 -1 nextj0+1j01之间的数都是不是 n e x t i next_i nexti的候选项。

证明见算阶。

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

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

next数组的求法

1.初始化 n e x t [ 1 ] = j = 0 next[1] = j = 0 next[1]=j=0,假设 n e x t [ 1 ∼ i − 1 ] next[1 \sim i-1] next[1i1]已经求出,现在求 n e x t [ i ] next[i] next[i]

2.不断尝试扩展长度 j j j,如果扩展失败(下一个字符不相等),则 j = n e x t [ j ] j=next[j] j=next[j]直到 j = 0 j=0 j=0(之后从头匹配)

3.如果能扩展成功,则扩展长度 j j j++, n e x t [ i ] = j next[i]=j next[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)。

【分析】
对于具有循环节性质的字符串,它的 n e x t next next数组有一个性质:
s [ 1 ∼ n e x t [ i ] ] = s [ i − n e x t [ i ] + 1 ∼ i ] s[1 \sim next[i]] = s[i-next[i]+1 \sim i] s[1next[i]]=s[inext[i]+1i] 一定相等且最大! 循环节长度就是: i / ( i − n e x t [ 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]
如果存在,请找出最短的循环节对应的 K K K 值(也就是这个前缀串的所有可能重复节中,最大的 K K K 值)。

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

i − n e x t [ i ] i - next[i] inext[i]能够整除i时候,那么 s [ 1   i − n e x t [ i ] ] s[1 ~ i-next[i]] s[1 inext[i]]就是 s [ 1   i ] s[1 ~ i] s[1 i]的最小循环元, 次数是: i / ( i − n e x t [ 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;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值