题意
数轴上有 n 个闭区间 [a_i, b_i]。取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)
Input
第一行1个整数N(N<=100)
第2~N+1行,每行两个整数a,b(a,b<=100)
Output
一个整数,代表选点的数目
输入样例
样例1
2
1 5
4 6
样例2
3
1 3
2 5
4 6
输出样例
样例1
1
样例2
2
分析
区间选点问题属于最优化问题,可以利用贪心算法来求解。
- 什么是贪心算法?
贪心算法是一种最接近人类思维的思考方式。人们在日常生活中总是会下意识地选择贪心算法来解决问题。
🌰在此——当我们到超市中去选购水果时,假设我们打算购买的或者能够购买的水果只有10个【虽然很少人会按个买水果233】,则我们一定会优先在超市柜台剩余的所有水果中选择最好的一个,直到选够数目。
因此,贪心算法的本质很容易理解——在一步步解决问题的过程中,总是选择当前的最优解。
这种算法的特别之处就在于它并不是从全局上去考虑问题的最优解,而是将这个问题的解决分解成很多步,在每一步上考虑最优解,并假设这样最终会得到全局的最优解。
- 贪心算法的证明和选择
不过,这种算法的问题也显而易见——你为何如此自信贪心算法一定会得到最优解❓
显然,这种算法不会再所有情况下都能得到最优解。因此,在使用贪心算法解决问题时,我们需要证明贪心算法所得的解一定为最优解。
证明方法有:
-
贪心算法领先
将解决问题的每一步都换成其他的算法,每一步得到的解如果不比贪心算法更优,则贪心算法所得一定为最优解。 -
交换证明
将一个最优解转换为贪心算法所得解,如果贪心解没有变的更差,则贪心算法所得最终解一定为最优解。
但值得注意的是,我们可以发现,同一个问题在利用贪心算法解决的时候,其中每一步选择最优解的标准可能有多种选择。不过,显然不是每一种选择都能得到最终的最优解。所以,在设计算法时应注意
继续买水果🌰——选择最好的水果意味着可以:1.选择最大的水果 2.选择最新鲜的水果 3.选择最贵的水果… 但很明显,如果我们的预算受限、能带走的水果重量受限、能储存的时间受限等等,我们并不能随意按其中一种标准来最优解决这个问题。
💡所以,在设计贪心算法时,不仅需要证明贪心算法可以得到该问题的最优解,更关键的是需要证明选择的贪心标准能得到最优解。
- 贪心标准的选择
选择最少的点,使得每个区间中都至少有一个点被选择,等同于,选择一些点,被最多的区间包含。
一个区间内是连续的,题目并没有要求取点必须离散,因此一个区间内可取的数实际上是无限个。说明解决该问题,一定不是穷尽所有无限可能,而是选择一些有特征的点,比如闭区间中的左端点、右端点、中点的等。
假设将要选取的点为以上所列举的三种情况,那么在这么多个区间中该如何选择呢?我们需要找到区间与区间之间的联系。
- 假设有n个左端点相同的区间,我们将它们按照右端点升序排列,很显然,长度最小的区间右端点(即最小的右端点)以及相同的左端点一定可以被所有区间包含
- 假设有n个右端点相同的区间,将它们按照左端点升序排列,则长度最小的区间的左端点以及相同的右端点可以被所有区间包含。
- 假设有n个左右端点互不相同的区间,将它们按左端点升序排列,我们无法确定以上哪一个点被最多区间包含
- 假设同上,但是将它们按右端点升序排列,会发现右端点一定是被最多区间包含的(小的右端点会被所有左端点小于它的区间包含)
将以上的情况归纳后,可以总结出贪心标准:将所有区间按照右端点升序排列,若其相同,按照左端点降序排列(当右端点相同时,左端点小的区间一定被左端点大的区间完全包含),从第一个区间开始,只选择右端点。
- 分析如何选点
在利用刚确定的基础贪心标准进行选点的过程中会发现存在不同的情况,因此需要对它们做出分析,对比检验标准是否正确,以此完善算法中的漏洞。
-
多个区间右端点相同
-
取序列中第一个区间的右端点,其后右端点相同的区间不需要进行选点
-
第n+1个区间的左端点大于前一个区间的右端点
-
不管前面n个区间的取点情况,当前区间重新取其右端点为新的一个点。因为前面的n个区间中的所有数都一定在该第n+1个区间之外。
-
当前区间的左端点小于等于前一个区间
此时不应和前一个区间的右端点进行比较来确定如何取点,而是当前区间的左端点应该和在此之前的最后一次取的最大右端点进行比较。- 若当前区间的左端点小于等于最新取点
该区间不需要重新取点,因为最新取点仍然被该区间包含 - 若当前区间的左端点大于最新取点
说明当前所取的最大右端点并不包含在该区间中,需要重新取点。此时取该区间的最右端点作为新点。
- 若当前区间的左端点小于等于最新取点
💡易错情况(WA)
在上面讨论的第三种情况的第二种小情况中:
当前区间的左端点小于等于前一个区间,且当前区间的左端点大于最新取点
我之前设计的解决办法——选择当前区间的上一个区间的右端点。
原因是:当前区间一定包含上一个区间的右端点,因此上一个区间的右端点一定不会是之前被比较的最新点,否则不会出现这种情况。那么,选择上一个区间的右端点,可以推断是最优解。
这种思路乍一看感觉没有问题,同样可以得出解。但关键在于这种思路得到的解并不一定是最优解。
比如以下的情况:
根据这种算法,当检索到4-7区间时,程序会将2-5区间的5选为新点。而当检索到6-8区间时又将4-7区间的7选为新点。因此最终所得解为3个点。
但根据观察可知,只须选取1-3区间的3以及4-7区间的7这两个点就可以满足题意。2才是最终的最优解。
因此,为了避免这种情况,应该将任意不包含最新点(即当前判断之前选出的最大右端点)的区间视为下一轮判断开始的第一个区间,自此向后重新选点。
总结
- 实践出真知🌞验证算法正确性和严谨性的最佳办法仍然是用数据测试。
- 用while做递减循环的时候,要注意while判断中所用的变量在之后的运算中是否需要用到❗️
代码
//
// main.cpp
// lab-b
//
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<pair<int, int>> sorting; //储存所有区间
bool cmp(const pair<int, int>& a,const pair<int, int>& b)
{
if( a.second != b.second ) //首先按右端点升序排列
return a.second<b.second;
return a.first>b.first; //否则按左端点降序排列
}
int main()
{
int n=0,m=1,right=0,num=0;
pair<int,int> interval; //存储区间
cin>>n;
num=n;
while( num-- ) //输入区间
{
cin>>interval.first>>interval.second;
sorting.push_back(interval);
}
sort(sorting.begin(),sorting.end(),cmp); //将所有区间排序
/* cout<<endl;
for( auto it=sorting.begin() ; it != sorting.end() ; it++ )
cout<<it->first<<" "<<it->second<<endl; //用迭代器输出pair类型时不需要用*取值,直接输出
*/
right=sorting[0].second;
for( int i = 1 ; i < n ; i++ ) //从第二个区间开始依次判定
{
// cout<<sorting[i].first<<" / "<<right<<" / "<<m<<endl;
if( sorting[i].first <= right )
//若当前区间的左端点小于当前选取点,则说明该区间包括该点,继续遍历
{
// right=sorting[i].second;
// cout<<"---->"<<endl;
continue;
}
else //反之说明当前区间不包含该点,需要重新选点
{
// cout<<"<----"<<endl;
m++; //选点个数+1
right=sorting[i].second; //不论这个区间是否与上一层相交,都应将该点的最右点选为新点
cout<<m<<endl;
return 0;
}