动态规划入门2

一.概括:这篇DP文章主要是来介绍LCS(最长公共子序列) LIS(最长上升子序列)  LCIS(最长公共上升子序列)问题的求解以及其各种优化。
二.LCS(最长公共子序列问题)


1.引例:什么是公共子序列?就是两个或者多个串里最长的公共部分。注:公共部分元素可以不连续但是前后出现顺序一定相同!现在要求你编写一个程序,求出输入的两个串的最长公共子序列长度。

Input: 7  8   1754839  14356289     output:  4(1489或者1589)


2.方法一:枚举起点终点暴力求解法(略)

3.方法二:DP求解(二维数组)
我们推一下公式的话不难发现:(1) 假如S1的最后一个元素 与 S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS  再加上 S1和S2相等的最后一个元素。

 (2)假如S1的最后一个元素 与 S2的最后一个元素不等,那么S1和S2的LCS就等于 : {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列。

于是板子公式:---dp[i][j] = {=dp[i-1][j-1]+1;(a[i]==b[j])

                                           {=max(dp[i][j-1],dp[i-1][j])(a[i]!=b[j])
代码:
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int numA[100];
int numB[100];
int DP[100][100];
int main()
{
    int T;
    cin>>T;
    int t=0;
    while(T--)
    {
        t++;
        memset(DP,0,sizeof(DP));
        int m,n;
        cin>>m>>n;
        for(int i=1;i<=m;i++)
            cin>>numA[i];
        for(int i=1;i<=n;i++)
            cin>>numB[i];
        for(int i=1;i<=m;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(numA[i]==numB[j])
                {
                    DP[i][j]=DP[i-1][j-1]+1;
                }
                else
                    DP[i][j]=DP[i-1][j]>DP[i][j-1]?DP[i-1][j]:DP[i][j-1];
            }
        }
        cout<<"Case "<<t<<": "<<DP[m][n]<<endl;
    }
    return 0;
}
4.方法三:DP求解(空间优化一维数组)
*思路:因为二维求解DP[i][j]时只用到了DP[i-1][j],DP[i][j-1],DP[i-1][j-1]即该元素左,上,和左上角对角元素!所以我们完全可以把DP[i][j]变为DP[i]表示第二个串的i位置与第一个串所有位置最大公共子序列的长度。但是注意:在01背包问题里我们优化空间时,里面的循环是逆序的,因为先序的话会使后面原本依赖的先前状态更新从而出错!(后面的更新使基于前面)所以我们这里也是一样的!例子看下面图片:

我们更新DP[3][3]时,在一维里映射DP[3],DP[3][3]=DP[2][2]+1依据的是对角线位置!但是DP[3]=DP[2]+1却是不对的!因为在DP[3]之前更新了DP[2],不再是DP[3][3]先前对应的对角线DP[2][2],而变成了DP[3][2]!所以我们不能直接更新!但是LCS不像背包有重量对应关系,所以我们这里需要每次记录一下下一个点依赖的原本位置!

*核心代码:

    for(int i=1;i<=m;i++)
        {
            int save=DP[0];
            for(int j=1;j<=n;j++)
            {
                int k=DP[j];//记录当前位置
                if(numA[i]==numB[j])DP[j]=save+1;//可以随便更新,若是相等,save就是对角线的原本位置
                else
                    DP[j]=DP[j]>DP[j-1]?DP[j]:DP[j-1];//否则取较大的
                save=k;//保存
            }
        }
*代码:
#include <iostream>
#include<cstring>
using namespace std;
int DP[100];
int numA[100];
int numB[100];
int main()
{
    int T;
    cin>>T;
    int t=0;
    while(T--)
    {
        memset(DP,0,sizeof(DP));
        t++;
        int m,n;
        cin>>m>>n;
        for(int i=1;i<=m;i++)
        {
           cin>>numA[i];
        }
        for(int i=1;i<=n;i++)
        {
            cin>>numB[i];
        }
        for(int i=1;i<=m;i++)
        {
            int save=DP[0];
            for(int j=1;j<=n;j++)
            {
                int k=DP[j];
                if(numA[i]==numB[j])DP[j]=save+1;
                else
                    DP[j]=DP[j]>DP[j-1]?DP[j]:DP[j-1];
                save=k;
            }
        }
        cout<<"Case "<<t<<": "<<DP[n]<<endl;
    }
    return 0;
}

5.LCS的路径输出(只有二维可以,一维无法保存路径)
从这个图中我们就可以看出,在我们求解最大数目的时候,每一个相同元素处的路径转折如图深色部分,所以如果我们想求出其一个解,只要按照来的方向回溯就可以了,回溯规则是:
(1)如果这个位置DP[i][j]是相同元素处,则i--;j--;回到对角线;

(2)如果这个位置DP[i][j]是DP[i][j]=DP[i-1][j]过来的,就是从这个地方的上面下来的,则i--回去;
(3)如果这个位置DP[i][j]是DP[i][j]=DP[i][j-1]过来的,就是从左面过来的,则j--回去;

因此为了表示关系,我们用到一个mark数组来标记。
代码1:(循环回溯)
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int DP[100][100];
int mark[100][100];
int numA[100];
int numB[100];
int output[100];
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        memset(DP,0,sizeof(DP));
        int n,m;
        cin>>n>>m;
        for(int i=1; i<=n; i++)
        {
            cin>>numA[i];
        }
        for(int j=1; j<=m; j++)
        {
            cin>>numB[j];
        }
        for(int i=1; i<=n; i++)
        {
            for(int j=1; j<=m; j++)
            {
                if(numA[i]==numB[j])
                {
                    DP[i][j]=DP[i-1][j-1]+1;
                    mark[i][j]=0;
                }
                else if(DP[i-1][j]>DP[i][j-1])
                {
                    DP[i][j]=DP[i-1][j];
                    mark[i][j]=1;
                }
                else
                {
                    DP[i][j]=DP[i][j-1];
                    mark[i][j]=2;
                }
            }
        }
        int k=DP[n][m];
        cout<<"The numbers of max bares is "<<DP[n][m]<<"."<<endl;
        int i=n,j=m;
        while(i&&j)//循环法
        {
            if(mark[i][j]==0)
            {
                output[k--]=numA[i];
                i--;
                j--;
            }
            else if(mark[i][j]==1)
            {
                i--;
            }
            else
                j--;
        }
        for(int p=1;p<=DP[n][m];p++)
        {
            if(p==1)
                cout<<output[p];
            else
                cout<<" "<<output[p];
        }
        cout<<endl;

    }
    return 0;
}

