LeetCode 周賽上分之旅 # 37 多源 BFS 與連通性問題

周賽 357

T1. 故障鍵盤(Easy)

  • 標(biāo)簽:模擬完残、字符串

T2. 判斷是否能拆分?jǐn)?shù)組(Medium)

  • 標(biāo)簽:思維

T3. 找出最安全路徑(Medium)

  • 標(biāo)簽:BFS业稼、連通性盗痒、分層并查集、極大化極小、二分查找

T4. 子序列最大優(yōu)雅度(Hard)

  • 標(biāo)簽:貪心俯邓、排序骡楼、堆

T1. 故障鍵盤(Easy)

https://leetcode.cn/problems/faulty-keyboard/

題解(模擬)

簡(jiǎn)單模擬題。

  • 在遇到 i 字符時(shí)對(duì)已填入字符進(jìn)行反轉(zhuǎn)稽鞭,時(shí)間復(fù)雜度是 O(n^2)鸟整;
  • 使用隊(duì)列和標(biāo)記位可以優(yōu)化時(shí)間復(fù)雜度,在遇到 i 時(shí)修改標(biāo)記位和寫入方向朦蕴,在最后輸出時(shí)根據(jù)標(biāo)記位輸出篮条,避免中間的反轉(zhuǎn)操作。
class Solution {
public:
    string finalString(string s) {
        vector<char> dst;
        for (auto& c : s) {
            if (c == 'i') {
                reverse(dst.begin(), dst.end());
            } else {
                dst.push_back(c);
            }
        }
        return string(dst.begin(), dst.end());
    }
};
class Solution {
public:
    string finalString(string s) {
        deque<char> dst;
        bool rear = true;
        for (auto& c : s) {
            if (c == 'i') {
                rear = !rear;
            } else {
                if (rear) {
                    dst.push_back(c);
                } else {
                    dst.push_front(c);
                }
            }
        }
        return rear ? string(dst.begin(), dst.end()) : string(dst.rbegin(), dst.rend());
    }
};

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(n) 線性遍歷和輸出時(shí)間吩抓;
  • 空間復(fù)雜度:O(n) 臨時(shí)字符串空間涉茧。

T2. 判斷是否能拆分?jǐn)?shù)組(Medium)

https://leetcode.cn/problems/check-if-it-is-possible-to-split-array/

題解(思維題)

思維題,主要題目的兩個(gè)條件只要滿足其中一個(gè)即可 ??

  • 條件 1:子數(shù)組的長(zhǎng)度為 1 ? 說(shuō)明數(shù)組長(zhǎng)度小于等于 2 的時(shí)候疹娶,一定可以滿足(子數(shù)組的長(zhǎng)度不大于 1)伴栓;
  • 條件 2:子數(shù)組元素之和大于或等于 m ? 需滿足子數(shù)組 {a1, a2, a3} 與 {a4, a5, a6} 的子數(shù)組和均大于等于 m。

結(jié)合兩個(gè)條件雨饺,如果我們能找到兩個(gè)相鄰的元素之和大于等于 m钳垮,那么總可以通過消除 1 個(gè)元素的方式完成題目要求。

例如在示例 3 [2, 3, 3, 2, 3] 中沛膳,我們以 [3,3] 為起點(diǎn)倒推:

  • [3, 3]
  • [2, 3, 3] 消除 2
  • [2, 3, 3, 2] 消除 2
  • [2, 3, 3, 2, 3] 消除 3
class Solution {
public:
    bool canSplitArray(vector<int>& nums, int m) {
        // 2 | 3, 3 | 2 | 3
        // 1, 3, 2, 2, 3
        // 1, 1, 1, 3, 3
        if (nums.size() <= 2) return true;
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] + nums[i - 1] >= m) return true;
        }
        return false;
    }
};

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(n) 線性遍歷時(shí)間扔枫;
  • 空間復(fù)雜度:O(1) 僅使用常量級(jí)別空間。

T3. 找出最安全路徑(Medium)

