CSP-S2022 初赛 完善程序-1
题面
已知两个长度均为 \(n\) 的有序数组 \(a_1\) 和 \(a_2\)(均为递增序,但不保证严格单调递增),并且给定正整数 \(k (1 \leq k \leq 2n)\),求数组 \(a_1\) 和 \(a_2\) 归并排序后的数组里第 \(k\) 小的数值。
试补全程序。
#include <bits/stdc++.h>
using namespace std;
int solve(int *a1, int *a2, int n, int k) {
int left1 = 0, right1 = n - 1;
int left2 = 0, right2 = n - 1;
while (left1 <= right1 && left2 <= right2) {
int m1 = (left1 + right1) >> 1;
int m2 = (left2 + right2) >> 1;
int cnt = ①;
if (②) {
if (cnt < k) left1 = m1 + 1;
else right2 = m2 - 1;
} else {
if (cnt < k) left2 = m2 + 1;
else right1 = m1 - 1;
}
}
if (③) {
if (left1 == 0) {
return a2[k - 1];
} else {
int x = a1[left1 - 1], ④;
return std::max(x, y);
}
} else {
if (left2 == 0) {
return a1[k - 1];
} else {
int x = a2[left2 - 1], ⑤;
return std:: max(x, y);
}
}
}
① ~ ⑤ 处应填( )
| 题号 | A | B | C | D |
|---|---|---|---|---|
| 1. | A. (m1 + m2) * 2 |
B. (m1 - 1) + (m2 - 1) |
C. m1 + m2 |
D. (m1 + 1) + (m2 + 1) |
| 2. | A. a1[m1] == a2[m2] |
B. a1[m1] <= a2[m2] |
C. a1[m1] >= a2[m2] |
D. a1[m1] != a2[m2] |
| 3. | A. left1 == right1 |
B. left1 < right1 |
C. left1 > right1 |
D. left1 != right1 |
| 4. | A. y = a1[k - left2 - 1] |
B. y = a1[k - left2] |
C. y = a2[k - left1 - 1] |
D. y = a2[k - left1] |
| 5. | A. y = a1[k - left2 - 1] |
B. y = a1[k - left2] |
C. y = a2[k - left1 - 1] |
D. y = a2[k - left1] |
答案
1~5 CBCCA
解析
我们先把握这份代码的大体思路。令最终第 \(k\) 小的数为 \(x\),记数组 \(a_1\) 中所有小于等于 \(x\) 的数为区间 \([0, k_1]\),数组 \(a_2\) 中所有小于等于 \(x\) 的数为区间 \([0, k_2]\)。则这份代码先通过二分求出 \(k_1\) 或 \(k_2\) 中的某一个,然后答案就容易确定了。
那么如何进行二分呢?我们发现这个二分的方式与我们平时所写的二分不同,各种“+1”、“-1”,甚至根据选项和代码的对称性推测出对于每一个数组,最终 \(\mathrm{left}\) 和 \(\mathrm{right}\) 的值不一样,这使得代码的理解难度高了许多。事实上,我们可以通过观察发现最终作为答案的是 \(\mathrm{left}\) 而不是 \(\mathrm{right}\)。又根据最后返回的下标类似于 \(\mathrm{left} - 1\)、\(k - 1\),我们可以推断出 \(\mathrm{left}\) 表示的是数组中小于等于 \(x\) 的元素个数而非下标。此时最后两空的答案就可以确定了:既然一个数组中有 \(\mathrm{left}\) 个元素小于等于 \(x\),那么另一个数组中就有 \(k - \mathrm{left}\) 小于等于 \(x\)。于是,第 4 空选 C,第 5 空选 A。
我们再回过头来看二分。观察到二分边界 \(\mathrm{right}\) 的初值是 \(n - 1\),但由于我们已经知道二分边界表示的是元素个数而不是下标,事实上最开始二分边界也应该包含元素个数为 \(n\) 的情况。又根据后面恶心的“+1”“-1”(注意到并没有某种情况取等),可以推知 \(\mathrm{left}, \mathrm{right}\) 表示的真正含义是:最终小于等于 \(x\) 的元素个数只可能在区间 \([\mathrm{left}, \mathrm{right} + 1]\) 中(注意这里有“+1”)。
然后二分的过程就容易理解了。我们对两个数组分别计算出 \(\mathrm{mid}\),表示我们要通过考虑 \(a_1\) 的前 \(\mathrm{mid}_1\) 个数和 \(a_2\) 的前 \(\mathrm{mid}_2\) 个数来该边二分的边界。在这些数中,我们取出 \(a_1\) 中最大的和 \(a_2\) 中最大的,分别记为 \(s_1\) 和 \(s_2\)。不妨设 \(s_1 \leq s_2\)。如果这些数总共就不足 \(k\) 个,由不等号的传递性,那么所有元素中小于等于 \(s_1\) 的数的个数就更不可能达到 \(k\) 了,因此最终 \(a_1\) 中小于等于 \(x\) 的数的个数一定比当前的 \(\mathrm{mid}_1\) 大,于是有 \(\mathrm{left}_1 \leftarrow \mathrm{mid}_1 + 1\);否则,这些数已经有 \(k\) 个了,那么所有元素中小于等于 \(s_2\) 的数的个数一定不小于 \(k\),因此最终 \(a_2\) 中小于等于 \(x\) 的数的个数一定不超过当前的 \(\mathrm{mid}_2\),于是有 \(\mathrm{right} + 1 \leftarrow \mathrm{mid}_2\),即 \(\mathrm{right}_2 \leftarrow \mathrm{mid}_2 - 1\)。
此时容易得出剩余空的答案。代码中 \(\mathrm{cnt}\) 表示 \(a_1\) 的前 \(\mathrm{mid}_1\) 个数和 \(a_2\) 的前 \(\mathrm{mid}_2\) 个数的总共数量,因此第 1 空填 C。第二空就是刚才讨论的情况。刚才我们通过“不妨设”只讨论了一种情况,还剩下另一种,但我们发现 if 里面执行的语句刚好符合刚才讨论的情况,因此第 2 空填 B。在第三空时,我们已经确定某一个数组中小于等于 \(x\) 的数的个数,此时应该有 \(\mathrm{left} = \mathrm{right} + 1\) 表示二分边界保证了只有一种可能的答案,即 \(\mathrm{left} > \mathrm{right}\),因此选 C。
综上,我们解决了此题。
浙公网安备 33010602011771号