动态规划(二)(线性DP、区间DP)

一.线性DP

所谓线性DP,就是说它的动态转移方程是线性的。

线性DP有三个经典的例题,如下:

1. LIS (Longest Increasing Subsequence,最长上升子序列)问题

问题描述:给定一个长度为 \(N\) 的数列 \(A\),求数值单调递增的子序列的最大长度是多少。\(A\) 的任意子序列 \(B\) 可表示为 $B = \({\)A_{k_{1}},A_{k_{2}},…,A_{k_{p}}$},其中 \(k_{1} < k_{2} < … <k_{p}\)

朴素做法:

状态表示:\(f[i]\) 表示从第一个数字开始算,以 \(a[i]\) 结尾的最大的上升序列。(以\(a[i]\) 结尾的所有上升序列中属性为最大值的那一个)。

状态计算(集合划分):\(j∈(0,1,2,..,i - 1)\), 在 \(a[i] > a[j]\) 时,
\(f[i] = max(f[i], f[j] + 1)\)
有一个边界,若前面没有比i小的,\(f[i]\)\(1\)(自己为结尾)。

最后再找 \(f[i]\) 的最大值。

时间复杂度
\(O(n^2)\) 状态数 \((n)\) \(*\) 转移数\((n)\)

代码如下:

#include <iostream>
using namespace std;

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

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    
    for(int i = 1; i <= n; i++) {
        dp[i] = 1;
        for(int j = 1; j < i; j++) {
            if(a[i] > a[j]) dp[i] = max(dp[i], dp[j] + 1);
        }
    }
    
    int ans = 0;
    
    for(int i = 1; i <= n; i++) {
        ans = max(ans, dp[i]);
    }
    printf("%d\n", ans);
    return 0;
}

优化版:

我们其实不难看出,对于 \(n^2\) 做法而言,其实就是暴力枚举:将每个状态都分别比较一遍。但其实有些没有必要的状态的枚举,导致浪费许多时间,当元素个数到了 \(10^4-10^5\) 以上时,就已经超时了。而此时,我们可以通过另一种动态规划的方式来降低时间复杂度:

将原来的dp数组的存储由数值换成该序列中,上升子序列长度为i的上升子序列,的最小末尾数值。

这其实就是一种几近贪心的思想:我们当前的上升子序列长度如果已经确定,那么如果这种长度的子序列的结尾元素越小,后面的元素就可以更方便地加入到这条我们臆测的、可作为结果、的上升子序列中。

代码如下:

#include <iostream>
using namespace std;

const int N = 100010;
int n;
int a[N];
int dp[N]; //dp[i]表示长度为i的子序列的结尾的最小值为多少 

int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		dp[i] = 0x3f3f3f3f;
	}
	
	dp[1] = a[1];
	int len = 1;
	
	for(int i = 2; i <= n; i++) {
		int l = 1, r = len;
		if(a[i] > dp[len]) dp[++len] = a[i]; //若当前处理的这一项大于末尾,则向后填充
		else { //否则就向前寻找第一个比它小的数(因为dp数组必然单调,所以可以二分) 
			while(l < r) { //其实就是lower_bound(),手写要快一些 
				int mid = l + r >> 1;
				if(dp[mid] >= a[i]) r = mid;
				else l = mid + 1; 
			}
			dp[l] = a[i]; 
		}
	} 
	printf("%d\n", len);
	return 0; 
}

B3637

2. LCS (Longest Common Subsequence,最长公共子序列)问题

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

集合表示\(f[i][j]\) 表示 \(a\) 的前 \(i\) 个字母,和 \(b\) 的前 \(j\) 个字母的最长公共子序列长度

集合划分:以 \(a[i]\) ,\(b[j]\) 是否包含在子序列当中为依据,因此可以分成四类:

\(a[i]\) 不在,\(b[j]\) 不在, \(dp[i][j] = dp[i − 1][j − 1]\)

