POJ - 1743 Musical Theme 后缀数组/哈希 二分答案

题目链接

题目大意

有N(1<=N<=20000)个音符的序列来表示一首乐曲,每个音符都是1~88范围内的整数,现在要找一个重复的片段,它需要满足如下条件: 1.长度至少为5个音符。

​ 2.在乐曲中重复出现(就是出现过至少两次)。(可能经过转调,“转调”的意思是主题序列中每个音符都被加上或减去了同一个整数值)

​ 3.重复出现的同一主题不能有公共部分。

求满足条件的最长子串长度

解题思路

​ 由于”转调“的片段属于同一类,所以我们很轻易的想到转而去求差分后的数组以取消相同”转调“片段的区别。然后我们要做的就是二分差分串的长度以求出最长的不重叠的相同差分串(其实应该不仅是不重叠,两个差分串之间应该还要再空一个数字,因为这样在原串中才能做到”不重叠“,但是题目中并没有给出这样的限制)。

哈希

​ 而二分中对某长度的判断函数就需要使用快速的字符串处理方法,一开始我想到的是使用map去存当前字符串哈希值最早的出现位置,然后判断当前位置与map记录位置的距离是否大于给定长度。然后他TLE了……

bool check(int len)
{
	map<ull,int>mp;
	for (int i = 1; i + len - 1 < n; i++) {
		ull nw = query(i, i + len - 1);
		if (mp[nw]) {
			if (i - mp[nw] > len) {
				return true;
			}
		}
		else {
			mp[nw] = i;
		}
	}
	return false;
}

​ 然后去网上看题解,发现网上为数不多的使用哈希的代码都是差不多一样的思路:

​ 使用类似链式前向星的方式存下不同哈希值的出现位置,然后在根据当前位置的哈希值与之前记录的位置逐一判断是否满足距离大于给定长度。但是会有一个问题是head数组无法存储usigned long long范围这么大小的数据,所以需要在存储哈希值的时候再取一下模,将哈希值映射到可接受的范围内。那么紧接着的问题就是可能会有多个不同的哈希值被映射到一起,所以要额外再记录一下原始的哈希值,在需要判断时再重复确认一下即可。

void addval(ull hs,int pos)		
{
	ed[cnt].hs = hs;
	ed[cnt].pos = pos;
	ed[cnt].ne = head[hs % maxn];
	head[hs % maxn] = cnt++;
}

bool check(int len)
{
	memset(head, -1, sizeof(head));
	cnt = 0;
	for (int i = 1; i + len - 1 < n; i++) {
		ull nw = query(i, i + len - 1);
		for (int p = head[nw % maxn]; ~p; p = ed[p].ne) {//由于hash值无法直接存储开头位置(map会超时…)所以将hash值取模后存储到一起,然后查询时再重新验证 
			if (ed[p].hs == nw && i - ed[p].pos > len) {//根据题意,应该是两组子串不能相邻(因为数组表示差分值,相邻差分值会共用一个元素) (但是实际数据好像没有考虑这一点) 
				return true;
			}
		}
		addval(nw, i);
	}
	return false;
}

​ 其实我还根据这个思路试了下使用vector存储的版本,思路与上面完全相同,但是代码应该更好理解一些,但是很可惜…他TLE了(果然STL还是被卡掉了),贴出来辅助理解上面的代码吧

void addval(ull hs,int pos)		
{
	G[hs % maxn].push_back(node(hs, pos));
}

bool check(int len)
{
	for (int i = 0; i < maxn; i++) {
		G[i].clear();
	}
	for (int i = 1; i + len - 1 < n; i++) {
		ull nw = query(i, i + len - 1);
		int yc = nw % maxn;
		for (int j = 0; j < G[yc].size(); j++) {
			node p = G[yc][j];
			if (p.hs == nw && i - p.pos >= len) {
				return true;
			}
		}
		addval(nw, i);
	}
	return false;
}
后缀数组

