一. 知识简介

  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. CODEVS——T 1297 硬币

    http://www.codevs.cn/problem/1297/  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 黄金 Gold 题解       题目描述 Descrip ...

  2. 安装Maven并搭建Maven私有仓库

    一.说明 Maven是基于项目对象模型(POM),可以通过一小段描述信息来管理项目的构建,报告和文档的软件项目管理工具.我们在进行Java代码开发的时候,Eclipse+Maven+Jetty是一个十 ...

  3. hdoj 1257最少拦截系统

     /*最少拦截系统 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Tota ...

  4. ScrollView嵌套ExpandableListView显示不正常的问题

    关于ScrollView嵌套ExpandableListView导致ExpandableListView显示不正常的问题解决方法有非常多,在这里介绍一种小编亲自測试通过的方法. 重写Expandabl ...

  5. spring之注解(一)概述

    Spring的核心是依赖注入(DI),而依赖注入的基础是依赖信息的配置.这些配置称之为元数据. 在之前的的学习中.一直採用的是基于xml的配置,这些元数据配置在spring内部被注冊成为BeanDef ...

  6. 关于TRIANGLE二维三角网格生成器在windows下的配置说明

    近期须要用到三角网格生成的一些东西,所以就把TRIANGLE这个库编译了一下,发现编译过程还是略微有些纠结,于是就想到写下来.希望以后有些童鞋看到少走一些弯路. 首先很感谢eryar的帮助,很感谢! ...

  7. LLVM每日谈之十九 LLVM的第一本系统的书&lt;Getting Started with LLVM Core Libraries&gt;

    作者:史宁宁(snsn1984) LLVM最终有了一本系统的书了--<Getting Started with LLVM Core Libraries>. 这本书号称是LLVM的第一本书, ...

  8. luogu3799 妖梦拼木棒

    题目大意 有n根木棒,现在从中选4根,想要组成一个正三角形,问有几种选法?木棒长度都<=5000. 题解 根据容斥原理,三角形两条边分别由长度相等的单根木棒组成,另一条边由两条小于该边长的木棒构 ...

  9. linux下图片转换工具[【转】

    本文转载自:https://linux.cn/article-8014-1.html 计算机术语中,批处理指的是用一个非交互式的程序来执行一序列的任务的方法.这篇教程里,我们会使用 Linux 命令行 ...

  10. Getting started with ASP.NET Core MVC and Visual Studio

    This tutorial will teach you the basics of building an ASP.NET Core MVC web app using Visual Studio ...