LIS(最长上升子序列)问题

LIS问题介绍:

    首先来说一下什么是LIS问题:

有一个长为n的数列a0, a1, ......, a(n-1)。请求出这个序列中最长的上升子序列的长度。上升子序列指的是对于任意的i<j都满足ai<aj的子序列,该问题被称为最长上升子序列(LIS,Longest Increasing Subsequence)的著名问题。

举个栗子:给你一个序列为(1,5 ,2,6,9,10,3,15),那么它的最长上升子序列为:(1,2,6,9,10,15)

这个问题用DP的思想很容易解决,那么现在再来说一下DP(动态规划)的思想。

DP简介(大佬可以忽略此标题里的内容)

别急一会 会!详!!!谈LIS问题以及它的优化方法,为了更好的理解LIS问题,所以先来谈一下DP,如果做过一些DP的题的可以忽略这段入门DP的讲解,如果刚开始接触建议耐心读完,相信会有很大收获。
一、DP思想:
1、把一个大的问题分解成一个一个的子问题。
2、如果得到了这些子问题的解,然后经过一定的处理,就可以得到原问题的解。
3、如果这些子问题与原问题有着结构相同,即小问题还可以继续的分解。
4、这样一直把大的问题一直分下去,问题的规模不断地减小,直到子问题小到不能再小,最终会得到最小子问题。
5、最小子问题的解显而易见,这样递推回去,就可以得到原问题的解。

二、DP的具体实现:
1、分析问题,得到状态转换方程(递推方程)。
2、根据状态转换方程,从原子问题开始,不断的向上求解,知道得到原问题的解。
3、在通过递推方程不断求解的过程,实际上是一个填表的过程。

刚才说的我自己都觉得不好理解,太抽象了,为此举个2个栗子,让大家更好的理解DP的思想。

第一个栗子://http://node2.vjmirror.rainng.com/contest/239734#problem/B

问题描述:给你一个有N(N是奇数 && 1<=N<=999999)个数的序列,而且保证这N个数中有一个数M的数量  >=  (N + 1)/2 ,让你找出这个数M。

Sample Input:
5
1 3 2 3 3
Sample Output:
3

按照DP的思想,把这个大问题先分解成若干个小问题。所以呢当N为N时,至少有(N + 1)/ 2个M,另外的数就先不管他; ......然后当N为5得时候,依据题意那么一定至少有3个M,另外两个数就先不管他;当N为3的时候,根据题意得,一定有两个数为M,另外一个数就先不管他;先 来看当N为1的时候,那么这个数一定是M。所以就可以把这个序列中得两个不同的数删去(只要有两个数不同就删去),最后剩下的一定是M;
举个栗子: 
intput: 9
           3 6 9 3 3 3 8 6 3
loc :  1 2 3 4 5 6 7 8 9
1、2位置删去 3、4位置删去 6、7位置删去 8、9位置删去,,还剩一个5位置,那么5位置的 3 就是要找的M .

AC代码:

#include<iostream>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cstring>
using namespace std;
int arr[1000006];
int dp[1000006];
int main()
{
    int n;
    while(scanf("%d", &n) != EOF){
        memset(arr, 0, sizeof(arr));
        memset(dp, 0, sizeof(dp));
        for(int i = 0; i < n; i++) scanf("%d", &arr[i]);
        int i = 0, j = 1;
        while(j < n){
            if(dp[i] == 1){
                while(dp[i] == 1)
                    i++;
            }
            if(arr[i] != arr[j]){
                i++;
                dp[j++] = 1;
            }
            else if(arr[i] == arr[j]){
                while(arr[i] == arr[j])
                    j++;
            }
        }
        while(dp[i] == 1) i++;
        printf("%d\n", arr[i]);
    }
    return 0;
}

再来个栗子://http://acm.hdu.edu.cn/showproblem.php?pid=2084

该问题是一个数塔问题:
有如下所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?

Input:输入数据首先包括一个整数C,表示测试实例的个数,每个测试实例的第一行是一个整数N(1 <= N <= 100),表示数塔的高度,接下来用N行数字表示数塔,其中第i行有个i个整数,且所有的整数均在区间[0,99]内。
Output:对于每个测试实例,输出可能得到的最大和,每个实例的输出占一行。
Sample Input:
1
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
Sample Output:
30
 

先分析一下这个题目,最左/最右 的大小是由其上面一个决定,中间的是由其上面的两个决定。
设i为层数,DP思想,把大问题化成子问题,大问题是求解从上面到最底下这一路的加和的最大值,那么最大值一定在最后一行,想找到最后一行的最大值,就得保证 n - 1 行包含 n - 1 行及其之前的最大值,这样递推上去,就有  当i == 3时 第三行,为了保证尽可能大,那么这时候中间的一个数就得选择了,因为中间的既可以加左上角的也可以加右上角的,那么就选一个最大的加在自己的身上;当i == 2时 第二行的每个数都加上最上面的数;当i为1的时候,最大就是他自己,

