LeetCode 周賽上分之旅 #45 精妙的 O(lgn) 掃描算法與樹上 DP 問題

LeetCode 雙周賽 113 概覽

T1. 使數(shù)組成為遞增數(shù)組的最少右移次數(shù)(Easy)

  • 標簽:模擬、暴力、線性遍歷

T2. 刪除數(shù)對后的最小數(shù)組長度(Medium)

  • 標簽:二分答案轻腺、雙指針帖努、找眾數(shù)、

T3. 統(tǒng)計距離為 k 的點對(Medium)

  • 標簽:枚舉宇姚、散列表

T4. 可以到達每一個節(jié)點的最少邊反轉次數(shù)(Hard)

  • 標簽:樹上 DP

T1. 使數(shù)組成為遞增數(shù)組的最少右移次數(shù)(Easy)

https://leetcode.cn/problems/minimum-right-shifts-to-sort-the-array/description/

題解一(暴力枚舉)

簡單模擬題尝哆。

由于題目數(shù)據(jù)量非常小秉撇,可以把數(shù)組復制一份拼接在尾部,再枚舉從位置 i 開始長為 n 的連續(xù)循環(huán)子數(shù)組是否連續(xù)较解,是則返回 (n - i)\%n

class Solution {
    fun minimumRightShifts(nums: MutableList<Int>): Int {
        val n = nums.size
        nums.addAll(nums)
        for (i in 0 until n) {
            if ((i + 1 ..< i + n).all { nums[it] > nums[it - 1]}) return (n - i) % n
        }
        return -1
    }
}
class Solution:
    def minimumRightShifts(self, nums: List[int]) -> int:
        n = len(nums)
        nums += nums
        for i in range(0, n):
            if all(nums[j] > nums[j - 1] for j in range(i + 1, i + n)):
                return (n - i) % n
        return -1

復雜度分析:

  • 時間復雜度:O(n^2) 雙重循環(huán)畜疾;
  • 空間復雜度:O(n) 循環(huán)數(shù)組空間赴邻。

題解二(線性遍歷)

更優(yōu)的寫法印衔,我們找到第一個逆序位置,再檢查該位置后續(xù)位置是否全部為升序姥敛,且滿足 nums[n - 1] < nums[0]

class Solution {
    fun minimumRightShifts(nums: List<Int>): Int {
        val n = nums.size
        for (i in 1 until n) { 
            // 第一段
            if (nums[i] >= nums[i - 1]) continue
            // 第二段
            if (nums[n - 1] > nums[0]) return -1
            for (j in i until n - 1) { 
                if (nums[j] > nums[j + 1]) return -1
            }
            return n - i
        }
        return 0
    }
}

復雜度分析:

  • 時間復雜度:O(n) i 指針和 j 指針總計最多移動 n 次奸焙;
  • 空間復雜度:O(1) 僅使用常量級別空間。

T2. 刪除數(shù)對后的最小數(shù)組長度(Medium)

https://leetcode.cn/problems/minimum-array-length-after-pair-removals/

題解一(二分答案)

問題存在單調性:

  • 當操作次數(shù) k 可以滿足時彤敛,操作次數(shù) k - 1 一定能滿足与帆;
  • 當操作次數(shù) k 不可滿足時,操作次數(shù) k + 1 一定不能滿足墨榄。

那么玄糟,原問題相當于求解滿足目標的最大操作次數(shù)。

現(xiàn)在需要考慮的問題是:如何驗證操作次數(shù) k 是否可以完成袄秩?

一些錯誤的思路:

  • 嘗試 1 - 貪心雙指針: nums[i] 優(yōu)先使用最小值阵翎,nums[j] 優(yōu)先使用最大值,錯誤用例:[1 2 3 6]之剧;
  • 嘗試 2 - 貪心: nums[i] 優(yōu)先使用最小值郭卫,nums[j] 使用大于 nums[i] 的最小值,錯誤用例:[1 2 4 6]背稼;
  • 嘗試 3 - 貪心: 從后往前遍歷贰军,nums[i] 優(yōu)先使用較大值,nums[j] 使用大于 nums[i] 的最小值蟹肘,錯誤用例:[2 3 4 8]词疼。

開始轉換思路:

