1 条题解
-
0
Guest
-
0
-
B 解释:
- A/D (冒泡/插入排序): 平均和最坏情况时间复杂度均为 O(n²),效率较低。
- B (快速排序): 期望(平均)时间复杂度为 O(n log n),是基于比较的排序算法中非常高效的一种。但其最坏情况(例如,对一个已经排好序的数组进行排序)下时间复杂度会退化到 O(n²)。
- C (计数排序): 是一种非比较排序,时间复杂度为 O(n+k)(其中 k 为整数范围),当 k 远小于 n² 时,效率很高,但不属于 O(n log n) 类别。
- 因此,快速排序的期望时间复杂度符合 O(n log n)。归并排序和堆排序也属于此类,且它们的最坏时间复杂度也是 O(n log n)。
-
D 解释: 排序算法的稳定性指,对于数组中两个值相等的元素,在排序后它们的相对位置不发生改变。
- A (选择排序): 不稳定。在每一轮选择最小元素时,将其与当前位置元素交换,这个交换操作可能会打乱其他相等元素的相对顺序。
- B (堆排序): 不稳定。建堆和调整堆的过程都会涉及元素交换,可能打乱相等元素的顺序。
- C (快速排序): 不稳定。Partition 操作中,基准值(pivot)的交换可能导致相等元素的顺序改变。
- D (归并排序): 稳定。在合并两个有序子数组时,如果遇到相等的元素,我们可以规定总是先取左边子数组的元素,这样就能保证它们的相对顺序不变。
-
B 解释: 二分查找的核心思想是每次将搜索范围缩小一半。这依赖于数据的单调性。即数据必须是升序或降序排列的。只有这样,我们才能通过比较中间值与目标值的大小,来确定目标值在哪一半区间,从而舍弃另一半。数组中可以有重复值,元素也可以是浮点数或其他可比较类型,长度也无特殊要求。
-
B 解释: 广度优先搜索(BFS)按照“逐层”的顺序进行遍历,即先访问所有距离起点为1的节点,再访问所有距离为2的节点,以此类推。这正好符合队列**“先进先出”(FIFO)**的特性。我们将起点入队,然后不断从队头取出节点,并将其所有未访问过的邻居节点加入队尾。
- 栈(先进后出 LIFO)通常用于实现深度优先搜索(DFS)。
-
C 解释:
- 首先,
1 << k会生成一个掩码(mask),这个掩码只有第 k 位是 1,其他位都是 0。 - 然后,
~(1 << k)对这个掩码进行按位取反,得到一个新掩码,它的第 k 位是 0,而其他所有位都是 1。 - 最后,将原始变量
x与这个新掩码进行按位与&运算。由于任何位与 1 相与都保持不变,与 0 相与都会变为 0,因此这个操作能精确地将x的第 k 位清零,同时保持其他位不变。 - 其他选项:A 是将第 k 位置1;B 是检查第 k 位是否为1;D 是将第 k 位翻转(0变1,1变0)。
- 首先,
-
B 解释: 当 n 较大时(例如
int类型的 n 接近其上限),n * (n + 1)的结果很可能会超出int的表示范围,导致整数溢出,得到一个错误的负数或小数值。- 写法 B 通过
(long long)n将变量 n 强制类型转换为 64 位的long long类型。这样一来,整个表达式(long long)n * (n + 1)都会在 64 位下进行计算,其结果的范围远大于int,从而极大地降低了溢出风险。 - A 和 C 的问题是相同的,乘法发生在
int类型下。D 循环相加虽然不会因乘法溢出,但效率远低于公式法,且累加过程也可能溢出。
- 写法 B 通过
-
C 解释:
- C (Dijkstra): 是解决非负权图单源最短路问题的标准算法。通过使用优先队列(通常是二叉堆)进行优化后,其时间复杂度可以达到 O(E log V) 或 O(E + V log V),非常高效。
- A (Floyd-Warshall): 用于计算图中任意两点间的最短路(所有源),时间复杂度为 O(V³),对于单源问题来说过于耗时。
- B/D (Bellman-Ford/SPFA): 这两种算法可以处理带负权边的图(但不能有负权环)。在没有负权边的情况下,它们的效率通常低于堆优化的 Dijkstra 算法。
-
A 解释: 哈希表通过哈希函数将键(key)映射到数组的索引上,理想情况下可以直接定位到元素。虽然存在哈希冲突(多个键映射到同一索引)的可能性,但在一个设计良好的哈希表中(哈希函数均匀、负载因子合适),解决冲突的开销很小。因此,其平均和摊还时间复杂度为O(1)。但在最坏情况下(所有键都冲突),查找时间会退化到 O(n)。
-
B 解释: 闰年的规则是:
- 能被4整除的年份是闰年(例如 2024)。
- 但是,如果该年份能被100整除,它就不是闰年(例如 1900)。
- 有一个例外:如果该年份能被400整除,它仍然是闰年(例如 2000)。
将这三条规则合并,就得到了 B 选项的逻辑表达式:
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)。
-
C 解释: C/C++ 为了性能,默认不进行数组边界检查。访问越界是一种未定义行为(Undefined Behavior, UB)。这意味着标准没有规定此时程序必须做什么。可能的结果包括:
- 程序崩溃(段错误)。
- 读取或修改了不相关的内存数据,导致程序逻辑错误且难以调试。
- 在某些情况下,程序可能看起来“正常”运行,但存在潜在的巨大风险。 它不会在编译或链接阶段报错,因为这是一个运行时问题。
-
B 解释: 当并查集同时使用路径压缩(在 find 操作时,将路径上所有节点直接指向根节点)和按秩合并(按大小或深度合并,将小的集合合并到大的集合上)这两种优化时,其操作的均摊时间复杂度可以达到 O(α(n))。
α(n)是反阿克曼函数,它增长得极其缓慢。对于任何现实世界中可能遇到的 n(例如宇宙中所有原子的数量),α(n)的值都不会超过 5。因此,这个复杂度可以认为是近似常数时间,但理论上它并非严格的 O(1)。
-
C 解释:
a在模m意义下的乘法逆元是指一个整数x,满足(a * x) ≡ 1 (mod m)。 这个逆元存在的充要条件是a和m互质,即它们的最大公约数gcd(a, m) = 1。- 如果满足此条件,可以使用扩展欧几里得算法求出逆元。
- 一个特殊情况是,当
m为质数且a不是m的倍数时,gcd(a, m)必然为 1,此时也可以用费马小定理 (a^(m-2) mod m) 来求逆元。
-
A 解释: 快速幂算法利用了指数
n的二进制表示。它通过不断地将底数a平方 (a -> a² -> a⁴ -> ...),并根据n的二进制位来决定是否将当前结果乘上对应的a的幂。由于n的二进制表示大约有log₂n位,算法的循环次数与log n成正比,因此时间复杂度为 O(log n),远快于 O(n) 的朴素乘法。 -
D 解释: A、B、C 都是比较排序,它们通过比较元素间的大小关系来确定元素的顺序,其时间复杂度的理论下界是 O(n log n)。
- D (计数排序): 是一种非比较排序(或称线性时间排序)。它不比较元素,而是通过统计每个元素出现的次数,然后根据这个统计信息直接计算出每个元素在排序后数组中的位置。它适用于整数范围不大(k 较小)的场景,时间复杂度为 O(n+k)。
-
B 解释: 在无权图中,任意一条边的权重都可以看作是 1。因此,两点间的最短路径就是包含边数最少的路径。
- B (BFS): 广度优先搜索(BFS)从源点开始,逐层向外扩展。它访问节点的顺序恰好是按照离源点的距离(即边数)从小到大进行的。因此,当 BFS 第一次到达目标节点时,所经过的路径必然是边数最少的,即最短路径。BFS 是解决无权图最短路问题最直接、最高效的方法,时间复杂度为 O(V+E)。
-
C 解释: 二分答案是一种将最优化问题(例如求最大值的最小值)转化为一系列判定性问题(“某个答案
x是否可行?”)的技巧。 能够使用该技术的前提是,这个判定函数check(x)必须具有单调性。例如,如果答案x是可行的,那么所有比x“更优”或“更容易”的答案(如比x小的值)也都是可行的。这种单调性使得我们可以在答案的定义域上进行二分搜索,从而快速逼近最优解。 -
D 解释: 由于计算机使用二进制表示浮点数,会存在精度误差。两个在数学上相等的数,在计算机中存储时可能存在微小的差异。
- A (直接使用
==): 非常不可靠,很可能因为微小的精度差而判断错误。 - D (使用
eps): 是标准做法。我们不直接判断a == b,而是判断它们差的绝对值是否足够小,即abs(a - b) < eps。其中eps是一个预先设定的非常小的正数(如1e-8或1e-9),称为精度或容差。
- A (直接使用
-
A 解释: 根据 C++ 标准,有符号整数的溢出是未定义行为(Undefined Behavior, UB)。
- B (回绕): 这是无符号整数(如
unsigned int)溢出时的行为,它们会进行模运算。 - 有符号整数溢出则没有任何保证。编译器可能会做任何事情:产生一个看似随机的值、程序崩溃、或者基于“溢出不会发生”的假设进行代码优化,导致难以预料的逻辑错误。因此,必须极力避免有符号整数溢出。
- B (回绕): 这是无符号整数(如
-
C 解释:
std::lower_bound(first, last, key)返回一个迭代器,指向有序区间[first, last)中第一个不小于(not less than)key的元素。在升序数组中,“不小于”就等价于**“大于或等于(>=)”**。- 与之对应的是
std::upper_bound,它查找第一个大于key的元素。
- 与之对应的是
-
D 解释: LCA 的倍增算法分为两个阶段:
- 预处理 (O(n log n)): 对树进行一次 DFS/BFS,计算每个节点的深度和父节点。然后通过动态规划(或递推)填充一个
up[i][j]数组,表示节点i向上跳2^j步所到达的祖先。这个过程需要 O(n log n) 的时间和空间。 - 查询 (O(log n)): 对于每次查询
LCA(u, v),先将较深的节点跳到与较浅节点相同的深度,然后再将两个节点同时向上跳(利用up数组),直到它们的父节点相同。这个跳跃过程的复杂度是 O(log n)。
- 预处理 (O(n log n)): 对树进行一次 DFS/BFS,计算每个节点的深度和父节点。然后通过动态规划(或递推)填充一个
-
B 解释:
- A/C: 当
l和r都很大时(例如接近INT_MAX),l + r的和可能会超出int的表示范围,导致溢出,得到一个错误的结果。>> 1本质上和/ 2一样,有相同的风险。 - B: 这种写法是最优的。它首先计算
r和l的距离(r - l),这个差值一定不会溢出(因为r >= l)。然后将距离的一半加到l上,数学上等价于(l+r)/2,但计算过程避免了两个大数直接相加,从而防止了溢出。 - D: 将
l+r转换为unsigned可能会避免溢出(无符号数会回绕),但如果结果超出了有符号数的正数范围,再转回int时可能会得到负数,同样是错误的。
- A/C: 当
-
B 解释: 在 C++ 中,当除法的两个操作数都是整数时,执行的是整数除法。整数除法的结果会向零取整(truncate towards zero),即直接舍弃小数部分。
3 / 2的数学结果是 1.5。舍弃小数部分后,得到1。- 同理,
-3 / 2的结果会是-1。
-
B 解释:
cin >> var;在读取数据后,会将我们输入时敲下的回车符(换行符\n)留在输入缓冲区中。getline函数会读取这一行直到遇到换行符,因此它会立即读到这个残留的换行符并结束,导致读入一个空字符串。- B (
cin.ignore()): 是标准且正确的做法。cin.ignore(numeric_limits<streamsize>::max(), '\n');会丢弃输入缓冲区中当前位置到下一个换行符(包括换行符)之间的所有字符,从而清空这一行,确保getline能正确读取下一行输入。 - A (
fflush(stdin)): 在 C++ 标准中是未定义行为,不保证有效且不应使用。
- B (
-
C 解释:
memset函数是按**字节(byte)**来对内存进行赋值的。- 一个
int通常占 4 个字节。-1 的二进制补码表示中,所有位都是 1(即0xFFFFFFFF),所以它的每个字节都是0xFF。因此memset(a, -1, sizeof a)恰好能将每个int都正确设置为 -1。 - 然而,
memset(a, 1, sizeof a)会将每个字节都设为 1(0x01),这会导致每个int的值变为0x01010101,而不是1。 - C (
std::fill): 是 C++ 中推荐的方式。fill是一个模板函数,它会正确地按元素类型进行赋值,无论初始化的值是多少,也无论数组是什么类型(int,double等),都安全可靠。
- 一个
-
B 解释:
- A:
for (auto x : v)默认会复制容器中的每个元素到变量x。修改x只是在修改这个副本,容器v本身的元素不会改变。 - B:
for (auto& x : v)使用了引用&。此时x成为容器中元素的别名(或引用),而不是副本。对x的任何修改都会直接作用于容器v中对应的原始元素。这是修改元素的正确方式。 - C:
const表示x是一个只读的副本,尝试修改它会导致编译错误。
- A:
-
C 解释: 不同的整型需要不同的格式化占位符来保证正确解析和输出:
%d: 用于int。%u: 用于unsigned int。%ld: 用于long int。%lld: 用于long long int。%llu: 用于unsigned long long int。 使用错误的占位符会导致输出错误的值,甚至程序崩溃。
-
D 解释:
- A (
x % 2 == 1): 对于负奇数是错误的。在 C++ 中,-5 % 2的结果是-1,所以-1 == 1为假。 - D (
x % 2 != 0): 这是最稳健的写法。对于任何奇数(正或负),模 2 的结果都是非零的(1 或 -1);对于任何偶数,结果都是 0。此判断完全正确。 - B (
x & 1): 利用位运算,判断x的最低二进制位是否为 1。奇数的最低位总是 1,偶数总是 0。这种方法正确且通常效率最高。在题目选项中,B 和 D 都正确,但考虑到对%运算符的兼容性,D 更具普适性。
- A (
-
A 解释:
unordered_map是基于哈希表实现的。- 平均情况 (O(1)): 在哈希函数良好、没有大量冲突的情况下,查找、插入和删除操作的平均时间复杂度是常数时间 O(1)。
- 最坏情况 (O(n)): 如果发生严重的哈希冲突,导致大量元素被映射到同一个桶(bucket)中,哈希表会退化成一个链表(或红黑树,在较新标准中)。此时,查找操作需要遍历这个长链表,时间复杂度退化为线性时间 O(n)。
-
B 解释: C/C++ 标准严格规定了位移操作的限制:
- 对负数进行左移是未定义行为 (UB)。
- 位移的位数是负数,或大于等于操作数类型的总位数(例如,对 32 位
int左移 32 位或更多),也是未定义行为 (UB)。 这意味着结果完全不可预测,程序可能崩溃或产生任何奇怪的结果,应绝对避免。
-
B 解释:
- 默认情况下,C++ 的 iostream(
cin,cout)与 C 的 stdio(scanf,printf)是同步的,以保证混合使用时输入输出顺序符合预期。但这会带来性能开销。 ios::sync_with_stdio(false);关闭同步,可以大幅提升cin/cout的性能。但关闭后,就不应再混用 C 和 C++ 的 IO 流,否则可能导致输入输出顺序错乱。cin.tie(nullptr);解绑cin和cout。默认cin绑定了cout,意味着每次执行输入操作 (cin) 之前,都会强制刷新输出缓冲区 (cout)。解绑可以避免这种不必要的刷新,进一步提速。- 在竞赛编程中,为追求极致性能,通常会同时执行这两项操作,并且之后只使用
cin/cout。
- 默认情况下,C++ 的 iostream(
-
C 解释: 这个问题是差分数组的经典应用。
- 差分数组
d[i] = a[i] - a[i-1]记录了原数组a相邻元素的差。 - 区间增加: 对原数组
a的[l, r]区间统一加上v,等价于在差分数组上执行两次单点修改:d[l] += v和d[r+1] -= v。操作复杂度为 O(1)。 - 单点查询: 查询原数组
a[i]的值,等于计算差分数组的前缀和sum(d[1]...d[i])。操作复杂度为 O(i)。 - 对于这个问题,差分数组实现简单,效率极高。而线段树等结构虽然也能实现,但代码更复杂,属于“杀鸡用牛刀”。
- 差分数组
-
C 解释: 这个问题同时涉及区间查询(最小值)和区间修改(批量增加),是线段树的典型应用场景。
- C (线段树): 线段树可以将区间信息(如最小值、和)存储在树节点上。对于区间修改,使用**懒标记(Lazy Propagation)**可以避免遍历整个区间,将修改操作的复杂度从 O(n) 降低到 O(log n)。区间查询的复杂度也是 O(log n)。
- D (稀疏表): 支持快速的区间查询(RMQ问题可达 O(1)),但不支持修改。
-
B 解释: 一个图是二分图,当且仅当它的所有顶点可以被分成两个不相交的集合 U 和 V,使得所有边都连接 U 和 V 中的顶点(集合内部没有边)。这等价于图中不存在奇数长度的环。
- B (染色法): 是判断二分图的标准方法。从任意一个未访问的顶点开始,用 DFS 或 BFS 遍历图。将起点染成颜色1,其所有邻居染成颜色2,邻居的邻居再染回颜色1,以此类推。如果在遍历过程中发现一个顶点和它的邻居颜色相同,说明存在奇数环,该图就不是二分图。
-
B 解释:
- B (Trie 树): 是为处理字符串前缀问题而生的数据结构。它将字符串按字符存入一棵树中,从根节点到任意节点的路径都代表一个前缀。要查找以 "pre" 为前缀的所有字符串,只需沿着 "pre" 的路径走到对应节点,该节点下的整个子树就代表了所有符合条件的字符串。效率极高。
- A/C (KMP/Z算法): 主要用于在一个文本串中查找一个模式串,不适合处理大量字符串的前缀匹配问题。
-
A 解释:
- A (双栈队列): 可以实现均摊 O(1) 的复杂度。一个栈(inStack)用于入队,另一个栈(outStack)用于出队。入队操作直接 push 到 inStack (O(1))。出队时,若 outStack 为空,则将 inStack 的所有元素依次弹出并压入 outStack,这个过程耗时 O(k),k 为当时 inStack 的大小。然后从 outStack 弹出栈顶。虽然单次出队最坏是 O(n),但每个元素一生最多只会被移动一次(inStack -> outStack),所以 n 次操作的总时间是 O(n),均摊到每次操作就是 O(1)。
- B 和 C 实现的队列,单次操作的最坏时间复杂度本身就是严格的 O(1)。
-
C 解释: 对于
10⁹级别的数据,暴力枚举和分解质因数都太慢了。- C (欧几里得算法): 也叫辗转相除法,基于
gcd(a, b) = gcd(b, a % b)的原理。它的时间复杂度大约是 O(log min(a, b)),对于大整数计算非常快,是求最大公约数的标准算法。 - D (扩展欧几里得算法): 用于求解
ax + by = gcd(a, b)的一组解 (x, y),虽然也能求出 gcd,但比单纯的欧几里得算法要复杂。
- C (欧几里得算法): 也叫辗转相除法,基于
-
B 解释:
- A (Dijkstra): 其贪心策略(每次选择当前最近的点)在有负权边时会失效,可能得到错误答案。
- B (Bellman-Ford): 能够正确处理带有负权边的图,只要图中不存在从源点可达的负权环。其时间复杂度为 O(V * E),是这类问题的标准解法。
- D (SPFA): 是 Bellman-Ford 的队列优化版本,在大多数情况下比 Bellman-Ford 快,但其最坏时间复杂度仍然是 O(V * E),并且在一些特殊构造的图上性能会退化,不如 Bellman-Ford 稳定。
-
B 解释:
- B (
std::fill): 是最清晰、最安全、最符合 C++ 风格的做法。它按元素类型进行赋值,你可以精确地将数组设置为你定义的任何极大值常量INF。 - A (
memset):memset(dist, 0x3f, sizeof(dist))是一个常用技巧。它将每个字节都设置为0x3f,使得每个 4 字节的int值为0x3f3f3f3f。这个数大约是10^9,足够大,且两个这样的数相加不会溢出int,方便用于最短路等算法。但它是一种技巧,可读性不如fill。 - C (
memset): 除非INF恰好是 0 或 -1,否则这种写法是错误的,因为它按字节赋值。
- B (
-
B 解释: 乘法逆元存在的充要条件是
gcd(a, p) = 1。a ≡ 0 (mod p)意味着a是p的倍数。- 由于
p是质数,a作为p的倍数(且a不为0时),它们的最大公约数gcd(a, p) = p。 - 因为
p是质数,所以p > 1。因此gcd(a, p) ≠ 1,故a在模p意义下不存在乘法逆元。
-
B 解释:
- A (0/1 背包): 每种物品只有一件,要么选,要么不选。
- B (完全背包): 每种物品有无限件,可以选任意多件。
- C (多重背包): 每种物品有有限的
k件。 - D (分组背包): 物品被分成若干组,每组中最多只能选一件。
-
信息
- ID
- 9810
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 0
- 上传者