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






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值