能否將數(shù)組拆分為兩部分,作為 nums[i] 的分為一組帘腹,作為 nums[j] 的分為一組寒跳。 例如,在用例 [1 2 | 3 6][1 2 | 4 6][2 3 | 4 8] 中竹椒,將數(shù)組的前部分作為 nums[i] 而后半部分作為 nums[j] 時童太,可以得到最優(yōu)解,至此發(fā)現(xiàn)貪心規(guī)律。

設數(shù)組的長度為 n书释,最大匹配對數(shù)為 k

  • 結論 1: 使用數(shù)組的左半部分作為 nums[i] 且使用數(shù)組的右半部分作為 nums[j] 總能取到最優(yōu)解翘贮。反之,如果使用右半部分的某個數(shù) nums[t] 作為 nums[i]爆惧,相當于占用了一個較大的數(shù)狸页,不利于后續(xù) nums[i] 尋找配對;
  • 結論 2: 當固定 nums[i] 時扯再,nums[j] 越小越好芍耘,否則會占用一個較大的位置,不利于后續(xù) nums[i] 尋找配對熄阻。因此最優(yōu)解一定是使用左半部分的最小值與右半部分的最小值配對斋竞。

總結:如果存在 k 對匹配,那么一定可以讓最小的 k 個數(shù)和最大的 k 個數(shù)匹配秃殉。

基于以上分析坝初,可以寫出二分答案:

class Solution {
    fun minLengthAfterRemovals(nums: List<Int>): Int {
        val n = nums.size
        var left = 0
        var right = n / 2
        while (left < right) {
            val k = (left + right + 1) ushr 1
            if ((0 ..< k).all { nums[it] < nums[n - k + it] }) {
                left = k
            } else {
                right = k - 1
            }
        }
        return n - 2 * left
    }
}

復雜度分析:

  • 時間復雜度:O(nlgn) 二分答案次數(shù)最大為 lgn 次,單次檢驗的時間復雜度是 O(n)钾军;
  • 空間復雜度:O(1) 僅使用常量級別空間鳄袍。

題解二(雙指針)

基于題解一的分析,以及刪除操作的上界 n / 2吏恭,我們可以僅使用數(shù)組的后半部分與前半部分作比較拗小,具體算法:

  • i 指針指向索引 0
  • j 指針指向索引 (n + 1) / 2
  • 向右枚舉 j 指針,如果 i樱哼、j 指針指向的位置能夠匹配哀九,則向右移動 i 指針;
  • 最后 i 指針移動的次數(shù)就等于刪除操作次數(shù)唇礁。
class Solution {
    fun minLengthAfterRemovals(nums: List<Int>): Int {
        val n = nums.size
        var i = 0
        for (j in (n + 1) / 2 until n) {
            if (nums[i] < nums[j]) i++
        }
        return n - 2 * i
    }
}

復雜度分析:

  • 時間復雜度:O(n) 線性遍歷勾栗;
  • 空間復雜度:O(1) 僅使用常量級別空間。

題解三(眾數(shù))

由于題目的操作只要滿足 nums[i] < nums[j]盏筐,即兩個數(shù)不相等即可围俘,那么問題的解最終僅取決于數(shù)組中的眾數(shù)的出現(xiàn)次數(shù):

  • 如果眾數(shù)的出現(xiàn)次數(shù)比其他元素少,那么所有元素都能刪除琢融,問題的結果就看數(shù)組總長度是奇數(shù)還是偶數(shù)界牡;
  • 否則,剩下的元素就是眾數(shù):s - (n - s)

最后漾抬,由于數(shù)組是非遞減的宿亡,因此可以在 O(1) 空間求出眾數(shù)的出現(xiàn)次數(shù):

class Solution {
    fun minLengthAfterRemovals(nums: List<Int>): Int {
        val n = nums.size
        var s = 1
        var cur = 1
        for (i in 1 until n) {
            if (nums[i] == nums[i - 1]) {
                s = max(s, ++ cur)
            } else {
                cur = 1
            }
        }
        if (s <= n - s) {
            return n % 2
        } else {
            return s - (n - s)
        }
    }
}

復雜度分析:

  • 時間復雜度:O(n) 線性遍歷;
  • 空間復雜度:O(1) 僅使用常量級別空間纳令。

題解四(找規(guī)律 + 二分查找)

