简单介绍
线性DP是动态规划问题中的一类问题,指状态之间(状态转移)具有线性关系的动态规划问题,但这种关系通常不明显。常见的线性DP有:
- LIS(longest increasing subsequence)即最长上升子序列;
- LCS(longest common subsequence)即最长公共子序列;
- ……。
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:A[i]==B[j],dp[i][j] = max(dp[i][j], dp[i-1][j-1]+1)
- 情况2:A[i]!=B[j],此时可以细分为1)选i,不选j;2)选j,不选i;3)i、j都不选;
- 选i,不选j:dp[i][j]=max(dp[i][j],dp[i][j-1]);
- 选j,不选i:dp[i][j]=max(dp[i][j],dp[i-1][j]);
- 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;
}