依次类推,那么这一路走来最大的一定在最后一行,遍历一下就得出答案了。
这个塔可以用数组存起来,如果把这个数组扩展一下,这个问题将更加简洁:

这样一来不管第几行第几列,都只需要看它的左上和正上哪一个大,就加到本身,这样才能保证一路走来,走到最后一行的时候,最后一行一定存在最大值。
由此可以找出状态转移方程(也就是递推方程) dp[ i ][ j ] = max(dp[ i - 1 ][ j ] , dp[ i - 1 ][ j - 1 ]) + arr[ i ][ j ];

AC代码:
 

//https://vjudge.net/contest/239734#problem/B
#include<iostream>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cstring>
using namespace std;
int arr[1000006];
int dp[1000006];
int main()
{
    int n;
    while(scanf("%d", &n) != EOF){
        memset(arr, 0, sizeof(arr));
        memset(dp, 0, sizeof(dp));
        for(int i = 0; i < n; i++) scanf("%d", &arr[i]);
        int i = 0, j = 1;
        while(j < n){
            if(dp[i] == 1){
                while(dp[i] == 1)
                    i++;
            }
            if(arr[i] != arr[j]){
                i++;
                dp[j++] = 1;
            }
            else if(arr[i] == arr[j]){
                while(arr[i] == arr[j])
                    j++;
            }
        }
        while(dp[i] == 1) i++;
        printf("%d\n", arr[i]);
    }
    return 0;
}

大家明白了这道题后可以自己尝试依靠自己AC一道题目,该题目和这个类型基本一样,就是比这个略微难一点点,但是可以加深理解。
地址:http://node2.vjmirror.rainng.com/contest/239734#problem/G

LIS详解:

首先我们来讲解一下他的递推关系式:
定义dp[ i ] 为:以 ai 为末尾的最长上升子序列的长度。
那么dp[ i ] 包含什么呢?
首先 1' : 只包含它自己,也就是说它前面的元素全部都比他大;举个栗子:一个序列(7, 9, 6, 10, 7, 1, 3)分别为 (a1, a2, a3, a4, a5, a6, a7)那么dp[ 6 ] == 1;
然后  2‘  :为了保证上升子序列尽可能的长,那么就有 dp[ i ]  尽可能的大, 但是再保证 dp[ i ] 尽可能大的基础上,还必须满足序列的上升; 所以呢 dp[ i ] = max ( 1 , dp[ j ] + 1 ) {  j < i && aj < ai   } 这里的1就是当 ai 前面的数都比他小的时候,他自己为一个子序列。 dp[ j ] + 1 指的是: 当第 i 个数前面有一个 第 j 个数满足 aj  <  ai  并且 j < i 这时候就说明 ai 元素可以承接在 aj 元素后面尽可能的增加子序列的长度,j 从 1 遍历到 i - 1  ,在这之间,找出尽可能大的dp[ i ];

上面说的还是有点笼统, 那么再举个栗子吧:
还是用刚才的序列:(7, 9, 6, 10, 7, 1, 3)分别为 (a1, a2, a3, a4, a5, a6, a7)

最开始a1 = 7,  令dp[ 1 ] = 1;
然后看下一个元素 a2 = 9, 令dp[ 2 ] = 1, 那么需要检查 i 前面是否有比他小的 因为 a1 < a2 而且 dp[ 1 ] + 1 > dp[ 2 ], 所以dp[ 2 ] = dp[ 1 ] + 1 == 2;
然后再看下一个元素 a3 = 6, 令 dp[ 3 ] = 1, 那么需要检查前面的元素 a1  与 a2 是否有比他小的, 一看没有,辣么 到目前为止,子序列就是他自己。
然后再看一下下一个元素 a4 = 10; 令 dp[ 4 ] = 1;  那么需要依次检查前面的元素 a1  与 a2 与 a3 是否有比他小的 , 一看a1比它小,而且呢,dp[ 1 ] + 1 > dp[ 4 ] 所以呢 dp[ 4 ] = dp[ 1 ] + 1 == 2, 说明此时 a1 与 a4 可以构成一个长度为2 的上升子序列,再来看看还可不可以构成更长的子序列呢,所以咱们再来看看 a2 , a2 < a4 而且呢 dp[ 2 ] + 1 == 3 > dp[ 4 ] == 2  所以呢dp[ 4 ] = dp[ 2 ] + 1 == 3,  因为 和 a2 构成序列的是 a1 a2 , 呢么此时的序列应该为:a1 a2 a4 ;然后再来看 a3 , a3 < a4 但是可惜的是d[ 3 ] + 1 == 2  < dp[ 4 ] == 3 ,  所以呢就不能把a4加在a3的后面.
然后就是重复上诉过程,找到最大的dp [ i ] 那么这个数就是最长上升子序列
代码实现如下:
 

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main()
{
    int arr[500], n, dp[500], ans = -1;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d", &arr[i]); //不建议用cin cout 他们执行的时候还得先分析数据类型,耗时比 scanf printf  多好多,很多题目就因为这个地方而超时,导致比赛的时候罚时
    /* 求解最长子序列的个数的核心代码 */
    /* ********************************************** */
    for(int i = 1; i <= n; i++){
        dp[i] = 1; //初始化
        for(int j = 1; j < i; j++){
            if(arr[j] < arr[i]) // 如果求最大下降子序列则反之
                dp[i] = max(dp[i], dp[j] + 1);
        }
        ans = max(dp[i], ans);
    }
    /* ********************************************** */

    printf("最长子序列的个数为: %d", ans);
    return 0;
}
/*
样例:
7
7 9 6 10 7 1 3
最长子序列的个数为: 3
*/

