信息竞赛语言环境滞后论
如标题所说,本文章主要分析信息竞赛的滞后性。
何谓滞后性?滞后性也就是我们所使用的与时代发展脱轨,存在严重的滞后、老旧等问题。
我是一名信息竞赛选手,虽然竞赛实力不强,但对算法有着非常浓厚的兴趣;虽然学习进度较慢,但我所掌握的算法一定是有深刻理解的。各个算法书籍,无论偏重竞赛(国内居多)还是偏重开发与应用(国外居多,国内也有),都在强调算法对于一个程序员的重要性。
:::info[作者简介] 我在接触信息竞赛之前,我是技术性的工程级业余开发者,掌握基本编译原理并可以自己写简单的编译器,以及正在跟随清华大学开源教程 rCore-Tutorial-Book 学习用 Rust 编写 RISC-V 架构的类 Unix 操作系统内核。从技术的角度讲,我比大多数纯粹的竞赛选手要厉害一些。但是对于算法,由于我也在学习阶段,所掌握的自然不如那些 NOIP 与 NOI 大佬们了。我学习算法的目的不仅是为了竞赛,更是将算法用到实处,发挥算法的真正威力。在我自己的简单编译器 RustIranta 中,我就使用了一定的算法来优化解析器。
此外,我的语言风格可能在某些人眼中看起来像 AI,那是因为我阅读 AI 的回答较多,也受到了一些影响。某些优良的 AI 的回答,文字还是挺有逻辑的,是值得学习的。再者,我的这篇文章议论性较强,也没有很多废话,自然看起来就像是一个严格遵循某个规范的 AI 回答一样。实际上,我要是写得轻松的话,你甚至可能会觉得我有点癫。
:::
关于 C++ 与 GCC 编译器的滞后性
最近得到许多消息,某选手线段树代码在 CCF 官方的环境下产生负优化,因多次内联递归调用而导致运行时内存空间超出限制,而遗憾失分。给 CCF 官方反应后,回复极少,且是否有具体行动还无从知晓。具体信息,可以自行搜索(洛谷平台也有相应专栏文章),在此我不过多赘述与分析。
从这件事中可以得出:
- CCF 官方使用的编译器与语言标准存在问题,而此问题有可能影响竞赛的公平性。例如,线段树是正解,但是因为负优化而与某个较优的非正解分数一致。此事件导致的失分并不多,但不能保证以后不会出现更严重的情况。
- 选手希望将更多的精力放在思维的发散与代码的实现上,而不希望过多涉及底层逻辑。这又有两个原因:
- 大多数选手其实对计算机的底层逻辑并不了解。
- 竞赛既然考察的是算法,就应该规避这种问题。
- CCF 官方对于此环境问题的态度不算特别好。什么样才叫态度好?可以参考 Codeforces 的 Mike 对于 C++ 提交选项出现的问题的解决方案。
我们一一进行分析。
编译器与语言标准问题
编译器过老,很多新的优化算法就无法使用,而这些编译优化算法往往是开源社区发现问题之后找到的相应解决方案。也就是说,对于内联递归的负优化来说,新的编译器是没有此问题的。经过测试,确实如此。
目前 CCF 官方使用的 C++ 标准还是 C++14,然而这已经是将近 10 年前的版本了。现代 C++ 提供了许多更好的标准库函数与容器。
举个例子:大多数人并不知道 std::midpoint 这个函数。这个函数在 C++20 中引入,在 <numeric> 头文件中。这个头文件也是处理各种数字问题的头文件。
简单来说,std::midpoint,顾名思义,就是求中点。它接收两个参数,这两个参数可以是数字(整型与浮点型均可),也可以是两个指针。它可以求出两个参数的中间值。
这有什么用呢?第一,是简化以下代码:
int a, b;
// ...
// Before
int res = (a + b) / 2;
// After
int res = std::midpoint(a, b);
这有什么用呢?
安全!
标准库提供的这个 std::midpoint 函数的内部实现绝对不仅仅是单纯的和我们手写的一样,它是在保证安全的情况下进行的。对于整型来说,可能是避免溢出。对于浮点型来说,它甚至可以保证精度,让相对误差更小。具体可见官方文档 https://en.cppreference.com/w/cpp/numeric/midpoint.html。
:::info[更多例子]{open} 最大公约数与最小公倍数:
再也不用手写啦!
int a, b;
// ...
// std::gcd | Since C++17, in header <numeric>
// See: https://en.cppreference.com/w/cpp/numeric/gcd.html
int res_gcd = std::gcd(a, b);
// std::lcm | Since C++17, in header <numeric>
// See: https://en.cppreference.com/w/cpp/numeric/lcm.html
int res_lcm = std::lcm(a, b);
基于范围的算法:
迭代器拜拜。
// std::ranges | Since C++20, in header <ranges>
// See: https://en.cppreference.com/w/cpp/ranges.html
std::vector<int> array;
int target;
// ...
// Before
auto where = std::find(array.begin(), array.end(), target);
// After
auto where = std::ranges::find(array, target);
// Before
std::sort(array.begin(), array.end());
// After
std::ranges::sort(array);
更高效的哈希表:
C++23 提供了基于内存连续的更高效的容器,适合小数据集和频繁查找的场景。
// std::flat_map | Since C++23, in header <flat_map>
std::flat_map<std::string, int> faster;
不再复制:
C++17 引入“保证拷贝省略”机制,在原有的左右值、移动语义的基础上,严格规定构造对象、返回对象、临时对象不得使用复制语义,而是移动语义,以减少不必要的性能开销。
三向比较操作符:
C++20 引入三向比较操作符(<=>,俗称“飞船操作符”),并提供三种比较类型:std::strong_ordering、std::weak_ordering 和 std::partial_ordering,旨在用一个运算符简化常见的多路比较场景。
std::strong_ordering 表示强比较,代表某个类型的数据必须有严格的比较结果。类似 Rust 语言中的 Ord 特征。
std::weak_ordering 表示弱比较,代表某个类型的数据没有严格的比较结果,例如字符串忽略大小写的比较。
std::partial_ordering 表示部分比较,允许某个类型在某些特殊情况下无法比较的情况,例如浮点数的 NaN 比较起来是没有意义的,可以返回 std::partial_ordering::unordered。类似 Rust 语言中的 PartialOrd 特征。
这里不再给出示例代码。
格式化字符串:
不再需要 std::sprintf 函数和 std::stringstream 类了!
class Student {
private:
std::string name;
int age;
public:
Student(std::string_view name, int age) : name(name), age(age) {}
std::string get_info() const;
std::string get_info_new() const;
};
// Before
std::string Student::get_info() const {
std::ostringstraem sout;
sout << "Student Name: " << name << "; Student Age: " << age << ".";
return sout.str();
}
// After
// std::format | Since C++20, in header <format>
std::string Student::get_info_new() const {
return std::format("Student Name: {}; Student Age: {}.", name, age);
}
视图操作:
感谢评论区指出我遗漏了这一个好用的东西。
这个东西我甚至没想到怎么去写对比……因为手动求值无法体现 std::views 的延时求值特性。
std::views 具有延时求值的特性,即你对一个序列进行操作之后,获得一个视图。在使用视图之前,操作都不会进行计算。等你真正使用该视图时,才会开始计算数据。
这里顺便还使用了 C++23 的 std::println 函数。这是 C++23 对于标准输出的优化,能够使用和 std::format 一样的格式化,并且速度比 std::cout 快很多,大多数情况可以和 C 语言接口 std::printf 齐平。
// std::views | Since C++20, in header <ranges>
// Before
void print_valid_handled_number(const std::vector<int>& numbers) {
std::vector<int> result;
for (const int num : numbers) {
if (num % 2 == 1) {
result.push_back(num * 3 + 1);
}
}
for (const int res : result) {
std::println("{}", res); // std::println | Since C++23, in header <print>
}
}
// After
void print_valid_handled_number_new(const std::vector<int>& numbers) {
const auto result = numbers
| std::views::filter([](const int x) { return x % 2 == 1; })
| std::views::transform([](const int x) { return x * 3 + 1; });
for (const int res : result) {
std::println("{}", res);
}
}
从这个例子中,似乎看不到 std::views 对于范围数据操作有多少简化。但是一旦数据操作变得更复杂,std::views 便能更好地操作数据。这也是 C++ 函数式编程的一部分。
:::
总结就是:总有人喷 C++ 不好用,那只不过是因为你总是在学旧的东西。
更新新功能有何意义
还是那句话,OI 选手不希望在底层逻辑和繁琐的细枝末节上花费时间。
我也是这样的。在洛谷上,我逐渐偏向于用洛谷提供的 C++23 来提交代码,这样我可以使用更多特性。
像 gcd、lcm、midpoint 等这种最基本的东西,没有必要每个都手写。
那有人就说:万一题目要考察此知识点呢?
两点反驳:
- 我就不演了,你见过现在哪次比赛能简单到考这种东西?
- 就算是作为附加知识点考察,也没有任何理由。我就问你,除了在某些需要在排序算法思想上做改进的算法,你什么时候手写过排序?哪次不是用
std::sort完成?而且,你知道吗,std::sort是混用了排序算法的。在长度不长的情况下,快速排序的实际效率并不优,可能还不如O\left(n^{2}\right) 的算法。所以标准库的std::sort函数采用的是 PDQSort(Pattern-Defeating Quicksort,模式可抵御快速排序)算法:对于短序列,使用插入排序;其他情况,使用快速排序;当快速排序表现不佳,且此情况出现了一定次数时,转为堆排序以保证最坏情况下复杂度仍为O\left(n \log n\right) 。
计算机的发展就是如此,“一层抽象不够就再加一层”,把底层繁琐的逻辑封装,只将最重要的部分暴露给用户。高级编程语言与编译器、操作系统、各个语言的标准库、面向对象编程与函数式编程,都是这么来的。那么对于 OI 而言,用户(即选手)自然不希望在这种细枝末节上花费大量时间。更新 C++ 标准与编译器版本,让标准库发挥最大的价值。想想当年的 C 语言与 Pascal 语言,为何被 OI 淘汰了?
我个人也是这种看法:既然要考算法,就应该摒弃无关的、重复的部分。
更新的困难
抛开 CCF 官方对此事的态度不说,我们还是要客观分析一下,更新语言标准与编译器版本存在哪些困难:
- 大多数选手对新标准不熟悉,这可能会导致在教学方面的问题。
- 新编译器必然在编译上与之前的版本有所不同,这可能导致我们的题目的相关限制与数据强度的设置需要重新测定。也许在某些方面,新编译器比旧编译器优化得更好。此时就有可能旧的数据构造标准无法满足相关卡常、极限数据等要求——接近正解的非正解做法可能会通过。
- NOI Linux 2 是基于 Ubuntu 20 的。Ubuntu 系统为了保证系统的稳定性,通常会限制你能够安装的最高版本。就算是目前最新的 LTS(Long Time Support)版本 Ubuntu 24,其上的 GCC 编译器、Python 3 等也不是最新的。如果想要使用更新的版本,必然就需要更新 NOI Linux 的版本。这会带来很多工作。
所以,我也希望各位选手能够换位思考。版本太低确实是个问题,但我们需要给予一定的时间与支持,毕竟这看似简单的更新,工作量其实还是挺大的。我们的选手在课余也可以学习更专业的计算机科学,如工程级编程等。这也会对我们自身竞赛有一定的帮助。最简单也是最直观的例子,代码可读性高更容易排查错误,学会使用调试器比手写输出调试信息要更方便,等等。
现在就希望 CCF 已经开始做这件事了吧。毕竟计算机科学高速发展,我们的教育要紧跟时代,竞赛自然不能落下。
结语
希望我的论述已经足够清楚,能够解决一些选手的疑惑。当然,若鄙人的拙著能有幸被 CCF 看到,自然也是非常开心的。
鄙人不才,还望各位道友不吝赐教,鄙人定万分感谢。(人话:发现任何勘误请在评论区指出,感谢支持!)