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
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;
}
区间覆盖问题
贪心求最小区间覆盖问题。
关注点:上一次选择的区间的右端点,初值为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次贪心选择的右端点”即可。
具体做法为:
明确算法后,下面看代码:
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 = A[q].b
r = max(r, A[q].b)
的效果与r = A[q].b
相同,因为A数组已经关于A[i].b
升序,r顺序地取区间的右端点,一定有A[q].b >= r
,所以两个表达式效果相同。