⭐每日一题⭐专栏

written by SJTU-XHW

本人学识有限,解析难免有错,恳请读者能够批评指正,本人将不胜感激!


LuoGu P1923. 求第 k 小的数

题目描述

输入 $n$($1 \le n < 5000000$ 且 $n$ 为奇数)个数字 $a_i$($1 \le a_i < {10}^9$),输出这些数字的第 $k$ 小的数。最小的数是第 0 小。

样例

样例 1 输入

1
2
5 1
4 3 2 1 5

样例 1 输出

1
2

思路 和 实现

最好想的方法是快速排序,再索引找,这种方法时间 $O(n\cdot log\space n)$,空间 $O(log\space n)$;但这稍微卡一下时间就不行,必须比快排更快一点;

回忆数据结构书上的内容,有两种做法:

  1. 使用最小化堆作优先级队列,建堆并出队 $k+1$ 次即可,时间 $O(n+k\cdot log(n))$,空间 $O(1)$(存储原始数据的空间不算辅助空间);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #include <iostream>
    using namespace std;

    void percolateDown(int* arr, int idx, int size) {
    int next = 2 * idx + 1; int tmp = arr[idx];
    while (1) {
    if (next + 1 < size && arr[next] > arr[next + 1]) ++next;
    if (next >= size || arr[next] > tmp) {
    arr[idx] = tmp; break;
    }
    arr[idx] = arr[next]; idx = next; next = 2 * idx + 1;
    }
    }

    int main() {
    int n, k; cin >> n >> k;
    int* arr = new int[n] {0};
    for (int i = 0; i < n; ++i)
    cin >> arr[i];
    for (int i = (n - 1) / 2; i >= 0; --i)
    percolateDown(arr, i, n);
    // 无需使用堆排序的办法,因为不要求保留原来的数据
    for (int i = 0; i < k; ++i) {
    arr[0] = arr[n - i - 1];
    percolateDown(arr, 0, n - i - 1);
    }
    cout << arr[0];
    delete[] arr;
    return 0;
    }
  2. 使用最大化堆作优先级队列,先以前 $k+1$ 项建堆,后 $n-k-1$ 项依次与队头比较:队头较大则出队,将当前数据进队;结束后队列中 $k+1$ 个数是数组中前 $k+1$ 小的数,所以队头是第 $k$ 小的数(题干说最小的数是第 0 小);时间 $O(n\cdot log(k))$,空间 $O(1)$;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #include <iostream>
    using namespace std;

    void percolateDown(int* arr, int idx, int size) {
    int next = 2 * idx + 1; int tmp = arr[idx];
    while (1) {
    if (next + 1 < size && arr[next] < arr[next + 1]) ++next;
    if (next >= size || arr[next] < tmp) {
    arr[idx] = tmp; break;
    }
    arr[idx] = arr[next]; idx = next; next = 2 * idx + 1;
    }
    }

    int main() {
    int n, k; cin >> n >> k;
    int* arr = new int[n] {0};
    for (int i = 0; i < n; ++i)
    cin >> arr[i];
    for (int i = k / 2; i >= 0; --i)
    percolateDown(arr, i, k + 1);
    for (int i = k + 1; i < n; ++i) {
    if (arr[i] < arr[0]) {
    int tmpOut = arr[0], tmpIn; arr[0] = arr[k]; // 先出队
    percolateDown(arr, 0, k);
    tmpIn = arr[k] = arr[i]; int cur = k, nxt = (cur - 1) / 2;
    while (1) { // 再入队
    if (cur == 0 || arr[nxt] > tmpIn) {
    arr[cur] = tmpIn; break;
    }
    arr[cur] = arr[nxt]; cur = nxt; nxt = (cur - 1) / 2;
    }
    arr[i] = tmpOut; // 没必要,但是这样做不损失数据
    }
    }
    cout << arr[0];
    delete[] arr;
    return 0;
    }

您猜怎么着?两个都 超时 了…… 这出题人明显是压时间了(咬牙切齿😬),看起来时间不做到完全 $O(n)$ 是没法通过的!

上面两个方法会慢一点的原因是,实际上这两种算法还是涉及到了排序的因素,而排序是不必要的,意味着不完全的排序来得到答案,可能花费的时间更少;

快排思想:先进行一轮快排,通过个数计算 第 $k$ 小的数在左右区间的哪个部分,只判断一个部分,这样($T(n)=T(\dfrac{n}{2})+O(n)$)就能快于快排。时间平均 $O(n)$;最坏 $O(n^2)$;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;

int divide(int* arr, int left, int right) {
int pivot = arr[left];
while (left < right) {
while (arr[right] > pivot && left < right) --right;
if (left < right) arr[left] = arr[right];
while (arr[left] <= pivot && left < right) ++left;
if (left < right) arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}

void quickGetK(int* arr, int left, int right, int k) {
if (left >= right) return;
int mid = divide(arr, left, right);
if (mid == k) return;
else if (mid < k) quickGetK(arr, mid + 1, right, k);
else quickGetK(arr, left, mid - 1, k);
}

int main() {
int n, k; cin >> n >> k;
int* arr = new int[n] {0};
for (int i = 0; i < n; ++i) cin >> arr[i];
quickGetK(arr, 0, n - 1, k);
cout << arr[k];
delete[] arr;
return 0;
}

结果,有一个测试集不超时了,还有一个仍然超时……行,方向是对的,那么再把臭名昭著的 cincout 替换成 prinfscanf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <bits/stdc++.h>
using namespace std;

int divide(int* arr, int left, int right) {
int pivot = arr[left];
while (left < right) {
while (arr[right] > pivot && left < right) --right;
if (left < right) arr[left] = arr[right];
while (arr[left] <= pivot && left < right) ++left;
if (left < right) arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}

void quickGetK(int* arr, int left, int right, int k) {
if (left >= right) return;
int mid = divide(arr, left, right);
if (mid == k) return;
else if (mid < k) quickGetK(arr, mid + 1, right, k);
else quickGetK(arr, left, mid - 1, k);
}

int main() {
int n, k; scanf("%d%d", &n, &k);
int* arr = new int[n] {0};
for (int i = 0; i < n; ++i) scanf("%d", &arr[i]);
quickGetK(arr, 0, n - 1, k);
printf("%d", arr[k]);
delete[] arr;
return 0;
}

然后我震惊了,最大的测试集的性能竟然提升了 714 ms !!!懂了,都是读入时候浪费的时间……这样上面提到的 3 种方法都能用,只不过要把 cin/cout 换成 scanf/printf

下次写算法题卡速度再也不敢用 cin/cout 了……【还是 C 靠谱,溜了溜了】


评论
昼夜切换阅读模式