繼續(xù)挖掘數(shù)據(jù)規(guī)律:

s <= n - s 等價于眾數(shù)的出現(xiàn)次數(shù)超過數(shù)組長度的一半挽荠,由于數(shù)組是有序的克胳,那么一定有數(shù)組的中間位置就是眾數(shù),我們可以用二分查找找出眾數(shù)在數(shù)組中出現(xiàn)位置的邊界圈匆,從而計算出眾數(shù)的出現(xiàn)次數(shù)漠另。

由此,我們甚至不需要線性掃描都能計算出眾數(shù)以及眾數(shù)的出現(xiàn)次數(shù)跃赚,Nice笆搓!

當然,最后計算出來的出現(xiàn)次數(shù)有可能沒有超過數(shù)組長度的一半纬傲。

class Solution {
    fun minLengthAfterRemovals(nums: List<Int>): Int {
        val n = nums.size
        val x = nums[n / 2]
        val s = lowerBound(nums, x + 1) - lowerBound(nums, x)
        return max(2 * s - n, n % 2)
    }

    fun lowerBound(nums: List<Int>, target: Int): Int {
        var left = 0
        var right = nums.size - 1
        while (left < right) {
            val mid = (left + right + 1) ushr 1
            if (nums[mid] >= target) {
                right = mid - 1
            } else {
                left = mid
            }
        }
        return if (nums[left] == target) left else left + 1
    }
}

復雜度分析:

  • 時間復雜度:O(lgn) 單次二分查找的時間復雜度是 O(lgn)满败;
  • 空間復雜度:O(1) 僅使用常量級別空間。

相似題目:


T3. 統(tǒng)計距離為 k 的點對(Medium)

https://leetcode.cn/problems/count-pairs-of-points-with-distance-k/

題解(散列表)

  • 問題目標:(x1 xor x2) + (y1 xor y2) == k 的方案數(shù)叹括;
  • 技巧: 對于存在多個變量的問題算墨,可以考慮先固定其中一個變量;

容易想到兩數(shù)之和的問題模板领猾,唯一需要思考的問題是如何設計散列表的存取方式:

對于滿足 (x1\ xor\ x2) + (y1\ xor\ y2) == k 的方案米同,我們抽象為兩部分 i + j = k骇扇,其中摔竿,i = (x1\ xor\ x2) 的取值范圍為 [0, k],而 j = k - i少孝,即總共有 k + 1 種方案继低。本題的 k 數(shù)據(jù)范圍很小,所以我們可以寫出時間復雜度 O(nk) 的算法稍走。

class Solution {
    fun countPairs(coordinates: List<List<Int>>, k: Int): Int {
        var ret = 0
        // <x, <y, cnt>>
        val map = HashMap<Int, HashMap<Int, Int>>()
        for ((x2, y2) in coordinates) {
            // 記錄方案
            for (i in 0 .. k) {
                if (!map.containsKey(i xor x2)) continue
                ret += map[i xor x2]!!.getOrDefault((k - i) xor y2, 0)
            }
            // 累計次數(shù)
            map.getOrPut(x2) { HashMap<Int, Int>() }[y2] = map[x2]!!.getOrDefault(y2, 0) + 1
        }
        return ret
    }
}

Python 計數(shù)器支持復合數(shù)據(jù)類型的建袁翁,可以寫出非常簡潔的代碼:

class Solution:
    def countPairs(self, coordinates: List[List[int]], k: int) -> int:
        c = Counter()
        ret = 0
        for x2, y2 in coordinates:
            # 記錄方案
            for i in range(k + 1):
                ret += c[(i ^ x2, (k - i) ^ y2)]
            # 累計次數(shù)
            c[(x2, y2)] += 1
        return ret

復雜度分析:

  • 時間復雜度:O(n·k) 線性枚舉,每個元素枚舉 k 種方案婿脸;
  • 空間復雜度:O(n) 散列表空間粱胜。

T4. 可以到達每一個節(jié)點的最少邊反轉次數(shù)(Hard)

https://leetcode.cn/problems/minimum-edge-reversals-so-every-node-is-reachable/

問題分析

初步分析:

  • 問題目標: 求出以每個節(jié)點為根節(jié)點時,從根節(jié)點到其他節(jié)點的反轉操作次數(shù)狐树,此題屬于換根 DP 問題

