CF2008-Solution
【萌新场!】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);
}
else
v.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();
}
}

浙公网安备 33010602011771号