CF Round 1003 Div4 部分题解
整体感受
比赛地址:Codeforces Round 1003 (Div. 4)
退役一年后,本来想着寒假找个 Div4 复健一下,结果打的汗流浃背的(以前打 Div3 都没这么折磨),有点超脱了过去对其“只涉及基本语法与简单思维”的认知。
AC 题目:A、B、C1、C2、D、F、G
A题:Skibidus and Amog'u(签到,字符串替换)
简要题意:给定若干个仅包含小写字母、类似 S + us 的字符串,将其转化为 S + i 的形式(例如将 abcus 改为 abci)。
思路很简单,主要是如何快速优雅的写出对应代码。由于数据量不大,下面分别给出 C++ 和 Python 的代码:
// Based on C++
#include <bits/stdc++.h>
using namespace std;
int main() {
int T;
cin >> T;
while (T--) {
string s;
cin >> s;
cout << s.substr(0, s.size() - 2) + "i" << endl;
}
return 0;
}
# Based on Python
T = int(input())
for _ in range(T):
s = input()
print(s[:-2] + "i")
B题:Skibidus and Ohio(思维)
题意:给定一个字符串 S,倘若字符串中存在连续两个相同的字符,那么可以选择用任意一个新字符替换掉它们(例如 abbc,可以将其转换为 a#c,# 可以由用户任意选择)。问:字符串 S 在反复操作后,其长度最小可变为多少?
对于一个不存在连续相同字符的字符串,无法进行操作,直接输出其长度即可。
相反,只要任意位置出现了连续两个相同字符,就可以执行下述的“消消乐操作”:
- 将“这两个相同字符”转化为新的“前一个字符”,那么就在“依然存在连续两个相同字符“的前提下,成功让字符串长度变小(例如将 abcdd 转化为 abcc,然后是 abb,然后是 aa)
- 当前半部分处理完毕后,将“这两个相同字符”转化为新的“后一个字符”(例如 aabcd 转化为 bbcd,然后依次是 ccd 和 dd)
- 最后整个字符串只剩下两个相同字符后,随意进行替换,此时 S 只剩下了一个字符,长度为 1
// Based on C++
#include <bits/stdc++.h>
using namespace std;
int solve() {
string s;
cin >> s;
for (int i = 0; i <= s.size() - 2; i++)
if (s[i] == s[i + 1]) return 1;
return s.size();
}
int main() {
int T;
cin >> T;
while (T--) {
cout << solve() << endl;
}
return 0;
}
# Based on Python
def solve():
s = input()
for i in range(len(s) - 1):
if s[i] == s[i + 1]:
return 1
return len(s)
T = int(input())
for _ in range(T):
print(solve())
C1题:Skibidus and Fanum Tax (easy version)(贪心)
题意:给定长度为 \(n(n\leq 2*10^5)\) 的序列 \(\{a_n\}\) 和数字 \(b\),对于序列的每个位置 \(i\),可以选择不改变对应的数字,也可以选择将对应的数字改为 \(b-a_i\)。问:能否将序列 \(\{a_n\}\) 变为一个单调不降的序列?
根据贪心的思想,对于最后一个位置 \(n\),显然应该选择成为 \(a_n\) 和 \(b-a_n\) 中更大的那一个,不妨记其为 \(c_n\)。
那么,当位置变为 \(n-1\) 时,则需要同时满足以下策略:
- 选择成为 \(a_{n-1}\) 和 \(b-a_{n-1}\) 中更大的那个
- 但选择的这个数不能比 \(c_n\) 更大
那么,本题的思路就呼之欲出了(这同样是 C2 的思路):
- 对于最后一个位置,选择成为更大的那个数
- 从后往前,每个位置在其大小不超过后一个数的限制下,应该尽可能选择更大的那个
- 如果在某个位置无法做出选择(所有可成为的数字都比后一个数大),返回 NO
时间复杂度:\(O(n)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
const int INF = -(1e9 + 10);
int n, m, a[N], b, c[N];
bool solve() {
// Read
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
cin >> b;
// Solve
c[n] = max(a[n], b - a[n]);
for (int i = n - 1; i >= 1; i--) {
c[i] = INF;
if (a[i] <= c[i + 1])
c[i] = max(c[i], a[i]);
if (b - a[i] <= c[i + 1])
c[i] = max(c[i], b - a[i]);
if (c[i] == INF) return false;
}
return true;
}
int main() {
int T;
cin >> T;
while (T--) {
cout << (solve() ? "YES" : "NO") << endl;
}
return 0;
}
C2题:Skibidus and Fanum Tax (hard version)(贪心,二分)
与 C1 Easy 版相比,C2 将数字 \(b\) 改为了长度为 \(m(m\leq 2*10^5)\) 的数组 \(\{b_m\}\),因而序列中位置 \(i\) 的数字,既可以保留 \(a_i\) 不变,也可以选择成为 \(b_j-a_i(1\leq j\leq m)\),即可选项从 2 项变为了 \(m+1\) 项。
Hard 版的整体思路与 Easy 版一致,但是在数字选择的部分不可能再一一比对了(\(O(nm)\) 的复杂度过高,无法接受)。值得高兴的是,由于 \(b_j-a_i\) 具有单调性,因此可以先对数组 \(\{b_m\}\) 进行排序,那么查询问题就变为了:在有序数组中寻找最大的 \(b_j\),使得 \(b_j\leq a_i+c_{i+1}\)。
这个二分可以自己手写,但更合适的方式是使用 STL 自带的 lower_bound/upper_bound,详细用法可自行查询,或询问类似 DeepSeek 的大语言模型。
时间复杂度:\(O(m\log m+n\log m)=O((n+m)\log m)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
const int INF = -(1e9 + 10);
int n, m, a[N], b[N], c[N];
bool solve() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= m; i++) cin >> b[i];
sort(b + 1, b + m + 1);
c[n] = max(a[n], b[m] - a[n]);
for (int i = n - 1; i >= 1; i--) {
c[i] = a[i] <= c[i + 1] ? a[i] : INF;
if (b[1] <= a[i] + c[i + 1]) {
int j = upper_bound(b + 1, b + m + 1, a[i] + c[i + 1]) - b - 1;
c[i] = max(c[i], b[j] - a[i]);
}
if (c[i] == INF) return false;
}
return true;
}
int main() {
int T;
cin >> T;
while (T--) {
cout << (solve() ? "YES" : "NO") << endl;
}
return 0;
}
D题:Skibidus and Sigma(贪心,数学)
题意:给定一个序列 \(\{a_n\}\),记 \(S_i\) 是序列中前 \(i\) 个数的和,那么这个序列的值就是 \(\sum_{i=1}^nS_i\)。接下来,给定 \(n\) 个长度为 \(m(n,m\leq 2*10^5)\) 的序列(记第 \(i\) 个序列为 \(A_i\)),请尝试对这些序列进行排序(但序列内部的顺序不变),使得排列后组成的,长度为 \(nm\) 的新序列的值尽可能的大。
如果是对某个数列内部进行排序,我们不难想到,把更大的数字排在前面,可以使得数列的最终值最大(因为数列中越靠前的元素,在值的计算公式中会被更多次的累加)。那么,对于元素是数列的序列,能否也找到一种特定的排序方式呢?
对于此类要求特定排列顺序,从而使得目标值最大化的问题,可以尝试使用基于邻项交换法的贪心进行解决。
考虑最简单的情况,假定有两个相邻数列 \(\{a_n\}\) 和 \(\{b_n\}\),其前缀和(内部前 \(i\) 个元素的和)分别为 \(A_i,B_i\)。那么,如果将 \(\{a_n\}\) 放在 \(\{b_n\}\) 前,那么这个新的长度为 \(2n\) 的序列的值就是:
反之,如果将 \(\{b_n\}\) 放在 \(\{a_n\}\) 前,那么这个新的长度为 \(2n\) 的序列的值就是 \(nB_n+\sum_{i=1}^n(A_i+B_i)\)。不难注意到,两个值的差别只取决于 \(A_n\) 和 \(B_n\),而它们分别代表数列 \(\{a_n\},\{b_n\}\) 的内部元素之和。
那么很显然,对于两个数列,我们应该选择让内部元素和更大的数列排在前面,这样可以使得新数列的值更大。
推广到有穷个数列的排序,我们也可以通过内部邻项交换的方式(对于任意两个相邻的数列,都可以使用上述结论来调整位置,其必然使得整体答案变得更优),从而实现“排序”的效果。
综上所述,我们应该对这些排序按照内部元素之和的大小进行排序,优先把内部和更大的数列排在靠前的位置。
时间复杂度:\(O(n\log{n}+nm)\)(对 vector 的交换是 \(O(1)\) 的)。
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
LL solve() {
int n, m;
cin >> n >> m;
vector<vector<LL>> A;
for (int i = 0; i < n; i++) {
vector<LL> a(m + 1, 0);
for (int j = 1; j <= m; j++) {
cin >> a[j];
a[0] += a[j];
}
A.push_back(a);
}
sort(A.begin(), A.end(), [](const vector<LL> &x, const vector<LL> &y) {
return x[0] > y[0];
});
LL ans = 0, cnt = n * m;
for (auto &a: A)
for (int j = 1; j <= m; j++)
ans += a[j] * cnt, cnt--;
return ans;
}
int main() {
int T;
cin >> T;
while (T--) {
cout << solve() << endl;
}
return 0;
}
F题:Skibidus and Slay(树的遍历,思维)
给定一棵 \(n(n\leq 5*10^5)\) 个节点的无根树,每个节点有一个值 \(a_i(1\leq a_i\leq n)\)。问:对于每个值 \(x(1\leq x\leq n)\),在树上是否存在一个简单路径,使得 \(x\) 是这个路径上所有节点的值所构成的有重集合的众数(如果某个数在有重集合中出现的次数严格过半,那么这个数就是众数)?
众数要求对应数字(不妨设为 \(x\))在集合中严格过半,考虑到这些数字在树上构成了一条链(一条路径),这就意味着,路径上必然存在着连续三个节点,其中有两个(或以上)的值为 \(x\):如果不存在,那么两个 \(x\) 之间至少隔着两个节点(类似 x 0 0 x 0 0 x 这样),其必然不会成为众数。
因此,我们只需要遍历这棵树上的所有三个节点构成的简单路径,如果某个路径里有数字出现过半(2 次或 3 次),那么这个路径即可对应该数字。
遍历方法:在深度优先遍历中,对于每个节点,将这个节点、节点的父亲、节点的所有儿子都扔进一个哈希表中,随后遍历哈希表,如果某个数字出现次数大于等于两次,那么就意味着存在一条长度为 3 的符合要求的路径,即可给该数字标记为“存在”。
时间复杂度:使用 unordered_map 的话,复杂度大致为 \(O(n)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
vector<int> G[N];
int n, a[N], vis[N];
void dfs(int x, int f) {
unordered_map<int, int> cnt;
cnt[a[x]]++, cnt[a[f]]++;
for (int y : G[x])
if (y != f) dfs(y, x), cnt[a[y]]++;
// must be used in C++17 or higher version
for (auto [k, v] : cnt)
if (v > 1) vis[k] = 1;
}
string solve() {
cin >> n;
for (int i = 1; i <= n; i++) G[i].clear();
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n - 1; i++) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
memset(vis, 0, sizeof(int) * (n + 1));
dfs(1, 0);
string res = "";
for (int i = 1; i <= n; i++)
res += vis[i] == 0 ? "0" : "1";
return res;
}
int main() {
int T;
cin >> T;
while (T--) {
cout << solve() << endl;
}
return 0;
}
G题:Skibidus and Capping(数学)
题意:给定一个长度为 \(n(n\leq 2*10^5)\) 的数列 \(\{a_n\}(2\leq a_i\leq n)\),请找出所有的数对 \((i,j)(i\leq j)\),使得 \(\operatorname{lcm}(a_i,a_j)\) 是半质数(如果一个数可以表示为两个质数之积,那么这个数就是半质数)。
要让 \(\operatorname{lcm}(x,y)\) 是半质数,大概有以下几种情形:
- \(x,y\) 是互不相同质数
- \(x,y\) 是相同的半质数
- \(x\) 是质数,\(y\) 是半质数,且 \(x\) 恰好是 \(y\) 的因子(或者反过来)
因此,我们只需要保留数列中所有的质数和半质数,随后分别扔进两个哈希表中,按上述三种情况分别处理即可,复杂度不会超过 \(O(n)\)(但考虑到复杂的下标限制,本题在上述统计处理环节的代码会相当繁琐,请大家自行参考代码,题解正文不再赘述)。
但上述的处理部分建立在一个前提:我们需要能够 \(O(1)\) 的检查一个数是否是质数或半质数(如果是半质数,还得额外 \(O(1)\) 的知道它的两个因子是啥)。好消息是,\(a_i\) 的范围为 \([2,n]\),这并不是一个很大的数量级,我们完全可以进行下述的预处理(即在读入正式数据前,先获取 \(2*10^5\) 范围内的下述数字信息,而非每次读入数据都重新做一次):
- 通过素数筛,快速筛出对应范围内的所有质数(复杂度不会超过 \(O(n\log n)\))
- 根据质数表,快速构造出所有在 \([2,n]\) 范围内的半质数(根据实际测算,\([2,2*10^5]\) 以内的半质数只有约 \(16236\) 个,因此构造速度很快)
详细代码如下所示,大家可以自行参考(出于简化代码的考虑,使用了大量 C++17 或更高的语法特性,请留意):
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 200010;
int isPrime[N];
vector<LL> prime;
void getPrime() {
const int n = N - 10;
memset(isPrime, 1, sizeof(isPrime));
for (int i = 2; i <= n; i++)
if (isPrime[i]) {
prime.push_back(i);
for (int j = 2; i * j <= n; j++)
isPrime[i * j] = 0;
}
sort(prime.begin(), prime.end());
}
unordered_map<LL, pair<LL, LL>> semiprime;
void getSemiPrime() {
for (int i = 0; i < prime.size(); i++)
for (int j = i; j < prime.size(); j++) {
LL x = prime[i] * prime[j];
if (x > N - 10) break;
semiprime[x] = {prime[i], prime[j]};
}
}
LL solve() {
int n;
cin >> n;
vector<LL> v1, v2;
for (int i = 1; i <= n; i++) {
LL x;
cin >> x;
if (isPrime[x])
v1.push_back(x);
else if (semiprime.count(x))
v2.push_back(x);
}
// Part1
LL ans1 = 0;
unordered_map<LL, LL> mp1;
for (LL x : v1) mp1[x]++;
LL cnt_mp1 = v1.size();
for (auto [k, v] : mp1)
ans1 += v * (cnt_mp1 - v);
ans1 /= 2;
// Part2
LL ans2 = 0;
unordered_map<LL, LL> mp2;
for (LL x : v2) mp2[x]++;
for (auto [k, v] : mp2)
ans2 += v * (v + 1) / 2;
// Part3
LL ans3 = 0;
for (auto [val, cnt] : mp2) {
auto [x, y] = semiprime[val];
if (mp1.count(x))
ans3 += cnt * mp1[x];
if (y != x && mp1.count(y))
ans3 += cnt * mp1[y];
}
return ans1 + ans2 + ans3;
}
int main() {
getPrime();
getSemiPrime();
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}

浙公网安备 33010602011771号