这样就完了吗?如果这样就完了就不叫详解啦~  一会会慢慢讲,它的优化还有他的标记路径的方法。

先歇会,既然学了就来几道模板提练练手把!(都会有题解哒,先自己来一边试试!)
https://vjudge.net/contest/218661#problem/A

改题目为一个模板题,直接求解最长上升子序列即可
AC代码:
 

//https://vjudge.net/contest/218661#problem/A
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int main()
{
    int n, arr[1005], ans = -1, dp[1005];
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d", &arr[i]);
    for(int i = 1; i <=n; i++){
        dp[i] = 1;
        for(int j = 1; j < i; j++){
            if(arr[j] < arr[i])
                dp[i] = max(dp[i], dp[j] + 1);
        }
        ans = max(dp[i], ans);
    }
    printf("%d", ans);
    return 0;
}

https://vjudge.net/contest/239734#problem/E

该题也是模板题,直接上代码:
 

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int dp[1005], n, MAX;
int arr[1005];
int main()
{
    while(scanf("%d", &n) != EOF){
        if(n == 0) break;
        MAX = -1;
        for(int i = 0; i < n; i++) scanf("%d", &arr[i]);
        for(int i = 0; i < n; i++){
            dp[i] = arr[i];
            for(int j = 0 ; j < i; j++){
                if(dp[i] < dp[j] + arr[i] && arr[i] > arr[j]){
                    dp[i] = dp[j] + arr[i];
                }
                MAX = max(dp[i], MAX);
            }
        }
        printf("%d\n", MAX);
    }
    return 0;
}

写着写着突然发现Vjudge又蹦了。。。 又从洛谷上找的题目,后续可能会再加上几个Vjudge的题目
https://www.luogu.org/problemnew/show/P1091

题目描述

NNN 位同学站成一排,音乐老师要请其中的( N−KN-KN−K )位同学出列,使得剩下的 KKK 位同学排成合唱队形。

合唱队形是指这样的一种队形:设K位同学从左到右依次编号为 1,2,…,K1,2,…,K1,2,…,K ,他们的身高分别为 T1,T2,…,TKT_1,T_2,…,T_KT1​,T2​,…,TK​ , 则他们的身高满足 T1<...<Ti>Ti+1>…>TK(1≤i≤K)T_1<...<T_i>T_{i+1}>…>T_K(1 \le i \le K)T1​<...<Ti​>Ti+1​>…>TK​(1≤i≤K) 。

你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

输入输出格式

输入格式:

共二行。

第一行是一个整数 N(2≤N≤100)N(2 \le N \le 100)N(2≤N≤100) ,表示同学的总数。

第二行有 nnn 个整数,用空格分隔,第 iii 个整数 Ti(130≤Ti≤230)T_i(130 \le T_i \le 230)Ti​(130≤Ti​≤230) 是第 iii 位同学的身高(厘米)。

输出格式:

一个整数,最少需要几位同学出列。

Sample input :
8
186 186 150 200 160 130 197 220

Sample output :
4

