[LeetCode] Range Module 范圍模塊


 

A Range Module is a module that tracks ranges of numbers. Your task is to design and implement the following interfaces in an efficient manner.

 

  • addRange(int left, int right) Adds the half-open interval [left, right), tracking every real number in that interval. Adding an interval that partially overlaps with currently tracked numbers should add any numbers in the interval [left, right) that are not already tracked.

 

 

  • queryRange(int left, int right) Returns true if and only if every real number in the interval [left, right) is currently being tracked.

 

 

  • removeRange(int left, int right) Stops tracking every real number currently being tracked in the interval [left, right).

 

Example 1:

addRange(10, 20): null
removeRange(14, 16): null
queryRange(10, 14): true (Every number in [10, 14) is being tracked)
queryRange(13, 15): false (Numbers like 14, 14.03, 14.17 in [13, 15) are not being tracked)
queryRange(16, 17): true (The number 16 in [16, 17) is still being tracked, despite the remove operation)

 

Note:

  • A half open interval [left, right) denotes all real numbers left <= x < right.
  • 0 < left < right < 10^9 in all calls to addRange, queryRange, removeRange.
  • The total number of calls to addRange in a single test case is at most 1000.
  • The total number of calls to queryRange in a single test case is at most 5000.
  • The total number of calls to removeRange in a single test case is at most 1000.

 

這道題讓我們實現一個RangeModule的類,里面有三個功能函數,分別好似插入范圍,查找范圍,刪除范圍,題目中的例子給的也很明確,基本不會引起什么歧義。其實不管范圍也好,區間也好,都是一回事,跟之前的區間的題目Insert IntervalMerge Intervals沒有什么不同。這里的插入范圍函數的實現方法跟之前那道Insert Interval的解法一樣,直接抄過來就好。然后對於查找范圍函數,由於題目中說只要有數字未被包括,就返回false。那么我們反過來想,只有當某個范圍完全覆蓋了這個要查找的范圍才會返回true,所以我們可以遍歷所有的范圍,然后看是否有一個范圍完全覆蓋了要查找的范圍,有的話返回true,循環結束后返回false。最后來看刪除范圍函數,其實現方法跟插入范圍函數很類似,但又有少許不同。首先我們還是新建一個數組res存結果,然后遍歷已有的范圍,如果當前范圍的結束位置小於等於要刪除的范圍的起始位置,由於題目中的范圍定義是左開右閉,那么說明沒有重疊,加入結果res,並且用變量cur自增1來記錄當前位置。如果當前范圍的起始位置大於等於要刪除的范圍的結束位置,說明咩有重疊,加入結果res。否則就是有重疊的情況,這里跟插入范圍有所不同的是,插入范圍只需要加入一個范圍,而刪除范圍操作有可能將原來的大范圍break成為兩個小的范圍,所以我們用一個臨時數組t來存break掉后的小范圍。如果當前范圍的起始位置小於要刪除的范圍的起始位置left,說明此時一定有一段范圍留下來了,即從當前范圍的起始位置到要刪除的范圍的起始位置left,將這段范圍加入臨時數組t,同理,如果當前范圍的結束位置大於要刪除的范圍的結束位置right,將這段范圍加入臨時數組t。最后將數組t加入結果res中的cur位置即可,參見代碼如下:

 

解法一:

class RangeModule {
public:
    RangeModule() {}
    
    void addRange(int left, int right) {
        vector<pair<int, int>> res;
        int n = v.size(), cur = 0;
        for (int i = 0; i < n; ++i) {
            if (v[i].second < left) {
                res.push_back(v[i]);
                ++cur;
            } else if (v[i].first > right) {
                res.push_back(v[i]);
            } else {
                left = min(left, v[i].first);
                right = max(right, v[i].second);
            }
        }
        res.insert(res.begin() + cur, {left, right});
        v = res;
    }
    
    bool queryRange(int left, int right) {
        for (auto a : v) {
            if (a.first <= left && a.second >= right) return true;
        }
        return false;
    }
    
