1. 程式人生 > >POJ 3693 重複次數最多的子串是什麼?【字尾陣列】

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;
}