代码2(递归回溯):

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int DP[100][100];
int mark[100][100];
int numA[100];
int numB[100];
void output(int a,int b)
{
    if(!a||!b)return;
    if(mark[a][b]==0)
    {
        output(a-1,b-1);
        cout<<numA[a]<<" ";
    }
    else if(mark[a][b]==1)
        output(a-1,b);
    else
        output(a,b-1);
}
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        memset(DP,0,sizeof(DP));
        int n,m;
        cin>>n>>m;
        for(int i=1; i<=n; i++)
        {
            cin>>numA[i];
        }
        for(int j=1; j<=m; j++)
        {
            cin>>numB[j];
        }
        for(int i=1; i<=n; i++)
        {
            for(int j=1; j<=m; j++)
            {
                if(numA[i]==numB[j])
                {
                    DP[i][j]=DP[i-1][j-1]+1;
                    mark[i][j]=0;
                }
                else if(DP[i-1][j]>DP[i][j-1])
                {
                    DP[i][j]=DP[i-1][j];
                    mark[i][j]=1;
                }
                else
                {
                    DP[i][j]=DP[i][j-1];
                    mark[i][j]=2;
                }
            }
        }
        cout<<"The numbers of max bares is "<<DP[n][m]<<"."<<endl;
        output(n,m);
        cout<<endl;
    }
    return 0;
}

二.LIS(最长上升子序列)问题求解及其优化
1.引例:什么是最长上升子序列? 就是给你一个序列,请你在其中求出一段不断严格上升的部分,它不一定要连续!
2.递推公式:我们用dp[i]表示以i为末尾的最长上升子序列的长度,则如果num[i-1]<num[i]:dp[i]=max(dp[i-1]+1,dp[i]);