\(a[i]\) 不在,\(b[j]\)

看似是 \(dp[i][j] = dp[i − 1][j]\) , 实际上无法用 \(dp[i − 1][j]\) 表示,因为 \(dp[i − 1][j]\) 表示的是在 \(a\) 的前 \(i - 1\) 个字母中出现,并且在 \(b\) 的前 \(j\) 个字母中出现,此时 \(b[j]\) 不一定出现,这与条件不完全相等,条件给定是 \(a[i]\) 一定不在子序列中,\(b[j]\) 一定在子序列当中,但仍可以用 \(dp[i − 1][j]\) 来表示,原因就在于条件给定的情况被包含在 \(dp[i − 1][j]\) 中,即条件的情况是 \(f[i − 1][j]\) 的子集,而求的是 \(max\),所以对结果不影响。

例如:要求 \(a\)\(b\)\(c\) 的最大值可以这样求:\(max(max(a, b),max(b, c))max(max(a, b),max(b, c))\) 虽然 \(b\) 被重复使用,但仍能求出 \(max\),求 \(max\) 只要保证不漏即可。

\(a[i]\) 在,\(b[j]\) 不在 原理同②

\(a[i]\) 在,\(b[j]\) 在, \(max = f[i − 1][j − 1] + 1\)

实际上,在计算时,①包含在②和③的情况中,所以①不用考虑

代码如下:

#include <iostream>

using namespace std;
const int N = 1010;
int n, m, f[N][N];
char a[N], b[N];
int main() {
	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][j], f[i - 1][j - 1] + 1);
		}
	}
	printf("%d", f[n][m]);
	return 0; 
}

二.区间DP

它以“区间长度”作为DP的“阶段”,使用两个坐标(区间的左右端点)描述每个维度,本质上它也属于线性DP的一种。

P1775 石子合并(弱化版)

思维导图:

由于只能合并相邻的两堆石子,所以最后一次合并时一定是左边连续的一部分和右边连续的一部分合并。

集合划分: 以最后一次合并时的分界限进行分类。

状态表示: \(f[i][j]\) 表示将 \(i\)\(j\) 这一段石子合并成一堆的方案的集合,属性 \(Min\)

状态计算:
(1) \(i<j\) 时,\(f[i][j] = \min _ {i ≤ k ≤ j - 1} f[i][k] + f[k + 1][j] + s[j] - s[i - 1]\)

(2)\(i=j\) 时, \(f[i][i] = 0\) (合并一堆石子代价为 \(0\)

问题答案:\(f[1][n]\)

所有的区间 \(dp\) 问题枚举时,第一维通常是枚举区间长度,并且一般 \(len = 1\) 时用来初始化,枚举从 \(len = 2\) 开始;第二维枚举起点 \(i\) (右端点 \(j\) 自动获得,\(j = i + len - 1\)

区间 \(DP\) 常用模版

for (int len = 1; len <= n; len++) {         // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
        int j = i + len - 1;                 // 区间终点
        if (len == 1) {
            dp[i][j] = 初始值
            continue;
        }

        for (int k = i; k < j; k++) {        // 枚举分割点,构造状态转移方程
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

本题代码如下:

#include <iostream>

using namespace std;

const int N = 310;

int n;
int s[N];
int dp[N][N];

int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) scanf("%d", &s[i]);
	for(int i = 2; i <= n; i++) s[i] += s[i - 1];
	
	for(int len = 2; len <= n; len++) {
		for(int i = 1; i + len - 1 <= n; i++) {
			int l = i, r = i + len - 1;
			dp[l][r] = 0x3f3f3f3f;
			for(int k = l; k < r; k++) {
				dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
			}
		}
	}
	printf("%d\n", dp[1][n]);
	return 0;
}

完结撒花!

posted @ 2023-09-26 15:42  Brilliant11001  阅读(21)  评论(0编辑  收藏  举报