思考實現(xiàn):

  • 暴力: 以節(jié)點 i 為根節(jié)點走一次 BFS/DFS焙压,就可以在 O(n) 時間內求出每個節(jié)點的解,整體的時間復雜度是 O(n^2)

思考優(yōu)化:

  • 重疊子問題: 相鄰邊連接的節(jié)點間存在重疊子問題抑钟,當我們從根節(jié)點 u 移動到其子節(jié)點 v 時涯曲,我們可以利用已有信息在 O(1) 時間算出 v 為根節(jié)點時的解。

具體實現(xiàn):

  • 1在塔、隨機選擇一個點為根節(jié)點 u幻件,在一次 DFS 中根節(jié)點 u 的反轉操作次數(shù):
  • 2、u → v 的狀態(tài)轉移:
    • 如果 u → v 是正向邊蛔溃,則反轉次數(shù) + 1绰沥;
    • 如果 u → v 是反向邊篱蝇,則反轉次數(shù) - 1(從 vu 不用反轉);
  • 3徽曲、由于題目是有向圖态兴,我們可以轉換為無向圖,再利用標記位 1-1 表示邊的方向疟位,1 為正向邊瞻润,-1 為反向邊。

題解(換根 DP)

class Solution {
    fun minEdgeReversals(n: Int, edges: Array<IntArray>): IntArray {
        val dp = IntArray(n)
        val graph = Array(n) { LinkedList<IntArray>() }
        // 建圖
        for ((from, to) in edges) {
            graph[from].add(intArrayOf(to, 1))
            graph[to].add(intArrayOf(from, -1))
        }

        // 以 0 為根節(jié)點
        fun dfs(i: Int, fa: Int) {
            for ((to, gain) in graph[i]) {
                if (to == fa) continue
                if (gain == -1) dp[0] ++
                dfs(to, i)
            }
        }

        fun dp(i: Int, fa: Int) {
            for ((to, gain) in graph[i]) {
                if (to == fa) continue
                // 狀態(tài)轉移
                dp[to] = dp[i] + gain
                dp(to, i)
            }
        }

        dfs(0, -1)
        dp(0, -1)
        
        return dp
    }
}

復雜度分析:

  • 時間復雜度:O(n) DFS 和換根 DP 都是 O(n)甜刻;
  • 空間復雜度:O(n) 遞歸椛茏玻空間與 DP 數(shù)組空間。

推薦閱讀

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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市祥绞,隨后出現(xiàn)的幾起案子非洲,更是在濱河造成了極大的恐慌,老刑警劉巖蜕径,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件两踏,死亡現(xiàn)場離奇詭異,居然都是意外死亡兜喻,警方通過查閱死者的電腦和手機梦染,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來朴皆,“玉大人帕识,你說我怎么就攤上這事∷煺。” “怎么了肮疗?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長扒接。 經(jīng)常有香客問我伪货,道長,這世上最難降的妖魔是什么珠增? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任超歌,我火速辦了婚禮,結果婚禮上蒂教,老公的妹妹穿的比我還像新娘巍举。我一直安慰自己,他們只是感情好凝垛,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布懊悯。 她就那樣靜靜地躺著蜓谋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪炭分。 梳的紋絲不亂的頭發(fā)上桃焕,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音捧毛,去河邊找鬼观堂。 笑死,一個胖子當著我的面吹牛呀忧,可吹牛的內容都是我干的师痕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼而账,長吁一口氣:“原來是場噩夢啊……” “哼胰坟!你這毒婦竟也來了?” 一聲冷哼從身側響起泞辐,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤笔横,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后咐吼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吹缔,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年汽烦,在試婚紗的時候發(fā)現(xiàn)自己被綠了涛菠。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莉御。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡撇吞,死狀恐怖,靈堂內的尸體忽然破棺而出礁叔,到底是詐尸還是另有隱情牍颈,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布琅关,位于F島的核電站煮岁,受9級特大地震影響,放射性物質發(fā)生泄漏涣易。R本人自食惡果不足惜画机,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望新症。 院中可真熱鬧步氏,春花似錦、人聲如沸徒爹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至界阁,卻和暖如春侯繁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背泡躯。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工贮竟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人较剃。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓坝锰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親重付。 傳聞我的和親對象是個殘疾皇子顷级,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容