https://leetcode.cn/problems/find-the-safest-path-in-a-grid/

題解一(多源 BFS + 二分答案)

根據(jù)題目描述锹安,每個(gè)節(jié)點(diǎn)的安全系數(shù)定位為該節(jié)點(diǎn)到「小偷」節(jié)點(diǎn)的最小曼哈頓距離短荐,而題目要求是尋找從 [0][0] 到 [n-1][n-1] 的最大安全系數(shù)√究蓿「使得最小曼哈頓距離最大」暗示可能是需要使用二分答案的極大化極小問題忍宋。

  • 多源 BFS 預(yù)處理: 先從每個(gè)「小偷」節(jié)點(diǎn)開始走 BFS 更新相鄰節(jié)點(diǎn)的最小曼哈頓距離,單次 BFS 的時(shí)間復(fù)雜度是 O(n^2)风罩,雖然我們可以用剪枝優(yōu)化糠排,但整體的時(shí)間復(fù)雜度上界是 O(n^4)。為了優(yōu)化時(shí)間復(fù)雜度超升,我們使用多源 BFS(也可以理解為拓?fù)渑判蛉牖拢看螐棾龅墓?jié)點(diǎn)的曼哈頓距離最小)室琢,整體的時(shí)間僅為 O(n^2)乾闰;
  • 二分答案: 安全系數(shù)與路徑可達(dá)性存在單調(diào)性:
    • 當(dāng)安全系數(shù)越大時(shí),越不容易可達(dá)盈滴;
    • 當(dāng)安全系數(shù)越小時(shí)涯肩,越容易可達(dá)。
    • 安全系數(shù)的下界為 0,上界為 n * 2 - 1病苗,通過二分答案尋找滿足可達(dá)性的最大安全系數(shù):
class Solution {
    fun maximumSafenessFactor(grid: List<List<Int>>): Int {
        val INF = Integer.MAX_VALUE
        val directions = arrayOf(intArrayOf(0,1), intArrayOf(1,0), intArrayOf(0,-1), intArrayOf(-1,0))
        val n = grid.size
        // 特判
        if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return 0
        // 多源 BFS(拓?fù)渑判颍?        val safe = Array(n) { IntArray(n) { -1 }}
        var queue = LinkedList<IntArray>()
        for (r in 0 until n) {
            for (c in 0 until n) {
                if (grid[r][c] == 1) {
                    queue.offer(intArrayOf(r, c))
                    safe[r][c] = 0
                }
            }
        }
        while (!queue.isEmpty()) {
            val temp = LinkedList<IntArray>()
            for (node in queue) {
                for (direction in directions) {
                    val newX = node[0] + direction[0]
                    val newY = node[1] + direction[1]
                    if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] != -1) continue
                    temp.offer(intArrayOf(newX, newY))
                    safe[newX][newY] = safe[node[0]][node[1]] + 1
                }
            }
            queue = temp
        }

        // for (row in safe) println(row.joinToString())

        // BFS(檢查只通過大于等于 limit 的格子疗垛,能否到達(dá)終點(diǎn))
        fun check(limit: Int) : Boolean {
            val visit = Array(n) { BooleanArray(n) }
            var queue = LinkedList<IntArray>()
            queue.offer(intArrayOf(0, 0))
            visit[0][0] = true
            while (!queue.isEmpty()) {
                val temp = LinkedList<IntArray>()
                for (node in queue) {
                    // 終止條件
                    if (node[0] == n - 1 && node[1] == n - 1) return true
                    for (direction in directions) {
                        val newX = node[0] + direction[0]
                        val newY = node[1] + direction[1]
                        if (newX < 0 || newX >= n || newY < 0 || newY >= n || visit[newX][newY] || safe[newX][newY] < limit) continue
                        temp.offer(intArrayOf(newX, newY))
                        visit[newX][newY] = true
                    }
                }
                queue = temp
            }
            return false
        }

        // 二分查找
        var left = 0
        var right = Math.min(safe[0][0], safe[n - 1][n - 1])
        while (left < right) {
            val mid = (left + right + 1) ushr 1
            if (!check(mid)) {
                right = mid - 1
            } else {
                left = mid
            }
        }
        return left
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(n^2·lgn^2) 其中 多源 BFS 時(shí)間為 O(n^2),單次檢查的 BFS 時(shí)間復(fù)雜度為 O(n^2)硫朦,二分的次數(shù)為 lgn^2贷腕,整體時(shí)間復(fù)雜度是 O(n^2·lgn^2)
  • 空間復(fù)雜度:O(n^2) safe 安全系數(shù)矩陣空間咬展。

