區間問題合集【Python】

這篇文章記錄了區間調度問題之重疊區間、區間合併以及求區間交集。
解決區間問題的一般思路是先排序,再操作。關於排序方式的選擇,不同的題型選擇不同的排序方式:

  • 對於重疊區間問題,往往是和貪心策略有關,因此根據右端點排序,維護end變量。
      1. 用最少數量的箭引爆氣球
      1. 無重疊區間(或求最多的無重疊區間個數)
  • 對於合併區間問題,習慣來說就是從左至右依次合併,因此根據左端點排序,維護一個res數組,每次從res中取最後一個區間的右端點作爲比較的標準
      1. 合併區間
      1. 劃分字母區間

252. 會議室

給定一個會議時間安排的數組,每個會議時間都會包括開始和結束的時間 [[s1,e1],[s2,e2],…] (si < ei),請你判斷一個人是否能夠參加這裏面的全部會議。

    def canAttendMeetings(self, intervals):
        """
        252. 會議室:判斷是否能夠參加所有的會議
        """
        if len(intervals) <= 1:
            return True

        def getFirst(alist):
            return alist[0]

        intervals.sort(key=getFirst)

        for i in range(1, len(intervals)):
            if intervals[i][0] < intervals[i - 1][1]:
                return False

        return True

253. 會議室II

給定一個會議時間安排的數組,每個會議時間都會包括開始和結束的時間 [[s1,e1],[s2,e2],…] (si < ei),爲避免會議衝突,同時要考慮充分利用會議室資源,請你計算至少需要多少間會議室,才能滿足這些會議安排。

輸入: [[0, 30],[5, 10],[15, 20]]
輸出: 2

我們將所有區間在座標軸上畫好,然後用一個垂直於x軸的掃描線從左至右掃描,那麼我們的目標就是求掃描線和區間最多的交點數。那麼如何去求交點數目呢?

我們發現,每次遇到一個區間的左端點,交點數目就+1,每次遇到一個區間的右端點,交點數目就-1。所以我們對每個區間[left, right],保存爲[left, 1][right, -1],然後對所有點排序遍歷,累加第二維的值作爲最終結果。

    def minMeetingRooms(self, intervals):
        """
        253 會議室2:求出最少需要的會議室數目
        掃描線的應用:求與x軸垂直的豎線和所有區間的最多交點
        求交點的做法:遇到區間的左端點就+1,遇到右端點就-1
        """
        if len(intervals) <= 1:
            return len(intervals)

        # 構建新列表,對於所有區間[left, right]都分割成兩個:左端點[left, 1],右端點[right, -1]
        tmp = []
        for i in range(len(intervals)):
            tmp.append([intervals[i][0], 1])
            tmp.append([intervals[i][1], -1])

        tmp.sort(key=lambda x: x[0])

        res = 0
        cur = 0
        for i in range(len(tmp)):
            cur += tmp[i][1]
            res = max(res, cur)
        return res

452. 用最少數量的箭引爆氣球

給定每個氣球的座標,我們想找到使得所有氣球全部被引爆,所需的弓箭的最小數量。

輸入:
[[10,16], [2,8], [1,6], [7,12]]
輸出:
2
解釋:
對於該樣例,我們可以在x = 6(射爆[2,8],[1,6]兩個氣球)和 x = 11(射爆另外兩個氣球)。

