【动态规划】求最长递增子序列问题

问题描述

最长递增子序列(Longest Increasing Subsequence,LIS

  • 子序列:对于任意序列s,它的子序列是通过删除其中零个或多个元素得到的另⼀个序列
    注:剩余元素的相对顺序保持不变

给定n个整数组成的序列 s [ 1... n ] s[1...n] s[1...n],求最长递增子序列LIS(的长度)

8 3 6 1 3 5 4 7

递推关系

建立递推关系的思路

假设能够求出 s [ 1... k − 1 ] s[1...k-1] s[1...k1]LIS,考虑能否由此推出 s [ 1... k ] s[1...k] s[1...k]LIS

  • 如果仅知道长度
    • 无法判断 s [ k ] s[k] s[k] 能否让LIS变长
  • 如果不仅知道长度,还知道具体序列 L
    • s [ k ] s[k] s[k] 能让 L 变长,那问题就解决了
    • 也许 L 就是 s [ 1... k ] s[1...k] s[1...k]LIS
    • 也许存在 s [ 1... k ] s[1...k] s[1...k] 的另⼀个LIS:L‘, s [ k ] s[k] s[k] 能让L’变长
    • 可能需要记住 s [ 1... k − 1 ] s[1...k-1] s[1...k1] 的所有LIS

原始子问题: 令 L ( k ) L(k) L(k) 表示 s [ 1... k ] s[1...k] s[1...k]LIS的长度,原问题即求解 L ( n ) L(n) L(n)

  • O(n)个子问题,但不容易建立递归关系

约束条件:以 s [ k ] s[k] s[k] 结尾

  • L ( k ) L(k) L(k) 表示 s [ 1... n ] s[1...n] s[1...n] s [ k ] s[k] s[k] 结尾LIS的长度
  • 原问题即为求解 max ⁡ 1 ≤ k ≤ n L ( k ) \max_{1\le k\le n}L(k) max1knL(k)
  • 基本情况: 如果 k = 1 k = 1 k=1,那么 L ( k ) = 1 L(k) = 1 L(k)=1
  • 归纳步骤
    • L k = max ⁡ { 1 , max ⁡ 1 ≤ i ≤ k − 1 { L ( i ) + 1 ∣ s [ k ] > s [ i ] } } L_k = \max \{1, \max_{1\le i \le k-1} \{ L(i) +1 | s[k] > s[i] \}\} Lk=max{ 1,max1ik1{ L(i)+1∣s[k]>s[i]}},其中, max ⁡ ⊘ \max \oslash max的值定义为0

在这里插入图片描述

  • 此时的递推关系:

L ( k ) = { 1 i f k = 1 max ⁡ { 1 , max ⁡ 1 ≤ i ≤ k − 1 { L ( i ) + 1 ∣ s [ k ] > s [ i ] } } i f k > 1 L(k) = \begin{cases} 1 &if\quad k=1\\ \max \{1, \max_{1\le i \le k-1} \{ L(i) +1 | s[k] > s[i] \}\} &if \quad k>1 \end{cases} L(k)={ 1max{ 1,max1ik1{ L(i)+1∣s[k]>s[i]}}ifk=1ifk>1

  • O ( n ) O(n) O(n) 个子问题,每个子问题复杂度为 O ( k ) O(k) O(k)。时间复杂度为 O ( n 2 ) O(n^2) O(n2)

约束条件:以 s [ k ] s[k] s[k] 开头

  • L ( k ) L(k) L(k) 表示 s [ 1... n ] s[1...n] s[1...n] s [ k ] s[k] s[k] 开头LIS的长度
  • 原问题即为求解 max ⁡ 1 ≤ k ≤ n L ( k ) \max_{1\le k\le n}L(k) max1knL(k)
  • 基本情况: 如果 k = n k = n k=n,那么 L ( k ) = 1 L(k) = 1 L(k)=1

在这里插入图片描述

  • 此时的递推关系:

L ( k ) = { 1 i f k = n max ⁡ { 1 , max ⁡ k + 1 ≤ i ≤ n { L ( i ) + 1 ∣ s [ k ] < s [ i ] } } i f k < n L(k) = \begin{cases} 1 &if\quad k=n\\ \max \{1, \max_{k+1\le i \le n} \{ L(i) +1 | s[k] < s[i] \}\} &if \quad k<n \end{cases} L(k)={ 1max{ 1,maxk+1in{ L(i)+1∣s[k]<s[i]}}ifk=nifk<n

  • O ( n ) O(n) O(n) 个子问题,每个子问题复杂度为 O ( k ) O(k) O(k)。时间复杂度为 O ( n 2 ) O(n^2) O(n2)

约束条件:增加子问题参数(前缀)

  • L ( i , j ) L(i,j) L(i,j) 表示 s [ j . . . n ] s[j...n] s[j...n] 中每个元素都大于 s [ i ] s[i] s[i] LIS的长度
  • s [ 0 ] = − ∞ s[0] =-\infty s[0]= ,原问题即求解 L ( 0 , 1 ) L(0,1) L(0,1)
  • 基本情况: 如果 j > n j> n j>n ,那么 L ( i , j ) = 0 L(i,j)= 0 L(i,j)=0

在这里插入图片描述

  • 归纳步骤
    • 如果 s [ i ] > s [ j ] s[i] > s[j] s[i]>s[j] L ( i , j ) = L ( i , j + 1 ) L(i,j) = L(i,j+ 1) L(i,j)=L(i,j+1)
    • 否则 L ( i , j ) = max ⁡ { L ( i , j + 1 ) , 1 + L ( j , j + 1 ) } L(i,j) = \max \{ L(i,j+ 1),1 + L(j,j+ 1)\} L(i,j)=max{ L(i,j+1),1+L(j,j+1)}
  • O ( n 2 ) O(n^2) O(n2)个子问题,每个子问题求解复杂度为 O ( 1 ) O(1) O(1),时间复杂度: O(n2); 空间复杂度: O(n2)
  • 此时的递推关系:
    L ( i , j ) = { 0 i f j > n L ( i , j + 1 ) i f s [ i ] ≥ s [ j ] max ⁡ { L ( i , j + 1 ) 1 + L ( j , j + 1 ) o t h e r w i s e L(i,j) = \begin{cases} 0 &if\quad j>n\\ L(i,j+1) &if\quad s[i]\ge s[j] \\ \max \begin{cases} L(i,j+1) \\ 1+L(j,j+1) \end{cases} &otherwise \end{cases} L(i,j)= 0L(i,j+1)max{ L(i,j+1)1+L(j,j+1)ifj>nifs[i]s[j]otherwise

在这里插入图片描述


约束条件:增加子问题参数(后缀)

  • L ( i , j ) L(i,j) L(i,j) 表示 s [ 1... j ] s[1...j] s[1...j] 中每个元素都小于 s [ i ] s[i] s[i] LIS的长度
  • s [ n + 1 ] = ∞ s[n+1] =\infty s[n+1]= ,原问题即求解 L ( n + 1 , n ) L(n+1,n) L(n+1,n)
  • 基本情况: 如果 j = 0 j=0 j=0 ,那么 L ( i , j ) = 0 L(i,j)= 0 L(i,j)=0

在这里插入图片描述

  • 归纳步骤

    • 如果 s [ i ] ≤ s [ j ] s[i] \le s[j] s[i]s[j] L ( i , j ) = L ( i , j − 1 ) L(i,j) = L(i,j- 1) L(i,j)=L(i,j1)
    • 否则 L ( i , j ) = max ⁡ { L ( i , j − 1 ) , 1 + L ( j , j − 1 ) } L(i,j) = \max \{ L(i,j- 1),1 + L(j,j- 1)\} L(i,j)=max{ L(i,j1),1+L(j,j1)}
  • 此时的递推关系:
    L ( i , j ) = { 0 i f j = 0 L ( i , j − 1 ) i f s [ i ] ≤ s [ j ] max ⁡ { L ( i , j − 1 ) 1 + L ( j , j − 1 ) o t h e r w i s e L(i,j) = \begin{cases} 0 &if\quad j=0\\ L(i,j-1) &if\quad s[i]\le s[j] \\ \max \begin{cases} L(i,j-1) \\ 1+L(j,j-1) \end{cases} &otherwise \end{cases} L(i,j)= 0L(i,j1)max{ L(i,j1)1+L(j,j1)ifj=0ifs[i]s[j]otherwise

在这里插入图片描述


约束条件:LIS长度为k且末尾元素最小

  • 对于长度为 k k k 的递增子序列,只需记住末尾元素最小的那个

  • 本质是寻找上限最高(可拓展性最强)的那个子序列

  • L ( k ) L(k) L(k) 表示 s [ 1... n ] s[1...n] s[1...n] 中长度为 k k k 且末尾元素最小的递增子序列,且 L ( k ) . l a s t L(k).last L(k).last 表示该序列中最后那个元素

  • 引理: L ( 1 ) . l a s t < L ( 2 ) . l a s t < . . . < L ( k ) . l a s t L(1) .last < L(2) .last < ... < L(k).last L(1).last<L(2).last<...<L(k).last

    • 假设 x ≥ y x \ge y xy,而 y ≥ z y \ge z yz,所以 x ≥ z x \ge z xz
    • 那么灰色元素构成一个长度为 k k k 且末尾元素最小的递增子序列,矛盾
      在这里插入图片描述
  • 归纳假设: 对长度小于 n n n 的序列,可以计算其所有的 L ( k ) L(k) L(k),并有序存储

  • 基本情况: 长度为1的序列,有 L [ 1 ] ← s [ 1 ] L[1]\leftarrow s[1] L[1]s[1]

  • 如何基于归纳假设求解 s [ 1.. n ] s[1..n] s[1..n] 的所有的 L ( k ) L(k) L(k)

    • L ( k ) . l a s t L(k).last L(k).last 构成的有序数组中查找插入位置 k ′ k' k,使得 s [ n ] s[n] s[n] 加入后仍然有序
    • 如果 k ′ = k + 1 k' =k+1 k=k+1,那么 L ( k + 1 ) + L ( k ) + 1 L(k + 1) + L(k) +1 L(k+1)+L(k)+1 L ( k + 1 ) . l a s t ← s [ n ] L(k + 1).last \leftarrow s[n] L(k+1).lasts[n]
    • 否则 L ( k ′ ) . l a s t ← s [ n ] L(k').last \leftarrow s[n] L(k).lasts[n],但 L ( k ′ ) L(k') L(k) 的值不变
  • 时间复杂度: O ( l o g n ) O(logn) O(logn)

在这里插入图片描述


运行实例

#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
using namespace std;

ostream& operator<<(ostream& os, const vector<int>& v) {
    
    
    for (auto e : v)
        os << e << ' ';
    return os;
}

int lis_dp1(const vector<int>& s, int n) {
    
    
    vector<int> dp(n + 1, 1); // 初始化dp数组,dp[i]表示以s[i]结尾的LIS的长度

    for (int k = 2; k <= n; ++k) {
    
    
        for (int i = 1; i < k; ++i) {
    
    
            if (s[k] > s[i]) {
    
    
                dp[k] = max(dp[k], dp[i] + 1);
            }
        }
    }

    return *max_element(dp.begin(), dp.end()); // 返回dp数组中的最大值作为整个序列的最长递增子序列的长度
}

int lis_dp2(const vector<int>& s, int n) {
    
    
    vector<vector<int>> dp(n + 2, vector<int>(n + 2, -1)); // 初始化dp数组,dp[i][j]表示L(i,j)

    for (int i = 0; i <= n + 1; ++i)
        dp[i][n + 1] = 0; // 基本情况:当 j > n 时,L(i,j) = 0

    for (int j = n; j >= 1; --j) {
    
    
        for (int i = 0; i < j; ++i) {
    
    
            if (s[i] >= s[j]) {
    
    
                dp[i][j] = dp[i][j + 1]; // 如果s[i] >= s[j],则L(i,j) = L(i,j+1)
            }
            else {
    
    
                dp[i][j] = max(dp[i][j + 1], 1 + dp[j][j + 1]); // 否则L(i,j) = max{L(i,j+1), 1+L(j,j+1)}
            }
        }
    }

    return dp[0][1]; // 返回L(0,1)作为整个序列的最长递增子序列的长度
}

int lis_dp3(const vector<int>& s, int n) {
    
    
    
    if (n == 0) return 0;

    set<int> L;

    L.insert(s[1]);

    for (int i = 2; i < n+1; ++i) {
    
    
        if (s[i] > * L.rbegin()) {
    
    
            L.insert(s[i]);
        }
        else {
    
    
            L.erase(L.lower_bound(s[i]));
            L.insert(s[i]);
        }
    }

    return L.size();
}

vector<int> find_lis_dp1(const vector<int>& s, int n) {
    
    
    vector<int> dp(n + 1, 1); // 初始化dp数组,dp[i]表示以s[i]结尾的LIS的长度
    vector<int> parent(n + 1, -1); // 记录每个元素的父节点索引

    for (int k = 2; k <= n; ++k) {
    
    
        for (int i = 1; i < k; ++i) {
    
    
            if (s[k] > s[i] && dp[k] < dp[i] + 1) {
    
    
                dp[k] = dp[i] + 1;
                parent[k] = i; // 更新父节点索引
            }
        }
    }

    int max_length = *max_element(dp.begin(), dp.end()); // 获取最长递增子序列的长度
    int max_index = distance(dp.begin(), find(dp.begin(), dp.end(), max_length)); // 获取最长递增子序列的结束索引

    vector<int> lis;
    while (max_index != -1) {
    
    
        lis.push_back(s[max_index]);
        max_index = parent[max_index]; // 根据父节点索引回溯
    }

    reverse(lis.begin(), lis.end()); // 反转得到正确顺序的最长递增子序列

    return lis;
}

vector<int> find_lis_dp2(const vector<int>& s, int n) {
    
    
    vector<vector<int>> dp(n + 2, vector<int>(n + 2, -1)); // 初始化dp数组,dp[i][j]表示L(i,j)
    vector<vector<int>> parent(n + 2, vector<int>(n + 2, -1)); // 记录每个元素的父节点索引

    for (int i = 0; i <= n + 1; ++i)
        dp[i][n + 1] = 0; // 基本情况:当 j > n 时,L(i,j) = 0

    for (int j = n; j >= 1; --j) {
    
    
        for (int i = 0; i < j; ++i) {
    
    
            if (s[i] >= s[j]) {
    
    
                dp[i][j] = dp[i][j + 1];
            }
            else {
    
    
                dp[i][j] = max(dp[i][j + 1], 1 + dp[j][j + 1]);
                if (dp[i][j] == dp[j][j + 1] + 1) {
    
    
                    parent[i][j] = j; // 更新父节点索引
                }
            }
        }
    }

    vector<int> lis;
    int i = 0, j = 1;
    while (j <= n) {
    
    
        if (dp[i][j] == dp[j][j + 1] + 1) {
    
    
            lis.push_back(s[j]);
            i = j;
        }
        ++j;
    }

    return lis;
}

vector<int> find_lis_dp3(const vector<int>& s, int n) {
    
    
    vector<int> lis;
    set<int> L;

    L.insert(s[1]);

    for (int i = 2; i < n + 1; ++i) {
    
    
        if (s[i] > *L.rbegin()) {
    
    
            L.insert(s[i]);
        }
        else {
    
    
            L.erase(L.lower_bound(s[i]));
            L.insert(s[i]);
        }
    }
    
    for (int num : L) {
    
    
        lis.push_back(num);
    }
    return lis;
}

int main(int argc, const char* argv[]) {
    
    
    vector<int> s = {
    
     -1, 8, 3, 6, 1, 3, 5, 4, 7 }; // 注意s[0]仅作标识,真实数据为s[1]~s[n]
    cout << lis_dp1(s, s.size() - 1) << endl;
    cout << find_lis_dp1(s, s.size() - 1) << endl;
    cout << "------------------------------"  << endl;
    cout << lis_dp2(s, s.size() - 1) << endl;
    cout << find_lis_dp2(s, s.size() - 1) << endl;
    cout << "------------------------------" << endl;
    cout << lis_dp3(s, s.size() - 1) << endl;
    cout << find_lis_dp3(s, s.size() - 1) << endl;
    cout << "------------------------------" << endl;
    return 0;
}

运行结果:

在这里插入图片描述


猜你喜欢

转载自blog.csdn.net/cold_code486/article/details/134664160