DP_最长不下降子序列(LIS)

序言

同类的问题还有“最长上升子序列”, “最长下降子序列”, …

他们的不同就在于定义的core规则不同, 有的是>=, 有的是>, 有的是<

由此启发, 我们可以在解决其他的问题, 不一定是比较数的大小的问题里面抽象出这种模型.

下面介绍这种动态规划入门都会介绍的问题的思路.

首先我们从头开始分析这个问题.

一. 最容易想到的最暴力的方法

对这个序列中的每一个数的”有”和”无”分两种情况讨论. 代码实现上就是递归.

时间复杂度就是O(2^n)

代码实现上较为简单. 不展示

二. 第二种方法是O(n^2)的DP方法

动态规划的问题是无后效性的, 每个问题都可以分解为更小的子问题, 从而求解.

这道题也不例外.

这个序列的每一个数为止都有一个解, 作为子问题的解. 后面的问题的解就是从这些子问题的最优解继承过来的.

so, 给这个序列的解建立数组dp[n], 0 - n分别是截止到Ai的解.

当下一个数要加入来的时候, 有两种情况

  1. 前面的数都比当前数更大, 因此以这个数为止的最长不下降子序列的长度就是1. 遍历到第一个数的情况也包含在内.

  2. 前面的数有不比当前数大的, 那么这个数的结果dp[i] = max(dp[i], dp[j] + 1). 这个过程遍历前面所有数的dp[j]进行比较.

最后的答案就是所有dp[i]里面的最大值.

这种方法的时间复杂度是O(n^2), 可以看到相比于前面暴力递归的方法有了极大的进步.

代码通过样例, 但不一定能过题, 请谨慎使用.

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

int main()
{
    int n, x;
    vector<int> v;
    cin >> n;
    for (int i = 0; i < n; ++i) {
        cin >> x;
        v.push_back(x);
    }
    int dp[(int)v.size()] = {1}, ans = 1;
    for (int i = 1; i < (int)v.size(); ++i) {
        for (int j = 0; j < i; ++j) {
            if (v[i] >= v[j]) dp[i] = max(dp[i], dp[j] + 1); // 状态转移方程
        }
        ans = max(ans, dp[i]);
    }
    cout << ans;
}
/*
8
1 2 3 -9 3 9 0 11
*/

三. O(nlogn)方法, 维护单调数组

这个方法也是DP方法

时间复杂度可以从O(n^2)降到O(n log n).

我们从最长上升子序列的角度来探讨

假设对一个序列n[1…9] = {2 1 5 3 6 4 8 9 7}, 维护一个单调数组, 使得这个数组为最长上升子序列. 设这个数组为d[ ].

对n[1] = 2, 使得d[1] = 2;

对n[2] = 1, 因为1比2小, 所以修改d[1]的值, 使其为1

对n[3] = 5, 5比1大, 所以len++, d[2] = 5

对n[4] = 3, 3比1大比5小, 所以替换掉5, 使d[2] = 3

对n[5] = 6, 6比d[2]大, 所以len++, d[3] = 6

对n[6] = 4, 4比3大比6小, 所以替换掉6, 使d[3] = 4

对n[7] = 8, 8比4大, 所以len++, 使d[4] = 8

对n[8] = 9, 9比8大, 所以len++, 使d[5] = 9

对n[9] = 7, 7比4大比8小, 所以替换掉8, 使d[4] = 7.

至此这个序列遍历完了, 最小的长度也出来了. 最后的序列是1 3 4 7 9, len = 5

仔细琢磨会觉得, 如最后一步操作, 为什么后面的7反而到他前面的8, 9的前面去了. 其实仔细一想并无问题, 因为即使7出现在前面, 它并不影响最终结果, 因为我们已经得出最后结果就是len, 而以7为结尾的最长序列在该题中是len - 1, 如果后面还有序列, 那么这里把7替换掉8会使得当前状态更优, 因为这样的修改是不会改变当前结果的, 但是确实后续最优状态的基础. 而这道题的动态规划思想就是这样, 不断地获取最优状态.

经过前面的分析我们也许会发现, 这个DP的过程无法存储中间结果, 也就是说我们只能知道最长的子序列是多长, 而无法得到是哪个序列. 可谓是有利有弊.

利用我们维护的数组的单调性, 我们可以用二分法查找这个比当前数更大数的位置, 从而方便的实现替换.

所以时间复杂度为O(n log n).

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

int main()
{
    int n, x;
    vector<int> v, vec;
    cin >> n;
    for (int i = 0; i < n; ++i) {
        cin >> x;
        v.push_back(x);
    }
    for (int i = 0; i < (int)v.size(); ++i) {
        if (i == 0) vec.push_back(v[0]);
        else {
            if (v[i] >= vec[vec.size() - 1]) vec.push_back(v[i]);
            else *upper_bound(vec.begin(), vec.end(), v[i]) = v[i];
        }
    }
    for (int i = 0; i < (int)vec.size(); ++i) {
        cout << vec[i] << ' ';
    }
    cout << endl;
    cout << "len == " << vec.size() << endl;
}

/*
9
2 1 5 3 6 4 8 9 7
*/
  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值