    void removeRange(int left, int right) {
        vector<pair<int, int>> res, t;
        int n = v.size(), cur = 0;
        for (int i = 0; i < n; ++i) {
            if (v[i].second <= left) {
                res.push_back(v[i]);
                ++cur;
            } else if (v[i].first >= right) {
                res.push_back(v[i]);
            } else {
                if (v[i].first < left) {
                    t.push_back({v[i].first, left});
                }
                if (v[i].second > right) {
                    t.push_back({right, v[i].second});
                }
            }
        }
        res.insert(res.begin() + cur, t.begin(), t.end());
        v = res;
    }

private:
    vector<pair<int, int>> v;
};

 

下面來看一種優化了時間復雜度的解法,我們使用TreeMap來建立范圍的起始位置和結束位置之間的映射,利用了TreeMap的自動排序功能,其會根據起始位置從小到大進行排序。既然是有序的,我們就可以利用二分法來快速進行查找了。

在加入范圍函數中,我們首先用upper_bound函數來查找第一個大於left的位置,標記為l,再用upper_bound函數來查找第一個大於right的位置,標記為r。我們其實是想找第一個不大於left和right的位置的,由於C++沒有floorKey這個函數,所以我們只能用upper_bound找大於left和right的位置,然后再往前移一個。如果l不是TreeMap中的第一個位置,且前面一個范圍的結束位置小於left,說明和前一個范圍沒有交集,那么還是回到當前這個范圍吧。如果此時l和r指向同一個位置,說明當前要加入的范圍沒有跟其他任何一個范圍有交集,所以我們直接返回即可,不需要其他任何操作。否則的話我們將left和l指向范圍的起始位置中的較小值賦給i,將right和r指向的前一個位置的結束位置的較大值賦給j,然后將l和r之間的范圍都刪除掉(注意這里r自增了1,是因為之前先自減了1),然后將i和j返回即可。返回后我們建立起這個映射即可。

在查找范圍函數中,我們先用upper_bound找出第一個大於left位置的范圍it,然后看如果it不是第一個范圍,並且如果其前面的一個范圍的結束位置大於等於right,說明已經完全包括這個要查找的范圍,因為前一個范圍的起始位置小於left,且結束位置大於等於right,直接返回true。

在刪除范圍函數中,查找重疊范圍的方法跟加入范圍函數中的操作一樣,所以抽出來放到了find函數中,由於刪除的范圍有可能完全覆蓋了原有范圍,也有可能只是部分覆蓋,將一個大的范圍拆成了一個或者兩個范圍。所以我們判斷,如果left大於覆蓋范圍的起始位置,那么將這段建立映射,同理,如果覆蓋范圍的結束位置大於right,同樣建立這段的映射,參見代碼如下:

 

解法二:

class RangeModule {
public:
    RangeModule() {}
    
    void addRange(int left, int right) {
        auto x = find(left, right);
        m[x.first] = x.second;
    }
    
    bool queryRange(int left, int right) {
        auto it = m.upper_bound(left);
        return it != m.begin() && (--it)->second >= right;
    }
    
    void removeRange(int left, int right) {
        auto x = find(left, right);
        if (left > x.first) m[x.first] = left;
        if (x.second > right) m[right] = x.second;
    }

private:
    map<int, int> m;
    
    pair<int, int> find(int left, int right) {
        auto l = m.upper_bound(left), r = m.upper_bound(right);
        if (l != m.begin() && (--l)->second < left) ++l;
        if (l == r) return {left, right};
        int i = min(left, l->first), j = max(right, (--r)->second);
        m.erase(l, ++r);
        return {i, j};
    }
};

 

類似題目:

Data Stream as Disjoint Intervals

Insert Interval

Merge Intervals

 

參考資料:

https://leetcode.com/problems/range-module/discuss/108914/c++-code

https://leetcode.com/problems/range-module/discuss/108912/C++-vector-O(n)-and-map-O(logn)-compare-two-solutions

 

LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM