最長遞增子序列算法
最長遞增子序列(Longest Increasing Subsequence, LIS)是計算機科學中的一個經典問題,目標是在給定的數列中找到一個非降序排列的子序列,使得該子序列的長度儘可能長。以下是一些解決最長遞增子序列問題的算法:
-
動態規劃法(Dynamic Programming):
- 初始化一個長度爲 n 的數組
dp
,其中dp[i]
表示以原序列中第i
個元素結尾的最長遞增子序列的長度。 - 遍歷輸入序列,對於每個元素
a[i]
,從dp[0]
到dp[i-1]
找出所有小於它的值對應的dp[j]
,更新dp[i]
爲這些值中的最大值加 1(因爲當前元素可以加入到那些子序列末尾形成新的更長遞增子序列)。 - 最後,數組
dp
中的最大值即爲原序列的最長遞增子序列的長度。
- 初始化一個長度爲 n 的數組
-
貪心 + 二分查找優化:
- 在動態規劃的基礎上,可以進一步優化時間複雜度至 O(n log n)。
- 維護一個有序序列(如使用堆或平衡二叉搜索樹),用於存儲當前遞增子序列的末尾元素。
- 對於每一個新元素,如果它比有序序列的末尾元素大,則將其添加到序列中;否則,用它替換有序序列中第一個大於它的元素,並調整有序序列保持遞增。
- 最後,有序序列的長度就是原序列的最長遞增子序列長度。
-
最長公共子序列變形法:
- 將原序列排序,然後計算原序列和排序後的序列之間的最長公共子序列,但這通常不是最有效的解法,因爲會改變原序列的相對順序,可能會導致錯誤的結果。
在實際編程實現時,動態規劃方法更爲常用且易於理解,而結合貪心策略和二分查找的優化版本則可以有效降低時間複雜度,適用於大規模數據。
使用動態規劃法
以下是一個使用動態規劃法(簡單樸素實現)在 PHP 中解決最長遞增子序列問題的示例代碼:
<?php
function longestIncreasingSubsequence($nums) {
$n = count($nums);
// 初始化 dp 數組,長度爲 n+1,dp[0] 作爲哨兵值
$dp = array_fill(0, $n + 1, 1);
// 遍歷輸入數組
for ($i = 1; $i <= $n; $i++) {
for ($j = 0; $j < $i; $j++) {
// 如果當前元素大於前一個元素,則可以形成更長的遞增子序列
if ($nums[$i - 1] > $nums[$j]) {
$dp[$i] = max($dp[$i], $dp[$j] + 1);
}
}
}
// 返回最長遞增子序列的長度
return max($dp);
}
// 測試數據
$nums = [10, 9, 2, 5, 3, 7, 101, 18];
$result = longestIncreasingSubsequence($nums);
echo "The length of the longest increasing subsequence is: ", $result;
?>
這個PHP代碼將計算給定數組$nums
的最長遞增子序列的長度。
複雜度
上面的PHP代碼實現的最長遞增子序列算法使用了動態規劃,其時間複雜度爲O(n^2),其中n是輸入數組$nums
的長度。這是因爲有兩個嵌套循環,外層循環遍歷整個數組,內層循環則對每個元素之前的每個元素進行比較。
空間複雜度方面,該代碼使用了一個大小爲n+1的一維數組$dp
來存儲子問題的解,因此空間複雜度爲O(n)。
使用貪心策略和二分查找
最長遞增子序列問題可以通過結合貪心策略和二分查找進行優化,從而將時間複雜度降低到O(n log n)。
以下是使用PHP實現的優化版本:
<?php
// 定義一個二分查找函數,用於在動態規劃數組中找到插入位置
function binarySearch(&$dp, $length, $target) {
$left = 0;
$right = $length - 1;
// 通過二分查找確定目標值應該插入的位置
while ($left <= $right) {
$mid = floor(($left + $right) / 2);
if ($dp[$mid] < $target) { // 目標值大於中間值,則在右半部分繼續查找
$left = $mid + 1;
} else { // 目標值小於等於中間值,在左半部分或剛好就是中間位置
$right = $mid - 1;
}
}
// 返回插入位置(左邊界+1,因爲這裏使用的是左閉右開區間)
return $left;
}
// 使用貪心策略和二分查找優化的最長遞增子序列算法
function longestIncreasingSubsequenceOptimized($nums) {
$n = count($nums);
// 初始化 dp 數組,長度爲 n+1,前面填充 0,實際只用到前 n 個元素存儲 LIS 的長度
$dp = array_fill(0, $n + 1, 0);
// tail 數組用來存儲每個長度對應的LIS末尾元素,保持單調遞增順序
$tail = array();
// 遍歷輸入數組
for ($i = 0; $i < $n; $i++) {
// 通過二分查找確定當前數字 nums[$i] 在 dp 數組中的插入位置
$pos = binarySearch($dp, $i, $nums[$i]);
// 更新 dp 數組對應位置的值爲當前遍歷到的數,表示以這個數結尾的遞增子序列長度
$dp[$pos] = $nums[$i];
// 更新 tail 數組,確保它始終包含各個長度遞增子序列的最後一個元素,並且有序
if ($pos == count($tail)) { // 當 pos 大於 tail 數組長度時,表示需要新增一個元素
$tail[] = $nums[$i];
} else {
$tail[$pos] = $nums[$i]; // 替換 tail 數組中的某個元素,保持遞增順序
}
}
// 返回最長遞增子序列的長度,即 tail 數組的長度
return count($tail);
}
// 測試數據
$nums = [10, 9, 2, 5, 3, 7, 101, 18];
$result = longestIncreasingSubsequenceOptimized($nums);
echo "The length of the longest increasing subsequence is: ", $result;
?>
在這個優化版本中,我們維護了一個單調遞增數組$tail
來記錄當前已找到的所有遞增子序列的最後一個元素,並且用一個輔助函數binarySearch()
進行二分查找以快速定位新元素應插入的位置。
這樣在遍歷整個輸入數組時,可以達到O(n log n)的時間複雜度。同時注意,這個版本只返回了最長遞增子序列的長度,如果需要找出具體的遞增子序列,還需要額外存儲路徑信息。
找出具體的遞增子序列
爲了找出最長遞增子序列的具體元素,我們需要在實現中額外存儲路徑信息。這裏提供一個基於上述優化算法的PHP代碼示例,同時返回最長遞增子序列的長度和具體序列:
<?php
function binarySearch(&$dp, $length, $target) {
$left = 0;
$right = $length - 1;
while ($left <= $right) {
$mid = floor(($left + $right) / 2);
if ($dp[$mid] < $target) {
$left = $mid + 1;
} else {
$right = $mid - 1;
}
}
return $left;
}
function longestIncreasingSubsequenceOptimized($nums) {
$n = count($nums);
$dp = array_fill(0, $n + 1, 0);
$tail = array(); // 存儲每個長度對應的LIS末尾元素
$prevIndices = array_fill(0, $n + 1, null); // 存儲前驅節點信息,用於回溯構建 LIS
for ($i = 0; $i < $n; $i++) {
$pos = binarySearch($dp, $i, $nums[$i]);
$dp[$pos] = $nums[$i];
$prevIndices[$pos] = $i; // 記錄當前位置的前驅節點(原數組中的索引)
// 更新 tail 數組,確保有序性
if ($pos == count($tail)) {
$tail[] = $nums[$i];
} else {
$tail[$pos] = $nums[$i];
}
}
// 回溯構建最長遞增子序列
$lis = [];
$idx = count($tail) - 1; // 最後一個有效位置對應最長遞增子序列的最後一個元素
while ($idx >= 0) {
$lis[] = $nums[$prevIndices[$idx]]; // 添加當前元素到 LIS
$idx = $prevIndices[$idx]; // 移動到前一個元素的位置
}
// 反轉 LIS 以得到正確的順序
$lis = array_reverse($lis);
// 返回最長遞增子序列的長度及具體序列
return [
'length' => count($lis),
'sequence' => $lis,
];
}
// 測試數據
$nums = [10, 9, 2, 5, 3, 7, 101, 18];
$result = longestIncreasingSubsequenceOptimized($nums);
echo "The length of the longest increasing subsequence is: ", $result['length'];
echo "\nThe longest increasing subsequence is: ", implode(', ', $result['sequence']);
?>
這段代碼在計算最長遞增子序列長度的同時,通過$prevIndices
數組記錄了每個位置的前驅節點,最後根據這些信息進行回溯,構造出具體的最長遞增子序列。
歡迎關注公-衆-號【TaonyDaily】、留言、評論,一起學習。
Don’t reinvent the wheel, library code is there to help.
文章來源:劉俊濤的博客
若有幫助到您,歡迎點贊、轉發、支持,您的支持是對我堅持最好的肯定(_)