Classrooms
The new semester is about to begin, and finding classrooms for orientation activities is always a headache.
There are $k$ classrooms on campus and $n$ proposed activities that need to be assigned a venue. Every proposed activity has specfic starting time $s_i$ and ending time $f_i$. Any such an activity should take place at one of the classrooms. Any of the $k$ classrooms is big enough to hold any of the proposed activities, and each classroom can hold at most one activity at any time. No two proposed activities can take place at the same classroom at the same time. Even if two proposed activities overlap momentarily (the ending time of one activity equals the starting time another activity), they cannot be assigned to the same classroom.
There are so many proposed activities that there may not be enough classrooms to hold all the activities. It is desirable to have as many activities as possible. At most how many proposed activities can be assigned to the classrooms?
Input
The first line contains two positive integers $n$ and $k$ ($k le n le 200000$), representing the number of proposed activities and number of classrooms, respectively.
The following $n$ lines each contains two positive integers: the $i$-th line among these $n$ lines contains $s_i$ and $f_i$ ($1 le s_i le f_i le 10^9$), indicating the starting time and ending time of proposed activity $i$.
Output
Output an integer indicating the maximum number proposed activities that can be scheduled.
Sample Input 1
4 2
1 4
2 9
4 7
5 8
Sample Output 1
3
Solution
从前在《挑战程序设计竞赛》看到过一个结论:
在$n$个区间$[l_1, r_1],cdots, [l_n, r_n]$中选择最多的两两不相交的区间, 可按如下贪心策略:
每次都选当前可选的区间中结束时间最早的那个区间.
其实这是个求DAG上最长链的问题.
上述情形恰好是本题的一个特例 ($k=1$), 所以我第一个想法是:
将所有区间按右端点从大到小排序.
按此顺序将区间放到一个链表 (std::list
) 里, 每次按上述贪心策略, 选择一个最长链, 并将这些区间从链表中删除.
可惜这做法是错的, 而且复杂度是$O(n^2)$.
然后就不知道怎么做了. 我做题有个不好的习惯: 每次WA后, 不会去动手出几组数据check以下, 验证自己想法的正确性.
其实对于我的做法反例很容易举出来:
后来在某博客上看到了正确的做法:
基本的想法也是对一个贪心策略的模拟:
仍然先将区间按右端点从小到大排序, 从左到右遍历这些区间, 同时用 std::multiset<int>
维护每个房间内当前正在进行那个活动的结束时间 (亦即区间右端点). 对于当前考虑的区间 $[l_i, r_i]$, 在multiset
中查询小于 (早于) $l_i$的最大的某个右端点, $r^$. 若$r^$存在就将其更新为$r_i$, 否则若multiset
的size()
$<k$就将$r_i$插入multiset
.
Implementation
考虑到std::multiset<>
只支持lower_bound()
和upper_bound
, 为了方便上述查询, 将区间右端点的相反数 (负值) 插入multiset
中.
#include <bits/stdc++.h>
using namespace std;
const int N{1<<18};
struct X{
int s, t;
void read(){
scanf("%d%d", &s, &t);
}
bool operator<(const X &rhs)const{
return t<rhs.t;
}
void out(){
cout<<s<<' '<<t<<endl;
}
}a[N];
multiset<int> mst;
int main(){
int n, k;
cin>>n>>k;
for(int i=0; i<n; i++)
a[i].read();
sort(a, a+n);
int res=0;
for(int i=0; i<n; i++){
// a[i].out();
auto it=mst.upper_bound(-a[i].s);
if(it==mst.end()){
if(mst.size()<k){
// puts("ADD1");
res++;
mst.insert(-a[i].t);
}
}
else{
res++;
// puts("ADD2");
mst.erase(it);
mst.insert(-a[i].t);
}
}
cout<<res<<endl;
return 0;
}
当然, 取相反数也可以实现上述查询, 这时这要查询low_bound(r[i])
的前趋.
#include <bits/stdc++.h>
using namespace std;
const int N{1<<18};
struct X{
int s, t;
void read(){
scanf("%d%d", &s, &t);
}
bool operator<(const X &rhs)const{
return t<rhs.t;
}
void out(){
cout<<s<<' '<<t<<endl;
}
}a[N];
multiset<int> mst;
int main(){
int n, k;
cin>>n>>k;
for(int i=0; i<n; i++)
a[i].read();
sort(a, a+n);
int res=0;
for(int i=0; i<n; i++){
// a[i].out();
auto it=mst.lower_bound(a[i].s);
if(it==mst.begin()){
if(mst.size()<k){
// puts("ADD1");
res++;
mst.insert(a[i].t);
}
}
else{
res++;
--it;
// puts("ADD2");
mst.erase(it);
mst.insert(a[i].t);
}
}
cout<<res<<endl;
return 0;
}
总结
这个贪心策略的正确性还是比较容易证明的.
首先将区间按活动的结束时间从早到晚排序, 按这样的顺序出个安排活动能够保证当对于任意两个相互冲突的两个活动, 我们优先选择结束时间较早的活动, 这显然比选结束时间晚的要更优.