[知识点] 4.3 动态规划基础模型——区间DP/LIS/LCS

总目录 > 4 动态规划 > 4.3 区间 DP / LIS / LCS

前言

本文主要介绍动态规划的其他几种基础模型——区间 DP、LIS、LCS。区间 DP,LIS 和背包 DP 一样都是线性动态规划(背包 DP 虽然也可以是二维数组,但其实质依然是线性的),相比之下,它们的状态转移更为复杂,而非简单的单向线性递推;而 LCS 是二维的,但递推方式比较简单。

子目录列表

1、区间 DP

2、LIS

3、LCS

 

4.3 区间 DP / LIS / LCS

1、区间 DP

[NOI 1995] 在一个环上有 n 个数 a1, a2, ..., an,进行 n - 1 次合并操作,每次操作将相邻的两堆合并成一堆,能获得新的一堆中的石子数量的和的得分。你需要最大化你的得分。

之前在 4.1 动态规划基础 中就有提到做 DP 的最核心步骤,第一步便是划分状态。

我们对这个合并的过程进行倒推:最终状态为进行了 n - 1 次合并,将[1, n] 这 n 堆数合并成了 1 堆。其中最后一次合并为将 [a1, ai] 和 [ai + 1, an] 两堆合并在一起,i 可以为 [1, n - 1] 的任意整数。其中,[a1, ai] 为 [a1, aj] 和 [aj + 1, ai](j 为 [1, i - 1] 的任意整数)合并而成,以此类推,这样我们就将整个问题拆分成若干个子问题了,而每个子问题包含两个变量:左端点 l 和 右端点 r,状态就设计好了,而转移方式也很一目了然。所以,可以得到:

状态数组:f[i][j] 表示 “区间 [i, j] 所有数合并在一起得到的最大的分”;

状态转移:f[i][j] = max(f[i, k] + f[k + 1, j] + sum[j] - sum[i - 1]),k 可以为 [i, j - 1] 的任意整数,sum 表示 a 的前缀和。

之前的推算步骤为倒推,所以在状态转移时应从 i = j 的情况开始处理,即 len = 1,而后从 1 到 n 枚举 len,总时间复杂度为 O(n ^ 3)。

代码:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define MAXN 105
 5 
 6 int n, a[MAXN], sum[MAXN], f[MAXN][MAXN];
 7 
 8 int main() {
 9     cin >> n;
10     for (int i = 1; i <= n; i++) 
11         cin >> a[i], sum[i] = sum[i - 1] + a[i];
12     for (int len = 1; len <= n; len++)
13         for (int i = 1; i <= 2 * n - 1; i++) {
14             int j = i + len - 1;
15             for (int k = i; k < min(j, 2 * n); k++)
16                 f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);
17         }
18     cout << f[1][n];
19     return 0;
20 } 

 

2、LIS 最长上升子序列

> 概念和 O(n ^ 2) 算法

最长上升子序列(Longest Increasing Subsequence),指在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能大。最长递增子序列中的元素在原序列中不一定连续

举个例子,对于数组 a[] = {4, 2, 3, 1, 5},其 LIS 为 {2, 3, 5},长度为 3。

相比之下,LIS 的状态构建和转移比区间 DP 似乎更好理解。由于 LIS 单调递增,所以转移时必然是从较小的数转移而来。比如例子中的 a[3] = 3,可以从 a[2] = 2 转移而来;a[5] = 5,可以从 a[1] = 4, a[2] = 2, a[3] = 3, a[4] = 1 转移而来;那么,直接定义一个一维数组记录当前位置的 LIS 长度即可。

状态数组:f[i] 表示 “序列前 i 个数的 LIS 长度”;

状态转移:f[i] = max(f[j]) + 1,其中 j ∈ [1, i),且 a[j] < a[i]。

时间复杂度为 O(n ^ 2)。

核心代码:

1 for (int i = 1; i <= n; i++)
2     for (int j = 1; j < i; j++)
3         if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1);

最终结果为 f[n] + 1。

不过,这并非最优解。

> 二分优化

由于数列本身没有单调性,不能直接使用二分。令 f[i] 表示 “LIS 长度为 i 时末尾的数”。

还是上方的例子。开始枚举:

① a[1] = 4:i = 1,即 f[1] = 4;

