题目地址
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;elsesumt -= 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;elsel = 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;elsel = 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;
}