401 - 蜗蜗的数列
我们设 c [ i ] = a [ i ] − b [ i ] c[i]=a[i]-b[i] c[i]=a[i]−b[i]。当对于所有 1 ≤ i ≤ n , c [ i ] = 0 1\leq i\leq n, c[i] = 0 1≤i≤n,c[i]=0均成立时,两数列相等。但是这样还是要每次操作更新 r − l + 1 r-l+1 r−l+1个位置,还是会超时。
斐波那契数列的性质是:对于所有 i > 2 i>2 i>2,均满足 f [ i ] = f [ i − 1 ] + f [ i − 2 ] f[i]=f[i-1]+f[i-2] f[i]=f[i−1]+f[i−2]。我们尝试根据这个性质,通过差分的思想来构造一个新的数列——
我们设 d [ i ] d[i] d[i]为这个新构造的数列,其中 d [ 1 ] = c [ 1 ] , d [ 2 ] = c [ 2 ] − c [ 1 ] d[1]=c[1],d[2]=c[2]-c[1] d[1]=c[1],d[2]=c[2]−c[1],对于 i > 2 i>2 i>2, d [ i ] = c [ i ] − c [ i − 1 ] − c [ i − 2 ] d[i]=c[i]-c[i-1]-c[i-2] d[i]=c[i]−c[i−1]−c[i−2]。之所以这样构造,是因为这样可以充分利用斐波那契数列的性质,实现差分的思想,每次操作只需要修正有限个数。
先来看如何通过这个构造的数列判断 A B AB AB两数列相等:
当
d
[
1
]
=
0
d[1]=0
d[1]=0时,
c
[
1
]
=
0
c[1]=0
c[1]=0。
当
d
[
1
]
=
d
[
2
]
=
0
d[1]=d[2]=0
d[1]=d[2]=0时,
c
[
2
]
=
0
c[2]=0
c[2]=0。
当
d
[
3
]
=
c
[
3
]
−
c
[
2
]
−
c
[
1
]
=
0
d[3]=c[3]-c[2]-c[1]=0
d[3]=c[3]−c[2]−c[1]=0时,可以推出
c
[
3
]
=
0
c[3]=0
c[3]=0。
以此类推我们可以发现,当对于所有
1
≤
i
≤
n
1\leq i\leq n
1≤i≤n,
d
[
i
]
=
0
d[i]=0
d[i]=0均成立时,
A
B
AB
AB两数列相等。
现预处理出前 n n n项模 M M M意义下的斐波那契数列 f [ i ] f[i] f[i]。
每次操作中,以修正 A A A操作为例,设该操作覆盖范围 [ L , R ] [L,R] [L,R]。
我们有:
C
L
→
C
L
+
f
[
1
]
C_L\rightarrow C_L+f[1]
CL→CL+f[1]
C
L
+
1
→
C
L
+
1
+
f
[
2
]
C_{L+1}\rightarrow C_{L+1}+f[2]
CL+1→CL+1+f[2]
C
L
+
2
→
C
L
+
2
+
f
[
3
]
C_{L+2}\rightarrow C_{L+2}+f[3]
CL+2→CL+2+f[3]
⋯
\cdots
⋯
C
R
−
2
→
C
R
−
2
+
f
[
R
−
L
−
1
]
C_{R-2}\rightarrow C_{R-2}+f[R-L-1]
CR−2→CR−2+f[R−L−1]
C
R
−
1
→
C
R
−
1
+
f
[
R
−
L
]
C_{R-1}\rightarrow C_{R-1}+f[R-L]
CR−1→CR−1+f[R−L]
C
R
→
C
R
+
f
[
R
−
L
+
1
]
C_R \rightarrow C_R+f[R-L+1]
CR→CR+f[R−L+1]
D
L
=
C
L
−
C
L
−
1
−
C
L
−
2
D_L=C_L-C_{L-1}-C_{L-2}
DL=CL−CL−1−CL−2,只有第一项增加了
f
[
1
]
=
1
f[1]=1
f[1]=1,所以
D
L
→
D
L
+
1
D_L\rightarrow D_L+1
DL→DL+1。
D
L
+
1
=
C
L
+
1
−
C
L
−
C
L
−
1
D_{L+1}=C_{L+1}-C_L-C_{L-1}
DL+1=CL+1−CL−CL−1,第一项增加了
f
[
2
]
=
1
f[2]=1
f[2]=1,第二项符号为负,减少了
f
[
1
]
=
1
f[1]=1
f[1]=1,抵消掉了,所以
D
L
+
1
→
D
L
+
1
D_{L+1}\rightarrow D_{L+1}
DL+1→DL+1。
D
L
+
2
=
C
L
+
2
−
C
L
+
1
−
C
L
D_{L+2}=C_{L+2}-C_{L+1}-C_L
DL+2=CL+2−CL+1−CL,第一项增加了
f
[
3
]
f[3]
f[3],第二项、第三项分别减少
f
[
2
]
,
f
[
1
]
f[2],f[1]
f[2],f[1],由于有
f
[
3
]
=
f
[
2
]
+
f
[
1
]
f[3]=f[2]+f[1]
f[3]=f[2]+f[1],所以相互抵消,
D
L
+
2
→
D
L
+
2
D_{L+2}\rightarrow D_{L+2}
DL+2→DL+2。
⋯
\cdots
⋯
(中间所有项均通过
f
[
i
]
=
f
[
i
−
1
]
+
f
[
i
−
2
]
f[i]=f[i-1]+f[i-2]
f[i]=f[i−1]+f[i−2]可得变化量为
0
0
0)
⋯
\cdots
⋯
D
R
+
1
=
C
R
+
1
−
C
R
−
C
R
−
1
D_{R+1}=C_{R+1}-C_R-C_{R-1}
DR+1=CR+1−CR−CR−1,第二项减少
f
[
R
−
L
]
f[R-L]
f[R−L],第三项减少
f
[
R
−
L
+
1
]
f[R-L+1]
f[R−L+1],第一项不改变,所以减少的是
f
[
R
−
L
]
+
f
[
R
−
L
+
1
]
=
f
[
R
−
L
+
2
]
f[R-L]+f[R-L+1]=f[R-L+2]
f[R−L]+f[R−L+1]=f[R−L+2],即
D
R
+
1
→
D
R
+
1
−
f
[
R
−
L
+
2
]
D_{R+1}\rightarrow D_{R+1}-f[R-L+2]
DR+1→DR+1−f[R−L+2]。
D
R
+
2
=
C
R
+
2
−
C
R
+
1
−
C
R
D_{R+2}=C_{R+2}-C_{R+1}-C_R
DR+2=CR+2−CR+1−CR,只有第一项减少
f
[
R
−
L
+
1
]
f[R-L+1]
f[R−L+1],即
D
R
+
1
→
D
R
+
1
−
f
[
R
−
L
+
1
]
D_{R+1}\rightarrow D_{R+1}-f[R-L+1]
DR+1→DR+1−f[R−L+1]。
所以这种构造方法,每进行一次操作,只需要修正四个位置的 d [ i ] d[i] d[i]即可。
对于 B B B数组的操作,只需要将 4 4 4个位置的变化量变为相反数即可。
每次操作结束后,如果对于所有 1 ≤ i ≤ n 1\leq i\leq n 1≤i≤n, d [ i ] = 0 d[i]=0 d[i]=0均成立,则输出Yes。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000005;
LL n, q, mod, l, r, z;
LL a[N], b[N], c[N], d[N], f[N];
char x;
void add(int i, int x) {
if (i > n) return;
if (d[i] == 0) --z;
d[i] += x;
d[i] = (d[i] % mod + mod) % mod;
if (d[i] == 0) ++z;
}
void main2() {
cin >> n >> q >> mod;
z = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
for (int i = 1; i <= n; ++i) {
cin >> b[i];
c[i] = a[i] - b[i];
}
d[1] = (c[1] % mod + mod) % mod;
d[2] = ((c[2] - c[1]) % mod + mod) % mod;
if (d[1] == 0) ++z;
if (d[2] == 0) ++z;
for (int i = 3; i <= n; ++i) {
d[i] = c[i] - c[i - 1] - c[i - 2];
d[i] = (d[i] % mod + mod) % mod;
if (d[i] == 0) ++z;
}
f[1] = f[2] = 1;
for (int i = 3; i <= n; ++i) {
f[i] = (f[i - 1] + f[i - 2]) % mod;
}
for (int i = 1; i <= q; ++i) {
cin >> x >> l >> r;
if (x == 'A') {
add(l, 1);
add(r + 1, -f[r - l + 2]);
add(r + 2, -f[r - l + 1]);
}
else {
add(l, -1);
add(r + 1, f[r - l + 2]);
add(r + 2, f[r - l + 1]);
}
if (z == n) cout << "Yes\n";
else cout << "No\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _;
// cin >> _;
_ = 1;
while (_--) main2();
return 0;
}
402 - 最大公约数
如果选择的几段的最大公约数的答案是 g g g,那么选择的所有段的内部元素和都是 g g g的倍数。所以我们可以认为,整个数列的和也是 g g g的倍数。反过来可以发现,所有的答案,只可能是整个数列的和的因子。
当我们分的段数越多,就意味着我们得到的最大公约数的答案越小。因为,假如说我分成 x x x段的答案是 g g g,那么将这 x x x个段合并成更少的段,答案一定也可以是 g g g。所以段越少,可能的答案就越大。
枚举所有的因数 x x x:
我们要找序列中所有和为 x x x的倍数的段,设满足条件的段的左端点为 L L L,右端点为 R R R。如果序列的前缀和为 p r e [ i ] pre[i] pre[i],那么一定有 p r e [ L ] m o d x = p r e [ R ] m o d x pre[L]\bmod x = pre[R]\bmod x pre[L]modx=pre[R]modx。换句话说,对于所有可能的 p r e [ i ] m o d x = a pre[i]\bmod x=a pre[i]modx=a,统计这样的 i i i的数量。找出最大的那个数量,设其为 t t t。由于序列是一个环,所以有 t t t个分段点,就有 t t t个满足的区间(最外侧靠环连在一起的那个区间也一定满足,因为中间连续的都是 x x x的倍数,所有和也是 x x x的倍数,那么剩下的外侧的数的和也一定是 x x x的倍数)。那么能形成 t t t个区间的值 a n s [ t ] = max ( a n s [ t ] , x ) ans[t]=\max(ans[t], x) ans[t]=max(ans[t],x)。
根据答案的单调性,分的段数越少,答案就应该越大,所以我们用段数多的 a n s ans ans来更新段数少的 a n s ans ans,让其变成自己与上一个 a n s ans ans之间的最大值。最后输出 a n s ans ans数组即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
LL n, sum;
LL a[2050], pre[2005], ans[2005];
vector<LL> fac;
void main2() {
cin >> n;
pre[0] = sum = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
sum += a[i];
}
for (LL i = 1; i * i <= sum; ++i) {
if (sum % i == 0) {
fac.push_back(i);
if (i != sum / i) fac.push_back(sum / i);
}
}
for (LL x: fac) {
map<LL, LL> mp;
LL res = 0;
for (int i = 1; i <= n; ++i) {
int mod = pre[i] % x;
++mp[mod];
res = max(res, mp[mod]);
}
ans[res] = max(ans[res], x);
}
for (int i = n - 1; i >= 1; --i) {
ans[i] = max(ans[i], ans[i + 1]);
}
for (int i = 1; i <= n; ++i) {
cout << ans[i] << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _;
// cin >> _;
_ = 1;
while (_--) main2();
return 0;
}
403 - 平方计数
根据题意,有 a i 2 + a j = x 2 a_i^2+a_j=x^2 ai2+aj=x2,移项可得 a j = x 2 − a i 2 = ( x − a i ) ( x + a i ) a_j=x^2-a_i^2=(x-a_i)(x+a_i) aj=x2−ai2=(x−ai)(x+ai)。我们发现, x − a i x-a_i x−ai和 x + a i x+a_i x+ai是 a j a_j aj的两个因子,而且这两个因子的差为 2 a i 2a_i 2ai。所以我们可以枚举每一个 a j a_j aj的两个因子,将这两个因子做差取绝对值,看看除以 2 2 2后是否在序列中存在。这种方法的时间复杂度是 O ( n n ) O(n\sqrt n) O(nn)的,在这道题目中会超时。
我们换一种枚举因子的方式,不以枚举 a j a_j aj入手,而是直接用倍数遍历的方法,用 O ( n log n ) O(n\log n) O(nlogn)的时间复杂度进行遍历,遍历每一个 i ( 1 ≤ i ≤ 1 0 6 ) i(1\leq i\leq 10^6) i(1≤i≤106),对于每一个 i i i,遍历其所有倍数 j j j,此时 j j j就是 a j a_j aj,两个因子分别就是 i i i和 j / i j/i j/i。由于我们的 i i i会依次遍历到这两个数,所以相同的情况我们会遇到 2 2 2次,最终情况需要除以 2 2 2。对于每一种情况,先计算两个因子的差的绝对值,如果这个差的绝对值是偶数,那么这个情况对答案的贡献就是差的绝对值在序列中出现的次数和 j j j在序列中出现的次数的乘积。
最后将整体答案除以 2 2 2。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int n;
int a[1000005], mp[1000005];
void main2() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
++mp[a[i]];
}
LL ans = 0;
for (int i = 1; i <= 1000000; ++i) {
for (int j = i; j <= 1000000; j += i) {
int d = abs(j / i - i);
if (d % 2 == 0) {
d /= 2;
ans += (mp[d] * mp[j]);
}
}
}
cout << ans / 2;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _;
// cin >> _;
_ = 1;
while (_--) main2();
return 0;
}
404 - 字典序最小
我们期望最后得到的数组是这个样子: [ 1 , 2 , 3 , 4 , ⋯ , n ] [1,2,3,4,\cdots,n] [1,2,3,4,⋯,n]。但是因为给定的数组有顺序,所以我们很有可能达不成我们理想的目标。
我们从左往右遍历给定的序列,然后维护一个栈。如果当前元素没有在栈中,而且栈顶中有一些元素是比这个数大的,如果栈顶的元素在后面还会出现,那么就让那个数后面再进栈,先让小的数进到他的前面来。如果这个数后面没有再出现过了,意味着后面不会有机会再进栈,那么就不弹出,直接将当前的数放到栈顶。
遍历 m m m个数之后,得到的栈的内容就是我们要输出的结果。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000005;
int m, n, si = 0;
int lst[N], a[N], ins[N], s[N];
void main2() {
cin >> m >> n;
for (int i = 1; i <= m; ++i) {
cin >> a[i];
lst[a[i]] = i;
}
for (int i = 1; i <= m; ++i) {
if (ins[a[i]]) continue;
while (si and s[si] > a[i] and lst[s[si]] > i) {
ins[s[si]] = 0;
--si;
}
s[++si] = a[i];
ins[a[i]] = 1;
}
cout << s[1];
for (int i = 2; i <= n; ++i) {
cout << ' ' << s[i];
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _;
// cin >> _;
_ = 1;
while (_--) main2();
return 0;
}
405 - 拆拆
很容易想到将 X X X进行质因数分解,我们现在有 Y Y Y个位置,首先,假设我们这 Y Y Y个位置全部放正数,那么结果就是考虑 X X X有多少种分解方法,可以让任意两种方法之间,保证不是每一个位置的数都一样。
质因数分解后,我们考虑合并这些质因数。但是单纯从合并上去考虑的话,计算起来非常复杂。我们可以把合并这个过程抽象成往一个盒子里放小球的过程。因为要防止完全相同的两种方案出现,由于质因数分解出来之后,会存在相同的数和不同的数,不能将所有数看成相同的小球或不同的小球。如果我们每一种质因数单独考虑的话,那么不同种质因数之间是无关的,每一种质因数的放置总数相乘,就是我们所求的问题的答案。
那么对于任意一个分解出来的质因数,我们设分解出来了
a
a
a个,一共有
Y
Y
Y个位置,问题就变成了有
a
a
a个完全相同的小球和
Y
Y
Y个不同的盒子(区别在位置关系),允许出现空盒子(默认里面有一个球表示正整数
1
1
1),有多少种不同的摆放方式。这个问题的答案是
(
a
+
Y
−
1
Y
−
1
)
\binom{a+Y-1}{Y-1}
(Y−1a+Y−1)。
一篇介绍得很好的8种球盒问题的博客:Ljnoit - 【C++】球盒问题总结(八种情况)
将所有的质因数的答案乘到一起,就是我们这 Y Y Y个位置全部放正数的答案,设这个答案为 S S S。那么现在要考虑的就只有符号的问题了。我们要保证有偶数个负号,一共有 Y Y Y个位置,所以可以分配负号的方案是 ( Y 0 ) + ( Y 2 ) + ( Y 4 ) + ⋯ = 2 Y − 1 \binom{Y}{0}+\binom{Y}{2}+\binom{Y}{4}+\cdots=2^{Y-1} (0Y)+(2Y)+(4Y)+⋯=2Y−1。
那么最终答案就是 S × 2 Y − 1 S\times 2^{Y-1} S×2Y−1。
需要提前预处理所有数的质因数。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL mod = 1e9 + 7;
LL jc[2000050], inv[2000050];
template<class T> T power(T a, LL b) {
T res = 1;
for (; b; b >>= 1) {
if (b % 2) res = (res * a) % mod;
a = (a * a) % mod;
}
return res;
}
void init(LL iN) {
jc[0] = 1;
for (LL i = 1; i <= iN + 1; ++i) {
jc[i] = (jc[i - 1] * i) % mod;
}
inv[iN + 1] = power(jc[iN + 1], mod - 2);
for (LL i = iN; i >= 0; --i) {
inv[i] = inv[i + 1] * (i + 1) % mod;
}
}
LL C(LL N, LL M) {
return jc[N] * inv[M] % mod * inv[N - M] % mod;
}
LL bib(LL n, LL m) {
return C(n + m - 1, m - 1);
}
LL x, y, ans;
vector<pair<LL, LL> > fac[1000005];
void main2() {
cin >> x >> y;
ans = power(2ll, y - 1);
for (auto [a, b]: fac[x]) {
ans = (ans * bib(b, y)) % mod;
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _;
init(2000000);
for (int i = 2; i <= 1000000; i++) {
if (fac[i].size()) continue;
for (int j = i; j <= 1000000; j += i) {
int t = j, cnt = 0;
while (t % i == 0) t /= i, cnt++;
fac[j].push_back({i, cnt});
}
}
cin >> _;
// _ = 1;
while (_--) main2();
return 0;
}
406 - “Z”型矩阵
题目链接
参考了严格鸽 - (离线/树状数组)代码源每日一题 Div1“Z”型矩阵
首先维护每一个点向左最远延申的数量 l [ i ] [ j ] l[i][j] l[i][j]和向右最远延申的数量 r [ i ] [ j ] r[i][j] r[i][j]。
我们想找"Z"型的矩阵,可以从对角线上入手。我们遍历每一条从右上到左下方向的斜线,考虑以这条线为"Z"的斜线的、边长大于 1 1 1的"Z"型矩阵数量。我们在斜线上选取一段连续的 z z z,从第一个点开始,如果这个点向左可以延伸,那么就按照向左的长度,寻找能够匹配向左延申的这些 z z z的范围,在这个点和后面的 l [ i ] [ j ] − 1 l[i][j]-1 l[i][j]−1个点上。也就是说,设这个点是连续 x x x个点中的第 i i i个点,那么我们在遍历完第 i + l [ i ] [ j ] − 1 i+l[i][j]-1 i+l[i][j]−1个点后,这个点就不会再产生贡献了。
总结:访问到第 i i i个点时,在这个点设一个标记 1 1 1,并在第 i + l [ i ] [ j ] − 1 i+l[i][j]-1 i+l[i][j]−1个点被访问完后,将这个标记删除。
如果这个点可以向右延申,设这个点是连续的 z z z的第 i i i个点,那么可以和这段匹配的是 [ i − r [ i ] [ j ] + 1 , i − 1 ] [i-r[i][j]+1,i-1] [i−r[i][j]+1,i−1]的点。这上面的点每有一个标记,就可以匹配一次。于是,统计这个区间内的 1 1 1的个数即可。
这个点遍历完后,看看有没有需要在这个点遍历完后,需要在这个时候删除的前面的点的标记。
实现方法:用一个vector: d e l [ i ] del[i] del[i]表示需要在第 i i i个点遍历完时,在此时需要移除的标记所在的点的编号的集合。用树状数组维护标记和求区间和。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3005;
int n, m;
int a[3005][3005], l[3005][3005], r[3005][3005];
int c[N];
int lowbit(int x) {
return x & (-x);
}
void add(int x, int k, int lim) {
while (x <= lim) {
c[x] += k; x += lowbit(x);
}
}
LL presum(int x) {
LL ans = 0;
while (x >= 1) {
ans += c[x]; x -= lowbit(x);
}
return ans;
}
vector<pair<int, int> > f;
vector<int> del[3005];
LL count(int lim) {
LL ret = 0;
int nn = f.size();
for (int i = 0; i <= nn; ++i) {
c[i] = 0;
del[i].clear();
}
int cnt = 0;
for (auto [x, y]: f) {
++cnt;
if (r[x][y] > 1) {
ret += presum(cnt - 1) - presum(cnt - r[x][y]);
}
if (l[x][y] > 1) {
add(cnt, 1, nn);
del[min(lim, cnt + l[x][y] - 1)].push_back(cnt);
}
for (int y: del[cnt]) {
add(y, -1, nn);
}
}
f.clear();
return ret;
}
void main2() {
cin >> n >> m;
LL ans = 0;
for (int i = 1; i <= n; ++i) {
string x; cin >> x;
for (int j = 0; j < m; ++j) {
if (x[j] == 'z') a[i][j + 1] = 1, ++ans;
else a[i][j + 1] = 0;
l[i][j + 1] = r[i][j + 1] = 0;
}
}
for (int i = 1; i <= n; ++i) {
l[i][0] = r[i][m + 1] = 0;
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (a[i][j]) l[i][j] = l[i][j - 1] + 1;
else l[i][j] = 0;
}
for (int j = m; j >= 1; --j) {
if (a[i][j]) r[i][j] = r[i][j + 1] + 1;
else r[i][j] = 0;
}
}
for (int j = 1; j <= m; ++j) {
for (int i = 1; i <= min(n, j); ++i) {
int x = i, y = j + 1 - i;
if (!a[x][y]) {
ans += count(min(n, j));
continue;
}
f.push_back({x, y});
}
if (f.size()) ans += count(min(n, j));
}
for (int j = 2; j <= n; ++j) {
for (int i = 1; i <= min(n, n - j + 1); ++i) {
int x = i + j - 1, y = m - i + 1;
if (!a[x][y]) {
ans += count(min(n, n - j + 1));
continue;
}
f.push_back({x, y});
}
if (f.size()) ans += count(min(n, n - j + 1));
}
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _;
// cin >> _;
_ = 1;
while (_--) main2();
return 0;
}
407 - 好序列
可以发现,只要包含唯一存在的数的区间都是合法区间。换句话说,如果发现了一个数在这一段区间内只出现一次,那么我们就无需考虑这个区间,而只需要考虑这个区间被这个数分开之后的两个区间是怎么样的。具体说,就是,假设在 [ L , R ] [L,R] [L,R]区间内有一个位置 x x x,在 [ L , R ] [L,R] [L,R]区间内,这个位置的数只出现过一次,那么 [ L , R ] [L,R] [L,R]肯定是合法区间,只需要考虑 [ L , x − 1 ] [L,x-1] [L,x−1]和 [ x + 1 , R ] [x+1,R] [x+1,R]是否合法即可。
所以我们需要维护两个数组: l s t [ i ] lst[i] lst[i]表示与第 i i i个数相同的、上一个出现的数的位置; n x t [ i ] nxt[i] nxt[i]表示与第 i i i个数相同的、下一个出现的数的位置。如果同时满足 l s t [ i ] < L lst[i] < L lst[i]<L和 n x t [ i ] > R nxt[i]>R nxt[i]>R,说明 i i i这个位置的数在 [ L , R ] [L,R] [L,R]内是唯一出现的。但是我们这样去分裂区间,会超时。因为整个的时间复杂度是 O ( n 2 ) O(n^2) O(n2)的。
这里要用到的思想就是启发式分裂的思想。我们同时从两边向中间搜索,那么离两端最近的一个唯一出现的位置 x x x,设其离边界的最近距离是 d d d。显然这个 d d d会先被发现,且一定有 d ≤ 1 2 ( R − L + 1 ) d\leq \frac{1}{2} (R-L+1) d≤21(R−L+1)。这样不断分裂的过程,和归并排序是非常相似的,所以我们可以感性地认为,这样的时间复杂度可以从 O ( n 2 ) O(n^2) O(n2)降低至 O ( n log n ) O(n\log n) O(nlogn)。
如果分裂到最后都没有问题,就输出"not boring";否则输出"boring"。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int n, mi;
int a[200005], lst[200005], nxt[200005], tmp[200005];
bool split(int l, int r) {
if (l >= r) return true;
int x = l, y = r;
while (x <= y) {
if (lst[x] < l and nxt[x] > r) {
return (split(l, x - 1) and split(x + 1, r));
}
if (lst[y] < l and nxt[y] > r) {
return (split(l, y - 1) and split(y + 1, r));
}
++x; --y;
}
return false;
}
void main2() {
cin >> n;
map<int, int> mp;
mi = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
if (!mp[a[i]]) mp[a[i]] = ++mi;
a[i] = mp[a[i]];
}
for (int i = 1; i <= mi; ++i) {
tmp[i] = 0;
}
for (int i = 1; i <= n; ++i) {
lst[i] = tmp[a[i]];
tmp[a[i]] = i;
}
for (int i = 1; i <= mi; ++i) {
tmp[i] = n + 1;
}
for (int i = n; i >= 1; --i) {
nxt[i] = tmp[a[i]];
tmp[a[i]] = i;
}
if (split(1, n)) cout << "non-boring\n";
else cout << "boring\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _;
cin >> _;
// _ = 1;
while (_--) main2();
return 0;
}