字符串 \(s\) 的border:若 \(s\) 的一個子串既是它的前綴又是它的后綴,則這個子串是它的border(一般不包含本身)
字符串 \(s\) 的period:循環節。用前 \(T\) 個字符向后不斷復制,能得到 \(s\),最后一次可以只復制一部分
引理1:如果有一個border \(k\) 長度大於 \(s\) 的一半,可以得出得 \(s\) 有周期 \(|s|-|k|\)
同時,中間重疊部分是一個border,綠色部分構成了一組等差數列border:
前 \(T\) 個字符是周期的話,后 \(T\) 個顯然也是周期:
當然,這些都不重要
引理2:如果 \(p,q\) 都為周期,則 \(gcd(p, q)\) 也為周期
感性理解非常顯然,證明同樣不難,不做贅述了
引理3:字符串 \(s\) 所有不小於 \(|s|\) 一半的border構成一個等差數列
顯然有\(|s|-|u|\) 為最小周期,能組成一個公差為 \(|s|-|k|\) 的等差數列;
證明不存在這個等差數列之外的border:
假設 \(u\) 是字符串的最大border,設另一個不小於 \(|s|\) 一半的border為 \(v\)
由引理1,2可得,\(gcd(|s|-|u|,|s|-|v|)\) 是一個周期,而 \(|s|-|u|\) 已經是最小周期了,所以 \(|s|-|v|\) 為最小周期的整數倍,得證。
引理4:可以把字符串分成 \(log|s|\) 段,每一段的border都是一個等差數列
引理3的擴展,建議先看下面的應用,再回來就很好理解了。
先把 \(s\) 分成兩半,由引理3,右邊的border是等差數列(這里的border指:以右邊點為終點的前綴作為border)
左邊的border呢?再拆成一半,新的右半邊又構成了等差數列。。。
所以一個字符串的所有border可以被我們分成log數量級個等差數列。
應用:為什么要考慮border中的等差數列?
如圖所示,\(s\) 的所有成等差數列的border,其下一位一定相同
在KMP匹配中,我們可以利用這個性質快速跳過一串border
具體而言,在一次跳border時,如果發現border長度不小於原串的一半,則接下來的border構成等差數列,直到一半以下(引理3)
可以直接跳到 \((x-(x/2/d)*d)\) 處,即比一半大的第一個位置(整除)
(網上博客直接跳到了 \(x\%d+d\) 處,經過幾道題檢驗也是對的,但不是很能理解)
一次至少跳一半,保證 \(log\) 次以內可以跳完
\(\quad\)
標解是建樹后LCA,我們這個穩定跳log次border的“暴力匹配”可以更優雅地過這個題
int n, m;
string s;
int nt[maxn];
void get_next(){
nt[1] = 0;
for(int i=2;i<=n;i++){
int p = nt[i-1];
while(p && s[p+1] != s[i]) p = nt[p];
if(s[p+1] != s[i]) nt[i] = 0;
else nt[i] = p + 1;
}
}
void solve() {
cin >> s; n = s.size();
s = '0' + s;
get_next();
cin >> m;
while(m--){
int x, y; cin >> x >> y;
x = nt[x], y = nt[y];
while(x != y){
if(x < y) swap(x, y);
if(nt[x] > x/2){
int d = x - nt[x];
if(y % d == x % d) x = y;
else x = x - (x/2/d) * d;//大優化
}else x = nt[x];
}
cout << x << '\n';
}
}
\(\quad\)
例題:2021ICPC沈陽站M
當前位置的答案即為上一位置的答案的某個border加上新字符;找到最小的 加上新字符后大於原答案的border即可。(有一定難度,具體見題解)
int n, m;
string s;
int nt[maxn];
int head = 0;
void expend_nt(int frm, char c){
if(frm == 0){
nt[frm+1] = 0;
return;
}
int p = nt[frm];
while(p && s[head+p+1] != c) p = nt[p];
if(s[head+p+1] != c) nt[frm+1] = 0;
else nt[frm+1] = p + 1;
}
void solve() {
cin >> s; n = s.size();
s = '0' + s;
nt[1] = 0;
cout << "1 1\n";
int lst = 1;
for(int i=2;i<=n;i++){
int x = lst, len = lst + 1;
while(x > 0){
if(s[i] > s[i-lst+x]) len = x + 1;
if(nt[x] > x/2){
int d = x - nt[x];
x = x % d + d;
}else x = nt[x];
}
if(s[i] > s[i-lst]) len = 1;
head = i - len;
expend_nt(len-1, s[i]);
lst = len;
cout << i-len+1 << ' ' << i << '\n';
}
}