3.代码:(普通递推)

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int dp[1000];
int num[1000];
int main()
{
    int n;
    memset(dp,0,sizeof(dp));
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>num[i];
        dp[i]=1;//初始化为1
    }
    int maxn=0;//记录最大长度
    for(int i=0;i<n;i++)//枚举结束点
    {
        for(int j=0;j<i;j++)//在结束点之前找条件
        {
            if(num[j]<num[i])
            {
                dp[i]=dp[i]>dp[j]+1?dp[i]:dp[j]+1;//更新
            }
        }
        maxn=maxn>dp[i]?maxn:dp[i];//更新最大值,此时最大值并不一定是DP[n]了;
    }
    cout<<maxn<<endl;
    return 0;
}

4.优化:一个比较神奇的二分法,先记着。。慢慢理解。注:采用B[k]数组二分法的方式,其只能求得其最长上升子序列的长度,而不能直接得出其最长上升子序列的序列的。如果想要得出其最长值序列的序列,需要采用动态规划的方式。下面摘一段博客: 谢谢大佬

2018 4 25 再理解//核心理解: 该算法主要是维护一个最长的序列在dp中,若下一个大于dp[len]则,整个序列的最长上升又增加了1,并且把该num[i]加到dp尾部; 如果小于dp[len],则把该num[i]插入到dp里面,把第一个大于等于num[i]的dp位置用这个num[i]替换掉,
尽量维护最长上升子序列的同时,使序列每个数字尽量的小!为以后的维护插入增长提供更多可能!!这就是nlogn解法的思想。

(1)解题心得:
1、在数据量比较大的时候n^2会明显超时,所以可以使用nlogn 的算法,此算法少了双重循环,用的lower_bound(二分法)。
2、lis中的数字并没有意义,仅仅是找到最小点lis[0]和最大点lis[len],其中,在大于lis[len]时len++,在小于lis[len]时可以将arr[i]在lis中的数进行替换掉。所以此算法主要是在不停的找最合适的起点和最合适的终点。

引用:
假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。n
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。
!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~于是算法的时间复杂度就降低到了O(NlogN)~!!

(2)核心代码:
  for(int i=0;i<n;i++)
    {
        if(num[i]>dp[len])//大于结尾,则++len,并且存入
            dp[++len]=num[i];
        else//否则找大于等于它的第一个点插入,更新
        {
            int pos=lower_bound(dp,dp+len,num[i])-dp;
            dp[pos]=num[i];
        }
    }
(3)代码:
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int num[1000];
int dp[1000];
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>num[i];
    }
    dp[0]=num[0];
    int len=0;
    for(int i=0;i<n;i++)
    {
        if(num[i]>dp[len])
            dp[++len]=num[i];
        else
        {
            int pos=lower_bound(dp,dp+len,num[i])-dp;//STL里面的lower_bound()返回大于等于num[i]的第一个元素位置迭代器
            dp[pos]=num[i];
        }
    }
    cout<<len+1<<endl;//因为从0开始的
    return 0;
}

(4)上面(3)只能求总的最长,我想知道每一个数字为结尾时的每个数字处最长长度,可以维护一个increase[],在连接插入的同时维护即可!原理一样:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int a[10005];
int increase[10005];
int dp[10005];
int main()
{
    int n;
    while(cin>>n)
    {
        for(int i=0; i<n; i++)
        {
            cin>>a[i];
            increase[i]  = 1;//每一个位置初始长度为自己 1
        }
        dp[0] = a[0];//dp数组维护最长序列
        int len = 0;//最长长度
        for(int i=0; i<n; i++){
            if(a[i]>dp[len]){//如果a[i]>dp[len],则尾部更新,len更新
                dp[++len] = a[i];
                increase[i] = len+1;//该处的最长 = len+1(因为len = 0开始)
            }
            else{
                int pos=lower_bound(dp,dp+len,a[i]) - dp;//否则找尽量小的可插入位置
                if(pos>=0){
                    dp[pos] = a[i];
                    increase[i] = pos+1;//该处的最长 = pos +1
                }
            }

        }
        for(int i=0;i<len;i++)
            cout<<increase[i]<<" ";
        cout<<endl;
        cout<<len+1<<endl;
    }
    return 0;
}

例题:uva10534 Wavio squence