碰到區間題,首先在座標軸上畫出來所有區間,然後這道題要使得所需弓箭最少,實際上是讓我們的每根掃描線與區間的交點數在大於等於1的情況下要最多。如下圖,紅色實線代表使得交點最多的掃描線,注意這邊②③兩條線,③比②更符合要求,因爲當出現了新的區間(圖中藍色虛線),因爲③更遠,所以更可能經過新的區間,這就是體現了貪心策略——每根掃描線是能夠經過當前所有區間的最遠位置。
在這裏插入圖片描述
區間題一個很重要的步驟就是對區間排序:可以根據區間的左端點和右端點進行排序,排序之後再根據端點的大小比較進行選擇操作。那麼究竟如何排序呢?這道題兩種排序方式都可以做,但是根據右端點排序會比較簡便。

  1. 根據左端點排序
    在這裏插入圖片描述
    根據以上討論,我們可以設置一個 max_end 標記, 它表示:在遍歷的過程中使用當前這隻箭能夠擊穿所有氣球的最遠距離。這個最遠距離,在每遍歷一個新區間的時候,都會檢查並更新:1)當新區間左端點在最遠距離外,需要一隻新的箭,更新最遠距離爲新區間的右端點。2)當新區間左端點在最遠距離內,無需新的箭,更新最遠距離爲min(當前的最遠距離,新區間的右端點)
    def findMinArrowShots(self, points):
        """
        452. 用最少數量的箭引爆氣球:
        貪心體現在當前這隻箭的位置使能夠射穿當前所有氣球的最遠位置 [1,5] [2,6] 那麼箭最好射在5處,因爲如果之後又來一個[4,7],那麼還是可以射穿
        這道題需要你體會按照左端點和右端點排序的差異
        """
        if len(points) <= 1:
            return len(points)

        # 1. 按照左端點排序
        points.sort(key=lambda x: x[0])
        res = 1
        max_end = points[0][1]  # 代表在遍歷的過程中使用當前這隻箭能夠擊穿所有氣球的最遠離
        for i in range(1, len(points)):
            # 新的區間左端點比箭的最遠距離還要遠,需要新的箭
            if points[i][0] > max_end:
                res += 1
                max_end = points[i][1]  # 更新當前這隻箭的最遠距離
            else:  # 新的區間左端點在最遠距離之內,不需要新的箭,但是要更新最遠距離
                max_end = min(max_end, points[i][1])
        return res
  1. 根據右端點排序
    可以看到,max_end標記表示在遍歷的過程中使用當前這隻箭能夠擊穿所有氣球的最遠距離,這個標記需要跟新區間的右端點進行比較來決定如何更新。那麼如果按照右端點排序的話,新區間的右端點一定是大於等於當前max_end的,所以省略了比較操作。
    def findMinArrowShots(self, points):
        """
        452. 用最少數量的箭引爆氣球:
        貪心體現在當前這隻箭的位置使能夠射穿當前所有氣球的最遠位置 [1,5] [2,6] 那麼箭最好射在5處,因爲如果之後又來一個[4,7],那麼還是可以射穿
        這道題需要你體會按照左端點和右端點排序的差異
        """
        if len(points) <= 1:
            return len(points)

        # 2. 按照右端點排序, 此時當新的區間左端點在最遠距離之內,就不需要更新最遠距離max_end
        points.sort(key=lambda x: x[1])
        res = 1
        max_end = points[0][1]  # 代表在遍歷的過程中使用當前這隻箭能夠擊穿所有氣球的最遠距離
        for i in range(1, len(points)):
            # 需要新的箭
            if points[i][0] > max_end:
                res += 1
                max_end = points[i][1]

        return res

435. 無重疊區間

給定一個區間的集合,找到需要移除區間的最小數量,使剩餘區間互不重疊。
注意:
可以認爲區間的終點總是大於它的起點。
區間 [1,2] 和 [2,3] 的邊界相互“接觸”,但沒有相互重疊。

輸入: [ [1,2], [2,3], [3,4], [1,3] ]
輸出: 1
解釋: 移除 [1,3] 後,剩下的區間沒有重疊。

這道題和求最多的無重疊區間是一個意思。
同樣,首先思考按照左端點還是右端點排序。因爲每一步都要使得後續區間的選擇範圍大,所以同參加最多的會議這個問題類似,按照右端點排序,結束時間越早,後續區間選擇範圍越大。

    def eraseOverlapIntervals(self, intervals):
        """
        435. 無重疊區間:找到需要移除區間的最小數量,使剩餘區間互不重疊。
        貪心體現在每一步都使得右端點最小,這樣可以使後續的區間選擇範圍更大。類似於參加儘可能多的會議
        """
        if len(intervals) <= 1:
            return 0

        # 按照右端點排序
        intervals.sort(key=lambda x:x[1])
        res = 0
        end = intervals[0][1]
        for i in range(1, len(intervals)):
            if intervals[i][0] < end:  # 有重疊
                res += 1  # 刪除數+1
            else:  # 無重疊,更新右邊界
                end = intervals[i][1]
        return res

56. 合併區間

給出一個區間的集合,請合併所有重疊的區間。

輸入: [[1,3],[2,6],[8,10],[15,18]]
輸出: [[1,6],[8,10],[15,18]]
解釋: 區間 [1,3] 和 [2,6] 重疊, 將它們合併爲 [1,6].

首先按照左端點對區間排序,因爲習慣來說我們就是從左到右合併的。然後維護一個結果數組res,每次從res取最後一個元素的右端點跟當前的區間左端點進行比較,決定是否合併。

    def merge(self, intervals):
        """
        56. 合併區間
        """
        if len(intervals) <= 1:
            return intervals

        intervals.sort(key=lambda x:x[0])

        # 先把第一個值加入res
        res = [intervals[0]]
        for i in range(1, len(intervals)):
            # 再去遍歷intervals,通過比較當前的第二維和res的第一維大小去決定是否要更新res的第二維
            # 需要更新
            if intervals[i][0] <= res[-1][1]:
                res[-1] = [res[-1][0], max(res[-1][1], intervals[i][1])]
            else:
                # 不需要更新
                res.append(intervals[i])

        return res

57. 插入區間

給出一個無重疊的 ,按照區間起始端點排序的區間列表。
在列表中插入一個新的區間,你需要確保列表中的區間仍然有序且不重疊(如果有必要的話,可以合併區間)。

示例 1:
輸入: intervals = [[1,3],[6,9]], newInterval = [2,5]
輸出: [[1,5],[6,9]]

示例 2:
輸入: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
輸出: [[1,2],[3,10],[12,16]]
解釋: 這是因爲新的區間 [4,8] 與 [3,5],[6,7],[8,10] 重疊。