​ 后缀数组判断思路要相对简单一些:看最长公共前缀大于要求长度的这些后缀的起始位置之间的最大距离,如果大于要求长度,则符合题意。

bool check(int len)
{
	int Max = SA[1], Min = SA[1];//记录公共前缀长度符合条件开始位置的最大距离 
	for (int i = 2; i <= n; i++) {
		if (Hight[i] < len)//如果以当前为前缀的最长公共子序列比len还小,不合题意,直接重置min,max 
			Max = Min = SA[i];
		else {
			if (SA[i] < Min) Min = SA[i]; 
			if (SA[i] > Max) Max = SA[i];
			if (Max - Min > len) {//如果在最长公共前缀满足条件的情况下,两后缀开始间隔大于要求,即找到答案。 
				return true;
			}
		}
	}
	return false;
}

AC代码

哈希
#include <iostream>
#include <fstream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
#pragma warning(disable:4996)
#define inr register int
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define debug(a) cout << #a << " " << a << endl
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double pi = acos(-1.0);
const double eps = 1e-8;
const int inf = 0x3f3f3f3f;
const int maxn = 100007;//1e5+7 
const ll mod = 1000000007;//1e9+7

short read()
{
	short res = 0, ch, flag = 0;

	if ((ch = getchar()) == '-')
		flag = 1;
	else if (ch >= '0' && ch <= '9')
		res = ch - '0';
	while ((ch = getchar()) >= '0' && ch <= '9')
		res = res * 10 + ch - '0';
	return flag ? -res : res;
}

int n;
short dif[maxn];
ull p = 23333;
ull hase[maxn];
ull base[maxn];

void fbase()//建立基数数组 
{
	base[0] = 1;
	for (int i = 1; i < maxn; i++) {
		base[i] = base[i - 1] * p;
	}
}

void Hash()//建立哈希数组
{
	hase[n] = 0;
	for (int i = n - 1; i >= 0; i--) {
		hase[i] = hase[i + 1] * p + (dif[i] + 90);
	}
}

ull query(int l, int r)//返回区间子串hashe 
{
	return hase[l] - hase[r + 1] * base[r - l + 1];
}

struct node{
	ull hs;
	int pos, ne;
}ed[maxn];

int head[maxn], cnt = 0;

void addval(ull hs,int pos)		
{
	ed[cnt].hs = hs;
	ed[cnt].pos = pos;
	ed[cnt].ne = head[hs % maxn];
	head[hs % maxn] = cnt++;
}

bool check(int len)
{
	memset(head, -1, sizeof(head));
	cnt = 0;
	for (int i = 1; i + len - 1 < n; i++) {
		ull nw = query(i, i + len - 1);
		for (int p = head[nw % maxn]; ~p; p = ed[p].ne) {//由于hash值无法直接存储开头位置(map会超时…)所以将hash值取模后存储到一起,然后查询时再重新验证 
			if (ed[p].hs == nw && i - ed[p].pos > len) {//根据题意,应该是两组子串不能相邻(因为数组表示差分值,相邻差分值会共用一个元素) (但是实际数据好像没有考虑这一点) 
				return true;
			}
		}
		addval(nw, i);
	}
	return false;
}

int main()
{
	fbase();
	while (~scanf("%d", &n) && n) {
		for (int i = 1; i <= n; i++) {
			dif[i] = read();
		}
		for (int i = 1; i < n; i++) {
			dif[i] = dif[i + 1] - dif[i];
		}
		Hash();
		int l = 1, r = n / 2;
		int ans = -1;
		while (l < r) {
			int mid = (l + r + 1) >> 1;
			if (check(mid)) {
				l = mid;
				ans = mid;
			}
			else {
				r = mid - 1;
			}
		}
		if (ans <= 3) {
			ans = -1;
		}
		printf("%d\n", ans + 1);
	}
	return 0;
}
后缀数组
#include <iostream>
#include <fstream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
#pragma warning(disable:4996)
#define inr register int
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define debug(a) cout << #a << " " << a << endl
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double pi = acos(-1.0);
const double eps = 1e-8;
const int inf = 0x3f3f3f3f;
const int maxn = 100007;//1e5+7 
const ll mod = 1000000007;//1e9+7

