【基础算法笔记】贪心算法中的区间问题

 在acwing上学习算法的一点思考和总结


感受:贪心算法难在他的证明,很多时候我们都是有一个感觉,然后去猜是否能用贪心做,借助几个实例去证明贪心算法的正确性。个人感觉自己对贪心的证明还不是很透彻,得多做点题悟一悟。这里暂时先不写贪心的证明了,等以后彻底搞懂了贪心再回来补上吧。

区间选点

【基础算法笔记】贪心算法中的区间问题_第1张图片

原创大大(智障也有春天):AcWing 905. 区间选点的贪心思路的正确性证明 - AcWing

要想看懂Y总的证明,关键是要弄清楚cnt的含义究竟是什么,其实cnt有两个含义
①是指,按照各区间按右端点从小到大排,再从前往后枚举各区间,若当前区间中已包含之前被选的区间右端点,则直接跳过该区间;否则,选择当前区间的右端点这样的贪心思路所选出来的右端点的数量。
②是指,所有区间中一定存在cnt个两两之间没有交集的区间。因为各区间按照右端点排序后,每一个被选择的区间右端点(共cnt个)都是没有被 (被选择的排序位置更靠前的区间的右端点) 覆盖到的区间的 右端点。

证明
①假设最优解为ans个,以上述贪心思路选出来的点为cnt个。即证明ans == cnt,等价于证ans >= cnt 同时 ans <= cnt
②首先,以上述贪心思路选择出的cnt个点,是一组可行方案。其覆盖了所有区间,满足题目要求。又因为ans是最优解,即为所有可行方案的最小值,那么最优解ans <= cnt成立
③其次由于所有区间中一定存在cnt个两两之间没有交集的区间,那么至少需要cnt个点才能将这些两两不交的区间进行覆盖,又因为题目要求选的点还要能覆盖所有别的区间,故最优解ans >= cnt成立

所以代码的实现就很简单了,先将右端点排序,然后遍历每个区间,看是否有重合。

#include
#include
#include

using namespace std;
const int N = 100010;
struct Range
{
    int l;
    int r;
    bool operator< (const Range &W)const
    {
        return r < W.r;
    }
}range[N];

int n;

int main()
{
    cin>>n;
    
    for(int i = 0; i < n; i ++)
    {
        int a,b;
        cin>>a>>b;
        range[i] = {a,b};
    }
    
    sort(range, range + n);
        
    int res = 0;
    int ed = -2e9;
    for(int i = 0; i < n; i ++)
        if(range[i].l > ed)
        {
            res ++;
            ed = range[i].r;
        }
        
    cout<

区间组合

【基础算法笔记】贪心算法中的区间问题_第2张图片原创大大(oceanrivers):算法基础课贪心部分每道题贪心策略的证明汇总 - AcWing

Ans<=cnt证明:
因为Ans是最优解,而我们根据贪心策略得出来的cnt肯定是一个合法解,所以必然有Ans<=cnt的

Ans>=cnt证明:
根据我们的贪心策略,对于第一个进入第cnt个分组的区间(我们把它命名为X区间),它和前面的cnt-1个分组必然存在交集(否则就不会产生第cnt组了),即我们必然能够在前面的cnt-1个分组的每一组中找到一个和它相交的区间,如此,我们便可以找到cnt-1个区间和X区间相交,即至少有cnt个区间彼此是相交的,如果要进行分组,这cnt个区间必然处于不同组,即至少需要分成cnt组,所以任何一种分组方案得到的结果都必然>=cnt,最优解与不例外,所以Ans>=cnt得证

最优步骤:每次要插入的区间去遍历前面的所有区间,找到所有右端点中的min,然后判断要插入区间的左端,即range[ i ].l 是否大于这个min。若是则说明可以合并到一组,反之,要新增加一个组。代码实现上就用一个小根堆来维护合并后(也可能没合并)的区间右端点的最小值

#include
#include
#include
#include

using namespace std;
const int N = 100010;
struct Range
{
    int l;
    int r;
    bool operator< (const Range & W)const
    {
        return l < W.l;
    }
}range[N];

int n;

int main()
{
    cin>>n;
    
    for(int i = 0; i < n; i ++)
    {
        int a,b;
        cin>>a>>b;
        range[i] = {a,b};
    }

    sort(range, range + n);

    priority_queue, greater> heap;
    for(int i = 0; i < n; i ++)
    {
        auto r = range[i];
        if(!heap.size() || r.l <= heap.top() ) heap.push(r.r); //不能合并到一组,需要作为新的一组区间加入到堆中
        else
        {
            //更新这个区间的右端点
            heap.pop(); 
            heap.push(r.r);
        }
    }
    cout<

区间覆盖

【基础算法笔记】贪心算法中的区间问题_第3张图片

原创大大(oceanrivers)算法基础课贪心部分每道题贪心策略的证明汇总 - AcWing

假设最优解有Ans个区间,用贪心策略得出的解是cnt个区间。我们依次枚举这ans个区间和cnt个区间,假设枚举到第i个区间发现两者不相同,即发现ans的第i个区间和cnt的第i个区间不是同一个区间时(此时ans的第i-1个区间和cnt的第i-1个区间相同)根据我们的贪心策略,cnt的第i个区间的右端点必然>=ans的第i个区间的右端点,如此我们把ans的第i个区间换成cnt的第i个区间之后,ans依然是一个合法解,并且此时ans中的区间个数没有发生改变;由此,每当出现不相同的区间时,我们都可以采用这种替换,最终最优解的每个区间在经过上述替换后,会等于我们用贪心策略得出的解的每个区间,即这两个解的区间个数是相同的,Ans=cnt得证

最优步骤:遍历所有在起点左边(小于等于)的区间中,右端点最大的。然后将原本的起点更新为这个右端点。由题目的性质可知,我们如果走了最优步骤,那么前面遍历过的区间都不会对后面的步骤产生任何影响。所以我们可以用双指针来遍历区间

#include
#include
#include

using namespace std;
const int N = 100010;
struct Range
{
    int l;
    int r;
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
}range[N];

int n, st, ed;


int main()
{
    cin>>st>>ed;
    cin>>n;
    for(int i = 0; i < n; i ++)
    {
        int a,b;
        cin>>a>>b;
        range[i] = {a, b};
    }
    
    sort(range, range + n);
    
    bool flag = false;
    int res = 0;
    
    for(int i = 0; i < n; i ++)
    {
        int j = i, tt = -2e9;
        while( j < n && range[j].l <= st )
        {   
            tt = max(tt,range[j].r);
            j ++;
        }
        
        if(tt < st) break;
        
        res ++;
        if( tt >= ed) 
        {
            flag = true;
            break;
        }
        
        st = tt;
        i = j-1;
        
    }
    if(!flag) res = -1;
    cout<

你可能感兴趣的:(算法,贪心算法,学习,c++,笔记)