[算法系列]动态规划02-线性DP

175 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

线性DP

本节我们主要介绍线性DP的经典题型,所谓线性DP就是状态转移发生在线性结构(比如数组)上的DP,我们后面还会讲到状态转移发生在其他地方的DP,如区间DP,树形DP,数位DP等等

最长上升子序列(普通)

例题:

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

样例

输入格式

第一行包含整数N。

第二行包含N个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1 ≤ N ≤ 1000, −1e9 ≤ 数列中的数 ≤ 1e9

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

状态表示: 我们用f[i]表示到第i为为止,最长严格递增子序列的长度

初始条件: f[0]=0,其余的f[i]至少为1

状态转移方程: 我们本次采用的方法是遍历到第i位时,找前面比i小的数j,取以j结尾的序列加入i这个数后的最长长度

因此,转移方程可以写成下面这样:

f[i]=max(f[i],f[j]+1)f[i]=max(f[i],f[j]+1)

代码实现:

#include<iostream>
using namespace std;
const int N=1100;
int f[N];
int a[N];
int n;
​
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    int res=-9;
    for(int i=1;i<=n;i++){
        f[i]=1;
        for(int j=1;j<i;j++){
            if(a[i]>a[j]){
                f[i]=max(f[i],f[j]+1);
            }
        }
        res=max(res,f[i]);
    }
    cout<<res<<endl;
    return 0;
}

最长上升子序列(进阶)

题目与上一题一样,但是数据范围扩大到了100000

区别: 上一题的做法时间复杂度是O(n^2),肯定过不了

因此,我们介绍一种二分优化DP的方法

状态表示: 用f[i]表示以长度为i的最长上升子序列,末尾最小的数字

初始条件: f[1]=a[0]

状态转移方程: 我们分析一下,由于这个f[i]是一个递增数组,我们可以这样考虑问题,先定义一个长度变量cnt,并且初始化好,依次遍历原数组,当遍历到的数字比 f 目前序列末尾数大的时候,长度加一,当遍历到的数字比f 目前末尾的小的时候,长度不变,但是可以更新f 中比a[i]大于或等于的地方的末尾数,因为数越小,涨的越慢,就更有可能越长,这也有点贪心的思路

状态转移方程在这里显得不那么必要,我们直接看代码更清晰一点

代码实现:

#include <iostream>
using namespace std;
const int N = 100100;
int n, cnt;
int w[N], f[N];
int main() {
    cin >> n;
    for (int i = 0 ; i < n; i++) cin >> w[i];
    f[cnt++] = w[0];
    for (int i = 1; i < n; i++) {
        if (w[i] > f[cnt-1]) f[cnt++] = w[i];
        else {
            int l = 0, r = cnt - 1;
            while (l < r) {
                int mid = (l + r) >> 1;
                if (f[mid] >= w[i]) r = mid;
                else l = mid + 1;
            }
            f[r] = w[i];
        }
    }
    cout << cnt << endl;
    return 0;
}

最长公共子序列

例题:

image.png

分析:

image.png

状态转移方程: 经过上述分析,我们不难写出状态转移方程:

f[i][j]={f[i1][j1]+1a[i]==b[j]max(f[i1][j],f[i][j1])a[i]!=b[j]f[i][j]=\begin{cases} f[i-1][j-1]+1& \text{} a[i]==b[j] \\ max(f[i-1][j],f[i][j-1])& \text{} a[i]!=b[j] \end{cases}

代码实现:

#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main() {
  cin >> n >> m >> a + 1 >> b + 1;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i] == b[j]) {
        f[i][j] = f[i - 1][j - 1] + 1;
      } else {
        f[i][j] = max(f[i - 1][j], f[i][j - 1]);
      }
    }
  }
  cout << f[n][m] <<endl;
  return 0;
}

最短编辑距离

例题:

image.png

分析:

image.png

状态转移方程:

f[i][j]={min(f[i1][j],f[i][j1])+1min(f[i][j],f[i1][j1]+(a[i]!=b[j]))f[i][j]=\begin{cases} min(f[i-1][j],f[i][j-1])+1& \text{} \\ min(f[i][j],f[i-1][j-1]+(a[i]!=b[j]))& \text{} \end{cases}

代码实现:

注意:本题需要初始化边界情况

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int m, n;
char a[N],b[N];
int dp[N][N];
int main()
{
    scanf("%d%s", &m, a + 1);   
    scanf("%d%s", &n, b + 1);
    cout << a[1] <<  endl;
    for(int i = 0; i <= m; i++) dp[i][0] = i;  
    for(int i = 0; i <= n; i++) dp[0][i] = i;  
    for(int i = 1; i <=m; i++)
        for(int j = 1; j <=n; j++)
        {
            dp[i][j] = min(dp[i][j-1], dp[i-1][j]) + 1;
            dp[i][j] = min(dp[i][j], dp[i-1][j-1] + (a[i] != b[j])); 
        }
    printf("%d\n",dp[m][n]);
    return 0;
}