short read()
{
	short res = 0, ch, flag = 0;

	if ((ch = getchar()) == '-')
		flag = 1;
	else if (ch >= '0' && ch <= '9')
		res = ch - '0';
	while ((ch = getchar()) >= '0' && ch <= '9')
		res = res * 10 + ch - '0';
	return flag ? -res : res;
}

int n, m;//串长度、符号范围
int st[maxn];//串
int SA[maxn];//字典序排第i的的下标为sa[i]
int rak[maxn];//以当前位置开头的串字典序排第几
int Hight[maxn];//以i开头的后缀与i-1开头的最长公共前缀
int tax[maxn];//用于基数排序
int tmp[maxn];


void Base_sort()
{
	for (int i = 1; i <= m; i++) tax[i] = 0;
	for (int i = 1; i <= n; i++) tax[rak[i]]++;
	for (int i = 2; i <= m; i++) tax[i] += tax[i - 1];
	for (int i = n; i >= 1; i--) SA[tax[rak[tmp[i]]]--] = tmp[i];
}

void get_SA()
{
	for (int i = 1; i <= n; i++) {
		rak[i] = st[i];
		tmp[i] = i;
	}
	Base_sort();
	for (int k = 1; k <= n; k <<= 1) {
		int cnt = 0;
		for (int i = n - k + 1; i <= n; i++) {
			tmp[++cnt] = i;
		}
		for (int i = 1; i <= n; i++) {
			if (SA[i] > k) {
				tmp[++cnt] = SA[i] - k;
			}
		}
		Base_sort();
		swap(rak, tmp);
		memset(rak, 0, sizeof(rak));
		int num = 1;
		rak[SA[1]] = 1;
		for (int i = 2; i <= n; i++) {
			if (tmp[SA[i]] == tmp[SA[i - 1]] && tmp[SA[i] + k] == tmp[SA[i - 1] + k]) {
				rak[SA[i]] = num;
			}
			else {
				rak[SA[i]] = ++num;
			}
		}
		m = num;
	}
}

void get_Hi()
{
	int k = 0;
	for (int i = 1; i <= n; i++) {
		if (rak[i] == 1) continue;
		int j = SA[rak[i] - 1];
		if (k) {
			k--;
		}
		while (i + k <= n && st[i + k] == st[j + k]) {
			k++;
		}
		Hight[rak[i]] = k;
	}


}

bool check(int len)
{
	int Max = SA[1], Min = SA[1];//记录公共前缀长度符合条件开始位置的最大距离 
	for (int i = 2; i <= n; i++) {
		if (Hight[i] < len)//如果以当前为前缀的最长公共子序列比len还小,不合题意,直接重置min,max 
			Max = Min = SA[i];
		else {
			if (SA[i] < Min) Min = SA[i]; 
			if (SA[i] > Max) Max = SA[i];
			if (Max - Min > len) {//如果在最长公共前缀满足条件的情况下,两后缀开始间隔大于要求,即找到答案。 
				return true;
			}
		}
	}
	return false;
}

int arr[maxn];

int main()
{
	
	while (~scanf("%d", &n) && n) {
		for (int i = 1; i <= n; i++) {
			arr[i] = read();
		}
		for (int i = 1; i < n; i++) {
			st[i] = arr[i + 1] - arr[i] + 90;
		}
		n--;
		m = 255;
		get_SA();
		get_Hi();
		//int l = 4, r = n / 2;
		int l = 1, r = (n+1) / 2;
		int ans = -1;
		while (l < r) {
			int mid = (l + r + 1) >> 1;
			if (check(mid)) {
				l = mid;
				ans = mid;
			}
			else {
				r = mid - 1;
			}
		}
		if (ans <= 3) {
			ans = -1;
		}
		printf("%d\n", ans + 1);
	}
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值