[CP / Codeforces] Codeforces Round 923 (Div. 3) A-D

Codeforces Round 923 (Div. 3)

两个小时只做出来四道题。虽然是赛后补题,不过即便有赛时 buff 的加持,我感觉自己也只能做出来四道题,而且一如既往地把简单的东西搞复杂了,写了好多绕来绕去的代码。

不过我并没有灰心,只是因为经验不足,而已。

A. Make it White

分析

分别找到 'B' 的最小、最大下标,作差即得答案。

代码

略。

B. Following the String

分析

考虑如下输入:

0,0,0,1,1,1,2,2,2,

我们可以构造出相应的答案:

a,b,c,a,b,c,a,b,c,

这实际上是一个 “分配” 的过程:对于输入数组中的每个数字,如果它是 0,则分配一个尚未使用的字母给它,并记录这一操作;如果是 1,则从已经分配过 1 次的字母中选出一个字母给它。以此类推……

我打算模拟这个分配的过程。对于上面给出的输入来说分配操作是很容易实现的,只需要在每次数字发生改变的时候,将待分配字母重置为 a 即可,但实际输入并不像这样 “有序”,所以我判断应当先进行排序,因为答案要按照原来的输入顺序构造,所以排序的时候需要保留下标信息。

那么排序的依据呢?由于 std::sort 是不稳定的排序,所以需要给出更严格的条件。考虑下面的输入:

0,1,0,1

排序之后变成这样:

0,0,1,1

对照的下标可能是:

2,0,1,3

那么得到的答案就变成了:

b,a,a,b

和输入发生了矛盾。问题在于,在排序时对于相同的数字,没有规定下标的先后顺序。

修改排序条件后,再逐个将分配字母放入下标对应的位置,即得答案。

代码(O(nlogn))

void solve() {
int n;
std::cin >> n;
std::vector<int> v(n);
std::vector<int> id(n);
for (int i = 0; i < n; i++) {
std::cin >> v[i];
}
std::iota(id.begin(), id.end(), 0);
std::sort(id.begin(), id.end(),
[&](int lhs, int rhs) { return v[lhs] == v[rhs] ? lhs < rhs : v[lhs] < v[rhs]; });
std::vector<char> ans(n);
int lastval = -1;
char c = 'a';
for (auto e : id) {
if (v[e] != lastval) {
c = 'a';
}
ans[e] = c++;
lastval = v[e];
}
for (auto c : ans) {
std::cout << c;
}
std::cout << '\n';
}

题解学习

基本思想依然是逐个 “分配” 字母,但并不需要使用排序。

再次观察输入数组,其中的数字表示的是字母的使用次数,所以每次扫描的时候只需要遍历所有字母,找到使用次数和当前数字对应的字母,输出,然后更新使用次数即可。

