一、意义
动态规划(dynamic programming),将一个目标大问题“大事化小,小事化了”,分成很多的子问题,得出子问题的解后得到目标大问题的解。动态规划相当于地狱难度的递推。
二、例题
1. 最长上升子序列
题目描述
对于给定的一个序列 < a 1 , a 2 , ⋯ , a N <a_1, a_2, \cdots, a_N <a1,a2,⋯,aN,我们也可以从中得到一些上升的子序列 < a i 1 , a i 2 , ⋯ , a i K > <a_{i1}, a_{i2}, \cdots, a_{iK}> <ai1,ai2,⋯,aiK>,这里 1 ≤ i 1 < i 2 < … < i K ≤ N 1 \le i1 < i2 < … < iK \le N 1≤i1<i2<…<iK≤N,但必须按照从前到后的顺序。比如,对于序列 < 1 , 7 , 3 , 5 , 9 , 4 , 8 > <1, 7, 3, 5, 9, 4, 8> <1,7,3,5,9,4,8>,我们就会得到一些上升的子序列,如 < 1 , 7 , 9 > , < 3 , 4 , 8 > , < 1 , 3 , 5 , 8 > <1, 7, 9>, <3, 4, 8>, <1, 3, 5, 8> <1,7,9>,<3,4,8>,<1,3,5,8> 等等,而这些子序列中最长的(如子序列 < 1 , 3 , 5 , 8 > <1, 3, 5, 8> <1,3,5,8>),它的长度为 4 4 4,因此该序列的最长上升子序列长度为 4 4 4。输入一个长度为 n n n 的序列,输出该序列最长上升子序列长度。
输入描述
两行,第一行包含一个整数 n n n,第二行包含 n n n 个整数。
输出描述
一行,一个整数,表示该序列最长上升子序列长度。
样例1
输入
7 1 7 3 5 9 4 10
输出
5
提示
1 ≤ n ≤ 1000 1 \le n \le 1000 1≤n≤1000
先来推出状态转移方程(以样例1为例):
a[] | 1 | 7 | 3 | 5 | 9 | 4 | 10 |
---|---|---|---|---|---|---|---|
子序列 | 1 | 1,7 | 1,3 | 1,3,5 | 1,3,5,9 | 1,3,4 | 1,3,5,9,10 |
dp[] | 1 | 2 | 2 | 3 | 4 | 3 | 5 |
在上面的列举中,dp[i]
表示的是以 a[i]
为子序列末尾的长度最大值。
而我们求出 dp[i]
的方法也有些麻烦:
- 向前遍历
a[]
:- 如果满足
a[i]>a[k]
(当前遍历到的数字a[i]
比之前遍历到的数字a[k]
大) - 打擂台求
dp[i]
的最大值
- 如果满足
综合的时间复杂度大约是 O ( n 2 ) O(n^2) O(n2),感觉勉强能过。
综上所述,我们写出如下代码:
#include <iostream>
using namespace std;
int n, maxn;
int a[1005];
int dp[1005];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
for (int k = i-1; k >= 1; k--)
if (a[k] < a[i])
{
dp[i] = max(dp[i], dp[k]+1);
maxn = max(maxn, dp[i]);
}
cout << maxn+1;
return 0;
}
2. 合唱队形(加强版)
题目描述
n n n 位同学站成一排,音乐老师要请其中的 n − k n−k n−k 位同学出列,使得剩下的 k k k 位同学排成合唱队形。
合唱队形是指这样的一种队形:设k位同学从左到右依次编号为 1 , 2 , ⋯ , k 1,2,\cdots,k 1,2,⋯,k,他们的身高分别为 t 1 , t 2 , ⋯ , t k t_1,t_2,\cdots,t_k t1,t2,⋯,tk,则他们的身高满足 t 1 < t 2 < ⋯ < t i − 1 < t i > t i + 1 > ⋯ > t k − 1 > t k t_1<t_2<\cdots<t_{i-1}<t_i>t_{i+1}>\cdots>t_{k-1}>t_k t1<t2<⋯<ti−1<ti>ti+1>⋯>tk−1>tk。题目保证 1 ≤ i ≤ k 1≤i≤k 1≤i≤k。
你的任务是,已知所有n位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入描述
共二行。
第一行是一个整数 n n n,表示同学的总数。
第二行有 n n n 个整数,用空格分隔,第 i i i 个整数 t i t_i ti 是第 i i i 位同学的身高
输出描述
一个整数,最少需要几位同学出列
样例1
输入
8 186 186 150 200 160 130 197 220
输出
4
提示
0 < n ≤ 1 0 5 , 1 ≤ t i ≤ 1 0 6 0<n≤10^5,1≤t_i≤10^6 0<n≤105,1≤ti≤106。
这道题目就是上一道题的加强版。这道题目会有两个 dp[]
数组:
dp1[i]
:以a[i]
为子序列结尾的最长上升子序列dp2[i]
:以a[i]
为子序列结尾的最长下降子序列
那么就有如下代码:
#include <iostream>
using namespace std;
int n, maxn;
int a[100005];
int dp1[100005];
int dp2[100005];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
{
dp1[i] = 1;
for (int j = 1; j < i; j++)
if (a[i] > a[j])
dp1[i] = max(dp1[i], dp1[j]+1);
}
for (int i = n; i >= 1; i--)
{
dp2[i] = 1;
for (int j = n; j > i; j--)
if (a[i] > a[j])
dp2[i] = max(dp2[i], dp2[j]+1);
}
for (int i = 1; i <= n; i++)
maxn = max(maxn, dp1[i]+dp2[i]-1);
cout << n-maxn;
return 0;
}
可是这样多半是超时。那么我们可以用一个数组 b[i]
来存储长度为 i
的情况下最后的一个值。这样,对于第一题的
<
1
,
3
>
<1,3>
<1,3> 和
<
1
,
7
>
<1,7>
<1,7> 就会选择
<
1
,
3
>
<1,3>
<1,3> 了。即:
a[] | 1 | 7 | 3 | 5 | 9 | 4 | 10 |
---|---|---|---|---|---|---|---|
子序列 | 1 | 1,7 | 1,3 | 1,3,5 | 1,3,5,9 | 1,3,4 | 1,3,5,9,10 |
dp[] | 1 | 2 | 2 | 3 | 4 | 3 | 5 |
b[] | 1 | 1,7 | 1,3 | 1,3,5 | 1,3,5,9 | 1,3,4,9 | 1,3,4,9,10 |
所以第一题的代码可以优化为:
#include <iostream>
#include <algorithm>
using namespace std;
int n, maxn;
int len;
int a[1005];
int b[1005];
int dp[1005];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
{
if (a[i] > b[len])
{
len++;
b[len] = a[i];
dp[i] = len;
}
else
{
int pos = lower_bound(b+1, b+len+1, a[i]) - b;
b[pos] = a[i];
dp[i] = pos;
}
}
for (int i = 1; i <= n; i++)
maxn = max(maxn, dp[i]);
cout << maxn;
return 0;
}
作业1
恭喜,题目 2 2 2 优化变成了你的作业(不怀好意地笑)。用
lower_bound()
函数进行优化。
3. 公共子序列
题目描述
现有一个数列 S S S,如果分别是两个已知数列的子序列,且是所有符合此条件序列中最长的,则 S S S 称为已知序列的最长公共子序列。
举个例子,如:有两条随机序列,如 1 3 4 5 5 1\ 3\ 4\ 5\ 5 1 3 4 5 5 和 2 4 5 7 5 6 2\ 4\ 5\ 7\ 5\ 6 2 4 5 7 5 6,则它们的最长公共子序列便是: 4 5 5 4\ 5\ 5 4 5 5。
现给定一个包含 n n n 个整数的整数序列和一个包含 m m m 个整数的整数序列,输出这两个序列的最长公共子序列长度。
输入描述
输入包括三行,第一行包含两个整数 n n n 和 m m m,第二行包含 n n n 个整数,第三行包含 m m m 个整数。
输出描述
输出包括一行,一个整数,表示这两个序列的最长公共子序列长度。
样例1
输入
5 6 1 3 4 5 5 2 4 5 7 5 6
输出
3
提示
1 ≤ n , m ≤ 1000 1\le n,m\le1000 1≤n,m≤1000。
按照题目的描述,我们可以有一个 dp[][]
数组。其中 dp[i][j]
表示当 a
有
i
i
i 个数、b
有
j
j
j 个数的状态下最长的公共子序列长度。根据样例1,则有以下存储:
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 0 | 1 | 1 | 1 | 1 | 1 |
4 | 0 | 1 | 2 | 2 | 2 | 2 |
5 | 0 | 1 | 2 | 3 | 3 | 3 |
所以得出式子:
- a[i] == b[i]
- dp[i][j] = dp[i-1][j-1]
- a[i] != b[i]
- dp[i][j] = max(dp[i-1][j], dp[i][j-1])
上代码:
#include <iostream>
using namespace std;
int n, m;
int a[1005];
int b[1005];
int dp[1005][1005];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= m; i++)
cin >> b[i];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
if (a[i] == b[j])
dp[i][j] = dp[i-1][j-1]+1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
cout << dp[n][m];
return 0;
}
4. 编辑距离
设 A , B A,B A,B 是两个字符串。我们要用最少的字符操作次数,将字符串 A A A 转换为字符串 B B B。这里所说的字符操作共有三种:
- 删除一个字符;
- 插入一个字符;
- 将一个字符改为另一个字符。
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
string a, b;
int dp[2005][2005];
int main()
{
cin >> a >> b;
int lena = a.length();
int lenb = b.length();
a = ' ' + a;
b = ' ' + b;
for (int i = 1; i <= lena; i++)
dp[i][0] = i;
for (int j = 1; j <= lenb; j++)
dp[0][j] = j;
for (int i = 1; i <= lena; i++)
for (int j = 1; j <= lenb; j++)
{
if (a[i] == b[j])
dp[i][j] = min({dp[i-1][j-1], dp[i-1][j]+1, dp[i][j-1]+1});
else
dp[i][j] = min({dp[i-1][j-1]+1, dp[i-1][j]+1, dp[i][j-1]+1});
}
cout << dp[lena][lenb];
return 0;
}