CSP-J 2019 入门级 第一轮(初赛) 完善程序(2)
【题目】
CSP-J 2019 入门级 第一轮(初赛) 完善程序(2)
(计数排序)计数排序是一个广泛使用的排序方法。下面的程序使用双关键字计数排序,将n对10000 以内的整数,从小到大排序。
例如有三对整数 (3,4)、(2,4)、(3,3),那么排序之后应该是(2,4)、(3,3)、(3,4) 。
输入第一行为n,接下来n行,第i行有两个数a[i]和b[i],分别表示第i对整数的第一关键字和第二关键字。
从小到大排序后输出。
数据范围
1
<
n
<
1
0
7
1<n<10^7
1<n<107,
1
<
a
[
i
]
,
b
[
i
]
<
1
0
4
1<a[i],b[i]<10^4
1<a[i],b[i]<104
提示:应先对第二关键字排序,再对第一关键字排序。数组 ord[] 存储第二关键字排序的结果,数组 res[] 存储双关键字排序的结果。
试补全程序。
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 10000000;
const int maxs = 10000;
int n;
unsigned a[maxn], b[maxn],res[maxn], ord[maxn];
unsigned cnt[maxs + 1];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i)
scanf("%d%d", &a[i], &b[i]);
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; ++i)
①; // 利用 cnt 数组统计数量
for (int i = 0; i < maxs; ++i)
cnt[i + 1] += cnt[i];
for (int i = 0; i < n; ++i)
②; // 记录初步排序结果
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; ++i)
③; // 利用 cnt 数组统计数量
for (int i = 0; i < maxs; ++i)
cnt[i + 1] += cnt[i];
for (int i = n - 1; i >= 0; --i)
④ // 记录最终排序结果
for (int i = 0; i < n; i++)
printf("%d %d", ⑤);
return 0;
}
- ①处应填()
A. ++cnt[i]
B. ++cnt[b[i]]
C. ++cnt[a[i] * maxs + b[i]]
D. ++cnt[a[i]] - ②处应填()
A. ord[–cnt[a[i]]] = i
B. ord[–cnt[b[i]]] = a[i]
C. ord[–cnt[a[i]]] = b[i]
D. ord[–cnt[b[i]]] = i - ③处应填()
A. ++cnt[b[i]]
B. ++cnt[a[i] * maxs + b[i]]
C. ++cnt[a[i]]
D. ++cnt[i] - ④处应填()
A. res[–cnt[a[ord[i]]]] = ord[i]
B. res[–cnt[b[ord[i]]]] = ord[i]
C. res[–cnt[b[i]]] = ord[i]
D. res[–cnt[a[i]]] = ord[i] - ⑤处应填()
A. a[i], b[i]
B. a[res[i]], b[res[i]]
C. a[ord[res[i]]],b[ord[res[i]]]
D. a[res[ord[i]]],b[res[ord[i]]]
【题目考点】
1. 索引排序
2. 计数排序
3. 排序的稳定性
4. 前缀和
【解题思路】
对数对进行排序,目标顺序为:先按第一个数字从小到大排序,如果第一个数字相同,再按第二数字从小到大排序。
如果使用稳定的排序算法,则可以先按第二个数字从小到大对所有数对排序,此时数对序列的顺序已经满足第二个数字是升序的。再按照第一个数字从小到大排序,如果第一个数字相同,由于使用的是稳定的排序算法,数对的第二个数字会按照其原有的升序顺序排列,最终结果就符合了目标顺序。
本题使用的是计数排序。
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 10000000;
const int maxs = 10000;
int n;
unsigned a[maxn], b[maxn],res[maxn], ord[maxn];
unsigned cnt[maxs + 1];
题目说了,有n对10000以内的整数,代码中常量maxs是10000,以maxs为长度的数组cnt应该为计数数组,cnt[i]
表示数值i出现的次数。
int main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i)
scanf("%d%d", &a[i], &b[i]);
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; ++i)
①; // 利用 cnt 数组统计数量
输入每个数对
(
a
i
,
b
i
)
(a_i,b_i)
(ai,bi),先将计数数组cnt每个元素初始化为0。而后要做的就是“利用 cnt 数组统计数量”。题目的提示中说了:“应先对第二关键字排序,再对第一关键字排序”,也就是应该先根据第二个关键字从小到大对各数对进行排序。先对第二个关键字进行计数,此时cnt[b[i]]
表示第二个关键字值为b[i]
的数对的数量。第i个数对为
(
a
i
,
b
i
)
(a_i,b_i)
(ai,bi),那么第二个关键字为b[i]
的数对的数量应该增加1,即cnt[b[i]]++
,第(1)空填cnt[b[i]]++
,选B。
for (int i = 0; i < maxs; ++i)
cnt[i + 1] += cnt[i];
for (int i = 0; i < n; ++i)
②; // 记录初步排序结果
遍历执行cnt[i+1] += cnt[i]
,cnt数组经过更新后变成了自己的前缀和。
cnt[b[i]]
的概念变为第二个关键字小于等于b[i]
的数对的数量,其中最后一个数对就是从1开始数的第cnt[b[i]]
个数对,也就是从0开始数的第cnt[b[i]]-1
个数对。
假想按照输入顺序得到原数对序列为
s
s
s,排序后的目标数对序列为
t
t
t,由于原序列
s
s
s和目标序列
t
t
t都是下标从0开始的,此时cnt[b[i]]-1
也就是所有第二个关键字小于等于b[i]
的数对按照第二个关键字升序排序后最后一个数对的下标,也就是第二个关键字等于b[i]
的最后一个数对的下标。
目标序列
t
t
t的第i个(下标i位置)的元素
t
[
i
]
t[i]
t[i]在原序列中
s
s
s的下标为ord[i]
,也就是满足
t
[
i
]
=
s
[
o
r
d
[
i
]
]
t[i] = s[ord[i]]
t[i]=s[ord[i]],ord数组即为索引数组。
看原序列
s
s
s的第i数对
s
[
i
]
s[i]
s[i],其第二个关键字为b[i]
。第二个关键字为b[i]
的所有数对在目标序列中最后一个数对的下标为cnt[b[i]]-1
,先让cnt[b[i]]
减少1,即--cnt[b[i]]
。将当前第i数对
s
[
i
]
s[i]
s[i]放在目标序列
t
t
t下标cnt[b[i]]
位置,也就是
t
[
c
n
t
[
b
[
i
]
]
]
=
s
[
i
]
t[cnt[b[i]]]=s[i]
t[cnt[b[i]]]=s[i],那么目标序列第cnt[b[i]]
元素在原序列中的下标为i,因此设ord[cnt[b[i]]] = i
。
cnt[b[i]]
减少1后,下一个第二个关键字为
b
i
b_i
bi的数对在目标序列中的最后一个元素的下标就是cnt[b[i]]-1
。可以重复上述过程,求出该数对在排序后的下标。因此第(2)空填ord[--cnt[b[i]]] = i
,选D。
看一个使用上述过程进行计数排序的例子:原序列a为1 2 1 2 1
遍历计数,得cnt[1]
:3,cnt[2]
:2
cnt变为自己的前缀和后,cnt[1]
:3,cnt[2]
:5
排序后的目标序列为t序列,ord[i]
是t[i]
在a序列中的下标。
遍历a序列,a[0]
为1,最后一个1应该放在cnt[1]-1=2
位置,所以先--cnt[1]
,cnt[1]
变为2,而后t[cnt[1]]=1
,ord[cnt[a[0]]]=ord[2]=0
。。
a[1]
为2,最后一个2应该放在cnt[2]-1=4
位置,所以先--cnt[2]
,而后t[cnt[2]]=2
,ord[cnt[a[1]]]=ord[4]=1
a[2]
为1,最后一个1应该放在cnt[1]-1=1
位置,所以先--cnt[1]
,cnt[1]
变为1,而后t[cnt[1]]=1
,ord[cnt[a[2]]]=ord[1]=2
依此类推,填表后结果为:
下标 0 1 2 3 4 t a[4]:1
a[2]:1
a[0]:1
a[3]:2
a[1]:2
ord 4 2 0 3 1
i从0到n-1循环,重复上述过程,即可求出ord数组,得到了每个在目标序列中的数值在原序列中的下标,也就是求出了按照第二个关键字 b i b_i bi排序后的目标序列。
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; ++i)
③; // 利用 cnt 数组统计数量
for (int i = 0; i < maxs; ++i)
cnt[i + 1] += cnt[i];
接下来要对根据第二关键字排序后的序列再根据第一关键字排序。
此时应该对a[i]
进行计数,cnt[a[i]]
此时表示a[i]
出现的次数。第(3)空填++cnt[a[i]]
,选C。
而后又是cnt[i+1] += cnt[i]
操作将cnt变为自己的前缀和。此时cnt[a[i]]
为第一个关键字小于等于a[i]
的数对的数量。序列下标从0开始,按第一个关键字排序后下标cnt[a[i]]-1
位置就是第一个关键字为a[i]
的最后一个数对的下标。
for (int i = n - 1; i >= 0; --i)
④ // 记录最终排序结果
for (int i = 0; i < n; i++)
printf("%d %d", ⑤);
return 0;
}
经过第一趟按照第二个关键字排序后得到数对序列
t
t
t,现在对
t
t
t序列进行遍历,按照第一个关键字进行计数排序,排序后的目标序列
u
u
u。目标序列
u
u
u中第i元素
u
[
i
]
u[i]
u[i]在原序列
s
s
s中的下标为res[i]
,即
u
[
i
]
=
s
[
r
e
s
[
i
]
]
u[i] = s[res[i]]
u[i]=s[res[i]],res也是索引数组。
其中
t
[
i
]
t[i]
t[i]的第一个关键字记为
t
[
i
]
.
a
=
s
[
o
r
d
[
i
]
]
.
a
t[i].a=s[ord[i]].a
t[i].a=s[ord[i]].a,就是a[ord[i]]
。第二个关键字记为
t
[
i
]
.
b
=
s
[
o
r
d
[
i
]
]
.
b
t[i].b=s[ord[i]].b
t[i].b=s[ord[i]].b,就是b[ord[i]]。
遍历
t
t
t序列,访问到第i个数对
t
[
i
]
t[i]
t[i],其第一个关键字为
t
[
i
]
.
a
t[i].a
t[i].a,第一个关键字为
t
[
i
]
.
a
t[i].a
t[i].a的最后一个数对在目标序列中
u
u
u的下标为
c
n
t
[
t
[
i
]
.
a
]
−
1
cnt[t[i].a]-1
cnt[t[i].a]−1。
先将
c
n
t
[
t
[
i
]
.
a
]
cnt[t[i].a]
cnt[t[i].a]减少1,而后可以设
u
[
c
n
t
[
t
[
i
]
.
a
]
]
=
t
[
i
]
u[cnt[t[i].a]] = t[i]
u[cnt[t[i].a]]=t[i]
已知
t
[
i
]
=
s
[
o
r
d
[
i
]
]
t[i] = s[ord[i]]
t[i]=s[ord[i]],那么
u
[
c
n
t
[
t
[
i
]
.
a
]
]
=
s
[
o
r
d
[
i
]
]
u[cnt[t[i].a]]=s[ord[i]]
u[cnt[t[i].a]]=s[ord[i]]
根据res的定义,有
r
e
s
[
c
n
t
[
t
[
i
]
.
a
]
]
=
o
r
d
[
i
]
res[cnt[t[i].a]]=ord[i]
res[cnt[t[i].a]]=ord[i]
已知
t
[
i
]
.
a
t[i].a
t[i].a就是a[ord[i]]
,那么有res[cnt[a[ord[i]]]]=ord[i]
整合上面将
c
n
t
[
t
[
i
]
.
a
]
cnt[t[i].a]
cnt[t[i].a]减1的过程,实际需要执行的语句为res[--cnt[a[ord[i]]]] = ord[i]
,第(4)空选A。
c
n
t
[
t
[
i
]
.
a
]
cnt[t[i].a]
cnt[t[i].a]减少1后,下一次遇到第一个关键字为
t
[
i
]
.
a
t[i].a
t[i].a的数对,该数对应该在目标序列
u
u
u的下标
c
n
t
[
t
[
i
]
.
a
]
−
1
cnt[t[i].a]-1
cnt[t[i].a]−1位置,可以重复上述过程。
为了满足排序的稳定性,第一个关键字相同时第二个关键字从小到大排列。当前算法对于每个第一个关键字相同的数对,在目标序列中都是从后向前赋值的,因此需要按照第二个关键字从大到小的顺序遍历,也就是要对
t
t
t序列从后向前遍历,因此该循环的循环控制变量i是从n-1到0循环的。
最后输出的是最终序列
u
u
u,
u
[
i
]
=
s
[
r
e
s
[
i
]
]
u[i]=s[res[i]]
u[i]=s[res[i]],因此输出的数对为a[res[i]]
,b[res[i]]
,第(5)空选B。
【答案】
- B
- D
- C
- A
- B