實際上還是合併區間,因爲給出的原區間列表是排序好的且沒有重疊區間,所以原區間列表中左端點小於等於新區間的左端點的區間全部可以1)首先全部存放到res中,2)然後再把newinterval合併到res中,3)最後把區間列表中剩下的區間再合併到res中。這邊要注意2)中newinterval合併到res中的幾種情況。

    def insert(self, intervals, newInterval):
        """
        57. 插入區間
        在列表中插入一個新的區間,你需要確保列表中的區間仍然有序且不重疊
        """
        if not intervals:
            return [newInterval]
        res = []
        # 1. 首先把原區間列表中左端點小於等於新區間的左端點的區間全部存放到res中
        for i in range(len(intervals)):
            if intervals[i][0] > newInterval[0]:
                break
            res.append(intervals[i])

        # 2. 把newinterval合併到res中
        if not res:  # res爲空,說明newinterval左端點最小,直接進入res
            res.append(newInterval)
        elif res[-1][1] >= newInterval[0]:  # newinterval和最後一個區間合併
            res[-1] = [res[-1][0], max(res[-1][1], newInterval[1])]
        else:  # newinterval和最後一個區間沒有交集,直接進入res
            res.append(newInterval)

        # 3. 繼續將後續的區間壓入到res中
        while i < len(intervals):
            if res[-1][1] >= intervals[i][0]:
                res[-1] = [res[-1][0], max(res[-1][1], intervals[i][1])]
            else:
                res.append(intervals[i])
            i += 1

        return res

763. 劃分字母區間

字符串 S 由小寫字母組成。我們要把這個字符串劃分爲儘可能多的片段,同一個字母只會出現在其中的一個片段。返回一個表示每個字符串片段的長度的列表。

輸入:S = “ababcbacadefegdehijhklij”
輸出:[9,7,8]
解釋:
劃分結果爲 “ababcbaca”, “defegde”, “hijhklij”。
每個字母最多出現在一個片段中。
像 “ababcbacadefegde”, “hijhklij” 的劃分是錯誤的,因爲劃分的片段數較少。

每次遍歷到一個字符,那麼該字符最後一次出現的位置也必須得包括在當前這個區間中。所以對於每一個字符,該字符的第一次出現和最後一次出現的位置都必須包括在當前這個區間中。因此我們保存每個字符的第一次出現和最後一次出現的位置,作爲一個區間的左端點和右端點。那麼這個問題就轉化成了區間合併的問題。最後只要輸出每個合併區間的長度即可。

    def partitionLabels(self, S):
        """
        763. 劃分字母區間
        當遍歷到一個字符,該字符的第一次出現和最後一次出現的位置都必須包括到這個區間中
        用哈希表記錄每個字符第一次出現和最後一次出現的位置,將問題轉換爲重疊區間的問題
        """
        if not S:
            return []

        map = dict()

        # 1. 計算每個字符的第一次出現和最後一次出現的位置
        for i in range(len(S)):
            if S[i] not in map:
                map[S[i]] = [i, i]  # 第一次出現的區間
            else:
                map[S[i]][1] = i  # 更新第二維最後一次出現的位置

        # 2. 保存到區間中
        intervals = []
        for _, value in map.items():
            intervals.append(value)
        
        # 3. 合併區間
        intervals.sort(key=lambda x:x[0])
        res = [intervals[0]]  # 合併後的區間結果
        for i in range(1, len(intervals)):
            if res[-1][1] > intervals[i][0]:
                res[-1] = [res[-1][0], max(res[-1][1], intervals[i][1])]
            else:
                res.append(intervals[i])

        # 4. 求每個合併後區間的長度
        final_res = []
        for i in range(len(res)):
            final_res.append(res[i][1] - res[i][0] + 1)

        return final_res

986. 區間列表的交集

給定兩個由一些 閉區間 組成的列表,每個區間列表都是成對不相交的,並且已經排序。
返回這兩個區間列表的交集。
(形式上,閉區間 [a, b](其中 a <= b)表示實數 x 的集合,而 a <= x <= b。兩個閉區間的交集是一組實數,要麼爲空集,要麼爲閉區間。例如,[1, 3] 和 [2, 4] 的交集爲 [2, 3]。)

輸入:A = [[0,2],[5,10],[13,23],[24,25]], B = [[1,5],[8,12],[15,24],[25,26]]
輸出:[[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]

思路就和合並有序數組/鏈表類似,雙指針。比較兩個待選區間的右端點,來決定移動哪個指針(小的右端點移動指針)。

    def intervalIntersection(self, A, B):
        """
        986. 區間列表的交集
        """
        i, j = 0, 0  # 區間的序號
        res = []
        while i < len(A) and j < len(B):
            l = max(A[i][0], B[j][0])
            r = min(A[i][1], B[j][1])  # 求i區間和j區間的左右
            if l <= r:
                res.append([l, r])
            if A[i][1] <= B[j][1]:  # 比較右端點,來決定移動哪個的區間
                i += 1
            else:
                j += 1
        return res
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章