[二分][后缀数组]Musical Theme POJ1743

23 篇文章 0 订阅
22 篇文章 0 订阅

A musical melody is represented as a sequence of N (1<=N<=20000)notes that are integers in the range 1..88, each representing a key on the piano. It is unfortunate but true that this representation of melodies ignores the notion of musical timing; but, this programming task is about notes and not timings.
Many composers structure their music around a repeating &qout;theme&qout;, which, being a subsequence of an entire melody, is a sequence of integers in our representation. A subsequence of a melody is a theme if it:

  • is at least five notes long
  • appears (potentially transposed -- see below) again somewhere else in the piece of music
  • is disjoint from (i.e., non-overlapping with) at least one of its other appearance(s)


Transposed means that a constant positive or negative value is added to every note value in the theme subsequence.
Given a melody, compute the length (number of notes) of the longest theme.
One second time limit for this problem's solutions!

Input

The input contains several test cases. The first line of each test case contains the integer N. The following n integers represent the sequence of notes.
The last test case is followed by one zero.

Output

For each test case, the output file should contain a single line with a single integer that represents the length of the longest theme. If there are no themes, output 0.

Sample Input

30
25 27 30 34 39 45 52 60 69 79 69 60 52 45 39 34 30 26 22 18
82 78 74 70 66 67 64 60 65 80
0

Sample Output

5

Hint

Use scanf instead of cin to reduce the read time.

题意: 有n个音符,用小于88的正整数表示,求差分数组中第2个~第n个数字间存在的长度不小于4,出现次数不小于两次,且出现部分不重合的最长数组长度。

分析: 题意是经过简化的,原本题意需要先想到用差分序列的后缀数组。由于答案符合二分的性质,所以可以二分出一个长度len,然后再去检验是否存在。检验的过程需要用到后缀数组,先nlogn求出差分数组的sa数组、height数组,考虑这样的一个序列1 1 1 1 1 6 6 6 6 6 5,其差分序列为1 0 0 0 0 5 0 0 0 0 -1,去掉第一个元素就是0 0 0 0 5 0 0 0 0 -1,如果要检验长度为4的子串是否为答案串只需要遍历一遍height数组,因为height本来就是在排序的前提下求得的,当遍历到一段区间其height值都大于等于4时,说明区间内每个点对应后缀的最长公共前缀都大于等于4,如果这些前缀不重合的话那我们就已经找到了答案串了,现在需要判断这些前缀是否会重合,可以通过sa数组去处理,这段区间内最大的sa值减去区间内最小的sa值就是距离最远的两前缀之间的距离,将其与4作比较,如果大于等于4就代表没有重合,对于二分出来的每一个长度进行上述检验即可。

具体代码如下: 

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 1e6+10;
int n, m;
int s[maxn];
int sa[maxn], height[maxn], x[maxn], y[maxn], rk[maxn], tong[maxn];

