线性DP

原题目均来自:https://www.acwing.com/about/

数字三角形

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

输入格式
第一行包含整数\(n\),表示数字三角形的层数。

接下来\(n\)行,每行包含若干整数,其中第 \(i\) 行表示数字三角形第 \(i\) 层包含的整数。

输出格式
输出一个整数,表示最大的路径数字和。

数据范围
\(1≤n≤500\)
\(−10000≤三角形中的整数≤10000\)
输入样例:

5
7
3 8
8 1 0 
2 7 4 4
4 5 2 6 5

输出样例:

30

思路:

个人感觉这应该是DP里边最简单的一道题了,这里表示的\(f[i][j]\)就表示以当前点为结尾的所有路径之和集合,而我们需要的集合属性是\(MAX\),而\(f[i][j]\)是从它的左上角和右上角转移而来,即\(f[i-1][j-1]\), \(f[i-1][j]\),状态转移方程:

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

  • 不要忘了初始化
  • 最后对最后一行取\(max\),得到需要的最大值

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, INF = 1e9;
int a[N][N];
int f[N][N];
int n;

int main()
{
    ios::sync_with_stdio(false), cin.tie(0);
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= i; j++)
        {
            cin >> a[i][j];
        }
    }
    for(int i = 0; i <= n; i++)
    {
        for(int j = 0; j <= i + 1; j++)
        {
            f[i][j] = -INF;
        }
    }
    f[1][1] = a[1][1];
    for(int i = 2; i <= n; i++)
    {
        for(int j = 1; j <= i; j++)
        {
            f[i][j] = max(f[i-1][j-1] + a[i][j], f[i-1][j] + a[i][j]);
        }
    }

    int res = -INF;
    for(int i = 1; i <= n; i++) res = max(res, f[n][i]);
    cout << res << endl;
    return 0;
}

最长上升子序列\(I\)

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

输入格式
第一行包含整数\(N\)

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

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

数据范围
\(1≤N≤1000\)
\(−10^9≤数列中的数≤10^9\)
输入样例:

7
3 1 2 1 8 5 6

输出样例:

4 

思路:

\(f[i]\)表示以当前数字结尾的所有子序列长度的集合,集合的属性就是\(MAX\),怎么进行状态计算呢?
对于\(f[i]\),以最长子序列的倒数第二个数分类,而最长子序列的倒数第二个数的下标可以是从\(0到i-1\)中任何一个,用\(f[j]\)表示,因为在\(f[i]\)之前的\(f[j]\)都已经被同样的方式更新过了,所以对于每一个\(i\)我们从小到大枚举\(j\),如果\(a[j] < a[i]\)那么\(f[i]\)就可以被\(f[j]\)更新,因为\(f[j]\)表示的是以下标\(j\)的数字结尾的最长上升子序列长度,所以状态转移方程:

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

  • \(f[i]\)的初始值是,因为它最少就只有它自己一个数字
  • 最后再枚举一遍\(f[]\), 取\(max\)
    时间复杂度\(O(N^2)\)

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, a[N], f[N];

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);
            }
        }
    }
    int res = 0;
    for (int i = 1; i <= n; i++) {
        res = max(res, f[i]);
    }
    cout << res << endl;
    return 0;
}

如果想要记录出最长上升子序列是啥,就只需要记录出每一步转移是从什么时候转移的就可以了,最后倒序输出,有点类似与迷宫的\(BFS\)记录路径。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, a[N], f[N], g[N]; //g数组保存每个转移是怎么做出来的

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for (int i = 1; i <= n; i++) {
        f[i] = 1;
        g[i] = 0; //表示只有一个数
        for (int j = 1; j < i; j++) {
            if (a[j] < a[i]) {
                if(f[i] < f[j] + 1) {
                    f[i] = f[j] + 1;
                    g[i] = j; //记录一下当前i是由哪个j转移过来的
                }
            }
        }
    }
    int k = 1;
    for (int i = 1; i <= n; i++) {
        if (f[k] < f[i]) {
            k = i; //找到最长子序列的末尾
        }
    }
    //倒序输出
    for (int i = 0, len = f[k]; i < len; i++) { //f[k] 存的是最长子序列的长度
        cout << a[k] << endl;
        k = g[k]; //因为k记录了i的转移过程
    }
    return 0;
}

最长公共子序列

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

输入格式
第一行包含两个整数\(N\)\(M\)

第二行包含一个长度为\(N\)的字符串,表示字符串\(A\)

第三行包含一个长度为\(M\)的字符串,表示字符串\(B\)

字符串均由小写字母构成。

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

数据范围
\(1≤N,M≤1000\)
输入样例:

4 5
acbd
abedc

输出样例:

3

思路:

\(f[i][j]\)表示所有在第一个序列的前\(i\)个字母中出现,且在第二个序列中的前\(j\)个字母中出现的子序列,集合属性\(MAX\),那怎么进行状态计算呢?
我们可以把集合分成四类:

  1. 不选\(a[i]\), \(b[j]\),即表示成状态就是\(f[i - 1][j - 1]\),即从第一个序列的前\(i-1\),第二个序列中前\(j-1\)个字母出现的公共子序列。
  2. \(a[i]\), \(b[j]\),即\(f[i-1][j-1] + 1\),前提是当前\(a[i] = b[j]\),需要特殊判断

还剩下两种情况分别为:不选\(a[i]\), 选\(b[j]\),和,选\(a[i]\), 不选\(b[j]\),这两种情况怎么表示呢,它们等价于\(f[i-1][j]\), \(f[i][j-1]\)吗,看定义:

  1. \(f[i-1][j]\)表示的是所有第一个序列的前\(i-1\)个字母出现,且在第二个序列中的前\(j\)个字母中出现的子序列,所以包含了这里的不选\(a[i]\), 选\(b[j]\)的这种情况,但是好在因为是求最大值,所以我们完全可以用\(f[i-1][j]\)来涵盖这种不选\(a[i]\), 选\(b[j]\)的情况,用\(1,0\)分别表示选和不选即:\(a[i] = 0, b[j] = 1 \in f[i-1][j]\), 那么整个\(max(a[i] = 0, b[j] = 1) \in max(f[i-1][j])\)
  2. \(f[i][j-1]\)表示与上同理。

要注意的是:
\((f[i - 1][j - 1] \cap (f[i-1][j-1] + 1) \cap f[i-1][j] \cap f[i][j-1]) != \emptyset\),即它们之间是有交集的,但是\(MAX\)只有一个,因此并不影响,还有就是既然有交集,那么可以看出,\(f[i - 1][j - 1] \in f[i-1][j]\), 因此在写代码的时候完全可以把\(f[i - 1][j - 1]\)省略掉啦!

时间复杂度:\(O(N^2)\)

代码:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
char a[N], b[N];
int f[N][N]; //i表示从a串中前i个字母中选的集合, j表示从b串中前j个字母中选的集合
int n, m;

int main() {
    cin >> n >> m;
    cin >> a >> b;
    
    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]) { //特别判断a[i] == b[j]才会有f[i-1][j-1] + 1
                f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
            }
        }
    }
    cout << f[n][m] << endl;
    return 0;
}
posted @ 2020-11-07 11:36  Xxaj5  阅读(75)  评论(0编辑  收藏  举报