POJ 3693 重複次數最多的子串是什麼?【字尾陣列】
聽說暴力優化0ms通關。 但是這裡要說的顯然不是暴力的方法~
還是喜歡中等大小的字型啦啦啦~
我知道論文上有講,但是如果論文上的看懂了,你一定不會在百度搜這個了……我反正當時沒看的很明白,大神的解釋語言十分精簡,精簡到了,對於弱B的我,我就看不懂了……
題目大意:
一個字串T,他可能裡面有一段子串S, S由一個S的子串P重複K次而成。
比如 qwerty abcabcabc 這個串,他後面那部分abcabcabc是由abc重複3次而成。
這個題要你求出最大的K所代表的子串,如果有多個,輸出字典序最小的那個。 對於上面的那個資料,就要輸出abcabcabc 。
希望題目意思我的解釋還算清楚……………………
======================華麗的分割線============
前提條件: 字尾陣列掌握, RMQ解法至少掌握一種。 RMQ不懂的可以看我前一篇文章,我感覺講的還算清楚,看不懂的話留言,如果感覺我有不對的地方也可以提出~大家共同進步……我會盡量用大家能看懂的方式來講解。(但是我的語文是體育老師教的,所以看不懂我的文章不是你弱……而是我的語文太弱了)
好了進入正題:
這個題的大體思想的第一步, “子串是由長度為L的串重複R次而成”。 對於這個問題,我們既不知道L,也不知道R……
所以第一步,我們要窮舉L。 (當然, R= 1的情況,只要找出整個串ASCII碼最小的一個字母輸出即可……重複1次嘛!那一個字母重複一次,就是字母本身啦! 當然1個最小的字母,一定也是字典序最小的啦! 以後對於R=1的情況我就不說啦,只考慮R>=2的情況。)
對於一個長度為L的串,他起點位置如果是i, 終點位置一定是i + L - 1。
因為至少重複2次,那麼這個串最短也要是2L, 那麼如果起點是i,終點位置一定是i + 2L -1
因為S串是長度為L的串重複而成,那麼S的長度首先必須是L的倍數…… 【1】
下面要引入一個小結論…… 其實大神都知道的……我還看了一會兒才明白……這也是這道題的核心部分。
對於陣列下標: 0 1 2 3 4 5 6 7 8 9 ,如果L=3,重複至少2次,那麼這個串,一定會經過0,3,6,9.... 這些3的整數倍的下標,其中連續的2個。
比如長度為6的串, 可以是[0 1 2 3 4 5](經過0,3) 也可以是[1,2,3,4,5,6](經過3,6) 也可以是[2,3,4,5,6,7](經過3,6) 但是不管怎麼樣,他一定經過0,3,6,9這些3的整數倍中,連續的兩個。【2】
下面對i 和 i + L求最長公共字首。 這個字首有啥特殊的地方呢!
我們來看一個例子i = 6 L = 3的情況 也就是s[6]和s[9]的最長公共字首。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
a b c a b c a ……
a b c a ……
如果這個串a[6] = a[9], a[7]=a[10] a[8]=a[11]的話,那麼整個串,的[6 7 8] = [9 10 11]。 如果這個串還能繼續匹配下去,出現a[9] = a[12] a[10] =a[13]的話, 其實就重複了!
因為a[6] = a[9], a[9] = a[12] 實際上就是a[6] = a[12]
同理,如果a[10] = a[13],那麼就是a[7]=a[13]。 也就是說,後面的串都是和a[6] a[7] a[8]是相同的! 【希望這一大段話,如果看不懂的話,最好自己動筆劃一劃,舉個例子來理解是很好的辦法……】
那麼現在好了,當前從i(上面的例子是6)位置開始, L(上面的例子是3)次一迴圈的串,最長延續到的位置我們已經知道了。
最長公共字尾是k的話,那麼顯然從i位置迴圈了k / L + 1次。 (最長公共字首是6的話,那麼顯然i = 6開始,一直到11, 從9開始一直到14都是一樣的串, K的長度只是從9開始的長度,K/L也就是從9開始往後的串的長度的迴圈次數, +1是因為[6 7 8]這個串沒算。)
現在問題來了! 不見得所有串都是從L的整數倍位置開始的!!! 因為既然有後綴,就一定也有字首!字首的道理同理~
但是呢,我們因為是把整個串的L的位置都窮舉了一遍(0,3,6,9…… 當L=3的時候,每一個3的整數倍我們都窮舉了一次)
實際上,我們只要考慮一點就行了, 比如i =6, L=3的時候,我們只要考慮是不是從4開始,或者5開始。 不用考慮從3開始的情況,因為從3開始的情況我們已經算過了。【3】 (這個地方我自己當時理解了好一會兒)
舉個例子:
3 4 5 6 7 8 9 10 11 12 13 14
a b c a b c a b c a b c
這個串,i =9,L=3。
9開始往後的串是abc abc
8開始往後的串是cab cab
7開始往後的串是bca bca
而6開始的呢? 6作為3的整數倍的點,我們已經算過啦!不需要考慮了。
現在的核心問題是,找字首的問題。 我們總不能因為i=6,L=3, 還把4,5,6(6,和3之間的部分)分別和4+ 3, 5+3,6+3的位置求一次公共字首吧?
其實不用的,因為整個串一定是L的整數倍。如果從s[6]和s[9]的最長公共字首是7的話,那麼我們必須從4開始,最長公共字首是9,才能算得到一個有用的串。 如果從5開始,那麼最長公共字首最多也就是8.(既然不是L的整數倍,有啥用呢?重複次數又不增加)
所以我們只需要根據i和i+L這2個串的最長公共字首的長度,來算出,【如果我要讓串的重複次數+1, 我必須從i - p 的位置開始,並且s[i - p .... i] 部分是相同的[i + L - p ....i + L]】。
根據上述描述,我們就可以求出2個東西
最多重複R次, L1,L2,L3…… 長度的串都可以重複R次。然後我們窮舉SA[1],SA[2]……
看看SA[i] 開頭長度為L1個串,是否能重複R次。 這裡的判定就比較簡單了, 直接看 sa[i] 和sa[i]+L的最長公共字首K, K/L+1是否等於R即可。 最先算出的SA[i],就一定是字典序最小的……
好了……希望大家都能看懂…… 到這裡我的字尾陣列練習也算暫時結束了……
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
using namespace std;
const int max_n = 100000 + 20;
char input[max_n];
int a[max_n], wa[max_n], wb[max_n], tub[max_n], wv[max_n];
int height[max_n], rank[max_n], sa[max_n];
inline bool cmp(int *r, int a, int b, int l)
{return r[a] == r[b] && r[a + l] == r[b + l];}
inline void da(int *r, int *sa, int n, int m)
{
int i, j, p, *x = wa, *y = wb;
for (i = 0; i != m; ++ i) tub[i] = 0;
for (i = 0; i != n; ++ i) tub[x[i] = r[i]] ++;
for (i = 1; i != m; ++ i) tub[i] += tub[i - 1];
for (i = n - 1; i >= 0; -- i) sa[--tub[x[i]]] = i;
for (j = 1; p != n; m = p, j *= 2)
{
for (p = 0, i = n - j; i != n; ++ i) y[p ++] = i;
for (i = 0; i != n; ++ i) if (sa[i] >= j) y[p ++] = sa[i] - j;
for (i = 0; i != n; ++ i) wv[i] = x[y[i]];
for (i = 0; i != m; ++ i) tub[i] = 0;
for (i = 0; i != n; ++ i) tub[wv[i]] ++;
for (i = 1; i != m; ++ i) tub[i] += tub[i - 1];
for (i = n - 1; i >= 0; -- i) sa[-- tub[wv[i]]] = y[i];
for (swap(x, y), i = 1, p = 1, x[sa[0]] = 0; i != n; ++ i)
x[sa[i]] = cmp(y, sa[i], sa[i - 1], j) ? p - 1: p ++;
}
}
inline void calheight(int *r, int *sa, int n)
{
int k = 0, j;
for (int i = 1; i <= n; ++ i) rank[sa[i]] = i;
for (int i = 0; i != n; height[rank[i ++]] = k)
for (k?k--:0, j = sa[rank[i] - 1]; r[i + k] == r[j + k]; ++ k );
}
int st[max_n][25];
inline void makermq(int *r, int n)
{
for (int i = 0; i != n; ++ i) st[i][0] = r[i] ;
for (int j = 1; (1 << j) <= n; ++ j)
for (int i = 0; i + (1 << j) - 1 < n; ++ i)
st[i][j] = min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
inline int ask(int l, int r)
{
int tmp = (int)(log(r - l + 1)/log(2));
return min(st[l][tmp], st[r - (1 << tmp) + 1][tmp]);
}
inline int lcp(int a, int b)
{
int A = rank[a], B = rank[b];
if (A > B) swap(A, B);
++A;
return ask(A, B);
}
int ans[max_n];
inline void doit(int n) //串長度
{
int mx = 0, cut = 0;
for (int l = 1; l != n; ++ l)
{
for (int j = 0; j + l < n ; j += l)
{
int k = lcp(j, j + l);
int r = k / l + 1; //重複次數
int pre = j - (l - k % l);
if (pre >= 0)
{
k = lcp(pre, pre + l);
if (k / l + 1 > r) r = k / l + 1;
}
if (r == mx) ans[cut ++] = l;
if (r > mx)
{
mx = r;
cut = 0;
ans[cut ++] = l;
}
}
}
bool flag = false;
int pos, rlen;
for (int i = 1; i <= n && !flag; ++ i)
for (int j = 0; j != cut; ++ j)
{
if (sa[i] + ans[j] > n) continue;
int tmp = lcp(sa[i], sa[i] + ans[j]);
if (tmp / ans[j] + 1 == mx)
{
pos = sa[i];
rlen = ans[j];
flag = true;
break;
}
}
for (int i = pos; i != pos + mx * rlen; ++ i) printf("%c", input[i]);
printf("\n");
}
int main()
{
int tt = 0;
while (1)
{
gets(input);
if (input[0] == '#') break;
printf("Case %d: ", ++tt);
int len = strlen(input);
for (int i = 0; i != len; ++ i) a[i] = input[i];
a[len] = 0;
da(a, sa, len + 1, 200); //字尾陣列求解
calheight(a, sa, len); //字尾陣列求height
makermq(height, len + 1); //構造ST演算法的RMQ預處理
doit(len); //最終的求解判定
}
return 0;
}