【基础算法】线性DP

简单介绍

线性DP是动态规划问题中的一类问题,指状态之间(状态转移)具有线性关系的动态规划问题,但这种关系通常不明显。常见的线性DP有:

  1. LIS(longest increasing subsequence)即最长上升子序列;
  2. LCS(longest common subsequence)即最长公共子序列;
  3. ……。

LIS(最长上升子序列)

例题

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

状态定义

  • 我们定义dp[i]为所有从前i个元素中选,且以i结尾的上升子序列的集合,dp[i]的属性为MAX。

状态转移方程

  • 我们可以考虑一个朴素的做法:假设当前枚举到第i个元素(a[i]),找到所有满足0<j<i并且a[j]<a[i]的j,我们就可以枚举这些j得到状态转移方程:dp[i]=max(dp[i],dp[j]+1), 该状态转移的时间复杂度为O(n^2)。
  • 优化做法(先看朴素做法的代码再回来看这里):我们定义一个队列q,大小为cnt,状态为:长度为i的子序列的结尾元素最小为q[i]。我们先行假设q是单调递增的
    • 维护步骤:对于当前的a[i],利用二分,从q中找到小于a[i]的最大数q[j],此时会有两种情况:1)找不到q[j]:扩大队列q,即q[cnt++]=a[i],表示当前最长上升子序列的长度为cnt+1;2)找到q[j]:有q[j]<a[i]<=q[j+1],将q[j+1]更新为a[i],即长度为j+1的上升子序列的结尾最小元素为a[i]。
    • 现在,我们需要证明q是单调的,否则时间复杂度仍然是O(n^2),对比朴素算法没有优化。
    • 证明思路:反证法。现在遍历到元素x,他会更新q中i的位置。假设q不是单调的,则有i<j,q[i]>q[j],即:长度为i的子序列的结尾元素比长度为j的结尾元素要大,那么长度为i的子序列的结尾元素也可以放在长度为j的子序列的结尾元素的后面,使得长度为j的子序列的长度变成j+1(这是更优的做法)。因此,q是单调的。

初始条件

  • 每一个元素都可以构成一个长度为1的子序列,即:dp[i] = 1。

朴素做法代码

#include <iostream>
#include <algorithm>

using namespace std;

int a[1010];
int f[1010];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) {
        cin>>a[i];
        f[i]=1;
    }

    for(int i=2;i<=n;i++){
        int j=i-1;
        while(j>=1){
            if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
            j--;
        }
    }

    int res=0;
    for(int i=1;i<=n;i++) res=max(res,f[i]);

    cout<<res;
    return 0;
}

优化做法代码

#include <iostream>
#include <algorithm>

using namespace std;

const int N=100010;
int a[N],f[N];
int q[N],hh=1,tt=0;
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) {
        cin>>a[i];
        f[i]=1;
    }

    for(int i=1;i<=n;i++){
        if(hh>tt) q[++tt]=a[i];
        else {
            int l=0,r=tt,mid;
            while(l<r){
                mid=l+r+1>>1;
                if(q[mid]>=a[i]) r=mid-1;
                else l=mid;
            }
            f[i]=max(f[i],l+1);
            if(l+1>tt) q[++tt]=a[i];//对应没有找到的情况,扩大队列q
            else q[l+1]=a[i];
        }
    }

    cout<<tt; //此时tt是队列q的末端下标,根据定义tt就是最长上升子序列的长度
    return 0;
}

LCS(最长公共子序列)

例题

给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

状态定义

  • 我们定义dp[i][j]为从A[1-i]和B[1-j]中选,有公共子序列的集合,属性为MAX。
  • 为什么要开二维?因为B序列可以看作是一个限制条件,一般每多一个限制条件就需要多开一维,然后我们可以使用滚动数组,数学的等价代换等各种优化方法降维,但是本题不可以

状态转移方程

  • 假设我们已经处理好了dp[i-1][j]、dp[i][j-1]和dp[i-1][j-1],现在要处理dp[i][j]。
    1. 情况1:A[i]==B[j],dp[i][j] = max(dp[i][j], dp[i-1][j-1]+1)
    2. 情况2:A[i]!=B[j],此时可以细分为1)选i,不选j;2)选j,不选i;3)i、j都不选;
      1. 选i,不选j:dp[i][j]=max(dp[i][j],dp[i][j-1]);
      2. 选j,不选i:dp[i][j]=max(dp[i][j],dp[i-1][j]);
      3. i、j都不选:dp[i][j]=max(dp[i][j],dp[i-1][j-1]);
      • 我们的状态定义是从A[1-i]和B[1-j]中选,那么很显然,dp[i][j-1]和dp[i-1][j]不一定可以选到i和j,但是为什么可以这样划分呢?因为本题要求的是最大值,划分时状态是可以重复的,但是状态是不可以缺失的,例如,求1、2、3中的最大值,我们可以将其划分为1、2和2、3,显然可以分别求出两个划分的最大值再取一次MAX就可以得到最大值。顺带一提,求数量时状态是不可以重复的

初始条件

代码

#include <iostream>

using namespace std;

const int N = 1e3 + 10;
int f[N][N];
char a[N], b[N];
int main()
{
    int n, m;
    cin >> n >> m >> a + 1 >> b + 1;
    
    for (int i = 1;i <= n; i++) {
        for (int j = 1; j <= m ;j++) {
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            if (a[i] == b[j]) f[i][j] = max(f[i - 1][j - 1] + 1, f[i][j]);
        }
    }
    
    cout << f[n][m];
    
    return 0;
}

END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值