Description
如果一个序列的任意连续子序列都至少有一个元素唯一,则称这个序列“不无聊”,否则称这个序列“无聊”。给定 \(T\) 个序列 \(a\),长度为 \(n\),求是否“无聊”。
Hint
- \(1\le n\le 2\times 10^5\)
- \(1\le \text{元素大小}\le 10^9\)
Solution
假如在整个数列中找到了一个位置 \(p\),\(a_p\) 的值在整个数列中是唯一的,由于子段 \(a[1, p-1]\) 和 \(a[p+1, n]\) 都满足条件,那么显然整个数列就满足条件。
这个结论对于本题非常重要 ,接下来给出一个简单的证明:
要使数列 \(a[l, r]\) 满足条件,必须满足:对于所有满足 \(l\le i\le j\le r\) 的数对 \((i, j)\),子段 \(a[i, j]\) 中存在一个唯一的元素。
而现在 \(a[l, p-1]\) 和 \(a[p+1, r]\) 已经满足条件,说明我们只要考虑 \(i\in[l,p],j\in[p,j]\) 的部分即可。
然而 \(a_p\) 已经是 \(a[l,r]\) 中唯一的元素了,自然也是 \(a[i, j]\) 中唯一的元素,那么 \(a[i, j]\) 中一定存在一个唯一的元素,即 \(a_p\)。
该命题于是就得证了。
有了这个结论,我们可以大致设计出这样一个算法:
function judge(l, r)
if l >= r then return true
p ← the position of unique element in a[l...r];
if p is undefined then //找不到就返回 false
return false;
return judge(l, p - 1) and judge(p + 1, r);
这是一个很显然的分治算法,但是我们又发现了一个新的问题:就是如何求 \(p\)。
我们可以通过求前面的最后面的与之相等的元素以及后面的最前面的与之相等的元素的位置来判断是否唯一(前驱后继)。
如果在一个区间内,我们从左往右找,那么若是目标位置在最右边,那么复杂度将会是 \(O(n^2)\)。
从右往左也有同样的问题。
于是 从两边向中间找! 看起来很荒谬,但的确可以解决问题:
- 如果目标在两端(最好情况),那么马上就能找到,\(T(n) = T(n-1) + O(1) = O(n)\);
- 如果在中间(最坏情况),那 \(T(n) = 2T(\frac{n}{2}) + O(n) = O(n\log n)\)。
可以设计出如下算法:
function judge(l, r)
if l >= r then return true
i ← l, j ← r
while i <= j do
if a[i] is unique element in a[l...r] then
return judge(l, i - 1) and judge(i + 1, r);
if a[j] is unique element in a[l...r] then
return judge(l, j - 1) and judge(j + 1, r);
end
return false;
这个算法效率非常高,因为最坏才达到 \(O(n\log n)\)。
Code
使用 map
求前驱后继(310 ms):
#include <cstdio>
#include <map>
using namespace std;
const int N = 2e5 + 5;
int pre[N], nxt[N];
bool judge(int* a, int l, int r) {
if (l >= r) return true;
for (register int i = l, j = r; i <= j; i++, j--) {
if (pre[i] < l && nxt[i] > r)
return judge(a, l, i - 1) && judge(a, i + 1, r);
if (pre[j] < l && nxt[j] > r)
return judge(a, l, j - 1) && judge(a, j + 1, r);
}
return false;
}
int n, a[N];
map<int, int> pos;
signed main() {
int tc; scanf("%d", &tc);
while (tc--) {
scanf("%d", &n);
for (register int i = 1; i <= n; i++)
scanf("%d", a + i);
pos.clear();
for (register int i = 1; i <= n; i++) {
if (pos.count(a[i]))
pre[i] = pos[a[i]];
else pre[i] = 0;
pos[a[i]] = i;
}
pos.clear();
for (register int i = n; i; i--) {
if (pos.count(a[i]))
nxt[i] = pos[a[i]];
else nxt[i] = n + 1;
pos[a[i]] = i;
}
if(judge(a, 1, n)) printf("non-boring\n");
else printf("boring\n");
}
return 0;
}
使用离散化求前驱后继(100 ms):
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
const int N = 2e5 + 5;
int pre[N], nxt[N], t[N];
bool judge(int* a, int l, int r) {
if (l >= r) return true;
for (register int i = l, j = r; i <= j; i++, j--) {
if (pre[i] < l && nxt[i] > r)
return judge(a, l, i - 1) && judge(a, i + 1, r);
if (pre[j] < l && nxt[j] > r)
return judge(a, l, j - 1) && judge(a, j + 1, r);
}
return false;
}
int n, a[N];
int c, v[N];
signed main() {
int tc; scanf("%d", &tc);
while (tc--) {
scanf("%d", &n);
for (register int i = 1; i <= n; i++)
scanf("%d", a + i);
c = 0;
for (register int i = 1; i <= n; i++)
v[++c] = a[i];
sort(v + 1, v + 1 + c);
c = unique(v + 1, v + 1 + c) - v - 1;
for (register int i = 1; i <= n; i++)
a[i] = lower_bound(v + 1, v + 1 + c, a[i]) - v;
memset(t, 0, sizeof(int) * (c + 5));
for (register int i = 1; i <= n; i++) {
pre[i] = t[a[i]];
t[a[i]] = i;
}
memset(t, 0x3f, sizeof(int) * (c + 5));
for (register int i = n; i; i--) {
nxt[i] = t[a[i]];
t[a[i]] = i;
}
if(judge(a, 1, n)) printf("non-boring\n");
else printf("boring\n");
}
return 0;
}