题意:求一个波浪形的最长子序列,方法就是求每一个数字的上升子序列长度和下降子序列长度(逆向求上升子序列),任何枚举判断哪个大即可!必须用二分维护!

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int a[10005];
int increase[10005];
int decrease[10005];
int dp[10005];
int main()
{
    int n;
    while(cin>>n)
    {
        for(int i=0; i<n; i++)
        {
            cin>>a[i];
            increase[i] = decrease [i] = 1;
        }
        dp[0] = a[0];
        int len = 0;
        for(int i=0; i<n; i++){
            if(a[i]>dp[len]){
                dp[++len] = a[i];
                increase[i] = len+1;
            }
            else{
                int pos=lower_bound(dp,dp+len,a[i]) - dp;
                if(pos>=0){
                    dp[pos] = a[i];
                    increase[i] = pos+1;
                }
            }

        }
//        for(int i=0;i<n;i++)
//            cout<<increase[i]<<" ";
//        cout<<endl;
        dp[0] = a[n-1];
        int len2 = 0;
        for(int i=n-1;i>=0;i--){
            if(a[i]>dp[len2]){
                dp[++len2] = a[i];
                decrease[i] = len2+1;
            }
            else{
                int pos=lower_bound(dp,dp+len2,a[i]) - dp;
                if(pos>=0){
                    dp[pos]= a[i];
                    decrease[i] = pos + 1;
                }
            }
        }
//        for(int i=0;i<n;i++)
//            cout<<decrease[i]<<" ";
//        cout<<endl;
        int maxx = 1;
        for(int i=0;i<n;i++){
           maxx = max(maxx,2*min(increase[i],decrease[i]) - 1);//全部判断
        }
        cout<<maxx<<endl;


    }
    return 0;
}


5.LCS转LIS(解决大数据超内存超时问题):uva10635prince and princess
(1)题目描述:一个王子和公主在n*n的格子中行走,这些格子是有1....n^2的编号的。现在给定p+1个数,再给定q+1个数,公主和王子可以选择其中某些格子行走,求他们最多能走几个相同的格子。其实就是找两个序列的最大公共子序列,但是!数量达到了250*250*250*250  所以肯定超时超内存!
(2)思路:既然是求两个序列的最大公共子序列,那么就是求第二个序列里出现前后顺序与第一个序列里最可能相同的序列,我们可以把第一个序列每个数字按照位置顺序编号,对于第二个序列相对于第一个序列来编号,那么第二个序列的编号顺序就是第一个序列的顺序重排,所以我们只要用二分求第二个序列的最大上升子序列的长度就是两序列的最大公共部分!(注意这里每个序列里数字出现不重复!没出现的位置标记为0,查的时候跳过即可!)

(3)代码(LCS->LIS:大数据转化)

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int numA[251*251];
int numB[251*251];
int dp[251*251];
int main()
{
    int T;
    cin>>T;
    int t=0;
    while(T--)
    {
        memset(numA,0,sizeof(numA));
        memset(numB,0,sizeof(numB));
        t++;
        int n,p,q;
        cin>>n>>p>>q;
        for(int i=1;i<=p+1;i++)
        {
            int a;
            cin>>a;
            numA[a]=i;//记录每个数字的位置编号(a的编号为i)
        }
        for(int i=1;i<=q+1;i++)
        {
            int a;
            cin>>a;
            numB[i]=numA[a];//将B里面的数字相对于A的编号放入B
        }
        int len=0;
        dp[len]=numB[1];
        for(int i=2;i<=q+1;i++)//二分求LIS
        {
            if(numB[i]>dp[len])
                dp[++len]=numB[i];
            else
            {
                int pos=lower_bound(dp,dp+len,numB[i])-dp;
                dp[pos]=numB[i];
            }
        }
        cout<<"Case "<<t<<": "<<len+1<<endl;//注意len从0开始!
    }
    return 0;
}

三.LCIS(最长公共上升子序列)
(1)问题:求a和b串的最长公共上升子序列的长度
(2)思路:定义状态:定义dp[i][j]表示数组A[0~i]和数组B[0~j]序列且以B[j]为结尾的最长公共生生子序列的长度!
递推:当A[i]!=B[j]时,因为以B[j]为结尾,所以dp[i][j]=dp[i-1][j]

