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\) 的序列的值就是:

\[\sum_{i=1}^nA_i+\sum_{i=1}^n(A_n+B_i)=nA_n+\sum_{i=1}^n(A_i+B_i) \]

反之,如果将 \(\{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;
}
posted @ 2025-02-10 14:17  cyhforlight  阅读(363)  评论(0)    收藏  举报