POJ-P1743 Music Theme(字符串Hash+二分答案)
题目传送门
原题面
描述
A musical melody is represented as a sequence of \(N~(1 \le N \le 20000)\) notes that are integers in the range \(1 \dots 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!
输入格式
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.
输出格式
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\).
参考翻译
描述
音乐旋律表示为 \(N~(1 \le N \le 20000)\) 个音符的序列,这些音符是 \(1 \dots 88\) 范围内的整数,每个音符代表钢琴上的一个键。许多作曲家围绕一个重复的“主题”构建他们的音乐,它是整个旋律(我们前面提到过的整数序列)的子序列。如果它:
- 至少有五个音符长
- 在乐曲中多次出现(可能有转调——见下文)
- 与至少一个相同序列没有任何重叠的部分
转调意味着主题子序列中的每个音符值都同时加上同一个整数。
给定一段旋律,计算最长主题的长度。
输入格式
输入包含多组数据。每组数据的第一行包含一个整数 \(N\)。接下来的 \(n\) 个整数表示音符序列。
输入以一个 \(0\) 结尾。
输出格式
对于每组数据,输出应包含一行,其中包含一个整数,表示最长主题的长度。
如果不存在最长主题,则输出 \(0\)。
样例
样例输入
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
样例输出
5
题解
这题我拿到手一看就是 \(Hash\)。(还没学后缀数组)
转调是一个难点,其实处理想好怎么处理转调这件事这题也差不多了。
对于一个数列,每一个数都加上同一个值后什么不变?
显而易见,它们两项之间的差不变。这就是差分的思想。
我们可以通过处理出一个差分数组,来达到把调 统一 的效果。
处理完后,这个问题就转变为了求最长相同子序列了。自然是直接上后缀数组 \(Hash\)。
可以处理出一个前缀 \(Hash\) 数组,求区间 \(Hash\) 值的时候会方便很很很很多。
然后就到了我们熟悉的字符串拆分的问题了
\(hash[i \dots j] = (hash[1 \dots j] - (hash[1 \dots i-1] \times k^{j-i+1}) ~\%~ p) ~\%~ p\)
这里面的 \(k^{j-i+1}\) 可以通过一个进制数组来记录,节省时间。
上代码:
#include <cstdio>
#include <cstring>
#define N 20100
#define MOD 10007
#define SEED 13331
using namespace std;
typedef unsigned long long ull;
struct HashMap {
int head[MOD], nxt[N], size, to[N];
ull value[N];
void init() {
size = 0;
memset(head, -1, sizeof(head));
}
int insert(ull val, int id) {
int h = val % MOD;
for (int i = head[h]; ~i; i = nxt[i]) {
if (val == value[i]) {
return to[i];
// 查找是否存在等于h的Hash值
// 有则返回位置(位置越前越好,因此直接返回)
}
}
to[++size] = id;
value[size] = val;
nxt[size] = head[h];
head[h] = size;
// 没有则添加
return to[size];
}
} H;
ull p[N], s[N];
// 自然溢出Hash
int a[N], n;
bool check(int x) {
H.init();
for (int i = x; i < n; i++) {
if (H.insert(s[i] - s[i - x] * p[x], i) < i - x) {
return 1;
// s[i] - s[i - x] * p[x]就是差分数组中i - x + 1 ~ i的Hash值
// 如果返回的相同子串的位置不重叠则答案合法,返回1(true)
}
}
return 0;
// 否则返回0(false)
}
int main() {
p[0] = 1;
for (int i = 1; i < N; i++) {
p[i] = p[i - 1] * SEED;
// 处理进制
}
while (~scanf("%d", &n) && n) {
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i < n; i++) {
a[i] = a[i + 1] - a[i];
// 处理差分数组
}
for (int i = 1; i < n; i++) {
s[i] = s[i - 1] * SEED + a[i];
// 处理差分数组的Hash前缀和
}
int l = 1, r = n / 2, ans = -1;
while (r - l >= 0) {
int mid = (l + r) >> 1;
if (check(mid)) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
// 二分答案
}
if (ans < 4) printf("0\n");
else printf("%d\n", ans + 1);
}
return 0;
}