求n個閉區間的所有交集(貪心+線段樹)

問題描述:給你n個閉區間,輸出這n個開區間的所有交區間,可能存在一個子區間有多次重複,一個交區間的定義是至少有兩個大區間都包含它,並且答案集中要儘可能地把所有區間合併。注:爲了避免歧義,頭對尾交於一個點則不算交


思路:這是對於力扣986地一個拓展,如果問題約束到一個交區間最多隻有兩個大區間包含它,那麼就可以先把區間預處理成像力扣986那樣的兩個遞增區間,用雙指針求交集。

對於這個問題我們也是要用到貪心的策略,先對左端點排序,之後維護一個全局的最右上限,每次都拿左端點跟這個最右上限比較,大於它就與之前的區間互斥,更新一下上限,否則求一下交。

但是發現問題還有個條件,儘量合併答案中的子區間,也就是說如果區間是這樣的[1,3],[3, 5]則要合併起來[1,5]。對於這個問題我們可以先求所有的交區間,然後對答案集中的區間再執行像力扣56那樣的並區間也是能夠解決的,但感覺有些複雜了,我們還可以在線地更新:

由於我們已經對左端點排序了,當尋找到一組交地時候,後面如果發現這個左端點在上一組交區間的左邊說明可以合併,於是就把它的右區間拉長,否則就添加新的一組交區間,由於按左端點排序了,當前沒有與前一個交區間k產生交,後面的區間就更不可能與它產生交了,說明那個交區間已經是最優。這樣實際上就解決了前面的最優子問題,後面照做就能達到全局最優,符合貪心的套路。

對於這個問題其實還有個更一般的做法,維護一個線段樹,因爲區間問題嘛,最後統計所有的sum大於1的點,用兩個指針掃一次O(n)O(n)即可,複雜度也是O(nlogn)O(nlogn),只不過這個做法常數可能會有點大


#include <bits/stdc++.h>
using namespace std;

struct interval {
    int l,r;
    bool operator < (const interval& A) const {
        return l<A.l;
    }
};

int main() {
    int n, l, r;
    vector<interval> val;
    vector<interval> ans;
    val.clear(); ans.clear(); 
    scanf("%d", &n);
    for (int i=0; i<n; i++) scanf("%d%d", &l, &r), val.push_back({l, r}); //讀入
    sort(val.begin(), val.end()); //按左端點排序
    if(n) {
        r = val[0].r; //全局的右端點
        for(int i=1; i<n; i++) {
            int cur_l = val[i].l;
            int cur_r = val[i].r;
            if (cur_l >= r) //說明這個區間與之前的區間互斥,由於按左端點排序,所以之後的點也與之前的互斥,否則肯定有交區間
                r = cur_r; 
            else if (ans.size() == 0) //發現是第一個交區間,所以沒有給他更新的,直接push,並維護全局右端點
                ans.push_back({cur_l, min(cur_r, r)}),r = max(r, cur_r); 
            else if (cur_r <= r) { //右端點完全被包含,之後要麼擴大原來的要麼增加新的要麼啥都沒做(之前交集的子區間),但最多隻會更新到cur_r
                if (cur_l <= ans.back().r) //擴大原來的
                    ans.back().r = max(cur_r, ans.back().r); //子區間的話就不更新
                else                       //增加新的
                    ans.push_back({cur_l, cur_r});
            } else {                       //右端點不被包含,擴大的交也只能到之前的全局右端點
                if (cur_l <= ans.back().r) //擴大原來的
                    ans.back().r = max(r, ans.back().r); //子區間的話就不更新 注意右端點只能到之前的全局右端點
                else                       //增加新的
                    ans.push_back({cur_l, r});
                r = cur_r; //更新全局右端點
            }          
        }

        for (int i=0; i<ans.size(); i++)
            printf("[%d,%d]\n", ans[i].l, ans[i].r);
    }
    return 0;
}

/*
4
1 5
3 8
4 7
6 12
*/

對於像LeetCode986的交問題實際上是我們這個問題的一個特例,把所有區間加到一個數組後對左端點排序之後和我們這題一樣做也能解決,相當於特殊問題一般化。注意那題相交於一點也算一個子區間。

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        return a[0]<b[0];    
    }
    
    vector<vector<int>> intervalIntersection(vector<vector<int>>& A, vector<vector<int>>& B) {
        vector<vector<int>> ans; ans.clear();
        if (A.size()==0 || B.size()==0) return ans;
        for(int i=0; i<B.size(); i++) A.push_back(B[i]);
        sort(A.begin(), A.end(), cmp);
        int r = A[0][1]; //全局的右端點
        for(int i=1; i<A.size(); i++) {
            int cur_l = A[i][0];
            int cur_r = A[i][1];
            if (cur_l > r) //說明這個區間與之前的區間互斥,由於按左端點排序,所以之後的點也與之前的互斥,否則肯定有交區間(注意這個條件相交於一點也算一個區間)
                r = cur_r; 
            else if (ans.size() == 0) //發現是第一個交區間,所以沒有給他更新的,直接push,並維護全局右端點
                ans.push_back({cur_l, min(cur_r, r)}),r = max(r, cur_r); 
            else if (cur_r <= r) { //右端點完全被包含,之後要麼擴大原來的要麼增加新的要麼啥都沒做(之前交集的子區間),但最多隻會更新到cur_r
                if (cur_l <= ans.back()[1]) //擴大原來的
                    ans.back()[1] = max(cur_r, ans.back()[1]); //子區間的話就不更新
                else                       //增加新的
                    ans.push_back({cur_l, cur_r});
            } else {                       //右端點不被包含,擴大的交也只能到之前的全局右端點
                if (cur_l <= ans.back()[1]) //擴大原來的
                    ans.back()[1] = max(r, ans.back()[1]); //子區間的話就不更新 注意右端點只能到之前的全局右端點
                else                       //增加新的
                    ans.push_back({cur_l, r});
                r = cur_r; //更新全局右端點
            }          
        }
        return ans;
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章