【题目】
CSP-J 2020 入门级 第一轮 完善程序(2)
(最小区间覆盖)给出 n 个区间,第 i 个区间的左右端点是
[
a
i
,
b
i
]
[a_i,b_i]
[ai,bi]。现在要在这些区间中选出若干个,使得区间
[
0
,
m
]
[0, m]
[0,m]被所选区间的并覆盖(即每一个
0
≤
i
≤
m
0\leq i\leq m
0≤i≤m都在某个所选的区间中)。保证答案存在,求所选区间个数的最小值。
输入第一行包含两个整数 n 和 m (
1
≤
n
≤
5000
1\le n \le 5000
1≤n≤5000,
1
≤
m
≤
1
0
9
1\le m \le 10^9
1≤m≤109)
接下来 n 行,每行两个整数
a
i
,
b
i
a_i,b_i
ai,bi(
0
≤
a
i
,
b
i
≤
m
0\le a_i,b_i \le m
0≤ai,bi≤m)
提示:使用贪心法解决这个问题。先用
O
(
n
2
)
O(n^2)
O(n2) 的时间复杂度排序,然后贪心选择这些区间。
试补全程序。
#include <iostream>
using namespace std;
const int MAXN = 5000;
int n, m;
struct segment { int a, b; } A[MAXN];
void sort() // 排序
{
for (int i = 0; i < n; i++)
for (int j = 1; j < n; j++)
if ( ① )
{
segment t = A[j];
②
}
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
cin >> A[i].a >> A[i]?b;
sort();
int p = 1;
for (int i = 1; i < n; i++)
if ( ③ )
A[p++] = A[i];
n = p;
int ans =0, r = 0;
int q = 0;
while (r < m)
{
while (④)
q++;
⑤;
ans++;
}
cout << ans << endl;
return 0;
}
- ①处应填( )
A. A[j].b>A[j-1].b
B. A[j].a<A[j-1].a
C. A[j].a>A[j-1].a
D. A[j].b<A[j-1].b - ②处应填( )
A. A[j+1]=A[j];A[j]=t;
B. A[j-1]=A[j];A[j]=t;
C. A[j]=A[j+1];A[j+1]=t;
D. A[j]=A[j-1];A[j-1]=t; - ③处应填( )
A. A[i].b>A[p-1].b
B. A[i].b<A[i-1].b
C. A[i].b>A[i-1].b
D. A[i].b<A[p-1].b - ④处应填( )
A. q+1<n&&A[q+1].a<=r
B. q+1<n&&A[q+1].b<=r
C. q<n&&A[q].a<=r
D. q<n&&A[q].b<=r - ⑤处应填( )
A. r=max(r,A[q+1].b)
B. r=max(r,A[q].b)
C. r=max(r,A[q+1].a)
D. q++
【题目考点】
1. 贪心
区间覆盖问题
【解题思路】
贪心求最小区间覆盖问题。
关注点:上一次选择的区间的右端点,初值为0。
贪心选择:在所有包含关注点的区间中,选择右端点最大的区间。
贪心选择性质的证明:
证明:最优解包含第一次的贪心选择:在所有包含0的区间中,选择右端点最大的区间
第0位置总该被包含到某个区间中。如果选择的区间不包括0,那么无法完成区间覆盖。
这一次选择的,也就是唯一包含第0位置的区间,记该区间为 a g a_g ag。
假设存在一组最优解不包含贪心选择: a 1 , a 2 , . . . , a n a_1, a_2, ..., a_n a1,a2,...,an是选择的区间, a 1 a_1 a1~ a n a_n an按左端点从小到大排序,其中没有 a g a_g ag。
其中 a 1 a_1 a1中一定包含第0位置,否则如果 a 1 a_1 a1的左端点大于0,后面的区间左端点都大于 a 1 a_1 a1的左端点,第0位置就不会被任何区间包含,这就不是一组解了。
用 a g a_g ag替换 a 1 a_1 a1,得到 a g , a 2 , . . . , a n a_g, a_2, ..., a_n ag,a2,...,an, a 1 a_1 a1和 a g a_g ag都包括第0位置,而 a g a_g ag的右端点大于 a 1 a_1 a1的右端点,替换后一定可以覆盖整个区间。
因此 a g , a 2 , . . . , a n a_g, a_2, ..., a_n ag,a2,...,an也是该问题的一组最优解。得到了包含贪心选择的最优解,假设不成立,原命题得证。
证明:在最优解包含前k次的贪心选择的情况下,存在最优解包含第k+1次的贪心选择
证明方法同上。把“第0位置”变为“第k次贪心选择的右端点”即可。
具体做法为:
- 先将所有区间按照左端点从小到大进行排序,关注点初值为0。
- 按顺序遍历所有区间,如果该区间的左端点小于等于关注点,则该区间包含关注点。
- 继续向后遍历,得到所有包含关注点的区间中,右端点最大的区间。
- 选择该区间,选择区间数量加1,将关注点设为该区间的右端点
- 最后输出选择区间的数量
明确算法后,下面看代码:
const int MAXN = 5000;
int n, m;
struct segment { int a, b; } A[MAXN];
//...
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
cin >> A[i].a >> A[i]?b;
segment类型用来表示一个区间,区间的左端点为属性a,右端点为属性b。
void sort() // 排序
{
for (int i = 0; i < n; i++)
for (int j = 1; j < n; j++)
if ( ① )
{
segment t = A[j];
②
}
}
接下来是排序,看形式是冒泡排序。i指的是冒泡次数,每次冒泡能确定一个数字的位置。j指的是当前比较数对中的第二个数字。也就是A[j-1]和A[j]进行比较。
要想选择“包含关注点的区间”,应该按区间的左端点从小到大进行排序。排序后应该满足A[j-1].a <= A[j].a
,如果不满足该条件,则要交换A[j-1]
和A[j]
。
①处应该填不满足A[j-1].a <= A[j].a
的条件,即A[j-1].a > A[j].a
,选B。
②处是交换两个变量的写法,要交换的是A[j]
和A[j-1]
。把A[j]
赋值给临时变量t后,应该把A[j-1]
赋值给A[j]
,再把刚才保存在t中的值赋值给A[j-1]
。选D。
int p = 1;
for (int i = 1; i < n; i++)
if ( ③ )
A[p++] = A[i];
n = p;
明显这是数组填充的过程,p是数组中已有的元素个数,填充到下标0~p-1。最后又让n变为填充后数组A中的元素个数。
第0个区间直接加入数组A。
这里是选择数组A中的部分数据,再填充到数组A。实际是删掉了A中的一些元素。
p是再次填充后A中元素(区间)的个数。A[p-1]
是上一次确定的区间。一定有 p-1 <= i。
当前A数组中的元素已经按照左端点排序,设里面存在两个元素,那么一定有A[p-1].a <= A[i].a
。
如果A[i].b <= A[p-1].b
,那么A[i]
就完全是A[p-1]
的子区间,或者说A[p-1]
完全覆盖了A[i]
,而且覆盖的区间可能更大。如果存在最优解要选择A[i]
,那么可以用覆盖A[i]
的A[p-1]
来替换A[i]
。因此就没有必要考虑A[i]
了,可以把A[i]
删掉,也就是不把A[i]
再次填充进数组A。
反过来,如果A[i].b > A[p-1].b
,就应该把A[i]
再次填充进数组A。
③处应该选A
这样做的结果是,经过再次填充后的数组A,关于右端点A[i].b
也是升序的。
int ans =0, r = 0;
int q = 0;
while (r < m)
{
while (④)
q++;
⑤;
ans++;
}
ans是选择的区间数量,r为上文提到的关注点(上一个选择的区间的右端点),q是数组A的下标,初值为0。
[
0
,
m
]
[0, m]
[0,m]是要被覆盖的区间。
只要关注点小于整个区间右端点m,那么应该遍历经过再次填充得到的A数组
- 如果第q+1区间左端点小于等于关注点r,就让q增加1,看下一个区间。
A数组下标从0~n-1,要保证q+1小于n,不能超过数组长度。
因此④处填q+1<n && A[q+1].a <= r
- 如果当前第q+1区间左端点大于关注点r,那么应该选择第q区间,该区间的右端点一定是包含关注点r的区间中右端点的最大值。把第q区间的右端点设为关注点r。
应该执行r = A[q].b
第⑤处,题目中给的是r = max(r, A[q].b)
的效果与r = A[q].b
相同,因为A数组已经关于A[i].b
升序,r顺序地取区间的右端点,一定有A[q].b >= r
,所以两个表达式效果相同。
【答案】
- B
- D
- A
- A
- B