简单DP入门(二) 最长上升子序列及其优化

最长上升子序列解决问题:

  有N个数,求出它最长的上升子序列并输出长度。

  在题里不会讲的这么直白,这个算法往往会与其他的算法混在一起使用。


 

  在这篇文章中不会出现其他的例题,为了让大家更好的理解,我只会对模板进行讲解。(谢谢大家的理解)

 


  1-朴素算法(时间复杂度炒鸡炒鸡高)

  首先,我们先列出一些无序的数进行观察,例如:1 7 4 2 3 6 8 9 (共8个数)。

  我们通过观察很快可以发现在这个序列中最长的上升序列时1,2,3,6,8,9,长度为6,我们可以把每种情况都遍历一遍,我们现在从1开始,当1为子序列第一个元素的时候,我们1后面比1大的数:7,4,2,3,6,8,9,然后我们一个一个试,如果下一个选择7,那么在寻找7后面比7大的数:8,9,我们选择8,然后再寻找8后面比8大的数9,然后我们选择9,此时后面已经没有元素了,记录下此时的最长上升子序列:1,7,8,9(长度为4),然后回到1,我们已经选择过7了,现在换成4,继续前面的操作,找到序列1,4,6,8,9(长度为5),比原来的4大,所以最长上升子序列更新为5,按照这样的操作,我们最终扫完后就可以得到了最长上升子序列:6;

  这种算法是最容易想的朴素算法,平淡无奇,理所当然,但是不动脑子的代价就是!!!(况且也不一定好打)

  我也不上代码了,相信上了大家也不会看(正好也不用打了)。

  2-动态规划V1.0(时间复杂度O(n^2))

  现在进入正题,最长上升子序列的动态规划做法是目前最普遍的,较简便的做法,在对时间复杂度不是特别高的时候,大部分人都会选择这种方法,因为它很好理解也好打。

  首先,作为DP,那么久要有两个要素:状态和转移方程。考虑到看这篇文章的通常都是动态规划的初学者,我在这里提一下动态规划的基本思想:将一个大问题分成多个小问题,通过对很好解决的小问题的求解,得出初始的状态,可以用动态规划解决的题目一般都能找出来从一个状态转移到另一个状态的方法,称为状态转移方程,一般从简单到复杂,数据范围从小到大进行转移,最终通过转移的结果得出最终的最优解往往就是答案(某些数位DP除外),动态规划简单来说就是用各阶段的最优解来推导下一阶段的最优解,相似于记忆化搜索,但是二者又不尽相同。为什么动态规划快呢,因为它减少了很多的不必要的重复搜索过程,以达到剪枝的效果。

  我们想要进行动态规划,要先设出状态,动态规划题中的状态需要和答案有密切的联系,并且可以利用你设的状态推导出转移方程。

  最长上升子序列的问题顾名思义是最长上升子序列的长度,所以我们的f[i]=最长的长度(在动态规划题中习惯用f[]或者dp[]来表示状态数组)而这个i表示什么呢,本题也没什么东西了,只有一串数,那么这个i就表示以第i个数结尾吧。这样我们的状态就出来了,f[i]表示以第i个数字结尾的最长上升子序列的长度。答案输出f[N]即可。

  状态出来了,我们就开始推导转移方程:在推导时我们需要关注的只是这一阶段和它的上一阶段之间的联系(可千万不要往深了想,很可能把自己绕晕),f[i]是以第i个数为结尾的,它的上一阶段只有可能是f[j](其中1<=j<=i-1) 因为f[j]表示的是结尾为第j个数的最长上升子序列的长度,所以在从f[j]推导到f[i]时我们只需考虑是否第j个数小于第i个数,如果小于那么f[i]=f[j]+1(从第j个数为结尾转换成以第i个数为结尾多了一个数,所以长度加1),因为不知道1到i-1中哪个最合适,就都便历一边,最后保留最长的f[i]即可完成这一状态的转移。书写出来就是f[i]=max(f[i],f[j]+1);(max指的是从两个数中选出最大的返回,如果这一状态转移过后还没有原来的f[i]长,那么肯定不转移了呀)。

  现在状态和转移方程都出来了,那么我可以上代码了。(代码十分的好理解)

 Code:

#include<iostream>
#include<cstdio>
using namespace std;
int f[101]={0};
int n,a[101]={0},ans=0;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<=n;i++){
        f[i]=1;
        for(int j=1;j<i;j++){
            if(a[j]<a[i]){
                f[i]=max(f[i],f[j]+1);
            }
        }
        ans=max(f[i],ans);
    }
    cout<<ans;
}

 

  没啥难以理解的。


 

  3-动态规划V2.0(时间复杂度O(NlogN))

  为什么我画了一条线呢?

  主要是为了与前面那些笨拙的算法区别开,这种算法比较巧,而且实用。

  在介绍这种算法之前呢,我要先介绍一个C++STL中函数lower_bound(),具体用法附上百度百科的链接https://baike.baidu.com/item/lower_bound/8620039?fr=aladdin

  相信大家都看懂了,下面我来讲下这个动态规划+二分优化的基本思想:我们定义一个数组f[i]这里面的i和上一个1.0的i可不一样了,这个i表示长度为i的最长上升子序列。而f[i]则表示长度为i的子序列的最小结尾。是不是没看懂,那让我来举个例子:例如有1,7,4,2,3,6,8,9 八个数,f[1]=1,因为长度为1的最长上升子序列中只含有一个数1,所以最小一定是1,接着我们看f[2],7比1大,可以将它加入,那么此时i=2,f[2]=7,因为此时的序列是1,7,以7结尾当然7是最小结尾,接着4<7所以我们将4替换到它第一个可以插入的不影响序列顺序的位置,与那个位置上比它大的数交换,更新该位上的f。比如这时,4比1大,比7小,所以应该和7交换,将队列变成1,4,i不变(因为队列长度还是2),所以我们这个算法的思想就是拼命维护长度为i的序列有最小的结尾,这样它就可以在后面容纳更多的数。接着,2>4,所以2将4替换掉,序列变成1,2。i仍然不变化,继续向下推,3小于2,所以i++,将3加入序列最后,此时序列为1,2,3,i=3。6>3,所以加入6,i=4。8>6所以将8加入序列,i=5。接着加入9,i=6。此时已经到了结尾,退出循环,我们看,此时的i就是最长上升子序列的长度了。

 

Code:

#include<iostream>
#include<cstdio>
using namespace std;
int f[101]={0},len;
int n,a[101]={0},ans=0;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<=n;i++){
        if(a[i]>f[len]) f[++len]=a[i];
        else if(a[i]<f[len]){
            *lower_bound(f+1,f+len+1,a[i])=a[i];
        }
    }
    cout<<len;
}

 

  我们会发现,这种方法和贪心有些像,所以这种方法也可以说是动态规划+贪心优化(动态规划V2.0)。

  好了,这篇文章到这里也该结束了,反正我这么弱,大家看看就好。

 

posted @ 2019-08-03 20:08  Dubhe0315  阅读(454)  评论(0编辑  收藏  举报