最長遞增子序列算法

最長遞增子序列算法

最長遞增子序列(Longest Increasing Subsequence, LIS)是計算機科學中的一個經典問題,目標是在給定的數列中找到一個非降序排列的子序列,使得該子序列的長度儘可能長。以下是一些解決最長遞增子序列問題的算法:

  1. 動態規劃法(Dynamic Programming):

    • 初始化一個長度爲 n 的數組 dp,其中 dp[i] 表示以原序列中第 i 個元素結尾的最長遞增子序列的長度。
    • 遍歷輸入序列,對於每個元素 a[i],從 dp[0]dp[i-1] 找出所有小於它的值對應的 dp[j],更新 dp[i] 爲這些值中的最大值加 1(因爲當前元素可以加入到那些子序列末尾形成新的更長遞增子序列)。
    • 最後,數組 dp 中的最大值即爲原序列的最長遞增子序列的長度。
  2. 貪心 + 二分查找優化:

    • 在動態規劃的基礎上,可以進一步優化時間複雜度至 O(n log n)。
    • 維護一個有序序列(如使用堆或平衡二叉搜索樹),用於存儲當前遞增子序列的末尾元素。
    • 對於每一個新元素,如果它比有序序列的末尾元素大,則將其添加到序列中;否則,用它替換有序序列中第一個大於它的元素,並調整有序序列保持遞增。
    • 最後,有序序列的長度就是原序列的最長遞增子序列長度。
  3. 最長公共子序列變形法:

    • 將原序列排序,然後計算原序列和排序後的序列之間的最長公共子序列,但這通常不是最有效的解法,因爲會改變原序列的相對順序,可能會導致錯誤的結果。

在實際編程實現時,動態規劃方法更爲常用且易於理解,而結合貪心策略和二分查找的優化版本則可以有效降低時間複雜度,適用於大規模數據。

使用動態規劃法

以下是一個使用動態規劃法(簡單樸素實現)在 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.

文章來源:劉俊濤的博客


若有幫助到您,歡迎點贊、轉發、支持,您的支持是對我堅持最好的肯定(_)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章