区间相关问题的学习: 最多不相交区间问题,区间选点问题与区间覆盖问题

区间相关问题包括: 1)最多不相交区间问题; 2)区间选点问题; 3)区间覆盖问题等。

最多不相交区间问题(又叫选择不相交区间,最大不相交覆盖等等),是指数轴上有n个开区间(a,b),选择尽量多个区间,使得这些区间两两没有公共点。

注意:
1)该问题也适用于闭区间[a,b],不过要加一条:如果一个区间的右端点和另一个区间的左端点重合,视为不相交。

思路: 贪婪法。首先将n个区间按右端点b 升序排序,先选择右端点最小的那个区间,这里如果有多个区间的右端点重合,则选择左端点最大的那个 (保证所选区间块头最小)。然后从左到右,选择第一个不与该区间相交的那个区间,依次扫描一遍数组即可。所以复杂度就是排序的复杂度O(nlogn)。

打印区间的时候用了一个变量overlapping,以保证一个区间不会被打印多次。

代码如下。选择区间为(2,6),(1,,4),(3,6),(2,4),(6,8),(2,4),(3,5)。注意第一个区间应该选(2,4)而不是(1,4)。另外(2,4)出现了2次,maxRegionNonOverlap()的else分支里面把第二个(2,4)skip了。

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

struct Region {
   int left;
   int right;
   Region(int x, int y): left(x), right(y) {}
   Region() {}
   bool operator < (const Region &r) const {
       return right < r.right;
   }
};

void print_region(Region &r) {
    cout<<"("<<r.left<<", "<<r.right<<")";
}

int maxRegionNonOverlap(vector<Region>& a) {
    int num = 1;
    sort(a.begin(), a.end());

    int save_index = 0;
    bool overlapping = false;
    for (int i=1; i<a.size(); i++){
        if (a[i].right > a[save_index].right) {
            if ((a[i].left <= a[save_index].right) && !overlapping) {  //overlap with the previous saved case.
                print_region(a[save_index]);
                overlapping = true;
            }

            if (a[i].left >= a[save_index].right) { //two ranges not overlap, good case.
                num++;
                print_region(a[i]);
                save_index = i;
                overlapping = false;
            }
        }
        else { //right border the same
            if (a[i].left > a[save_index].left) { //left border different
                save_index = i;
            }
        }

    }

    return num;
}


int main()
{
    vector<Region> region;

    region.push_back(Region(2,6));
    region.push_back(Region(1,4));
    region.push_back(Region(3,6));
    region.push_back(Region(2,4));
    region.push_back(Region(6,8));
    region.push_back(Region(2,4));
    region.push_back(Region(3,5));

    cout<<"original list"<<endl;
    for_each(region.begin(), region.end(), print_region);
    cout<<endl;

    cout<<maxRegionNonOverlap(region);

    return 0;
}

上面的解法是参考的算法竞赛入门经典(第2版),我在网上看到的所有的讨论也是基于类似的方法。但我们再思考一下,可不可以按左端点排序,然后从最右边的端点往左遍历呢?这两种方法我觉得是等价的,因为最后两种方法都归结到a1<=a2<=…<=an, b1<=b2<=…<=bn (ai, bi分别为区间i的左右端点),所以它们得到的最优解是一样多的。不过,这两种方法所选的区间则不一定相同。

区间选点问题是说数轴上有n个闭区间[ai, bi]。取尽量小的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)

该题与最多不相交区间问题类似,也是贪婪法把所有区间按b从小到大排序(若b相同,则a从大到小排序)。如果区间重合,则大区间不予考虑,因为小区间包含的点大区间都包含。从最左边的区间开始,取其右端点与右边的区间一一比较:
1)如果小于后面区间的左端点,则取该后面的右端点再与更后面的区间一一比较。
2)如果大于后面区间的左端点,则说明该右端点也在后面区间内部 (因为按b排序), 什么都不用做。
3) 如果等于后面区间的左端点,也什么都不用做。

代码如下:

int selectPoints(vector<Region> &a) {

    sort(a.begin(), a.end());
    int num = 0;
    int loc = -INT_MAX;
    for (int i=0; i<a.size(); i++) {
        if (loc < a[i].left) {
            loc = a[i].right;
            cout<<"location is "<<loc<<endl;
            num++;
        }
    }

    return num;

}

区间覆盖问题是指:数轴上有n个闭区间[ai,bi],选择尽量少的区间覆盖一条指定线段[s,t]
思路也是贪婪法。把左端点ai从小到大排序,然后从左端点s开始,选择覆盖s的区间里面的右端点最长的那个。然后取该区间的右端点,继续往右扫描,重复上述步骤,直到到达右端点t。注意这里跟最多不相交区间不一样,我们按ai排序后,不是从最右端的区间开始往左扫描,而是从最左端区间的左端点s开始,选择覆盖s的最长的那个,然后取其右端点,继续往右扫描。

