Educational Codeforces Round 6 复盘
AC:3+1
状态:前好后差,看到蓝题心态崩了。
题目难度:红,橙,黄,蓝。
前三题50min不到秒了,T4很难,但在老师和AI的帮助下AC并理解了。
T1
虽然是红题,代码行数个位数(不压行),但是没有那么水。
题意:给了两点坐标,从一个坐标到另一个坐标的最少步数(每一步可以往相邻的八个格子走)。
我想到了两点间距离公式,但是行不通。
很快,我想到了正解,可以朝着目标一直走斜向,直到走到和他有同一个x或y坐标位置,因为这样一次性,可以减少到目标的横向距离即abs(x1 - x2)(在后文叫做c1),也可以减少到目标的纵向距离即abs(y1 - y2)(在后文叫做c2), 这样一直走下去,一定会有一个先变成0,那么直线走完剩下的距离就可以了。
于是走斜向的次数就是min(c1, c2), 剩下的次数就是max(c1, c2) - min(c1, c2), 总次数就是min(c1, c2) + max(c1, c2) - min(c1, c2) = max(c1, c2)。
核心代码完全可以写成一行输出。
第一次写红题思路这么认真。
T2
小模拟,没有任何思维难度,题目怎么说就怎么做,不用任何优化。
这为什么会是橙题,个人认为比T1简单,而且跑的和T1一样快。
T3
核心思路很快就想了出来。
题面
CF620C Pearls in a Row
题目描述
有 \(n\) 颗珍珠排成一行。我们用整数从 \(1\) 到 \(n\) 从左到右对它们编号。第 \(i\) 颗珍珠的类型为 \(a_{i}\)。
我们称一段连续的珍珠为一个区间。如果该区间内至少有两颗相同类型的珍珠,则称其为“好区间”。
请将珍珠的排列划分成最多个好区间。注意:每颗珍珠必须恰好属于一个区间。
由于输入/输出数据量可能很大,建议使用高效的输入输出方式:例如,C++中建议使用 scanf/printf 代替 cin/cout,Java 中建议使用 BufferedReader/PrintWriter 代替 Scanner/System.out。
输入格式
第一行包含一个整数 \(n\)(\(1 \le n \le 3 \cdot 10^{5}\)),表示珍珠的数量。
第二行包含 \(n\) 个整数 \(a_{i}\)(\(1 \le a_{i} \le 10^{9}\)),表示第 \(i\) 颗珍珠的类型。
输出格式
第一行输出一个整数 \(k\),表示分割获得的好区间的最大数量。
接下来的 \(k\) 行,每行输出两个整数 \(l_{j}, r_{j}\)(\(1 \le l_{j} \le r_{j} \le n\)),表示第 \(j\) 个区间中最左和最右珍珠的编号。
注意,你需要输出正确的区间划分方案,使每颗珍珠恰好属于一个区间且每个区间都含有两颗相同类型的珍珠。
如果存在多种最优方案,可以输出任意一种。区间输出的顺序不限。
如果无法将珍珠划分成符合要求的区间,输出 -1。
输入输出样例 #1
输入 #1
5
1 2 3 4 1
输出 #1
1
1 5
输入输出样例 #2
输入 #2
5
1 2 3 4 5
输出 #2
-1
输入输出样例 #3
输入 #3
7
1 2 1 3 1 2 1
输出 #3
2
1 3
4 7
说明/提示
由 ChatGPT 5 翻译
他的目标是最大化段数,每一段至少要有两个及以上个相同的数,我很快发现了特性。
从前往后面遍历输入的数组(类似于双指针,但是不用多一个内循环),一旦你的这一段拥有了两个相同的数字,就可以去找下一段了,因为这一段已经满足要求了,多留一点数字给后面,就可以更快的完成后面的分段(即i更小,i就是遍历到的下标)(至少不会变慢),这样就可划分出更多的段数(至少不会变少)。
当然,最后可能还留下一段不能划分,把他给能划分的最后一段就可以了(设最后一段原本是[l, r], 把他改成[l, n]就可以了,输出的时候注意一下就可以了)
T4
此系列比赛中第一次切的蓝题(虽然是补的题目,也有外界帮助)
形式化题面
给你两个数组 \(a\) 和 \(b\),每次操作你可以交换两个数组中的一对数。如,\([1, 2, 3]\) 和 \([4, 5, 6]\) 经过一次操作可变为 \([4, 2, 3]\) 和 \([1, 5, 6]\)。
你至多进行两次操作,请最小化两个数组和的差的绝对值。
本来一看,题面简洁(即使原题面也很好理解和简洁,对于我这种语文蒟蒻来说很友好),还是\(O(n^2)\)复杂度,一定不会很难,至少代码会比较好写。
然而,我全猜错了...
这其中,有着数学,分类讨论,二分,预处理,复杂度也是\(O(n^2 log m)\)(n、m范围一样),84行代码T2不才是模拟吗 。
干货太多了看看代码吧!
#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int N = 2e3 + 2;
int n, m, a[N], b[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int sa = 0, sb = 0, ans = 0, op = 0;
/*
sa:原a数组的和
sb:原b数组的和
ans:最小的两数组差值的绝对值
op:改几次后的ans最优
*/
int ax = 0, ay = 0, bx = 0, by = 0;
/*
ax & bx: 第一对交换的两个数组的元素下标
ay & by: 第二对交换的两个数组的元素下标
*/
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
sa += a[i]; // 统计sa
}
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> b[i];
sb += b[i]; // 统计sb
}
// 不交换也是一种选择
ans = abs(sa - sb);
//不用改变op,默认就是0
// 这是挑一组交换的情况
// n, m至少就是1,不会有空数组, 不用特判
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int cur = abs((sa - sb) - 2 * (a[i] - b[j])); // 当前差值
if (cur < ans) {
ans = cur;
op = 1;
ax = i;
bx = j;
}
}
}
if (n >= 2 && m >= 2) { // 如果有一个
/*
如果一个是1,一个是2貌似也可以交换,但是试一下就知道这就是换了一次的情况
*/
vector<tuple<int, int, int>> v;
/*
<0>:b里面选出两个元素的和
<1>:b里面选出的第一个元素下标
<2>:b里面选出的第二个元素下标
*/
for (int i = 1; i <= m; i++) { // 预处理b的组合
for (int j = i + 1; j <= m; j++) { // 例如组合{1, 2} {2, 1}是一样的,
v.push_back({b[i] + b[j], i, j});
}
}
sort(v.begin(), v.end()); // 按照元素和排序,方便后面的二分,是实现logm的关键
for (int i = 1; i <= n; i++) { // 枚举a的组合
for (int j = i + 1; j <= n; j++) {
int suma = a[i] + a[j]; // a选出的两个元素之和
/*
这里是重点!
我们suma,v都在干嘛?
这里我们让v<0>叫sumb
我们想要的结果就是要让,这个式子最小,想一会就能理解
abs((sa - sb) - 2 * (ax + ay) + 2 * (bx + by))
= abs(sa - sb - 2 * suma + 2 * sumb)
= abs(2 * sumb - 2 * suma + sa - sb)
= abs(2 * sumb - (2 * suma - sa + sb))
= abs(2 * sumb - (2 * suma - (sa - sb)))
发现:这就是要让 2 * sumb 尽可能接近 2 * suma - (sa - sb)
也就是让sumb 尽可能接近 suma - (sa - sb) / 2
*/
int tg = 2 * suma - (sa - sb); // 我们想要的sumb 的 两倍
int need = (tg + 1) / 2; // 真正想要的sumb,
/*
为什么要四舍五入?
其实这里是向上取整,偶数得到的就是准确值,奇数得到的会向上取整(避免小数)。
为什么是向上,后面会说
*/
int pos = lower_bound(v.begin(), v.end(), make_tuple(need, 0, 0)) - v.begin();
// 如果向下取整下面就是upperbound,但是怕避开准确值,lowerbound至少找到了就是准的,至少不会有准确值而错过
for (int d = 0; d <= 1; d++) { // 偏移量, lowerbound找到了>= need的,找一个< need且最接近need的(应为排序),看看哪个更接近。(其他元素的一定会被这两个代替)
int sy = pos - d; // 下标
if (sy < 0 || sy >= v.size()) continue; // 越界判定
int sumb = get<0>(v[sy]); // 得到sumb
int cur = abs((sa - sb) - 2 * suma + 2 * sumb); // 这个公式前面说过
if (cur < ans) { // 更新
ans = cur;
op = 2;
ax = i;
ay = j;
bx = get<1>(v[sy]);
by = get<2>(v[sy]);
}
}
}
}
}
cout << ans << '\n';
cout << op << '\n';
if (op == 1) {
cout << ax << " " << bx;
} else if (op == 2) {
cout << ax << " " << bx << '\n';
cout << ay << " " << by;
}
return 0;
}
总结
1.看数据,如果复杂度只能很低,找规律。
2.有的时候看似有很多选择,实际上只有很少的个数。
3.选项不多时,可以看一看要不要分类讨论。
4.有的时候不妨把题目要求的式子列出来,展开,看看什么地方可以化简(如预处理掉),以及非暴力以更快速度求出(如二分)。
5.遇到蓝题不要慌,比赛时我看不到难度,我有机会!

浙公网安备 33010602011771号