几道基础的经典线性DP

独自空忆成欢 提交于 2020-01-22 12:44:01

部分资源来自AcWing

1.数字三角形

链接:http://acm.hdu.edu.cn/showproblem.php?pid=2084

这是一道比较简单的线性DP问题,把f [ i, j ] 分成从左边来的和从上边来的两类就可以了,下面是代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <limits.h>
#define N 110
using namespace std;

int t, n;
int g[N][N];
int f[N][N];

int main()
{
	cin >> t;
	while(t--)
	{
		cin >> n;
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= i; j++)
				cin >> g[i][j];

		for (int i = 0; i <= n; i++)
			for (int j = 0; j <= i+1; j++)
				f[i][j] = -INT_MAX;

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

2.最长上升子序列

https://www.acwing.com/activity/content/problem/content/1003/1/

状态 f [ i ] 表示的是只考虑前 i 个元素,从中选出的上升子序列(要求必包含第 i 个元素)所构成的集合所具有的最大长度。f [ i ] 的划分标准如下:按照子序列中的倒数第二个元素是原数列中第 0 (表示该子序列只存在一个元素) , 1, 2, 3,… ,i - 1 个元素来划分。代码如下:

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

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

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> list[i];
    
    for (int i = 1; i <= n; i++)
    {
        f[i] = 1; // f[i] 最小值必为1
        for (int j = 1; j <= i - 1; j++)
        {
            if(list[j] < list[i]) // 构成上升子序列的充分条件
                f[i] = max(f[i], f[j] + 1);
        }
    }
    
    int res = 0; // f[i] 必然大于0
     for (int i = 1; i <= n; i++)
        res = max(res, f[i]);
        
    cout << res << endl;
    
    return 0;
}

但其实这样做时间复杂度难免有点高,在最坏的情况下可以达到n^2,当数据范围较大时会 tle,比如下面这道题:

https://www.acwing.com/problem/content/898/

仅仅是改了一下数据范围,我们就需要重新考虑一下,看看之前的这个状态计算方法是否有些冗余。

首先对于这样的例子:3,1,2,4,5  我们可以发现,所有可以与3构成上升子序列的数也一定可以与1构成上升子序列,而且我们很明显可以知道,用 1 来领导子序列比用 3 来领导更有发展潜力,比如 2 就可以接在 1 后面,而不能再接在 3 的后面。因此,我们实际上可以只存储具有最小结尾数字的不同长度的上升子序列。

再者,我们可以证明以子序列的长度为下标,以该长度的最小的结尾数字为值的数组 q [ n ] 一定时单调递增的。(用反证法)

有了这些基础,我们大概可以理一下思路:从第 1 个元素开始遍历原始数组 a 的每一个元素,针对每一个 a [ i ],我们在 q 数组中查找一个最大的小于 a [ i ] 的元素 q [ j ] ,那么这个 a [ i ] 就可以接在 q [ j ] 之后,构成一个长度为 j + 1 的子序列,并且其结尾元素 a [ i ] < q [ j + 1 ] ,于是我们把 q [ j + 1 ] 更新成 a [ i ] , 然后如此循环即可。

再说查找,由于 q 数组是严格递增的,我们就可以采用二分查找的方法。代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
#define N 100010
using namespace std;

int n;
int a[N], q[N];

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
        
    int len = 0;
    q[0] = -2e9;
    for (int i = 1; i <= n; i++)
    {
        int l = 0, r = len;
        while(l < r)
        {
            int mid = (r + l + 1) / 2; // 注意由于区间的变化为 l = mid,因此需要向上取整
            if(q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1); // r + 1 为得到的新的子序列的长度,且此时 l == r
        q[r + 1] = a[i];
    }
    
    cout << len << endl;
    return 0;
}

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!