Codeforces Round 970 (Div. 3)题解
题目地址
https://codeforces.com/contest/2008
锐评
本次D3的前四题还是比较简单的,没啥难度区分,基本上差不多,属于手速题。E的码量比F大一些,实现略显复杂一些。G的数学思维较明显,如果很久没有训练这个知识点,可能会一下子反应不过来,比如说我,需要花一点点时间观察,然后确认最优策略,整体不算太难,约等于D2的C题左右。H题较难,感觉原超过了D3 Rating范围,需要一定的优化技巧,但也不是不可做,思维量也没那么大。综合评估,个人觉得整场难度中等偏上了。
题解
Problem A. Sakurako’s Exam
题目大意
给定 a ( 0 ≤ a < 10 ) a(0 \leq a \lt 10) a(0≤a<10)个整数 1 1 1和 b ( 0 ≤ b < 10 ) b(0 \leq b \lt 10) b(0≤b<10)个整数 2 2 2,每个整数可以选择不变或者变为它的相反数,问是否存在一种情况满足所有改变操作后,所有整数求和等于 0 0 0?
题解思路:二进制枚举/数学分类讨论
首先, a , b a,b a,b最大不过 10 10 10,最直接粗暴的方式是暴力枚举出每个数的可能性,然后求和判断即可,时间复杂度为 O ( 2 a + b max ( a , b ) ) O(2^{a + b}\max(a,b)) O(2a+bmax(a,b))。
采用上面的方式,好像有点冒进。当 a , b a,b a,b都为 9 9 9时, 2 9 + 9 × max ( 9 , 9 ) = 262144 × 9 = 2359296 2^{9 + 9} \times \max(9, 9) = 262144 \times 9 = 2359296 29+9×max(9,9)=262144×9=2359296,外面还套了个 t ( 1 ≤ t ≤ 100 ) t(1 \leq t \leq 100) t(1≤t≤100),只给了 1 s 1s 1s的时限,极有可能会超时,好在过了,可能判定结束得早吧,勉强飘过。
重新来分析。 2 2 2怎么消掉?只能用 1 1 1个自己或者 2 2 2个 1 1 1。 1 1 1呢?也只能用 1 1 1个自己或者用自己 2 2 2个去抵掉 1 1 1个 2 2 2。观察发现 1 1 1只能一对一对消失,所以当 a a a为奇数时必然无解。因此 a a a一定为偶数,所以它自己一定能相互抵消。我们考虑 2 2 2怎么抵消,显然 2 2 2自己相互抵消是最好的,它最多只会多出来 1 1 1个,然后问 1 1 1借两个就行,因此时间复杂度为 O ( 1 ) O(1) O(1)。
参考代码(C++)
二进制枚举代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
void solve() {
cin >> n >> m;
for (int i = 0, len = 1 << n; i < len; ++i) {
int sum = 0;
for (int j = 0; j < n; ++j) {
if ((i >> j) & 1)
++sum;
else
--sum;
}
for (int j = 0, siz = 1 << m; j < siz; ++j) {
int sumt = 0;
for (int k = 0; k < m; ++k) {
if ((j >> k) & 1)
sumt += 2;
else
sumt -= 2;
}
if (sumt + sum == 0) {
cout << "YES\n";
return;
}
}
}
cout << "NO\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
数学分类讨论代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
void solve() {
cin >> n >> m;
if (n & 1)
cout << "NO\n";
else {
m &= 1;
cout << (n >= m ? "YES\n" : "NO\n");
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
Problem B. Square or Not
题目大意
给定一个长度为 n ( 2 ≤ n ≤ 2 ⋅ 1 0 5 ) n(2 \leq n \leq 2 \cdot 10^5) n(2≤n≤2⋅105)的字符串 s s s,仅包含字符 0 0 0和 1 1 1。
问能否将这个字符串变化为 r r r行 c c c列的二维矩阵,其中要求 r = c 且 r × c = n r = c且r \times c = n r=c且r×c=n,还要满足字符串依次从左到右从上到下填充方格后,最外层边界上的字符是 1 1 1,不在最外层边界上的字符是 0 0 0。
题解思路:模拟 & 枚举
先判断 n n n是不是一个平方数,如果不是,显然就没有答案。否则就枚举一遍,判断对应位置字符是不是符合要求即可,时间复杂度为 O ( n ) O(n) O(n)。
参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 200'005;
int n;
string str;
void solve() {
cin >> n >> str;
int m = sqrt(n + 0.5);
if (m * m != n)
cout << "No\n";
else {
for (int i = 0; i < n; ++i) {
int x = i / m, y = i % m;
if (x == 0 || x == m - 1 || y == 0 || y == m - 1) {
if (str[i] != '1') {
cout << "No\n";
return;
}
} else {
if (str[i] != '0') {
cout << "No\n";
return;
}
}
}
cout << "Yes\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
Problem C. Longest Good Array
题目大意
构造一个严格单调递增的数组,并且从左到右相邻两个数的差也要是严格单调递增的。给定 l , r ( 1 ≤ l ≤ r ≤ 1 0 9 ) l,r(1 \leq l \leq r \leq 10^9) l,r(1≤l≤r≤109),表示该数组元素的数据范围。问能构造的数组长度最大是多少?
题解思路:数学 & 枚举
因为要保持严格单调递增,不考虑数据范围上限,那么显然相邻差的下限为 1 1 1,相邻差的序列形如 1 , 2 , 3 , 4 , ⋯ 1,2,3,4,\cdots 1,2,3,4,⋯,显然这是一个公差为 1 1 1的等差数列,因为数组又要求严格单调递增,假如最多 n n n项,根据等差数列求和公式,则有最大数为 n ( n − 1 ) 2 \frac{n(n - 1)}{2} 2n(n−1)。显然,假如最大数据范围为 l i m lim lim,构造的数组长度不会超过 l i m \sqrt{lim} lim。根据题目给定的数据范围 1 0 9 10^9 109可知,第一个数为 l l l,然后直接枚举构造就可以了,时间复杂度为 O ( r − l + 1 ) O(\sqrt{r - l + 1}) O(r−l+1)(不严格)。
参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 200'005;
int a[maxn];
int l, r;
int n;
void solve() {
cin >> l >> r;
int id = 1, d = 1;
a[0] = l;
while (a[id - 1] + d <= r) {
a[id] = a[id - 1] + d;
++id;
++d;
}
// cout << "res:" << a[id - 1] << '\n';
cout << id << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
Problem D. Sakurako’s Hobby
题目大意
给定一个长度为 n ( 1 ≤ n ≤ 2 ⋅ 1 0 5 ) n(1 \leq n \leq 2 \cdot 10^5) n(1≤n≤2⋅105)的排列 p 1 , p 2 , ⋯ , p n ( 1 ≤ p i ≤ n ) p_1,p_2,\cdots,p_n(1 \leq p_i \leq n) p1,p2,⋯,pn(1≤pi≤n)。
有一个操作,假如是第 i i i个位置,我们可以从位置 i i i跳跃到位置 p i p_i pi,即从位置 i i i可以到达位置 p i p_i pi,以此类推,我们可以重复跳跃过程到达某个位置。
再给定一个长度为 n n n的字符串 s s s,仅包含字符 0 0 0和字符 1 1 1, 0 0 0表示黑色, 1 1 1表示白色。字符串第 i i i个位置的字符 s [ i ] s[i] s[i]表示上述排列第 p i p_i pi个位置为该字符,即为该位置的颜色。
求第 i ( 1 ≤ i ≤ n ) i(1 \leq i \leq n) i(1≤i≤n)个位置能到达的所有位置中位置颜色为黑色的有多少个?
题解思路:置换环 & 并查集
从一个点跳跃到另一个点,跳跃过的点就不能再跳跃,那么这个过程他肯定会终止,而且它会陷入一个循环。图论中叫置换环(有兴趣可以自行查阅)。我们可以模拟这个过程,然后一轮跳跃中的点为一组,他们可以互相到达。
那么怎么计算黑色个数呢?可以用并查集来解决这个问题,最终只需要计算出每个位置 i i i所在组的代表元的黑色个数,即是这一位置的答案,整体时间复杂度为 O ( n ) O(n) O(n)(并查集接近线性,实际不是,跟阿克曼函数有关,这里并不严格)。
参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 200'005;
struct dsu {
int p[maxn], siz[maxn];
int n;
void init(int n) {
this->n = n;
for (int i = 0; i < n; ++i)
p[i] = i, siz[i] = 1;
}
int findp(int x) {
return p[x] == x ? x : p[x] = findp(p[x]);
}
bool unionp(int x, int y) {
int fx = findp(x);
int fy = findp(y);
if (fx == fy)
return false;
if (siz[fx] >= siz[fy]) {
p[fy] = fx;
siz[fx] += siz[fy];
} else {
p[fx] = fy;
siz[fy] += siz[fx];
}
return true;
}
bool same(int x, int y) {
return findp(x) == findp(y);
}
int sizep(int x) {
return siz[findp(x)];
}
} d;
int a[maxn], cnt[maxn];
bool vis[maxn];
string str;
int n;
void solve() {
cin >> n;
for (int i = 0; i < n; ++i) {
cin >> a[i];
--a[i];
vis[i] = false;
cnt[i] = 0;
}
cin >> str;
d.init(n);
for (int i = 0; i < n; ++i)
if (!vis[i]) {
int p = i;
do {
vis[p] = true;
d.unionp(i, p);
p = a[p];
} while (!vis[p]);
}
for (int i = 0; i < n; ++i)
if (str[i] == '0')
++cnt[d.findp(a[i])];
for (int i = 0; i < n; ++i)
cout << cnt[d.findp(i)] << (" \n"[i == n - 1]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
Problem E. Alternating String
题目大意
交替串的定义:给定两个字符 a a a和 b b b( a , b a,b a,b可以相同),形如 a b a b a b ⋯ ababab\cdots ababab⋯且长度为偶数的字符串。
现在给你一个长度为 n ( 1 ≤ n ≤ 2 ⋅ 1 0 5 ) n(1 \leq n \leq 2 \cdot 10^5) n(1≤n≤2⋅105)的字符串 s s s(只包含小写英文字母)。你可以进行如下两个操作。
1.删除某个位置上的字符(注意:此操作最多只能用一次)。
2.将某个位置上的字符替换为任意的小写英文字母。
问如何用最少的上述操作次数,使得该字符串为交替串?
题解思路:前后缀分解 & 前/后缀和 & 枚举
假如当前字符串长度为偶数,因为操作1最多只能用一次,而题目要求最终长度要为偶数,所以这种情况下就不能使用操作1,只能使用操作2。因此,我们只需要统计出奇数位置和偶数位置每个字母都分别有多少个,最后枚举将奇/偶数位置换成每个字母需要的操作次数取最小值即可。
假如当前字符串长度为奇数,因为操作1最多只能用一次,而题目要求最终长度要为偶数,所以这种情况下操作1就必须要使用了。因此,我们可以枚举删除的字符位置,然后将左右两边的字符串拼接起来,使用上面原始串长度为偶数一样的处理方式,将左右两边的计数汇总起来,然后枚举将奇/偶数位置换成每个字母需要的操作次数取最小值即可(注意,因为缺失了一个位置,所以这个位置后面位置所处的位置奇偶性发生了变化,计数要取对立位置的)。
为了处理简单些,我同时求了前缀和和后缀和,整体时间复杂度为 O ( n ) O(n) O(n)(忽略了小写英文字母个数26,实际是否能通过还是要考虑这个常数的)。
参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 200'005;
const int mod = 1'000'000'007;
int cp[maxn][2][26], cs[maxn][2][26];
string str;
int n;
int qpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1)
ans = 1LL * ans * a % mod;
a = 1LL * a * a % mod;
b >>= 1;
}
return ans;
}
void solve() {
cin >> n >> str;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < 26; ++j) {
cp[i + 1][0][j] = cp[i][0][j];
cp[i + 1][1][j] = cp[i][1][j];
}
++cp[i + 1][i & 1][str[i] - 'a'];
}
for (int j = 0; j < 26; ++j)
cs[n][0][j] = cs[n][1][j] = 0;
for (int i = n - 1; i >= 0; --i) {
for (int j = 0; j < 26; ++j) {
cs[i][0][j] = cs[i + 1][0][j];
cs[i][1][j] = cs[i + 1][1][j];
}
++cs[i][(n - 1 - i) & 1][str[i] - 'a'];
}
int ans = n;
if (n & 1) {
for (int i = 0; i < n; ++i) {
vector<vector<int>> cnt(2, vector<int>(26, 0));
for (int j = 0; j < 26; ++j) {
cnt[0][j] += cp[i][0][j];
cnt[1][j] += cp[i][1][j];
}
for (int j = 0; j < 26; ++j) {
cnt[0][j] += cs[i + 1][1][j];
cnt[1][j] += cs[i + 1][0][j];
}
int maxe = 0, maxo = 0;
for (int j = 0; j < 26; ++j) {
maxe = max(maxe, cnt[0][j]);
maxo = max(maxo, cnt[1][j]);
}
ans = min(ans, n - maxe - maxo);
}
} else {
int maxe = 0, maxo = 0;
for (int i = 0; i < 26; ++i) {
maxe = max(maxe, cp[n][0][i]);
maxo = max(maxo, cp[n][1][i]);
}
ans = min(ans, n - maxe - maxo);
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
Problem F. Sakurako’s Box
题目大意
给定一个长度为 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \leq n \leq 10^5) n(2≤n≤105)的数组 a 1 , a 2 , ⋯ , a n ( 0 ≤ a i ≤ 1 0 9 ) a_1,a_2,\cdots,a_n(0 \leq a_i \leq 10^9) a1,a2,⋯,an(0≤ai≤109)。
问随机选择两个不同位置的数,求这两个数乘积的数学期望是多少?
题解思路:数学期望 & 费马小定理 & 前/后缀和
根据数学期望的定义,有下式。
E
(
x
)
=
p
12
⋅
(
a
1
⋅
a
2
)
+
p
13
⋅
(
a
1
⋅
a
3
)
+
⋯
+
p
(
n
−
1
)
n
⋅
(
a
n
−
1
⋅
a
n
)
=
∑
i
=
1
n
−
1
∑
j
=
i
+
1
n
p
i
j
⋅
(
a
i
⋅
a
j
)
\displaystyle E(x) = p_{12} \cdot (a_1 \cdot a_2) + p_{13} \cdot (a_1 \cdot a_3) + \cdots + p_{(n - 1)n} \cdot (a_{n - 1} \cdot a_n) = \sum_{i = 1}^{n - 1}\sum_{j = i + 1}^{n}p_{ij} \cdot (a_i \cdot a_j)
E(x)=p12⋅(a1⋅a2)+p13⋅(a1⋅a3)+⋯+p(n−1)n⋅(an−1⋅an)=i=1∑n−1j=i+1∑npij⋅(ai⋅aj)
p
i
j
p_{ij}
pij都是相等的(因为概率相同),即为
1
C
n
2
\frac{1}{C_n^2}
Cn21。合并同类项后,得到如下公式。
E
(
x
)
=
1
C
n
2
⋅
(
a
1
⋅
∑
i
=
2
n
a
i
+
a
2
⋅
∑
j
=
3
n
a
j
+
⋯
+
a
n
−
1
⋅
∑
k
=
n
n
a
k
)
\displaystyle E(x) = \frac{1}{C_n^2} \cdot (a_1 \cdot \sum_{i = 2}^{n}a_i + a_2 \cdot \sum_{j = 3}^{n}a_j + \cdots + a_{n - 1} \cdot \sum_{k = n}^{n}a_k)
E(x)=Cn21⋅(a1⋅i=2∑nai+a2⋅j=3∑naj+⋯+an−1⋅k=n∑nak)
观察式子,很显然就是前/后缀和,问题得解。至于怎么消掉分数,可以看我上一篇文章《快速幂》,时间复杂度为
O
(
n
)
O(n)
O(n)(快速幂的指数是固定的,所以是常数)。
参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 200'005;
const int mod = 1'000'000'007;
int a[maxn], suf[maxn];
int n;
int qpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1)
ans = 1LL * ans * a % mod;
a = 1LL * a * a % mod;
b >>= 1;
}
return ans;
}
void solve() {
cin >> n;
for (int i = 0; i < n; ++i)
cin >> a[i];
suf[n - 1] = a[n - 1];
for (int i = n - 2; i >= 0; --i)
suf[i] = (suf[i + 1] + a[i]) % mod;
int p = 0;
for (int i = 1; i < n; ++i) {
p += 1LL * a[i - 1] * suf[i] % mod;
p %= mod;
}
p = (p << 1) % mod;
int q = 1LL * n * (n - 1) % mod;
cout << 1LL * p * qpow(q, mod - 2) % mod << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
Problem G. Sakurako’s Task
题目大意
给定两个整数 n , k ( 1 ≤ n ≤ 2 ⋅ 1 0 5 , 1 ≤ k ≤ 1 0 9 ) n,k(1 \leq n \leq 2 \cdot 10^5,1 \leq k \leq 10^9) n,k(1≤n≤2⋅105,1≤k≤109),再给定一个长度为 n n n的数组 a 1 , a 2 , ⋯ , a n ( 1 ≤ a i ≤ 1 0 9 ) a_1,a_2,\cdots,a_n(1 \leq a_i \leq 10^9) a1,a2,⋯,an(1≤ai≤109)。
你可以进行如下操作任意次数(只要满足条件可以一直进行下去):
选择两个不同的下标 i i i和 j j j且 a i ≥ a j a_i \geq a_j ai≥aj,然后对 a i a_i ai进行赋值操作, a i = a i − a j a_i = a_i - a_j ai=ai−aj或者 a i = a i + a j a_i = a_i + a_j ai=ai+aj。
你需要求出进行若干次操作后,这个数组缺失的第 k k k小非负整数,并且尽可能让这个数最大,这个数最大是多少?
数组缺失的第 k k k小非负整数是什么?举个例子,假如数组是 [ 1 , 2 , 3 ] [1,2,3] [1,2,3],那么缺失的第1小非负整数是0,第2小非负整数是4;假如数组是 [ 0 , 1 , 2 , 3 , 5 , 7 ] [0,1,2,3,5,7] [0,1,2,3,5,7],那么缺失的第1小非负整数是4,第2小非负整数是6。
题解思路:裴蜀定理 & 枚举
首先要使得这个数尽可能大,那么比较小的数应该尽可能地多。因此加法操作看起来好像没啥用哦。我们先只看减法操作,嘿,有点眼熟,好像更相减损术啊,那么是不是应该是求最大公约数。
通过上面的分析,这个减法操作最终得到的数好像有迹可循,进而联想到裴蜀定理。我们先来看看裴蜀定理的定义。
对任意两个整数 a a a、 b b b,设 d d d是它们的最大公约数。那么关于未知数 x x x和 y y y的线性丢番图方程(称为裴蜀等式): a x + b y = m ax + by = m ax+by=m有整数解 ( x , y ) (x, y) (x,y)当且仅当 m m m是 d d d的整数倍。裴蜀等式有解时必然有无穷多个解。
推广到 n n n个整数如下。
设 a 1 , ⋯ , a n a_1,\cdots,a_n a1,⋯,an为 n n n个整数, d d d是它们的最大公约数,那么存在整数 x 1 , ⋯ , x n x_1,\cdots,x_n x1,⋯,xn使得 x 1 ⋅ a 1 + ⋯ + x n ⋅ a n = d x_1 \cdot a_1 + \cdots + x_n \cdot a_n = d x1⋅a1+⋯+xn⋅an=d。
上面定理说明了什么问题呢?它说明,对于两个数,无论你怎么操作,最终操作后的这个数它一定是这两个数的最大公约数的倍数, n n n个数也同样如此,这样就好办了。
通过上面的分析,我们应该让最大公约数尽可能小,这样它的倍数才能占据尽可能多的位置。而多个数的最大公约数只可能减小,不可能变大,所以我们只需要对所有数取最大公约数,这样就可以用有限次操作把所有数都变为最大公约数。其实这时候想象下,可以使用加法操作构造出首项为0,公差为最大公约数的等差数列(假如数组长度为1,首项不能为0,因为没办法操作)。
最后,我们只需要枚举一下这个等差数列,看看空隙处能填多少个数,如果不够,往最后补齐即可。整体时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)(主要是求最大公约数的部分,枚举部分时间复杂度为 O ( n ) O(n) O(n))。
参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 200'005;
const int mod = 1'000'000'007;
int a[maxn], b[maxn];
int n, m;
int qpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1)
ans = 1LL * ans * a % mod;
a = 1LL * a * a % mod;
b >>= 1;
}
return ans;
}
void solve() {
cin >> n >> m;
int cd = 0;
for (int i = 0; i < n; ++i) {
cin >> a[i];
cd = gcd(cd, a[i]);
}
b[0] = n == 1 ? cd : 0;
for (int i = 1; i < n; ++i)
b[i] = i * cd;
int ans = 0, last = -1;
for (int i = 0; i < n && m; ++i) {
int cnt = b[i] - last - 1;
int minv = min(cnt, m);
m -= minv;
ans = last + minv;
last = b[i];
}
if (m)
ans = last + m;
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
Problem H. Sakurako’s Test
题目大意
给定两个整数 n , q ( 1 ≤ n , q ≤ 1 0 5 ) n,q(1 \leq n,q \leq 10^5) n,q(1≤n,q≤105),再给定一个长度为 n n n的数组 a 1 , a 2 , ⋯ , a n ( 1 ≤ a i ≤ n ) a_1,a_2,\cdots,a_n(1 \leq a_i \leq n) a1,a2,⋯,an(1≤ai≤n)。
对于给定的一个整数 x x x,你可以进行如下操作任意次数(只要满足条件可以一直进行下去):选择一个下标 i i i且 a i ≥ x a_i \geq x ai≥x,然后对 a i a_i ai进行赋值操作, a i = a i − x a_i = a_i - x ai=ai−x。
问,给定 q q q个这样的整数 x x x,每行一个整数,你需要求进行若干次操作后,这个数组从小到大排序后的中位数最小可以是多少?
本题中位数定义为:对于偶数长度 n n n,取第 n + 2 2 \frac{n + 2}{2} 2n+2个位置的数,对于奇数长度 n n n,取第 n + 1 2 \frac{n + 1}{2} 2n+1个位置的数。
题解思路:预处理 & 前缀和 & 二分 & 调和级数
要使得中位数最小,那么经过处理后的数组的值应该是越小越好。根据操作的定义,最终对每个数执行若干次操作后的实际效果其实就是 a i = a i m o d x a_i = a_i \bmod x ai=aimodx。
本题的时限仅仅为1s,而 q , n q,n q,n最大都可以为 1 0 5 10^5 105。显然,对于时间复杂度超过 O ( q n ) O(qn) O(qn)的代码都不足以通过此题(例如每个数都进行取模操作,然后排序,时间复杂度为 O ( q n l o g n ) O(qnlogn) O(qnlogn))。
怎么优化呢? q q q我们肯定改变不了,毕竟读入就要 q q q。因此,我们只能寄希望于降低循环体内的查询时间。因为当前查询的是 x x x,那么答案是多少呢?显然,答案在区间 [ 0 , x − 1 ] [0,x - 1] [0,x−1]内,因为取模后所有数肯定是小于 x x x的。我们知道排序后,数字都是从小到大的,那么中位数是否具有单调性呢?答案是肯定的。为什么呢?因为某个数 y y y是中位数,意味着小于等于 y − 1 y - 1 y−1的元素个数要小于上面中位数定义的位置,且小于等于 y y y的元素个数要大于等于上面中位数定义的位置(第一个条件如果是大于等于,说明中位数位置被占了, y y y肯定不在那个位置上,显然不可能是中位数;第二个条件如果是小于,那就说明, y y y都不够填充到中位数的位置,中位数显然最小是 y + 1 y + 1 y+1),所以可以二分答案。
根据上面的分析,时间复杂度好像是 O ( q l o g x ) O(qlogx) O(qlogx),有戏。咦?好像又不太对,我们必须用 O ( 1 ) O(1) O(1)时间复杂度检查出上面提到的计数问题是否合法。 n n n个数, O ( 1 ) O(1) O(1)?逗我,臣妾做不到啊!先放弃,考虑下这个问题的普通做法,最朴素的当然是一个一个枚举,显然不可行!我们注意到,对于小于等于某个数 y y y的元素个数,当元素数据范围限定在某个区间内,我们可以用空间换取时间。例如,本题 1 ≤ a i ≤ n 1 \leq a_i \leq n 1≤ai≤n,我们用 c n t i cnt_i cnti表示数组中有多少个数等于 i i i,那么我们把小于等于 i i i的计数加起来就是整个数组中小于等于 i i i的元素个数,该问题可以用前缀和线性解决。
好像还是没啥用啊?问题依旧没解决。别急,我们继续看。对于题目中每个询问的
x
x
x,其实将
n
n
n分为了很多类似上面前缀和计数的块,其中每个块求小于等于某个数的个数可以
O
(
1
)
O(1)
O(1)求出来,请看如下规律。
0
,
1
,
⋯
,
x
−
1
,
x
,
x
+
1
,
⋯
,
2
⋅
x
−
1
,
2
⋅
x
,
2
⋅
x
+
1
,
⋯
,
3
⋅
x
−
1
,
3
⋅
x
,
3
⋅
x
+
1
,
⋯
0,1,\cdots,x - 1,x,x + 1,\cdots,2 \cdot x - 1,2 \cdot x,2 \cdot x + 1,\cdots,3 \cdot x - 1,3 \cdot x,3 \cdot x + 1,\cdots
0,1,⋯,x−1,x,x+1,⋯,2⋅x−1,2⋅x,2⋅x+1,⋯,3⋅x−1,3⋅x,3⋅x+1,⋯
对
x
x
x取模后,得到如下规律。
0
,
1
,
⋯
,
x
−
1
,
0
,
1
,
⋯
,
x
−
1
,
0
,
1
,
⋯
,
x
−
1
,
0
,
1
,
⋯
0,1,\cdots,x - 1,0,1,\cdots,x - 1,0,1,\cdots,x - 1,0,1,\cdots
0,1,⋯,x−1,0,1,⋯,x−1,0,1,⋯,x−1,0,1,⋯
根据上面的规律,对于每个要查询的
x
x
x,我们将
n
n
n分成了
⌈
n
x
⌉
\lceil \frac{n}{x} \rceil
⌈xn⌉块(向上取整)。对于每一块可以用前缀和在
O
(
1
)
O(1)
O(1)时间内计算出小于等于某个数的元素个数。
合并截至目前的分析结果,得到时间复杂度为 O ( q ⋅ l o g x ⋅ n x ) O(q \cdot logx \cdot \frac{n}{x}) O(q⋅logx⋅xn)。这个复杂度十分依赖测试数据给定的 x x x,出题人肯定没那么友好,最简单的 1 0 5 10^5 105个 x = 1 x = 1 x=1就卡死了,况且还有Hack阶段。测试一下,果然超时了,后面也给出代码供参考。
上面测试数据全是
1
1
1的猜想给出了一个优化方向,考虑到有大量重复的计算,某一个
x
x
x算过了,再给定同样的
x
x
x又重新算了一遍。那么我们何不一次性计算出所有答案,即预处理所有可能的
x
x
x,对于每个查询直接
O
(
1
)
O(1)
O(1)输出答案。因为
1
≤
x
≤
n
1 \leq x \leq n
1≤x≤n,每个
x
x
x计算的时间复杂度为
O
(
l
o
g
x
⋅
n
x
)
O(logx \cdot \frac{n}{x})
O(logx⋅xn),故总的计算量为
∑
i
=
1
n
(
l
o
g
i
⋅
n
i
)
\displaystyle\sum_{i = 1}^{n}(logi \cdot \frac{n}{i})
i=1∑n(logi⋅in)。公式有点眼熟,哦!原来是调和级数!依据如下。
∑
i
=
1
∞
1
i
=
1
+
1
2
+
1
3
+
⋯
\displaystyle\sum_{i = 1}^{\infty}\frac{1}{i} = 1 + \frac{1}{2} + \frac{1}{3} + \cdots
i=1∑∞i1=1+21+31+⋯
调和级数的第
n
n
n项部分和为:
∑
i
=
1
n
1
i
=
1
+
1
2
+
1
3
+
⋯
+
1
n
\displaystyle\sum_{i = 1}^{n}\frac{1}{i} = 1 + \frac{1}{2} + \frac{1}{3} + \cdots + \frac{1}{n}
i=1∑ni1=1+21+31+⋯+n1
也叫作第
n
n
n个调和数。第
n
n
n个调和数与
n
n
n的自然对数的差值(即
∑
i
=
1
n
1
i
−
ln
n
\displaystyle\sum _{i=1}^{n}\frac{1}{i}-\ln n
i=1∑ni1−lnn)收敛于常数(欧拉-马歇罗尼常数)。
综上所述,整体时间复杂度为 O ( q + n ⋅ ln n ⋅ l o g n ) O(q + n \cdot \ln n \cdot logn) O(q+n⋅lnn⋅logn), 1 s 1s 1s可过。
参考代码(C++)
超时代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100'005;
const int mod = 1'000'000'007;
int cnt[maxn];
int n, q;
int qpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1)
ans = 1LL * ans * a % mod;
a = 1LL * a * a % mod;
b >>= 1;
}
return ans;
}
int calc(int lim, int step) {
if (lim < 0)
return 0;
int ans = 0, lastc = 0;
for (int i = 0; i <= n; i += step) {
int j = min(i + lim, n);
ans += cnt[j] - lastc;
lastc = cnt[min(i + step - 1, n)];
}
return ans;
}
void solve() {
cin >> n >> q;
for (int i = 1; i <= n; ++i)
cnt[i] = 0;
int x, id = n >> 1;
for (int i = 0; i < n; ++i) {
cin >> x;
++cnt[x];
}
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = 0; i < q; ++i) {
cin >> x;
int l = 0, r = x - 1, ans = -1;
while (l <= r) {
int mid = (l + r) >> 1;
int cl = calc(mid - 1, x), ce = calc(mid, x);
if (cl <= id && ce > id) {
ans = mid;
r = mid - 1;
} else if (cl > id)
r = mid - 1;
else
l = mid + 1;
}
cout << ans << (" \n"[i == q - 1]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}
通过代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100'005;
const int mod = 1'000'000'007;
int cnt[maxn], ans[maxn];
int n, q;
int qpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1)
ans = 1LL * ans * a % mod;
a = 1LL * a * a % mod;
b >>= 1;
}
return ans;
}
int calc(int lim, int step) {
if (lim < 0)
return 0;
int ans = 0, lastc = 0;
for (int i = 0; i <= n; i += step) {
int j = min(i + lim, n);
ans += cnt[j] - lastc;
lastc = cnt[min(i + step - 1, n)];
}
return ans;
}
void solve() {
cin >> n >> q;
for (int i = 1; i <= n; ++i)
cnt[i] = 0;
int x, id = n >> 1;
for (int i = 0; i < n; ++i) {
cin >> x;
++cnt[x];
}
for (int i = 1; i <= n; ++i)
cnt[i] += cnt[i - 1];
for (int i = 1; i <= n; ++i) {
int l = 0, r = i - 1, res = -1;
while (l <= r) {
int mid = (l + r) >> 1;
int cl = calc(mid - 1, i), ce = calc(mid, i);
if (cl <= id && ce > id) {
res = mid;
r = mid - 1;
} else if (cl > id)
r = mid - 1;
else
l = mid + 1;
}
ans[i] = res;
}
for (int i = 0; i < q; ++i) {
cin >> x;
cout << ans[x] << (" \n"[i == q - 1]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t = 1;
cin >> t;
while (t--)
solve();
return 0;
}