代码如下:

#include <iostream>
#include <algorithm>
#include <vector>
#include <climits>

using namespace std;

struct Region {
   int left;
   int right;
   Region(int x, int y): left(x), right(y) {}
   Region() {}
   bool operator < (const Region &r) const {
       return left < r.left;
   }
   bool operator != (const Region &r) const {
       return (left != r.left) || (right != r.right);
   }
};

void print_region(Region &r) {
    cout<<"("<<r.left<<", "<<r.right<<")"<<endl;
}

inline inRange(int x, Region r) {
    return (x>=r.left) && (x<=r.right);

}

int minRegionOverlapping(vector<Region>& a, int s, int t) {
    int num = 0;
    int loc = s;
    vector<Region> new_a;

    sort(a.begin(), a.end());

    //pre-processing
    for (vector<Region>::iterator iter=a.begin(); iter!=a.end(); iter++) {
        if ((iter->left >= s) && (iter->right <= t)) {
            new_a.push_back(*iter);
        }
        else if ((iter->left <=s) && (iter->right >= t)) {
            // 1 range is enough
            print_region(*iter);
            return 1;
        }
        else if ((iter->left <= s) && (iter->right >= s)) {
              new_a.push_back(*iter);
        }
        else if ((iter->left <= t) && (iter->right >= t)) {
              new_a.push_back(*iter);
        }
    }

    if (!new_a.size()) {
        cout<<"No solution!"<<endl;
        return 0;
    }

    sort(new_a.begin(), new_a.end());

    loc = s; //start from the left border of the line
    int new_loc = -INT_MAX;
    Region save_region = new_a[0];

    for (vector<Region>::iterator iter=new_a.begin(); iter!=new_a.end(); iter++) {
        if (inRange(loc, *iter) && (new_loc < iter->right)){
            new_loc = iter->right; //multiple ranges cover the loc, choose the one whose right-border is the furtherest
            save_region = *iter;
        }
        else if (!inRange(loc, *iter)) {
            if (inRange(new_loc, *iter)) { //it is time to determine the next loc
                num++;
                print_region(save_region);
                loc = new_loc;
                if (inRange(t, *iter)) {  //reach the end
                    num++;
                    print_region(*iter);
                    return num;
                }
            }
            else {
                cout<<"No solution!"<<endl;
                return 0;
            }
        }
    }
    return num;
}

int main()
{
    vector<Region> region;

    region.push_back(Region(2,6));
    region.push_back(Region(1,4));
    region.push_back(Region(3,6));
    region.push_back(Region(2,4));
    region.push_back(Region(5,8));
    region.push_back(Region(2,4));
    region.push_back(Region(3,5));

    cout<<"original list"<<endl;
    for_each(region.begin(), region.end(), print_region);
    cout<<endl;

    cout<<minRegionOverlapping(region, 6, 9);
    return 0;
}

一点总结:
这三种区间相关问题给出的解法中,最多不相交区间问题和区间选点问题按右端点排序,从左到右扫描,区间覆盖问题按左端点排序,从左到右扫描

我们来归纳一下,什么样的问题就按左端点排序,什么样的问题就按右端点排序呢?其实根据对称性,这三个问题不管是根据左端点排序还是右端点排序都是可以的。但是因为最多不相交区间问题和区间选点问题是要选择尽量小的区间,所以如果它们是按右端点排序的话,就要从最左边的端点开始找,反之如果我们按左端点排序的话,就要从最右边的端点开始找。

为什么呢?我们可以这么来看:
按右端点排序后右b1<=b2<=…<=bn,
如果a1>a2, 则区间1包含在区间2内,区间1不能要。所以我们一开始一定是选ai里面最小的那个,也就是从最左边的端点开始选。
按左端点排序的情形也是类似的,在此不再赘述。

而对于区间覆盖问题,我们要选择尽量大的区间。如果我们按左端点排序后a1<=a2<=…<=an,
假设a1, a2, a3都覆盖起始点s,则显然要选bi最大的那个。如果我们从最右端点开始扫描则无法知道哪个会覆盖起始点s。所以只能从最左端开始扫描,并选覆盖起始点s的端点中bi最大的那个。
同样,该题也可以按右端点排序,然后选择所有包含t的区间中左端点最长的那个,然后从右向左扫描。

如果本文内容有任何不当之处,欢迎指正。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值