当A[i]==B[j]时:首先保证了最小长度是1,其次我们找剩下的最大长度就要在dp[i-1][j-1]里面找了,那么在第一维里,选i-1肯定是最好的(i-2肯定要比i-1差),然后在第二维里我们就需要参照LIS问题的求解了,一方面要公共另一方面要上升,所以我们不知道第二维里哪个B[k]<B[j],所以我们要寻找满足B[k]<B[j]而且dp[i-1][k]最长的前序,再来加上1就是当前dp[i][j]的最长长度!
综上:递推公式:dp[i] [j]= dp[i-1][j] (A[i]!=B[j] )

                                                                                 = max( dp[i-1][k])+1(A[i]==B[j]&&B[j]>B[k]&&k>=1&&k<j)
核心代码:  
        int maxlen=0;//复杂度n3
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=m;j++)
            {
                if(numA[i]!=numB[j])
                    dp[i][j]=dp[i-1][j];
                else
                {
                    for(int k=1;k<j;k++)//遍历找到最大dp[i-1][0~j-1]
                    {
                        if(numB[j]>numB[k]&&dp[i][j]<dp[i-1][k])
                            dp[i][j]=dp[i-1][k];
                    }
                    dp[i][j]++;//加一,加上当前相等
                }
                maxlen=max(maxlen,dp[i][j]);
            }
        }

(3)优化时间:在便利寻找最大B[k]时,浪费了时间。因为只是寻找在j之前的最大dp[i-1][k],所以我们可以在循环的时候顺便保存最大值,在A[i]=B[j]时直接dp[i][j]=max+1即可!
而且:并不是所有的最大值都保存,只有B[i]>B[k]的才可以用,而当A[i]==B[j]时,以前有B[k]<B[j],所以更新条件是A[i]>B[k]&&dp[i-1][k]>maxn

核心代码:

         int maxlen=0;
         for(int i=1;i<=n;i++)
         {
             int maxn=0;
             for(int j=1;j<=m;j++)
             {
                 dp[i][j]=dp[i-1][j];
                 if(numB[j]<numA[i]&&dp[i-1][j]>maxn)//this two  can't take place toghter!
                    maxn=dp[i-1][j];
                 else if(numA[i]==numB[j])
                    dp[i][j]=maxn+1;
                if(dp[i][j]>maxlen)
                    maxlen=dp[i][j];
            }
         }

(4)输出路径(表示不懂,先码着)
poj2724

代码:

#include <iostream>
//#include<bits/stdc++.h>
#include<cstring>
#include<vector>
using namespace std;
int dp[1000][1000];
int pre[1000][1000];//保存路径
int numA[1000];
int numB[1000];
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        memset(dp,0,sizeof(dp));
        memset(pre,-1,sizeof(pre));
        int n,m;
        cin>>n;
        for(int i=1; i<=n; i++)
        {
            cin>>numA[i];
        }
        cin>>m;
        for(int i=1; i<=m; i++)
        {
            cin>>numB[i];
        }
        for(int i=1; i<=n; i++)
        {
            int maxn=0,pa=0;
            for(int j=1; j<=m; j++)
            {
                dp[i][j]=dp[i-1][j];
                if(numB[j]<numA[i]&&dp[i-1][j]>maxn)//this two  can't take place toghter!
                {
                    maxn=dp[i-1][j];
                    pa=j;//更新pa
                }
                else if(numA[i]==numB[j])
                {
                    dp[i][j]=maxn+1;
                    pre[i][j]=pa;//保存pa
                }
            }
        }
        int maxlen=0,e;
        for(int i=1;i<=m;i++)
        {
            if(maxlen<dp[n][i])
            {
                maxlen=dp[n][i];
                e=i;
            }
        }
        vector<int> num;
        cout<<maxlen<<endl;
        for(int i=n;i>=1;i--)
        {
            if(pre[i][e]!=-1)
            {
                num.push_back(numA[i]);
                e=pre[i][e];
            }
        }
        for(int i=num.size()-1;i>=0;i--)
        {
            if(i!=0)
                cout<<num[i]<<" ";
            else
                cout<<num[i];
        }
       cout<<endl;
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_40772692/article/details/79688303