題解二(多源 BFS + 堆)

思路參考雪景式的題解花履。

在題解一預(yù)處理的基礎(chǔ)上,同樣走一次 BFS 也能夠算出最大安全系數(shù)挚赊,思路類似于 Dijkstra 最最短路算法中使用當(dāng)前最短路最短的節(jié)點(diǎn)去松弛相鄰邊,我們優(yōu)先讓當(dāng)前曼哈頓距離最大的節(jié)點(diǎn)去松弛相鄰節(jié)點(diǎn)济瓢,以保證每個(gè)節(jié)點(diǎn)都能夠從較大的路徑轉(zhuǎn)移過來(lái)荠割。

class Solution {
    fun maximumSafenessFactor(grid: List<List<Int>>): Int {
        ...
        // 類最短路(使用曼哈頓距離最大的節(jié)點(diǎn)去松弛相鄰邊)
        val heap = PriorityQueue<IntArray>() { e1, e2 ->
            e2[0] - e1[0]
        }
        heap.offer(intArrayOf(safe[0][0], 0, 0))
        val visit = Array(n) { BooleanArray(n) }
        visit[0][0] = true
        while (!heap.isEmpty()) {
            val node = heap.poll()
            if (node[1] == n - 1 && node[2] == n - 1) return node[0]
            for (direction in directions) {
                val newX = node[1] + direction[0]
                val newY = node[2] + direction[1]
                if (newX < 0 || newX >= n || newY < 0 || newY >= n || visit[newX][newY]) continue
                // 松弛相鄰邊
                heap.offer(intArrayOf(Math.min(node[0], safe[newX][newY]), newX, newY))
                visit[newX][newY] = true
            }
        }
        return 0
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(n^2·lgn^2) 其中 多源 BFS 時(shí)間為 O(n^2),基于堆的 BFS 的時(shí)間復(fù)雜度為 O(n^2·lgn^2)旺矾;
  • 空間復(fù)雜度:O(n^2) safe 安全系數(shù)矩陣空間蔑鹦。

題解三(多源 BFS + 分層并查集)

思路參考靈神的題解。

其實(shí)箕宙,求從 [0][0] 到 [n - 1][n - 1] 的最大安全系數(shù)嚎朽,也相當(dāng)于連通性問題的變形,而連通性問題有并查集的解法柬帕。為了求得最大安全系數(shù)哟忍,我們使用分層并查集:

  • 首先,在預(yù)處理階段求出每個(gè)節(jié)點(diǎn)的最小曼哈頓距離陷寝,并將節(jié)點(diǎn)按照曼哈頓距離分類锅很;
  • 其次,我們從最大的曼哈頓距離開始逆序合并凤跑,當(dāng) [0][0] 和 [n - 1][n - 1] 連通時(shí)返回結(jié)果爆安。
class Solution {
    fun maximumSafenessFactor(grid: List<List<Int>>): Int {
        val directions = arrayOf(intArrayOf(0,1), intArrayOf(1,0), intArrayOf(0,-1), intArrayOf(-1,0))
        val n = grid.size
        // 特判
        if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return 0
        // 多源 BFS(拓?fù)渑判颍?        val safe = Array(n) { IntArray(n) { -1 }}
        // 分層
        val groups = LinkedList<LinkedList<IntArray>>()
        var queue = LinkedList<IntArray>()
        for (r in 0 until n) {
            for (c in 0 until n) {
                if (grid[r][c] == 1) {
                    queue.offer(intArrayOf(r, c))
                    safe[r][c] = 0
                }
            }
        }
        groups.add(queue)
        while (!queue.isEmpty()) {
            val temp = LinkedList<IntArray>()
            for (node in queue) {
                for (direction in directions) {
                    val newX = node[0] + direction[0]
                    val newY = node[1] + direction[1]
                    if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] != -1) continue
                    temp.offer(intArrayOf(newX, newY))
                    safe[newX][newY] = safe[node[0]][node[1]] + 1
                }
            }
            queue = temp
            if (!queue.isEmpty()) groups.add(queue)
        }

        // for (row in safe) println(row.joinToString())
        // for (row in groups) println(row.joinToString())

        val helper = UnionFind(n)
        // 逆序合并
        for (i in groups.size - 1 downTo 0) {
            for (node in groups[i]) {
                val x = node[0]
                val y = node[1]
                for (direction in directions) {
                    val newX = x + direction[0]
                    val newY = y  + direction[1]
                    // 合并曼哈頓距離大于等于當(dāng)前層的節(jié)點(diǎn)
                    if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] < i) continue
                    helper.union(x * n + y, newX * n + newY)
                }
            }
            if (helper.find(0) == helper.find(n * n - 1)) return i
        }
        return 0
    }

    class UnionFind(private val n: Int) {
        private val parents = IntArray(n * n) { it }
        private val ranks = IntArray(n * n)

        fun find(x: Int): Int {
            var cur = x
            while (cur != parents[cur]) {
                parents[cur] = parents[parents[cur]]
                cur = parents[cur]
            }
            return cur
        }

        fun union(x: Int, y: Int) {
            val rootX = find(x)
            val rootY = find(y)
            if (ranks[rootX] < ranks[rootY]) {
                parents[rootX] = rootY
            } else if (ranks[rootX] > ranks[rootY]){
                parents[rootY] = rootX
            } else {
                parents[rootY] = rootX
                ranks[rootX]++
            }
        }
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(n^2) 其中 多源 BFS 時(shí)間為 O(n^2),基于路徑壓縮和按秩合并的并查集時(shí)間復(fù)雜度為 O(n^2)仔引;
  • 空間復(fù)雜度:O(n^2) safe 安全系數(shù)矩陣空間扔仓。

T4. 子序列最大優(yōu)雅度(Hard)

https://leetcode.cn/problems/maximum-elegance-of-a-k-length-subsequence/

題解(反悔貪心 + 堆)

  • 固定一個(gè)維度: 題目定義的優(yōu)雅度 total_profit + distinct_categories^2 存在兩個(gè)維度的變量,我們考慮固定其中一個(gè)維度來(lái)簡(jiǎn)化問題討論:
    • 對(duì)所有節(jié)點(diǎn)按照利潤(rùn)從大到小逆序排列咖耘,并選擇前 k 個(gè)節(jié)點(diǎn)翘簇,此時(shí)的 total_profit 是最大的;
    • 在此基礎(chǔ)上鲤看,我們繼續(xù)遍歷剩余的 n - k 個(gè)節(jié)點(diǎn)缘揪,并考慮替換前 k 個(gè)節(jié)點(diǎn)中的某個(gè)節(jié)點(diǎn),由于已經(jīng)選擇的節(jié)點(diǎn) total_profit 是最大的,因此需要讓替換后的類目數(shù)變多找筝。
  • 分類討論(替換哪個(gè)):
    • 1蹈垢、如果某個(gè)已選節(jié)點(diǎn)與第 i 個(gè)節(jié)點(diǎn)的類目相同,那么替換后不會(huì)讓類目數(shù)變大袖裕,不可能讓優(yōu)雅度變大曹抬;
    • 2、如果某個(gè)已選節(jié)點(diǎn)與第 i 個(gè)節(jié)點(diǎn)的類目不同急鳄,但只出現(xiàn)一次谤民,那么替換出不會(huì)讓類目變大,不可能讓優(yōu)雅度變大疾宏。否則张足,如果出現(xiàn)多次,替換后類目數(shù)變大坎藐,有可能讓優(yōu)雅度變大为牍;
    • 3、為了讓優(yōu)雅度盡可能大岩馍,我們期望替換后的 total_profit 的減少量盡可能小碉咆,同時(shí)數(shù)目類別應(yīng)該增大,否則無(wú)法獲得更大的優(yōu)雅度蛀恩。為了讓替換后的 total_profit 的減少量盡可能小疫铜,我們應(yīng)該替換已選列表中利潤(rùn)最小同時(shí)重復(fù)的節(jié)點(diǎn)。
  • 怎么高效替換:
    • 使用堆維護(hù)利潤(rùn)最小同時(shí)重復(fù)的元素双谆,由于我們是從大到小線性枚舉的壳咕,因此直接使用線性表模擬堆的能力;
    • 新替換進(jìn)去的不會(huì)被替換出去(想想為什么)顽馋。
class Solution {
    fun findMaximumElegance(items: Array<IntArray>, k: Int): Long {
        Arrays.sort(items) { e1, e2 ->
            e2[0] - e1[0]
        }
        var ret = 0L
        var totalProfit = 0L
        // duplicate:小頂堆
        val duplicate = LinkedList<Int>()
        // categorySet:類目表
        val categorySet = HashSet<Int>()
        for ((i, item) in items.withIndex()) {
            val profit = item[0]
            val category = item[1]
            if (i < k) {
                totalProfit += item[0]
                // 記錄重復(fù)節(jié)點(diǎn)
                if (categorySet.contains(category)) {
                    duplicate.add(profit)
                }
                categorySet.add(category)
            } else if (!duplicate.isEmpty() && !categorySet.contains(category)){
                // 替換
                totalProfit += profit - duplicate.pollLast()
                categorySet.add(category)
            } else {
                // 不會(huì)讓類目數(shù)量變大
            }
            // println(duplicate.joinToString())
            ret = Math.max(ret, totalProfit + 1L * categorySet.size * categorySet.size)
        }
        return ret
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(nlgn) 瓶頸在排序囱井;
  • 空間復(fù)雜度:O(n) 堆空間。

推薦閱讀

LeetCode 上分之旅系列往期回顧:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末庞呕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子程帕,更是在濱河造成了極大的恐慌住练,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件愁拭,死亡現(xiàn)場(chǎng)離奇詭異讲逛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)岭埠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門盏混,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蔚鸥,“玉大人,你說(shuō)我怎么就攤上這事许赃≈古纾” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵混聊,是天一觀的道長(zhǎng)弹谁。 經(jīng)常有香客問我,道長(zhǎng)句喜,這世上最難降的妖魔是什么预愤? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮咳胃,結(jié)果婚禮上植康,老公的妹妹穿的比我還像新娘。我一直安慰自己展懈,他們只是感情好向图,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著标沪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嗜傅。 梳的紋絲不亂的頭發(fā)上金句,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音吕嘀,去河邊找鬼违寞。 笑死,一個(gè)胖子當(dāng)著我的面吹牛偶房,可吹牛的內(nèi)容都是我干的趁曼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼棕洋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼挡闰!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起掰盘,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤摄悯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后愧捕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奢驯,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年次绘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瘪阁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撒遣。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖管跺,靈堂內(nèi)的尸體忽然破棺而出义黎,到底是詐尸還是另有隱情,我是刑警寧澤伙菜,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布轩缤,位于F島的核電站,受9級(jí)特大地震影響贩绕,放射性物質(zhì)發(fā)生泄漏火的。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一淑倾、第九天 我趴在偏房一處隱蔽的房頂上張望馏鹤。 院中可真熱鬧,春花似錦娇哆、人聲如沸湃累。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)治力。三九已至,卻和暖如春勃黍,著一層夾襖步出監(jiān)牢的瞬間宵统,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工覆获, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留马澈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓弄息,卻偏偏與公主長(zhǎng)得像痊班,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子摹量,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容