② a[2] = 2:2 < 4,由于对于后面的数而言,2 显然更优(即 > 4 的数是 > 2 的数的子集),所以直接替换,即 f[1] = 2;

③ a[3] = 3:3 > 2,可以构成递增子序列,直接加入,f[2] = 3,说明 LIS 长度为 2 时末尾数为 3;

④ a[4] = 1:1 > 3,不可递增,从头比较:1 > 2,同②,直接替换,f[1] = 1;

⑤ a[5] = 5:5 > 3,同③,直接加入,f[3] = 5.

所以,最终答案为 3。

(越写越觉得这不像 DP。。)

其本质是持续维护一个 LIS,如果当前数比这个 LIS 最末尾数(最大数)更大,直接加入;如果小,则在 LIS 之中找到刚好比它大的一个数替换掉。

感觉其实这不算 DP 了,更像贪心的思路套上 DP 的模板,但还是写在这提供一种思路吧。

暴力做的话同样也是 O(n ^ 2),好就好在 LIS 本身是单调的,所以在 LIS 中查找时可以直接二分,这样就可以做到 O(n log n) 了。

代码:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define MAXN 105
 5 
 6 int n, o, m, f[MAXN], top;
 7 
 8 int main() {
 9     cin >> n;
10     for (int i = 1; i <= n; i++) {
11         cin >> o;
12         if (o > f[top]) top++, f[top] = o;
13         else {
14             int l = 1, r = top;
15             while (l <= r) {
16                 m = (l + r) >> 1;
17                 if (o < f[m]) {
18                     if (o < f[m - 1]) r = m;
19                     else {
20                         f[m] = o;
21                         break;
22                     }
23                 }
24                 else l = m + 1;
25             }
26         }
27     }
28     cout << top;
29     return 0;
30 } 

 

3、LCS 最长公共子序列

> 概念

最长公共子序列(Longest Common Subsequence),指在两个给定的数值序列中,找到一个两个序列都包含的公共子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能大。最长递增子序列中的元素在原序列中不一定连续

例如:a[] = {1, 2, 3, 3, 5}, b[] = {1, 4, 3, 5, 2},它们的 LCS 为 {1, 3, 5}。

> 二维动态规划

其实几维并不重要,这里强调是二维的,是为了给后面较为复杂的动态规划进行铺垫——DP 并不都是线性的,它可以在任何结构上进行状态转移

延续 LIS 的思路,状态数组包含序列位置和 LCS 长度关系。因为存在两个需要比较的序列,则需要两个变量:

状态数组:f[i][j] 表示 “a 的前 i 位和 b 的前 j 位构成的 LCS 长度”;

状态转移: 

观察上方例子:{1, 3, 5} 中,5 是由 a[5] = b[4] = 5 提供的,而b[5] = 2,不影响结果,也就是说,a.len = 5, b.len = 4 时和 b.len = 5 时结果一致,可得:

f[i][j] = f[i][j - 1],if a[i] != b[j];

同理,也可能碰到数组 a 多出不相等的一位,即f[i][j] = f[i - 1][j],if a[i] != b[j];

综上,在两者中取较大值为较优解,即:

f[i][j] = max(f[i][j - 1], f[i - 1][j]),if a[i] != b[j];

3 是由 a[3] = b[3] = 3 提供的,也就是说,a.len = 3, b.len = 3 时结果为 2;而 a[4] = 3 时结果同样不变;那么最后的 5 是 a 和 b 同时往后枚举一位递推得到的,可得:

f[i][j] = f[i - 1][j - 1], if a[i] = b[j]。

语言描述可能有点抽象,下面给出三个图。

图 1 演示的是第二种情况,即 a 的前 i 位和 b 的前 j 位相等;

图 2 演示的是第一种情况,即 a 的前 i 位和 b 的前 j 位不相等;

图 3 演示的是最终结果,即 ans = f[5][5] = 3

核心代码:

1 for (int i = 1; i <= n; i++)
2     for (int j = 1; j <= m; j++)
3         f[i][j] = a[i] == b[j] ? f[i - 1][j - 1] + 1 : max(f[i - 1][j], f[i][j - 1]);

最终答案显然为 f[n][m]。时间复杂度为 O(n ^ 2)。

posted @ 2020-04-17 00:06  jinkun113  阅读(432)  评论(0编辑  收藏  举报