题解:他的意思就是说想尽可能的留下学生。而且身高是中间高两边低的,那么如果说最高的要是在最左边,那么直接求他的最长上升子序列就够了,如果说在左边,那么直接从右边求他的最长上升子序列就够了,但是呢不确定最高的在哪里最合适。
现在关键 只要确定了最高的那个,求他的从最左边的最长上升子序列和他从最右边的最长升降子序列,这样就可以保证留下的人数最多,换句话地说就是剔除的人数最少。设dp_1[ i ] 为从左边上升第 i 个学生的最高上升的个数, dp_2[ n - i ] 为从右边上降第 i 个学生的最高上升的个数,求和 dp_3[ i ] = dp_1[ i ] + dp_2[ i ] 那么最大的 dp_3[ i ] ,以这个为终点左边依次低,右边也依次低, 那么最多留下人数就是 dp_3[ i ] - 1
减去他自己重复了两遍的
栗子:

                    序号 i :     1      2    3     4      5     6    7     8
                    身高          186 186 150 200 160 130 197 220
从左边上升dp_1[ i ] =     1      1    1     2      2     1     3    3
从右边上降dp_2[ i ] =     4      4    2     3      2     1     1    1
           总和dp_3[ i ] =     5      5    3     5      4     2     4    4

为了保证中间的左边的尽可能多,右边的也尽可能多,所以选择 总和 dp_3[ i ] 最大的 这里就是 当i == 1 || i == 2 || i == 4的时候 dp_3[ i ] - 1 == 4 (减去他自己重复的一遍), 所以答案是 8 - 4 == 4  , 因为减去留下的最多的,就是答案咯~
AC代码:
 

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main()
{
    int N, arr[105], dp[105] = {0}, dp2[105] = {0}, dp_all[105] = {0};
    scanf("%d", &N);
    for(int i = 0; i < N; i++) scanf("%d", &arr[i]);
    for(int i = 0; i < N; i++){  //求最长上升子序列
        dp[i] = 1;
        for(int j = 0; j < i; j++){
            if(arr[j] < arr[i] && dp[i] < dp[j] + 1)
                dp[i] = dp[j] + 1;
        }
    }
    for(int i = N - 1; i >= 0; i--){  //从后往前求最长上升子序列
        dp2[i] = 1;
        for(int j = N - 1; j > i; j--){
            if(arr[j] < arr[i] && dp2[i] < dp2[j] + 1)
                dp2[i] = dp2[j] + 1;
        }
    }
    int MAX = -1;
    for(int i = 0; i < N; i++){
        dp_all[i] = dp[i] + dp2[i];
        MAX = max(MAX, dp_all[i]);
    }
    printf("%d", N - MAX + 1);
    return 0;
}

LIS的nlogn的优化:
LIS的优化说白了其实是贪心算法,比如说让你求一个最长上升子序列把,一起走一遍。

比如说(4, 2, 3, 1, 2,3,5)这个序列,求他的最长上升子序列,那么来看,如果求最长的上升序列,那么按照贪心,应该最可能的让该序列的元素整体变小,以便可以加入更多的元素。
现在开辟一个新的数组,arr[ 10 ], { .......} --> 这个是他的空间 ,现在开始模拟贪心算法求解最长上升子序列,第一个数是4,先加进去,那么为{ 4 }再来看下一个数2,它比4小,所以如果他与4替换是不是可以让目前子序列(他这一个元素组成的子序列)变得更小,更方便以后元素的加入呢?是的。所以现在为{ 2 } 再来看他的下一个元素3,他要比2大,所以呢加在他的后面,{ 2, 3}
再看下一个元素是1,它比3要小,所以呢为了保证子序列整体尽可能的小(以便可以加入更多的元素),从目前的序列中查找出第一个比他大的数替换掉,那么就变成了{ 1, 3},继续。。 下一个数是2,那么序列变为{ 1,2},再下一个数为3,那么序列为{1,2,3},在下一个数为5,那么序列为{1,2,3,5},完。 目前序列里又4个元素,所以他的最长子序列的个数为4,但是这个序列是一个伪序列,里面的元素,并不是真正的最长上升子序列,而仅仅和最长上升子序列的个数一样。因为查找的时候用的二分查找,所以时间复杂度为o(nlogn)。

实现代码:
 

#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int main()
{
    int arr[500], n, dp[500], ans = -1;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d", &arr[i]);
    /* 求解最长子序列的个数的核心代码 */
    /* ********************************************** */
    int k = 1;
    dp[k] = arr[1];
    for(int i = 2; i <= n; i++){
        if(dp[k] < arr[i]) dp[++k] = arr[i]; //如果比最后一个元素大,那么就添加再最后末尾处
        else *(lower_bound(dp + 1, dp + 1 + k, arr[i])) = arr[i]; //如果比最后一个元素小,那么就替换该序列第一个比他大的数;
    }
    /* ********************************************** */

    printf("最长子序列的个数为: %d", k);
    return 0;
}

优化后得路径标记:

 

猜你喜欢

转载自blog.csdn.net/ltrbless/article/details/81318935