【萌新场!】CF2008解析
今天带来的是 CF2008:Codeforces Round 970(Div 3)的前 8 题解析!非常入门,适合基础较弱的同学食用!
Div3 的题目,往往思维含量较低,更多的是套路的教学,适合扩充知识面。
时间匆忙,难免有错漏之处,仅供娱乐!
时间限制均为 2s,内存限制均为 250 MB .
A. Sakurako's Exam
【题目描述】
给定 \(a, b\) 。
给出 \(a\) 个数字一,和 \(b\) 个数字二。
你必须在每个数字前面加上 +
或者 -
,问是否存在策略让这些数字的和为 \(0\) 。(Yes or No)
【数据规模与约定】
\(0 \le a, b < 10\)
【图片留白】
【个人解析】
\(a, b\) 的范围都很小,\(O(ab)\) 暴力枚举给数字一的加号数量,给数字二的加号数量即可。
#include <bits/stdc++.h>void solve()
{int a, b;std::cin >> a >> b;for (int i = 0; i <= a; i++){for (int j = 0; j <= b; j++){if (i + 2 * j == a - i + 2 * (b - j)){std::cout << "YES\n";return;}}}std::cout << "NO\n";
}int main ()
{int _;std::cin >> _;while (_--){solve();}
}
本题也存在 \(O(1)\) 做法,直接给出代码,不再赘述。
#include <bits/stdc++.h>
using namespace std;
void solve ()
{int a, b;cin >> a >> b;if (a % 2) cout << "NO\n";else if (b % 2 && a < 2) cout << "NO\n";else cout << "YES\n";
}
int main ()
{int _;cin >> _;while (_--) solve();
}
B. Square or Not
【题目描述】
给定一个字符串 \(s\) 。
漂亮的二进制矩阵是指边缘为 1、内部为 0 的矩阵。
四个漂亮的二进制矩阵示例。
今天,樱子在玩一个大小为 \(r \times c\) 的漂亮二进制矩阵,她通过写下矩阵的所有行,从第一行开始,到 \(r\) 为止,创建了一个二进制字符串 \(s\) 。更正式地说,矩阵中 \(i\) -th行和 \(j\) -th列的元素对应于字符串的 \(((i-1)*c+j)\) -th元素。
您需要检查从中得到字符串 \(s\) 的美丽矩阵是否可以平方。换句话说,你需要检查字符串 \(s\) 是否可以从一个平方的美丽二进制矩阵(即 \(r=c\) )中得到。
【图片留白】
【个人解析】
- 首先 \(n\) 必须是完全平方数,要熟练 check 。
- 其次根据 \(\sqrt{n}\) 的值,从 \(s\) 还原出这个 01 矩阵,判断它是否是周围一圈 1,中间全是 0 。
#include <bits/stdc++.h>void solve()
{int n;std::cin >> n;std::string s;std::cin >> s;int sz = sqrt(n);if (sz * sz != n){std::cout << "No\n";return;}for (int i = 1; i <= sz; i++){int L = (i - 1) * sz + 1;int R = i * sz;if (i == 1 || i == sz){int f = 1;for (int j = L; j <= R; j++){if (s[j - 1] != '1') f = 0;} if (!f){std::cout << "No\n";return;}}else{int f = 1;for (int j = L + 1; j < R; j++){if (s[j - 1] != '0') f = 0;} if (s[L - 1] == '0' || s[R - 1] == '0')f = 0;if (!f){std::cout << "No\n";return;} }}std::cout << "Yes\n";
}
int main ()
{int _;std::cin >> _;while (_--){solve();}
}
C. Longest Good Array
【题目描述】
给定 \(l,r\) 。
今天,樱子在学习数组。长度为 \(n\) 的数组 \(a\) 如果并且只有在以下情况下才被认为是好数组:
- 数组 \(a\) 是递增的,即所有 \(2 \le i \le n\) 都满足 \(a_{i - 1} < a_i\) ;
- 相邻元素之间的差值是递增的,即所有 \(2 \le i < n\) 中的 \(a_i - a_{i-1} < a_{i+1} - a_i\) 。
樱子想出了边界 \(l\) 和 \(r\) ,并想构造一个最大长度的好数组 \(a\) ,其中所有 \(i\) 满足 \(l \le a_i \le r\)
请求出给定的 \(l\) 和 \(r\) 的好数组的最大长度。
【图片留白】
【数据规模与约定】
\(1 \le l \le r \le 10^9\)
【个人解析】
首先考虑什么样的数组 \(a\) 是尽可能长的,它一定是这样:
\(a_1=l\)
\(a_2=l+1\)
\(a_3=l+1+2\)
\(a_4=l+1+2+3\)
以此类推,那么问题转化为如何快速找到最小的 \(n\) ,满足: \(a_n=l+1+2+3+\cdots +n-1 \le r\)
移项:\(\sum\limits_{i=1}^{n-1} i\le r - l\)
代入等差数列求和公式:
\(\dfrac{n(n-1)}{2}\le r - l\)
左右两边同时乘 \(2\):
\(n(n-1) \le 2(r - l)\)
那么 \(n\) 就在 \(\sqrt{2(r-l)}\) 这个位置,或者往下一点点。
调整一下,找到这个 \(n\) 。这里的调整方法可以看代码,也是一个小技巧。
#include <bits/stdc++.h>void solve()
{int l, r;std::cin >> l >> r;int len = r - l;int k = sqrt(2 * len);while (k * (k + 1) > 2 * len) k--;std::cout << k + 1 << "\n";
}
int main ()
{int _;std::cin >> _;while (_--){solve();}
}
D. Sakurako's Hobby
【题目描述】
给定长度为 \(n\) 的排列 \(p\),并且给定其中每个元素是黑色还是白色。
对于某个排列 \(p\),如果可以通过赋值 \(i=p_i\) 一定次数让 \(i\) 等于 \(j\) ,则我们称整数 \(j\) 可以从整数 \(i\) 到达。
如果是 \(p=[3,5,6,1,2,4]\),那么 \(1\) 可以到达 \(4\) 。
因为 \(i=1\rightarrow i=p_1=3\rightarrow i = p_3=6 \rightarrow i = p_6 = 4\) .
现在是 \(i=4\),所以 \(4\) 可以从 \(1\) 到达。
排列中的每个数字都被染成黑色或者白色。
记函数 \(F(i)\) 为从 \(i\) 出发可以到达的黑色整数的个数。
请你对所有 \(i\) 计算 \(F(i)\) 。
【数据规模与约定】
\(1 \le n \le 2 \cdot 10^5\)
【图片留白】
【个人解析】
要想到这个问题本质上是个图论问题,先把图建出来,然后看下有没有更高级的图论工具来处理。
对每个点 \(i\) ,向 \(p_i\) 连一条有向边。问题转化为了对有向图上的每个点 \(i\) ,从每个点 \(i\) 出发能抵达多少个黑点。
需要想到,这样建出来的一张图,就是若干个单向环。
因此,处理好每个环上有多少个黑点就可以了。
对于这个环上的每个点 \(i\),\(F(i)\) 的值就是环上黑点数量。
#include <bits/stdc++.h>void solve()
{int n;std::cin >> n;std::vector<int> p(n + 1, 0);for (int i = 1; i <= n; i++) std::cin >> p[i];std::string s;std::cin >> s;std::vector<int> f(n + 1, 0);std::vector<int> color(n + 1, 0);int cnt = 0;int sum = 0;std::function<void(int)> dfs = [&](int u){f[u] = cnt;if (s[u - 1] == '0') sum++;if (!f[p[u]]) dfs(p[u]); };for (int i = 1; i <= n; i++){if (f[i]) continue;cnt = cnt + 1;sum = 0;dfs(i);color[cnt] = sum;}for (int i = 1; i <= n; i++){std::cout << color[f[i]] << " ";}std::cout << "\n";
}
int main ()
{int _;std::cin >> _;while (_--){solve();}
}
Alternating String
【题目描述】
给定只包含小写英文字母的字符串 \(s\) 。
称字符串 \(s\) 是交替字符串,当且仅当它满足:偶数位置上的字符全部相同,奇数位置上的字符全部相同。
给出字符串 \(s\) ,您可以对它做两种操作:
- 选择索引 \(i\) 并从字符串中删除第 \(i\) 个字符,使得长度减 \(1\) 。这个操作的次数不超过 \(1\) 次。
- 选择索引 \(i\) 并将 \(s_i\) 替换为其他字母。
请你计算让 \(s\) 成为交替字符串的最少操作次数。
【数据规模与约定】
\(1 \le |s| \le 2 \cdot 10^5\)
【图片留白】
【个人解析】
这是一个经典的动态规划问题。
设计状态:\(dp_{i,j,k,l}\) 表示仅考虑原串的前 \(i\) 个字符,奇数位置的字符是 \(j\),偶数位置的字符是 \(k\),是否用过第一种操作。
转移方程见代码。
#include <bits/stdc++.h>
const int N = 2e5 + 5;
int dp[2][26][26][2];
void solve()
{int n;std::cin >> n;std::string s;std::cin >> s;for (int i = 0; i < 2; i++){for (int j = 0; j < 26; j++){for (int k = 0; k < 26; k++){for (int l = 0; l < 2; l++){dp[i][j][k][l] = 1e9;}}}}for (int i = 0; i < 26; i++) for (int j = 0; j < 26; j++)dp[0][i][j][0] = 0;int f = 1;for (int i = 1; i <= n; i++, f ^= 1){for (int j = 0; j < 26; j++) for (int k = 0; k < 26; k++) for (int l : {0, 1})dp[f][j][k][l] = 1e9;for (int j = 0; j < 26; j++){for (int k = 0; k < 26; k++){if (i % 2 == 1){if (s[i - 1] - 'a' == j){dp[f][j][k][0] = std::min(dp[f][j][k][0], dp[f ^ 1][j][k][0]);dp[f][j][k][1] = std::min(dp[f][j][k][1], dp[f ^ 1][j][k][0] + 1);}else{dp[f][j][k][0] = std::min(dp[f][j][k][0], dp[f ^ 1][j][k][0] + 1);dp[f][j][k][1] = std::min(dp[f][j][k][1], dp[f ^ 1][j][k][0] + 1);}if (s[i - 1] - 'a' == k){dp[f][j][k][1] = std::min(dp[f][j][k][1], dp[f ^ 1][j][k][1]);}else{dp[f][j][k][1] = std::min(dp[f][j][k][1], dp[f ^ 1][j][k][1] + 1);}}else{if (s[i - 1] - 'a' == k){dp[f][j][k][0] = std::min(dp[f][j][k][0], dp[f ^ 1][j][k][0]);dp[f][j][k][1] = std::min(dp[f][j][k][1], dp[f ^ 1][j][k][0] + 1);}else{dp[f][j][k][0] = std::min(dp[f][j][k][0], dp[f ^ 1][j][k][0] + 1);dp[f][j][k][1] = std::min(dp[f][j][k][1], dp[f ^ 1][j][k][0] + 1);}if (s[i - 1] - 'a' == j){dp[f][j][k][1] = std::min(dp[f][j][k][1], dp[f ^ 1][j][k][1]);}else{dp[f][j][k][1] = std::min(dp[f][j][k][1], dp[f ^ 1][j][k][1] + 1);} }}}}int Min = 1e9;for (int i = 0; i < 26; i++){for (int j = 0; j < 26; j++){Min = std::min(Min, dp[f ^ 1][i][j][n % 2]);}}std::cout << Min << "\n";
}
int main ()
{int _;std::cin >> _;while (_--){solve();}
}
F. Sakurako's Box
【题目描述】
给定长度为 \(n\) 的数组 \(a\) 。计算从 \(a\) 中随机挑选两个元素的乘积的期望。(模 \(10^9+7\) 下的结果)
【数据规模与约定】
\(2 \le n \le 2 \cdot 10^5, 0 \le a_i \le 10^9\)
【图片留白】
【个人解析】
考察数学基本功,是否知道期望。
选择 \(a_i\) 和 \(a_j\) ,对答案的贡献是:\(\dfrac{2a_i\times a_j}{n(n-1)}\)
固定选择 \(a_i\) ,另一个任选,对答案的贡献是:
\(\sum\limits_{j=1}^{n,j \ne i} \dfrac{2a_i \times a_j}{n(n-1)}=\dfrac{2a_i}{n(n-1)}\times \sum\limits_{j=1}^{n,j\ne i} a_j\)
记所有元素和为 \(sum\) ,式子可以进一步写为:
\(\dfrac{2a_i}{n(n-1)}\times (sum - a_i)\)
对所有 \(a_i\) 用这样的方法计算它的贡献,求和。
\(\sum\limits_{i=1}^n \dfrac{2a_i}{n(n-1)} \times (sum - a_i)\) 。
最后注意:选择 \(a_i,a_j\) 和选择 \(a_j,a_i\) 被计算了两次,需要再整体除 \(2\) :
\(\sum\limits_{i=1}^n \dfrac{a_i}{n(n-1)} \times (sum - a_i)\)
#include <bits/stdc++.h>
const int N = 2e5 + 5;
const int mod = 1e9 + 7;
long long qpow(long long a, long long b)
{long long ans = 1, base = a;base %= mod;while (b){if (b & 1) ans = ans * base % mod;base = base * base % mod;b /= 2;}return ans;
}
void solve()
{int n;std::cin >> n;std::vector<int> a(n + 1, 0);for (int i = 1; i <= n; i++)std::cin >> a[i];long long sum = 0;for (int i = 1; i <= n; i++) sum += a[i];long long ans = 0;for (int i = 1; i <= n; i++){ans = ans + 1ll * a[i] * ((sum - a[i]) % mod) % mod;ans %= mod;}ans = ans * qpow(1ll * n * (n - 1), mod - 2) % mod;std::cout << ans << "\n";
}
int main ()
{int _;std::cin >> _;while (_--){solve();}
}
G. Sakurako's Task
【题目描述】
给定长度为 \(n\) 的数组 \(a\) 。
你每次可以选择一组 \((i, j)\) ,满足 \(i \ne j\) 和 \(a_i \ge a_j\) ,然后让 \(a_i = a_i - a_j\) 或者 \(a_i = a_i + a_j\) 。
你可以操作任意次数。
问数组的 \(mex_k\) 最大值是多少?
\(mex_k\) 最大值指数组中不存在的第 \(k\) 小的非负整数。
【数据规模与约定】
\(1 \le n \le 2 \cdot 10^5, 1 \le k \le 10^9, 1 \le a_i \le 10^9\)
【图片留白】
【个人解析】
前置知识:辗转相除法求最大公因数
让 \(mex_k\) 尽可能大,那需要让最终的数组 \(a\) 排列的尽可能紧凑(整体尽可能靠近 \(0\),同时元素尽可能多样化)
如果只允许你选择 \(i,j=(1,2)\) 来操作呢?
最紧凑的方式就是模拟”辗转相除法“,最后留下 \(0, \gcd(a_1,a_2)\) .
然后选择 \(i,j=(2,3)\),继续模拟”辗转相除法“,最后留下:
\(0, \gcd(a_1,a_2,a_3)\) 。
以此类推,最终我们可以构造出一个 \(0,0,0,0,\cdots ,0,\gcd\limits_{i=1}^n a_i\) 这样一个序列。
记 \(\gcd_{i=1}^n a_i\) 的值是 \(g\) 。
然后我们把序列的最后一个元素加给剩下的元素,可以构造出:
\(0,g,2g,3g,4g,\cdots ,(n-1)g\)
这就是一个最紧凑的序列。受笔者水平所限,这道题的做法有很多不严谨的地方,例如:
- 为什么这样的一个序列就可以让 \(mex_k\) 最大化?如何证明不存在别的更优方案?
确定了最后的序列状态,问题转化为了求这个序列的 \(mex_k\) ,就非常简单了。
#include <bits/stdc++.h>void solve()
{int n, k;std::cin >> n >> k;std::vector<int> a(n + 1, 0);for (int i = 1; i <= n; i++) std::cin >> a[i];int gg = 0;for (int i = 1; i <= n; i++){gg = std::__gcd(gg, a[i]);}std::vector<long long> v;if (n > 1)for (int i = 0; i < n; i++){v.push_back(1ll * i * gg);}elsev.push_back(gg);long long L = 0, R = n + k, ans = 0;while (L <= R){long long mid = (L + R) / 2;int sum = 0;for (auto & u : v){if (u < mid) sum++;}if (mid + 1 - sum <= k){ans = mid;L = mid + 1;}else{R = mid - 1;}}std::cout << ans << "\n";
}
int main ()
{int _;std::cin >> _;while (_--){solve();}
}
H. Sakurako's Test
【题目描述】
给定长度为 \(n\) 的数组 \(a\) ,和 \(q\) 次询问。每次询问给出一个 \(x\) 。
你可以操作任意次数:
- 选择一个索引 \(i\),使得 \(a_i \ge x\) 。
- 让 \(a_i\) 减掉 \(x\) 。
对每次询问,请你构造操作策略使得数组 \(a\) 的中位数最小。
【数据规模与约定】
\(n,q \le 10^5, 1 \le a_i \le n, 1 \le x \le n\)
【图片留白】
【个人解析】
对于给定的 \(x\) ,要通过操作让中位数尽可能小,就等价于让数组 \(a\) 中的每个元素尽可能小,等价于让每个 \(a_i\) 变成 \(a_i \pmod x\)
对于所有 \(x \le \sqrt{n}\) :
-
可以 \(O(n\sqrt{n})\) 预处理出 \(a_i \pmod x\) 的值。
-
具体处理方法如下:
-
记 \(d_{i,j}\) 表示模 \(i\) 的值为 \(j\) 的元素数量。其中 \(1 \le i \le \sqrt{n}\) 。
-
\(O(n\sqrt{n})\) 预处理数组 \(d\) 。
-
-
这样对于 \(x \le \sqrt{n}\) 的询问,最小中位数就等价于找到最小的 \(i\),使得 \(\sum\limits_{j=0}^i d_{x,i} > \dfrac{n}{2}\)
这是可以 \(O(\sqrt{n})\) 做的。
对于所有 \(x > \sqrt{n}\) :
- 记满足 \(l \le a_i \le r\) 的 \(a_i\) 数量为 \(c[l, r]\) 。这可以用前缀和的技巧 \(O(1)\) 维护。
- 二分求中位数,设当前 check 的中位数是 \(mid\) 。问题可以转化为:
- 求 $[0, mid], [x, mid], [2x, mid], \cdots $ 这些区间中的元素数量只和,是否大于 \(\dfrac{n}{2}\) .
- 如果是,说明 \(mid\) 可能是中位数,先记为答案,往更大的找。
- 如果不是,往更小的找。
- 处理单次询问的时间复杂度是 \(O(\sqrt{n}\log n)\)
整体复杂度 \(O(n\sqrt{n}\log n)\) .
#include <bits/stdc++.h>const int N = 1e5 + 5;
const int B = 600;
int d[B][N];
void solve()
{int n, q;std::cin >> n >> q;std::vector<int> a(n + 1, 0), c(n + 1, 0);for (int i = 1; i <= n; i++) std::cin >> a[i];for (int i = 1; i <= n; i++) c[a[i]]++;for (int i = 1; i <= n; i++) c[i] += c[i - 1];int sz = sqrt(n);for (int i = 1; i <= sz; i++){for (int j = 0; j < i; j++){d[i][j] = 0;}for (int j = 1; j <= n; j++){d[i][a[j] % i]++;}}std::vector<int> Ans;while (q--){int x;std::cin >> x;if (x <= sz){int sum = 0, ans = 0;for (int i = 0; i < x; i++){sum += d[x][i];if (sum > n / 2){ans = i;break;}}Ans.push_back(ans);}else{int L = 0, R = x, ans = 0;while (L <= R){int mid = (L + R) / 2;int sum = 0;for (int i = 1; i <= n; i++){int l = (i - 1) * x, r = (i - 1) * x + mid;r = std::min(r, n);if (l > r) break;sum += c[r] - (l > 0 ? c[l - 1] : 0);}if (sum > n / 2){ans = mid;R = mid - 1;}else{L = mid + 1;}}Ans.push_back(ans);}}for (auto & u : Ans) std::cout << u << " ";std::cout << "\n";
}int main ()
{int _;std::cin >> _;while (_--){solve();}
}
进一步思考,对于所有不同的 \(x\) ,记录下答案,避免重复计算相同的询问。
那么对 \(x > \sqrt{n}\) 的做法,每次的循环长度是 \(\dfrac{n}{x}\) 。
现在保证了 \(x\) 两两不同,循环长度之和是:
\(O(n + \dfrac{n}{2} + \dfrac{n}{3} + \cdots + \dfrac{n}{n})\) ,这个式子的整体复杂度是 \(O(n\log n)\) 的(调和级数),再套上外层二分的复杂度,整体时间复杂度为 \(O(n\log^2 n)\)
因此,前面的根号分治做法完全多余,直接对 \(x\) 去重就可以,然后全部上 \(x > \sqrt{n}\) 的做法就可以了。
#include <bits/stdc++.h>const int N = 1e5 + 5;
void solve()
{int n, q;std::cin >> n >> q;std::vector<int> a(n + 1, 0), c(n + 1, 0);for (int i = 1; i <= n; i++) std::cin >> a[i];for (int i = 1; i <= n; i++) c[a[i]]++;for (int i = 1; i <= n; i++) c[i] += c[i - 1];std::vector<int> Ans;std::map<int, int> mp;while (q--){int x;std::cin >> x;if (mp.count(x)) Ans.push_back(mp[x]);else {int L = 0, R = x, ans = 0;while (L <= R){int mid = (L + R) / 2;int sum = 0;for (int i = 1; i <= n; i++){int l = (i - 1) * x, r = (i - 1) * x + mid;r = std::min(r, n);if (l > r) break;sum += c[r] - (l > 0 ? c[l - 1] : 0);}if (sum > n / 2){ans = mid;R = mid - 1;}else{L = mid + 1;}}Ans.push_back(ans);mp[x] = ans;}}for (auto & u : Ans) std::cout << u << " ";std::cout << "\n";
}int main ()
{int _;std::cin >> _;while (_--){solve();}
}