void get_sa()
{
    for(int i = 1; i <= n; i++) tong[x[i] = s[i]] ++;//初始第一关键字的排名就设置为其ASCII码即可
    for(int i = 2; i <= m; i++) tong[i] += tong[i-1];//计算前缀和,注意只算到m即可
    for(int i = n; i; i--) sa[tong[x[i]]--] = i;//以第一关键字计算排名
    for(int k = 1; k <= n; k <<= 1) {//倍增计算sa
        int num = 0;//计算Y,排名从1开始,指针指向0
        //Y[i] = j -> 以第二关键字排名,排名为i的首字母下标为j(即第j个后缀)
        for(int i = n-k+1; i <= n; i++) y[++num] = i;//后k个没有第二关键字,所以排名最小先分配排名
        for(int i = 1; i <= n; i++) {
            if(sa[i] <= k) continue;//前k个第一关键字不能作为某个后缀的第二关键字
            y[++num] = sa[i] - k;//sa为以第一关键字计算的排名,从小到大枚举排名,对应的下标其实是第 sa[i] - k 个后缀的第二关键字
        }
        for(int i = 0; i <= m; i++) tong[i] = 0;//初始化桶,范围为1-m即可
        for(int i = 1; i <= n; i++) tong[x[i]]++;//计算新的第一关键字每个排名有几个
        for(int i = 2; i <= m; i++) tong[i] += tong[i-1];//计算前缀和,1-m即可
        for(int i = n; i; i--) sa[tong[x[y[i]]]--] = y[i], y[i] = 0;
        //y[i] 表示以第二关键字排序,排名为i的后缀的下标
        //x[y[i]] 表示上述后缀按照第一关键字的排名
        //tong[x[y[i]]] 表示小于等于上述排名的数量,也就是该后缀的排名
        //sa记录上述排名对应的下标为 y[i],从后往前枚举第二关键字的排名,使得第一关键字相同的后缀也可以依靠第二关键字区分
        swap(x,y);//接下来要更新X数组,且要用到旧的X数组,Y数组接下里用不到,
        //则将两者交换,目的是将旧的X存到Y中,后面所有的Y实际就是旧的X
        // 将当前的第一关键字和第二关键字当做下一轮的第一关键字,sa中存的就是按照双关键字排序的结果。
        x[sa[1]] = 1, num = 1;  //sa[1]对于的后缀新X的排名也为1
        for(int i = 2; i <= n; i++) 
            //如果新排名为i的后缀和新排名为i-1的后缀的第一关键字排名相同(前一个 == )
            //并且它们的第二关键字排名也相同(后一个 == ),那么两个后缀的新X排名相同,否则不同
            x[sa[i]] = (y[sa[i]] == y[sa[i-1]] && y[sa[i] + k] == y[sa[i-1] + k]) == 1 ? num : ++ num;
        if(n == num) return;//如果已经完全区分出n个后缀了,则可以结束循环
        m = num;//更新离散化后的rank范围
    }
}
//height[i] 表示排名为i和排名为i-1的后缀的最长公共前缀
void get_height() 
{
    for(int i = 1; i <= n; i++) rk[sa[i]] = i;//rk[i] = j - > 第i个后缀的排名为j
    for(int i = 1, k = 0; i <= n; i++) {
        if(rk[i] == 1) continue;//排名为1的height不用计算
        //设h[i]表示height[rk[i]],即位于第i个的后缀与排名在它前一个的后缀的最长公共前缀
        if(k) k--;//由于h[i]>=h[i-1]-1,所以从k-1开始枚举
        int j = sa[rk[i]-1];//排名在i前一个的后缀的下标
        while(i + k <= n && j + k <= n && s[i+k] == s[j+k]) k++;//如果相等,则最长公共前缀+1
        height[rk[i]] = k;//更新height
    }
}

bool check(int x)//判断是否存在长度为x的两个相同字符串 
{
	int l = -1, r, minsa = sa[1], maxsa = sa[1];
	for(int i = 1; i <= n; i++)
	{
		if(height[i] >= x)
		{
			if(l == -1)
				l = i;
			r = i;
			minsa = min(minsa, sa[i]);
			maxsa = max(maxsa, sa[i]);
		}
		if(height[i] < x || i == n)
		{
			if(l != -1)
			{
				l = -1;
				if(maxsa-minsa >= x)//距离最远的两个后缀串如果前缀没有重叠,不能写等于因为要保证原序列不重叠,但题目好像表述不清 
					return true;
			}
			maxsa = sa[i]; 
			minsa = sa[i];
		}
	}
	return false;
}

void solve() 
{
    for(int i = 0; i <= n-1; i++)
    	scanf("%d", &s[i]);
    for(int i = n-1; i >= 1; i--)
    	s[i] = s[i]-s[i-1]+88;
    n--;
    m = 200;//字符集的最大值 
    get_sa();
    get_height();
    int l = 1, r = (n+1)/2, ans = -1;
    while(l <= r)
    {
    	int m = l+r>>1;
    	if(check(m))
    	{
    		ans = m;
    		l = m+1;
		}
		else
			r = m-1;
	}
	if(ans >= 4)
		printf("%d\n", ans+1);
	else
		puts("0");
}
signed main()
{
    while(cin >> n)
    {
    	if(n == 0)
    		break;
    	for(int i = 0; i <= m; i++)//初始化桶 
    		tong[i] = 0;
        solve();
	}
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值