1. 程式人生 > >[LeetCode] Set Intersection Size At Least Two 設定交集大小至少為2

[LeetCode] Set Intersection Size At Least Two 設定交集大小至少為2

An integer interval [a, b] (for integers a < b) is a set of all consecutive integers from a to b, including a and b.

Find the minimum size of a set S such that for every integer interval A in intervals, the intersection of S with A has size at least 2.

Example 1:

Input: intervals = [[1, 3], [1, 4], [2, 5], [3, 5]]
Output: 3
Explanation:
Consider the set S = {2, 3, 4}.  For each interval, there are at least 2 elements from S in the interval.
Also, there isn't a smaller size set that fulfills the above condition.
Thus, we output the size of this set, which is 3.

Example 2:

Input: intervals = [[1, 2], [2, 3], [2, 4], [4, 5]]
Output: 5
Explanation:
An example of a minimum sized set is {1, 2, 3, 4, 5}.

Note:

  1. intervals will have length in range [1, 3000].
  2. intervals[i] will have length 2, representing some integer interval.
  3. intervals[i][j] will be an integer in [0, 10^8]
    .

這道題給了我們一些區間,讓我們求一個集合S,使得S和每個區間的交集至少為2,即至少有兩個相同的數字。博主最開始分析題目中的例子的時候,以為要求的集合S也必須是一個連續的區間,其實不需要的,離散的數字就可以了。比如如果區間是[1,3], [5,6]的話,那麼返回的集合長度是4,而不是5。這道題可以是用貪婪演算法來解,一般來說Hard的題目能用貪婪演算法而不是DP解的是少之又少,這道題為我大Greedy演算法正名了~!為了使得集合S中的數字儘可能的小,我們希望處理區間的時候從小區間開始,如果區間b完全覆蓋了區間a,那麼和區間a有兩個相同數字的集合,一定和區間b也有兩個相同數字。同樣,我們不希望一會處理一個前面的區間,一會又處理一個很後面的區間,我們希望區間是有序的。那麼如何給區間排序呢,是按起始位置排,還是按結束位置排,這裡我們按結束位置從小往大排,當兩個結束位置相同時,起始位置大的排前面先處理,這也符合我們先處理小區間的原則。那麼遍歷區間的時候,當前區間就和我們維護的集合S有三種情況:

1. 二者完全沒有交集,這時候我們就需要從當前區間中取出兩個數字加入集合S,取哪兩個數呢?為了儘可能少使用數字,我們取當前區間中的最大兩個數字,因為我們區間位置不斷變大,所以取大的數字有更高的概率能和後面的區間有交集。

2. 二者有一個數字的交集,那麼這個交集數字一定是區間的起始位置,那麼我們需要從當前區間中再取一個數字加入集合S,根據上面的分析,我們取最大的那個數,即區間的結束位置。

3. 二者有兩個及兩個以上數字的交集,那麼不用做任何處理。

好,分析到這裡,程式碼也就不難寫出來了,我們用個數組v來表示集合S,初始化放兩個-1進去,因為題目中說明了區間都是大於0的,所以我們放這兩個陣列進去是為了防止越界的,不會有任何影響,最後統計長度的時候減去這個兩個數字就可以了。先給區間排序,然後遍歷每個區間,如果區間的起始位置小於等於陣列的倒數第二個數字,說明此時已經有兩個相同的數字了,直接跳過當前區間。否則如果區間的起始位置大於陣列的最後一個位置,說明二者沒有任何交集,我們此時先把區間的倒數第二小的數字加入陣列v中。然後統一再把區間的結束位置加入陣列v中,因為不管區間和陣列有一個交集還是沒有任何交集,結束位置都要加入陣列中,所以不用放在if..else..中。最後迴圈結束,返回陣列的大小減2,參見程式碼如下:

解法一:

class Solution {
public:
    int intersectionSizeTwo(vector<vector<int>>& intervals) {
        vector<int> v{-1, -1};
        sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b){
            return a[1] < b[1] || (a[1] == b[1] && a[0] > b[0]);
        });
        for (auto &interval : intervals) {
            int len = v.size();
            if (interval[0] <= v[len - 2]) continue;
            if (interval[0] > v.back()) v.push_back(interval[1] - 1);
            v.push_back(interval[1]);
        }
        return v.size() - 2;
    }
};

我們可以對空間複雜度進行優化,我們不用儲存整個集合S,因為結果只讓我們返回長度即可,所以我們用兩個變數p1和p2,其中p1表示集合S中倒數第二大的數字,p2為集合S中最大的數字。我們的整個邏輯跟上面的解法是相同的。遍歷區間的時候,如果區間的起始位置小於等於p1,則跳過當前區間。否則如果起始位置大於p2,說明沒有交集,需要加上兩個數字,結果res自增2,然後p2賦值為當前區間的結束位置,p1賦值為第二大的數字。否則說明只有一個交集,結果res自增1,然後p1賦值為p2,p2賦值為當前區間的結束位置即可,參見程式碼如下:

解法二:

class Solution {
public:
    int intersectionSizeTwo(vector<vector<int>>& intervals) {
        int res = 0, p1 = -1, p2 = -1;
        sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b){
            return a[1] < b[1] || (a[1] == b[1] && a[0] > b[0]);
        });
        for (auto &interval : intervals) {
            if (interval[0] <= p1) continue;
            if (interval[0] > p2) {
                res += 2;
                p2 = interval[1];
                p1 = p2 - 1;
            } else {
                ++res;
                p1 = p2;
                p2 = interval[1];
            }
        }
        return res;
    }
};

討論:這道題的一個拓展就是一般化,改為交集大小至少為k,那麼我們該怎麼做呢?其實也不是很難,我們可以在解法一的基礎上進行修改,我們還是用陣列v來表示集合S,只不過我們初始化加入k個-1。然後還是要給區間排序,之後進行遍歷,如果起始位置小於等於倒數第k個數,跳過當前區間。然後就是重點部分了,我們還是用起始位置跟陣列v的最後面的數字比較,總共比到倒數第k-1個數就行了,因為小於等於倒數第k個數已經跳過了。如果大於最後一個數,則將區間後k個數加入陣列;否則如果大於倒數第二個數,則將區間後k-1個數加入陣列;否則如果大於倒數第三個數,則將陣列後k-2個數加入陣列,以此類推,直到比完倒數第k-1個數就行了,最後返回的是陣列v的長度減去k。

參考資料: