一. 知识简介

  1. 学习 LCIS 的预备知识: 动态规划基本思想, LCS, LIS

  2. 经典问题:给出有 n 个元素的数组 a[] , m 个元素的数组 b[] ,求出它们的最长上升公共子序列的长度.

  3. 例如:

a[] data:
5
1 4 2 5 -12 b[] data:
4
-12 1 2 4 LCIS is 2
LCIS 所含元素为 1 4

二.LCIS问题分析

  1. 确定状态

      可以定义 dp[i][j] 表示以 a 数组的前 i 个整数与 b 数组的前 j 个整数且以 b[j] 为结尾构成的公共子序列的长度。

      对于解决DP问题,第一步定义状态是很重要的!

      需要注意,以 b[j] 结尾构成的公共子序列的长度不一定是最长公共子序列的长度!

  2. 确定状态转移方程

    • 当 a[i] == b[j] 时,我们只需要在前面找到一个能将 b[j] 接到后面的最长的公共子序列.

      • 之前最长的公共子序列在哪呢?首先我们要去找的 dp[][] 的第一维必然是 i - 1 ,因为 i 已经拿去和 b[j] 配对去了,不能用了。并且也不能是 i - 2 ,因为 i - 1 必然比 i - 2 更优。
      • 第二维呢?那就需要枚举 b[1]...b[j-1] 了,因为你不知道这里面哪个最长且哪个小于 b[j] 。
      • 这里还有一个问题,可不可能不配对呢?也就是在 a[i] == b[j] 的情况下,需不需要考虑 dp[i][j] == dp[i-1][j] 的决策呢?答案是不需要。因为如果 b[j] 不和 a[i] 配对,那就是和之前的 a[1]...a[j-1] 配对(假设 dp[i-1][j]>0 ,等于0不考虑),这样必然没有和 a[i] 配对优越。(为什么必然呢?因为 b[j] 和 a[i] 配对之后的转移是 max(dp[i-1][k])+1 ,而和之前的 i1 配对则是 max(dp[i1-1][k])+1 。
    • 当 a[i] != b[j] 时, 因为 dp[i][j] 是以 b[j] 为结尾的LCIS,如果 dp[i][j] > 0 那么就说明 a[1]..a[i] 中必然有一个整数 a[k] 等于 b[j] ,因为 a[k] != a[i] ,那么 a[i] 对 dp[i][j] 没有贡献,于是我们不考虑它照样能得出 dp[i][j] 的最优值。所以在 a[i] != b[j] 的情况下必然有 dp[i][j] == dp[i-1][j] 。
  • 综上所述,我们可以得到状态转移方程:

    ① a[i] != b[j], dp[i][j] = dp[i-1][j]

    ② a[i] == b[j], dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k])

三. LCIS算法实现及递进的优化方法

  • O(N * M^2) 算法

    分析: 根据状态转移方程可以得到非常容易的写出 O(N * M^2) 的算法
// 复杂度 O(N * M^2)
int lengthOfLCIS(int *a, int n, int *b, int m) {
int ans = 0;
int dp[505][505] = {0};
int max_dp = 0;
for (int i = 0 ; i < n ; ++i) {
for (int j = 0 ; j < m ; ++j) {
dp[i + 1][j + 1] = dp[i][j + 1];
if (a[i] == b[j]) {
max_dp = 0;
for (int k = 0 ; k < j ; ++k) {
if (b[j] > b[k] && max_dp <= dp[i][k + 1]) {
max_dp = dp[i][k + 1];
}
}
dp[i + 1][j + 1] = max_dp + 1;
}
ans = ans > dp[i + 1][j + 1] ? ans : dp[i + 1][j + 1];
}
}
return ans;
}
  • **O(N * M * log(M))**算法

    分析: 观察O(N * M^2)的算法,我们可以发现当 a[i] == b[j]在前面找到一个能将 b[j] 接到后面的最大长度,这一个查找过程是可以优化的。

    设置新的辅助数组 len[],len[i] = value 代表长度为 i 的所有公共子序列中最后一个数字为的最小值为value。

    • 为什么要这样设计辅助数组 len[] ?

        首先我们优化的过程是一个查找过程( dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k]) ),实际上这个查找过程是在一个一维数组中查找满足条件 b[j] > b[k] 的一个最大值。

        然后,既然是优化查找过程,可以采用 hash 或者是二分来将查找问题从O(N)降到一个可以接受的复杂度,大体判断一下二分似乎更加适合该问题。

        由于二分查找的对象必须具有单调性,而在最长公共上升子序列中,所以的子序列是具有单调性的,所以我们可以将子序列的结尾数字作为数组中的值。

        为什么用数组 len[i] 来维护长度为 i 的所有公共子序列中最后一个数字为的最小值?因为维护一个最小值能够判断 b[j] 到底能够接在哪个位置,试想一下,如果维护一个最大值,本来 b[j] 能接在长度为 i 的某一个子序列后面,因为 b[j] < 最大值导致查找失败,这是不是很 **: ) **...
    • len[] 更新策略 ( 请参考下面代码 )

        由于本人能力有限,len[] 数组的更新策略不能够足够严谨、清晰的表述出来,其中有一些细节着实不知道该怎么说,如果有好的见解请一定提出!

        首先用二分查找找到 b[j] 能“嫁接”到之前子序列中的最长长度( ind是二分出来的len[]数组下标,根据上面的定义,ind就是以b[j]结尾的最长公共上升子序列长度 )。

        当a[i] == b[j],这时候 b[j] 将“嫁接”到之前子序列中的合适位置,同时更新 len[ind] = b[j] 。

        当a[i] != b[j],我们也需要更新 len[] 的信息,因为后面的更新需要前面的数据。当 b[j] 与 a[i] 适配,那么我们需要查看 b[j] 与前 i - 1个数字所构成的以 b[j] 为结尾数字的最长的公共子序列。如果有多个跟 dp[i][j + 1] ( 因为所有 i , j 都 + 1 所以是dp[i][j + 1])相同长度的子序列,查看 len[dp[i][j + 1]] 的值是否比 d[j] 小,如果比 d[j] 小则更新 len[dp[i][j + 1]]的值。
