關(guān)于最長(zhǎng)遞增子序列問(wèn)題概述
一、最長(zhǎng)遞增子序列問(wèn)題概述
1. 問(wèn)題定義
給定一個(gè)整數(shù)序列,例如 nums = [10, 9, 2, 5, 3, 7, 101, 18]
,要找出它的一個(gè)最長(zhǎng)的子序列,使得這個(gè)子序列中的元素是嚴(yán)格遞增的。
在上述例子中,最長(zhǎng)遞增子序列是 [2, 3, 7, 101]
或者 [2, 5, 7, 101]
等,長(zhǎng)度為 4。
2. 常規(guī)動(dòng)態(tài)規(guī)劃解法思路及缺點(diǎn)
思路:
- 通常可以定義一個(gè)
dp
數(shù)組,其中dp[i]
表示以nums[i]
為結(jié)尾的最長(zhǎng)遞增子序列的長(zhǎng)度。 - 狀態(tài)轉(zhuǎn)移方程一般為
dp[i] = max(dp[j]) + 1
(其中0 <= j < i
且nums[j] < nums[i]
),也就是遍歷前面所有小于nums[i]
的元素對(duì)應(yīng)的dp
值,取最大的那個(gè)再加 1 來(lái)更新dp[i]
。 - 最后整個(gè)序列的最長(zhǎng)遞增子序列長(zhǎng)度就是
dp
數(shù)組中的最大值。
缺點(diǎn):
- 這種常規(guī)解法的時(shí)間復(fù)雜度是 ,當(dāng)輸入序列長(zhǎng)度
n
較大時(shí),效率會(huì)比較低 - 所以需要進(jìn)行優(yōu)化來(lái)降低時(shí)間復(fù)雜度,提升求解效率
二、優(yōu)化解法一:貪心 + 二分查找(時(shí)間復(fù)雜度優(yōu)化至nlogn )
1. 貪心思想
維護(hù)一個(gè)數(shù)組 tail
,它用來(lái)存儲(chǔ)當(dāng)前找到的最長(zhǎng)遞增子序列的 “尾巴” 元素,這個(gè)數(shù)組的長(zhǎng)度其實(shí)就代表了當(dāng)前找到的最長(zhǎng)遞增子序列的長(zhǎng)度(初始時(shí)長(zhǎng)度為 0)。
對(duì)于新遍歷到的元素 nums[i]
,我們希望以一種貪心的策略把它盡可能合理地添加到 tail
數(shù)組中,使得 tail
數(shù)組始終保持一種有序的狀態(tài)(因?yàn)檫f增子序列的特性決定了 “尾巴” 元素是有序遞增的),這樣就能通過(guò)后續(xù)的操作高效地找到最長(zhǎng)遞增子序列。
2. 二分查找的運(yùn)用
每當(dāng)遍歷到一個(gè)新元素 nums[i]
時(shí),我們?cè)?tail
數(shù)組中通過(guò)二分查找找到第一個(gè)大于等于 nums[i]
的元素位置 pos
(可以利用 Java 中的 Arrays.binarySearch
等二分查找相關(guān)方法實(shí)現(xiàn),若沒(méi)找到則返回插入點(diǎn),即合適的位置)。
- 如果
pos
等于tail
數(shù)組當(dāng)前長(zhǎng)度,說(shuō)明nums[i]
比當(dāng)前所有的 “尾巴” 元素都大,那它就可以作為新的 “尾巴” 元素添加到tail
數(shù)組末尾,使得最長(zhǎng)遞增子序列長(zhǎng)度加 1,即tail = Arrays.copyOf(tail, tail.length + 1); tail[tail.length - 1] = nums[i];
。 - 如果
pos
小于tail
數(shù)組當(dāng)前長(zhǎng)度,說(shuō)明nums[i]
可以替換掉tail[pos]
,因?yàn)檫@樣做不會(huì)破壞遞增子序列的性質(zhì),而且有可能在后續(xù)找到更長(zhǎng)的遞增子序列,即tail[pos] = nums[i];
。
3. Java 代碼示例
import java.util.Arrays; public class LongestIncreasingSubsequence { public static int lengthOfLIS(int[] nums) { int[] tail = new int[nums.length]; int len = 0; for (int num : nums) { int pos = Arrays.binarySearch(tail, 0, len, num); if (pos < 0) { pos = -(pos + 1); } tail[pos] = num; if (pos == len) { len++; } } return len; } public static void main(String[] args) { int[] nums = {10, 9, 2, 5, 3, 7, 101, 18}; int result = lengthOfLIS(nums); System.out.println("最長(zhǎng)遞增子序列長(zhǎng)度為: " + result); } }
在上述代碼中:
lengthOfLIS
方法實(shí)現(xiàn)了優(yōu)化后的最長(zhǎng)遞增子序列求解邏輯。通過(guò)不斷遍歷輸入數(shù)組nums
,利用二分查找在tail
數(shù)組中定位合適位置來(lái)更新tail
數(shù)組,同時(shí)維護(hù)最長(zhǎng)遞增子序列的長(zhǎng)度len
。main
方法進(jìn)行簡(jiǎn)單測(cè)試,傳入示例數(shù)組并輸出最終計(jì)算得到的最長(zhǎng)遞增子序列長(zhǎng)度。
三、優(yōu)化解法二:動(dòng)態(tài)規(guī)劃 + 狀態(tài)壓縮(時(shí)間復(fù)雜度仍為O(n^2) ,但空間復(fù)雜度優(yōu)化)
1. 思路
原始動(dòng)態(tài)規(guī)劃解法中我們使用了一個(gè) dp
數(shù)組來(lái)記錄以每個(gè)元素為結(jié)尾的最長(zhǎng)遞增子序列長(zhǎng)度,但是其實(shí)在計(jì)算 dp[i]
時(shí),我們只需要知道前面元素中小于 nums[i]
的那些元素對(duì)應(yīng)的 dp
值情況,并不需要把所有之前元素對(duì)應(yīng)的 dp
值都完整保存下來(lái)。
所以可以通過(guò)狀態(tài)壓縮,只使用一個(gè)長(zhǎng)度為 n
的一維數(shù)組來(lái)模擬動(dòng)態(tài)規(guī)劃過(guò)程,每次更新當(dāng)前元素對(duì)應(yīng)的 dp
值時(shí),及時(shí)覆蓋之前不再需要的值,從而節(jié)省空間。
2. Java 代碼示例
public class LongestIncreasingSubsequence { public static int lengthOfLIS(int[] nums) { int n = nums.length; int[] dp = new int[n]; int maxLen = 1; for (int i = 0; i < n; i++) { dp[i] = 1; for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } maxLen = Math.max(maxLen, dp[i]); } return maxLen; } public static void main(String[] args) { int[] nums = {10, 9, 2, 5, 3, 7, 101, 18}; int result = lengthOfLIS(nums); System.out.println("最長(zhǎng)遞增子序列長(zhǎng)度為: " + result); } }
在這個(gè)代碼示例中:
lengthOfLIS
方法里,通過(guò)一個(gè)一維的dp
數(shù)組來(lái)進(jìn)行動(dòng)態(tài)規(guī)劃求解,內(nèi)層循環(huán)中不斷更新dp[i]
的值,并且實(shí)時(shí)維護(hù)最大的最長(zhǎng)遞增子序列長(zhǎng)度maxLen
,最后返回maxLen
作為結(jié)果。main
方法同樣是用于簡(jiǎn)單的測(cè)試場(chǎng)景,展示如何調(diào)用lengthOfLIS
方法并輸出結(jié)果。
通過(guò)這些優(yōu)化解法,可以更高效地解決最長(zhǎng)遞增子序列問(wèn)題,在不同的應(yīng)用場(chǎng)景和數(shù)據(jù)規(guī)模下根據(jù)實(shí)際需求選擇合適的優(yōu)化方式來(lái)提升算法性能。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Spring Boot最核心的27個(gè)注解,你了解多少?
這篇文章主要介紹了詳解Spring Boot最核心的27個(gè)注解,你了解多少?文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08Java簡(jiǎn)單使用redis-zset實(shí)現(xiàn)排行榜
這篇文章主要介紹了Java簡(jiǎn)單使用redis-zset實(shí)現(xiàn)排行榜,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12