646. 最長數對鏈 : 常規貪心 DP 運用題
題目描述
這是 LeetCode 上的 646. 最長數對鏈 ,難度為 中等 。
Tag : 「貪心」、「排序」、「二分」、「序列 DP」、「LIS」
給出 n
個數對。 在每一個數對中,第一個數字總是比第二個數字小。
現在,我們定義一種跟隨關係,當且僅當 b < c
時,數對 $(c, d)$ 才可以跟在 $(a, b)$ 後面。我們用這種形式來構造一個數對鏈。
給定一個數對集合,找出能夠形成的最長數對鏈的長度。你不需要用到所有的數對,你可以以任何順序選擇其中的一些數對來構造。
示例:
輸入:[[1,2], [2,3], [3,4]] 輸出:2 解釋:最長的數對鏈是 [1,2] -> [3,4]
提示:
- 給出數對的個數在 $[1, 1000]$ 範圍內。
排序 + 貪心 DP(轉移剪枝)
起始先將 pairs
根據第一維排升序(或直接雙關鍵字排升序)。
考慮定義 $f[i]$ 為以 $pairs[i]$ 為結尾的最長數對鏈長度,所有 $f[i]$ 中的最大值為答案。
不失一般性考慮 $f[i]$ 該如何轉移:不難發現 $f[i]$ 為所有滿足「下標範圍在 $[0, i - 1]$,且 $pairs[j][1] < pairs[i][0]$」條件的 $f[j] + 1$ 的最大值。
但實際上,我們只需要從 $j = i - 1$ 開始往回找,找到第一個滿足 $pairs[j][1] < pairs[i][0]$ 的位置 $j$ 即可。
容易證明該做法的正確性: 假設貪心解(該做法)找到的位置 $j$ 不是最優位置,即存在比 $j$ 更小的合法下標 $j'$ 滿足 $f[j'] > f[j]$。根據我們的排序規則必然有 $pairs[j'][0] <= pairs[j][0]$ 的性質,則可知 $pairs[j]$ 必然可以代替 $pairs[j']$ 接在原本以 $pairs[j']$ 為結尾的最優數鏈上(最優數鏈長度不變,結果不會變差),則至少有 $f[j'] = f[j]$。
程式碼:
class Solution { public int findLongestChain(int[][] pairs) { Arrays.sort(pairs, (a,b)->a[0]-b[0]); int n = pairs.length, ans = 1; int[] f = new int[n]; for (int i = 0; i < n; i++) { f[i] = 1; for (int j = i - 1; j >= 0 && f[i] == 1; j--) { if (pairs[j][1] < pairs[i][0]) f[i] = f[j] + 1; } ans = Math.max(ans, f[i]); } return ans; } }
DP
排序 + 貪心 DP(優化轉移)
根據上述分析,我們知道對於一個特定的 $pairs[i]$ 而言,其所有合法(滿足條件 $pairs[j][1] < pairs[i][0]$)的前驅狀態 $f[j]$ 必然是非單調遞增的。
根據 LIS
問題的貪心解的思路,我們可以額外使用一個數組記錄下特定長度數鏈的最小結尾值,從而實現二分找前驅狀態。
具體的,建立 $g$ 陣列,其中 $g[len] = x$ 代表數鏈長度為 $len$ 時結尾元素的第二維最小值為 $x$。
如此一來,當我們要找 $f[i]$ 的前驅狀態時,等價於在 $g$ 陣列中找滿足「小於 $pairs[i][0]$」的最大下標。同時,我們不再需要顯式維護 $f$ 陣列,只需要邊轉移變更新答案即可。
不瞭解 LIS
問題的同學可以看前置 : LCS 問題與 LIS 問題的相互關係,以及 LIS 問題的最優解證明
:tada::tada::tada:
程式碼:
class Solution { public int findLongestChain(int[][] pairs) { Arrays.sort(pairs, (a,b)->a[0]-b[0]); int n = pairs.length, ans = 1; int[] g = new int[n + 10]; Arrays.fill(g, 0x3f3f3f3f); for (int i = 0; i < n; i++) { int l = 1, r = i + 1; while (l < r) { int mid = l + r >> 1; if (g[mid] >= pairs[i][0]) r = mid; else l = mid + 1; } g[r] = Math.min(g[r], pairs[i][1]); ans = Math.max(ans, r); } return ans; } }
DP
最後
這是我們「刷穿 LeetCode」系列文章的第 No.646
篇,系列開始於 2021/01/01,截止於起始日 LeetCode 上共有 1916 道題目,部分是有鎖題,我們將先把所有不帶鎖的題目刷完。
在這個系列文章裡面,除了講解解題思路以外,還會盡可能給出最為簡潔的程式碼。如果涉及通解還會相應的程式碼模板。
為了方便各位同學能夠電腦上進行除錯和提交程式碼,我建立了相關的倉庫: https://github.com/SharingSou... 。
在倉庫地址裡,你可以看到系列文章的題解連結、系列文章的相應程式碼、LeetCode 原題連結和其他優選題解。
更多更全更熱門的「筆試/面試」相關資料可訪問排版精美的合集新基地 :tada::tada:
本文由mdnice多平臺釋出
- 天翼雲全場景業務無縫替換至國產原生作業系統CTyunOS!
- 以羊了個羊為例,淺談小程式抓包與響應報文修改
- 這幾種常見的 JVM 調優場景,你知道嗎?
- 如此狂妄,自稱高效能佇列的Disruptor有啥來頭?
- 為什麼要學習GoF設計模式?
- 827. 最大人工島 : 簡單「並查集 列舉」運用題
- 手把手教你如何使用 Timestream 實現物聯網時序資料儲存和分析
- 850. 矩形面積 II : 掃描線模板題
- Java 併發程式設計解析 | 基於JDK原始碼解析Java領域中的併發鎖,我們可以從中學習到什麼內容?
- 【手把手】光說不練假把式,這篇全鏈路壓測實踐探索
- 大廠鍾愛的全鏈路壓測有什麼意義?四種壓測方案詳細對比分析
- 寫個續集,填坑來了!關於“Thread.sleep(0)這一行‘看似無用’的程式碼”裡面留下的坑。
- 857. 僱傭 K 名工人的最低成本 : 列舉 優先佇列(堆)運用題
- Vue3 實現一個自定義toast(小彈窗)
- 669. 修剪二叉搜尋樹 : 常規樹的遍歷與二叉樹性質
- 讀完 RocketMQ 原始碼,我學會了如何優雅的建立執行緒
- 效能調優——小小的log大大的坑
- 1582. 二進位制矩陣中的特殊位置 : 簡單模擬題
- elementui原始碼學習之仿寫一個el-switch
- 646. 最長數對鏈 : 常規貪心 DP 運用題