改进代码(O(26n)

虽然从输入规模来看,我的代码跑得不一定比改进代码慢,但是——第一,为了存储下标消耗了大量的额外空间,第二,为了写出这样的代码,我花了许多现实意义上的时间,如果是排行榜上的那些大佬的话,估计在我做这道题的时间里都快 AK 这场比赛了。总之,我被完爆了……不过这个方法,我记住了!

void solve() {
int n, t;
std::cin >> n;
std::string ans;
std::vector<int> cnt(26);
for (int i = 0; i < n; i++) {
std::cin >> t;
for (int j = 0; j < 26; j++) {
if (cnt[j] == t) {
++cnt[j];
ans.push_back('a' + j);
break;
}
}
}
std::cout << ans << '\n';
}

C. Choose the Different Ones!

分析

这道题的官方题解倒是和我的想法意外地一致。

题目的意思很简单,两个数组各取 k2 个数,判断是否存在这样的取法,使这 k 个数能组成 k 元排列。

努力压制脑中有关暴力搜索的想法,我开始思考。如果能事先确定排列中的某些数,便能缩小搜索的范围。这样的数符合什么样的条件呢?

对于两个输入数组来说,首先可以去掉 >k 的数字,因为它们对答案没有贡献。对于剩余的数字来说,有两种情况:第一是这个数字仅由某个数组独有;第二是这个数字由两个数组共有。

很明显,前者就是我想要寻找的 “可以被事先确定” 的数字,因为不选它们就无法构成完整的排列了。

选完它们后,再从那些共有的数字中挑选能够补全排列的数字,最后再检查取法是否符合题目要求

代码

void solve() {
int n, m, k, t;
std::cin >> n >> m >> k;
std::vector<int> flag(k + 3);
for (int i = 0; i < n; i++) {
std::cin >> t;
if (t <= k) {
flag[t] |= 1;
}
}
for (int i = 0; i < m; i++) {
std::cin >> t;
if (t <= k) {
flag[t] |= 2;
}
}
int a = 0, b = 0;
for (int i = 1; i <= k; i++) {
if (flag[i] == 0) {
std::cout << "NO\n";
return;
}
if (flag[i] == 1) {
++a;
flag[i] = -1;
} else if (flag[i] == 2) {
++b;
flag[i] = -1;
}
}
int check = 0;
for (int i = 1; i <= k; i++) {
if (flag[i] != -1) {
++check;
}
}
if (a <= k / 2 && b <= k / 2 && k - a - b == check) {
std::cout << "YES\n";
} else {
std::cout << "NO\n";
}
}

D. Find the Different Ones!

分析

考虑反面情况,即

a[i]=a[j]

所以可以把输入数组划分成很多段,段内的元素都是相等的。对于每次 query,分别判断左边界和右边界对应的段序号是否一致,若一致,说明所查询区间内的元素都是相等的,输出 "-1 -1",否则选择两个段的边界输出即可。

这里的 “两个段” 的选择是有讲究的,第一是不能单纯地选择最左边的段和最右边的段,因为这两个段的元素可能是相等的,比如下面这种情况:

0,0,0,1,1,1,1,1,0,0

最简单的选法就是选择相邻的两个段,因为它们各自对应的元素必定不相同。

第二,如果选择的段有一个是最左边的段,那么应当选择这个段的右边界;如果选择的段有一个是最右边的段,那么应当选择这个段的左边界。这么做是为了保证输出的两个数字在所查询区间之内。

所以我开了两个 vector 存放区间的左右端点,每次查询用二分查找确定段序号,调了好久的 bug,最后发现问题出在区间数字是从 1 开始而不是从 0 开始的,修改之后重新 AC。

代码(O(n+qlogq)

void solve() {
int n, t;
std::cin >> n;
std::vector<int> v(n);
std::vector<int> sL, sR;
t = 0;
std::cin >> v[0];
for (int i = 1; i < n; i++) {
std::cin >> v[i];
if (v[i] != v[i - 1]) {
sL.push_back(t + 1);
sR.push_back(i);
t = i;
}
}
sL.push_back(t + 1);
sR.push_back(n);
int q, l, r;
std::cin >> q;
for (int i = 0; i < q; i++) {
std::cin >> l >> r;
auto t1 = std::distance(sL.begin(), std::upper_bound(sL.begin(), sL.end(), l));
auto t2 = std::distance(sR.begin(), std::lower_bound(sR.begin(), sR.end(), r));
if (t1 - 1 == t2) {
std::cout << "-1 -1\n";
} else {
std::cout << sR[t1 - 1] << ' ' << sL[t1] << '\n';
}
}
}

题解学习(新的分段技巧!)

在预处理时,不需要手动存放每个段的左、右端点,而是使用当前段的左端点或右端点标记相应位置上的元素,这样在后续的查询中就能以 O(1) 的时间返回查询结果,实现起来也比我的方法要简单许多。

之所以不直接用段序号来标记,是因为这样做无法在常数时间内找到当前段的上一段 / 下一段,而是需要对整个段进行扫描,直到段序号发生变化。

改进代码(O(n+q)

void solve() {
int n;
std::cin >> n;
std::vector<int> v(n);
std::vector<int> left(n);
for (int i = 0; i < n; i++) {
std::cin >> v[i];
}
for (int i = 1; i < n; i++) {
left[i] = (v[i] == v[i - 1]) ? left[i - 1] : i;
}
int q, l, r;
std::cin >> q;
for (int i = 0; i < q; i++) {
std::cin >> l >> r;
--l, --r;
if (left[l] != left[r]) {
std::cout << left[r] << ' ' << left[r] + 1 << '\n';
} else {
std::cout << "-1 -1\n";
}
}
}

之后再补 E-G 题。

posted @   ZXPrism  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示