// 复杂度 O(N * M * log(M))
int binary_search(int *len, int length, int value) {
int l = 0, r = length - 1, mid;
while (l < r) {
mid = (l + r) >> 1;
if (len[mid] >= value) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
int lengthOfLCIS(int *a, int n, int *b, int m) {
int ans = 0;
int dp[505][505] = {0};
int len[505];
for (int i = 0 ; i < n ; ++i) {
len[0] = INT_MIN;
for (int j = 0 ; j < m ; ++j) {
len[j + 1] = INT_MAX;
int ind = binary_search(len, j + 2, b[j]);
if (a[i] != b[j]) {
if (b[j] < len[dp[i][j + 1]]) { // 需要注意这里的更新策略,只有当b[j]的值小于当前len[dp[i][j + 1]]的最小值时才能更新
len[dp[i][j + 1]] = b[j];
}
dp[i + 1][j + 1] = dp[i][j + 1];
} else {
dp[i + 1][j + 1] = ind;
len[ind] = b[j];
}
ans = ans > dp[i + 1][j + 1] ? ans : dp[i + 1][j + 1];
}
}
return ans;
}
  • O(N * M)算法

    分析:到这里好像没有更好的方法来优化查找了,那让我们再仔细的分析一下问题。

        当a[i] != b[j], dp[i][j] = dp[i-1][j],这对问题并没有什么影响。

        当a[i] == b[j],的时候 dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k])我们发现一个特点,其实 a[i] ( b[j] )与 b[k] 的关系早就在之前就可以确定了!( i 是最外层循环, j` 是内层循环,当 j` 遍历到 k 时,就足以判断 b[j] ? b[j`] 的大小关系了),因此我们只需要在内层循环与外层循环直接维护一个 max_dp 的值,等到 a[i] == b[j] 的时候,直接令 dp[i][j] = max_dp + 1即可,时间复杂度降到了 O(N * M)。

    注意:这里的优化由问题的特性决定的,即上一段黑色加粗部分文字。对于这种情况下,普通优化技巧已经很无力了,那我们要做的就是对问题不断的思考,不断的寻找探究问题的本质以及其独有的特性,学而不思则惘!
// 复杂度 O(N * M)
int lengthOfLCIS(int *a, int n, int *b, int m) {
int ans = 0;
int dp[505][505] = {0};
for (int i = 0 ; i < n ; ++i) {
int max_dp = 0;
for (int j = 0 ; j < m ; ++j) {
dp[i + 1][j + 1] = dp[i][j + 1];
if (a[i] > b[j] && max_dp < dp[i + 1][j + 1]) max_dp = dp[i + 1][j + 1];
if (a[i] == b[j]) {
dp[i + 1][j + 1] = max_dp + 1;
}
ans = max(ans, dp[i + 1][j + 1]);
}
}
return ans;
}
  • 将空间复杂度优化至O(M)

    分析:现在空间复杂度是O(N * M),经过观察可以发现,对于当 dp[i + 1][j + 1] 来说,我只需要上一层的数据和当前这一层左侧的数据,因此 dp[][] 数组第一维开为 2 就足够使用了,这种不断利用两层或者几层数组滚动求解的技巧叫滚动数组

    以下样例以O(N * M)算法为基础进行了空间上的优化
int lengthOfLCIS(int *a, int n, int *b, int m) {
int ans = 0;
int dp[2][505] = {0};
for (int i = 0 ; i < n ; ++i) {
int max_dp = 0;
for (int j = 0 ; j < m ; ++j) {
dp[(i + 1) % 2][j + 1] = dp[i % 2][j + 1];
if (a[i] > b[j] && max_dp < dp[(i + 1) % 2][j + 1]) max_dp = dp[(i + 1) % 2][j + 1];
if (a[i] == b[j]) {
dp[(i + 1) % 2][j + 1] = max_dp + 1;
}
ans = max(ans, dp[(i + 1) % 2][j + 1]);
}
}
return ans;
}

四.LCIS练习题

传送门:HDU 1423 Greatest Common Increasing Subsequence (http://acm.hdu.edu.cn/showproblem.php?pid=1423)

AC 代码 :

/*************************************************************************
> File Name: LCIS-1.cpp
> Author:
> Mail:
> Created Time: 2017年09月03日 星期日 14时26分47秒
************************************************************************/ #include <stdio.h>
#include <stdlib.h>
#include <limits.h> #define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b)) #define N2 #ifdef N2
int lengthOfLCIS(int *a, int n, int *b, int m) {
int ans = 0;
int dp[2][505] = {0};
for (int i = 0 ; i < n ; ++i) {
int max_dp = 0;
for (int j = 0 ; j < m ; ++j) {
dp[(i + 1) % 2][j + 1] = dp[i % 2][j + 1];
if (a[i] > b[j] && max_dp < dp[(i + 1) % 2][j + 1]) max_dp = dp[(i + 1) % 2][j + 1];
if (a[i] == b[j]) {
dp[(i + 1) % 2][j + 1] = max_dp + 1;
}
ans = max(ans, dp[(i + 1) % 2][j + 1]);
}
}
return ans;
}
#endif int main() {
int n, m, T;
int a[505], b[505];
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 0 ; i < n ; ++i) {
scanf("%d", &a[i]);
}
scanf("%d", &m);
for (int j = 0 ; j < m ; ++j) {
scanf("%d", &b[j]);
}
printf("%d\n", lengthOfLCIS(a, n, b, m));
if (T) printf("\n");
}
return 0;
}

LCIS 最长公共上升子序列问题DP算法及优化的更多相关文章

  1. [CodeForces10D]LCIS(最长公共上升子序列) - DP

    Description 给定两个数列,求最长公共上升子序列,并输出其中一种方案. Input&Output Input 第一行一个整数n(0<n<=500),数列a的长度. 第二行 ...

  2. CF10D LCIS 最长公共上升子序列

    题目描述 This problem differs from one which was on the online contest. The sequence a1,a2,...,an a_{1}, ...

  3. LCIS最长公共上升子序列

    最长公共上升子序列LCIS,如字面意思,就是在对于两个数列A和B的最长的单调递增的公共子序列. 这道题目是LCS和LIS的综合. 在LIS中,我们通过两重循环枚举当序列以当前位置为结尾时,A序列中当前 ...

  4. LCIS(最长公共上升子序列)Vijos1264神秘的咒语

    描述 身为拜月教的高级间谍,你的任务总是逼迫你出生入死.比如这一次,拜月教主就派你跟踪赵灵儿一行,潜入试炼窟底. 据说试炼窟底藏着五行法术的最高法术:风神,雷神,雪妖,火神,山神的咒语.为了习得这些法 ...

  5. LCIS 最长公共上升子序列

    这个博客好久没写了,这几天为了准备清华交叉研究院的夏令营,在复习大一大二ACM训练时的一些基础算法,正好碰到LICS,发现没有写在博客里,那就顺便记录一下好了. 参考链接:http://blog.cs ...

  6. LCIS(最长公共上升子序列)模板

    求出LCIS并输出其路径. 1 #include <iostream> 2 #include <cstdio> 3 #include <string> 4 #inc ...

  7. CodeForces 10D. LCIS 最长公共上升子序列模板题 + 打印路径

    推荐一篇炒鸡赞的blog. 以下代码中有打印路径. #include <algorithm> #include <iostream> #include <cstring& ...

  8. 【简单dp】poj 2127 Greatest Common Increasing Subsequence【最长公共上升子序列】【模板】

    Sample Input 5 1 4 2 5 -12 4 -12 1 2 4 Sample Output 2 1 4 题目:给你两个数字序列,求出这两个序列的最长公共上升子序列.输出最长的长度,并打表 ...

  9. 【线型DP模板】最上上升子序列(LIS),最长公共子序列(LCS),最长公共上升子序列(LCIS)

    BEGIN LIS: 一个数的序列bi,当b1 < b2 < … < bS的时候,我们称这个序列是上升的.对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序 ...

随机推荐

  1. Optimization on content service with local search in cloud of clouds

    曾老师的这篇文章发表于Journal of Network and Computer Applications,主要解决的是利用启发式算法决定如何在cloud of clouds中进行副本分发,满足用 ...

  2. SpringBoot中logback.xml使用application.yml中属性

    教你如何使用 springProfile 与 springProperty 让你的logback.xml 配置显得更有逼格,当别人还在苦苦挣扎弄logback-{profile}.xml的时候 你一个 ...

  3. binlog

    binlog基本定义:二进制日志,也成为二进制日志,记录对数据发生或潜在发生更改的SQL语句,并以二进制的形式保存在磁盘中: 作用:MySQL的作用类似于Oracle的归档日志,可以用来查看数据库的变 ...

  4. sendfile学习

    参考 https://zhuanlan.zhihu.com/p/20768200?refer=auxten 而成本很多时候的体现就是对计算资源的消耗,其中最重要的一个资源就是CPU资源. Sendfi ...

  5. [Javascript] Highlights from IO18 Javascript new features

    Latest Javascript features, not supported by all broswers, but can use with babel. # try-catch-final ...

  6. C++开发人脸性别识别教程(12)——加入性别识别功能

    经过之前几篇博客的解说,我们已经成功搭建了MFC应用框架,并实现了主要的图像显示和人脸检測程序,在这篇博文中我们要向当中加入性别识别代码. 关于性别识别,之前已经专门拿出两篇博客的篇幅来进行解说.这里 ...

  7. C#替换字符串起始/结尾指定的字符串

    #region 替换字符串起始位置(开头)中指定的字符串 /// <summary> /// 替换字符串起始位置(开头)中指定的字符串 /// </summary> /// & ...

  8. Java Mocking入门—使用Mockito

    Java Mocking入门—使用Mockito 2014/03/10 | 分类: 基础技术 | 0 条评论 | 标签: 单元测试 分享到:8 本文由 ImportNew - liken 翻译自 dz ...

  9. Android 删除新版安卓fragment_main.xml

    在新版本号的ADT中,创建androidproject时默认会产生两个xml文件--fragment_main和activity_main. 个人建议把fragment_main这个文件删除掉 1)将 ...

  10. Selenium API 介绍

    Selenium API 介绍 我们先前学习过元素定位,大家不知道学习得怎么样了,当你学会元素定位之后就能够跟着我的脚步学习本节Selenium 经常使用的API 介绍 Seleium 为什么能模拟人 ...