动态规划思想
1.将原问题分解为子问题
- 把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决。
- 子问题的解一旦求出就会被保存,所以每个子问题只需求 解一次。
2.确定状态
- 在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状 态”。一个“状态”对应于一个或多个子问题, 所谓某个“状态”下的“值”,就是这个“状 态”所对应的子问题的解。
- 所有“状态”的集合,构成问题的“状态空间”。“状态空间”的大小,与用动态规划解决问题的时间复杂度直接相关。
- 整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。在数字三角形里每个“状态”只需要经过一次,且在每个状态上作计算所花的时间都是和N无关的常数。
3.确定一些初始状态(边界状态)的值
- 以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。
4.确定状态转移方程
- 定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(递推型)。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。
具体请参考以下文章:
教你彻底学会动态规划——入门篇
教你彻底学会动态规划——进阶篇
例题
数字三角形(POJ1163)
在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99
输入格式:
5 //表示三角形的行数 接下来输入三角形
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
解法1:递归法
缺点:多次重复计算,浪费大量时间和空间。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<vector<int> > D;
int n;
int MaxSum(int i, int j) {
if (i == n-1) {
return D[i][j];
}
int left = MaxSum(i + 1, j);
int right = MaxSum(i + 1, j + 1);
max_sum[i][j] = max(left, right) + D[i][j];
return max_sum[i][j];
}
int main() {
scanf("%d", &n);
D.resize(n);
int temp;
for (int i = 0; i < n; ++i) {
D[i].resize(i+1);
for (int j = 0; j <= i; ++j) {
scanf("%d", &temp);
D[i][j] = temp;
}
}
printf("%d", MaxSum[0][0]);
system("pause");
return 0;
}
解法2:记忆递归型动态规划
利用中间计算过程,使得每个状态的值只用计算一次。
可能会因递归层数太深导致栈溢出,函数调用带来额外时间开销。总体来说,比递推型慢。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<vector<int> > D;
int n;
vector<vector<int> > max_sum;
int MaxSum(int i, int j) {
if (max_sum[i][j] != -1) {
return max_sum[i][j];
}
if (i == n-1) {
return D[i][j];
}
int left = MaxSum(i + 1, j);
int right = MaxSum(i + 1, j + 1);
max_sum[i][j] = max(left, right) + D[i][j];
return max_sum[i][j];
}
int main() {
scanf("%d", &n);
D.resize(n);
max_sum.resize(n);
int temp;
for (int i = 0; i < n; ++i) {
D[i].resize(i+1);
max_sum[i].resize(i + 1, -1);
for (int j = 0; j <= i; ++j) {
scanf("%d", &temp);
D[i][j] = temp;
if (i == n-1) {
max_sum[i][j] = temp;
}
}
}
printf("%d", MaxSum[0][0]);
system("pause");
return 0;
}
解法3:递推型动态规划
在解法2的基础上,将递归改为递推,减少栈空间。但会计算许多没有用到的点
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<vector<int> > D;
int n;
vector<vector<int> > max_sum;
int main() {
scanf("%d", &n);
D.resize(n);
max_sum.resize(n);
int temp;
for (int i = 0; i < n; ++i) {
D[i].resize(i+1);
max_sum[i].resize(i + 1, -1);//max_sum所有值初始化为-1
for (int j = 0; j <= i; ++j) {
scanf("%d", &temp);
D[i][j] = temp;
if (i == n-1) {
max_sum[i][j] = temp;
}
}
}
for (int i = n - 2; i >= 0; --i) {
for (int j = 0; j <= i; ++j ) {
max_sum[i][j] = max(max_sum[i + 1][j], max_sum[i + 1][j + 1]) + D[i][j];
}
}
printf("%d", max_sum[0][0]);
system("pause");
return 0;
}
解法4:
在解法3的基础上,进一步节省max_sum空间,只用一行存储。但时间复杂度和解法3相同。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<vector<int> > D;
int n;
int main() {
scanf("%d", &n);
D.resize(n);
vector<int> max_sum;
max_sum.resize(n);
int temp;
for (int i = 0; i < n; ++i) {
D[i].resize(i+1);
for (int j = 0; j <= i; ++j) {
scanf("%d", &temp);
D[i][j] = temp;
if (i == n-1) {
max_sum[j] = temp;
}
}
}
for (int i = n - 2; i >= 0; --i) {
for (int j = 0; j <= i; ++j) {
max_sum[j] = max(max_sum[j], max_sum[j + 1]) + D[i][j];
}
}
printf("%d", max_sum[0]);
system("pause");
return 0;
}
最长公共子序列LCS
参考:
常考的经典算法–最长公共子序列(LCS)与最长公共子串(DP)
算法之最长公共子序列问题
Java基于动态规划法实现求最长公共子序列及最长公共子字符串示例
子序列指的是序列中的字母会按顺序出现在原始字符串中,每个字母可以不是连续的。如eo 就是hello 的一个子序列。
公共子序列就是在多个字符串中都出线的子序列。如ho 就是hello与hope的最长公共子序列。
假设有两个字符串
A
=
[
a
1
,
a
2
,
a
3
,
.
.
.
,
a
m
]
,
B
=
[
b
1
,
b
2
,
b
3
,
.
.
.
,
b
n
]
,
m
<
n
A = [a_1,a_2,a_3,...,a_m],B=[b_1,b_2,b_3,...,b_n], m<n
A=[a1,a2,a3,...,am],B=[b1,b2,b3,...,bn],m<n。
C
=
[
c
1
,
c
2
,
c
3
,
.
.
.
.
,
c
k
]
C=[c_1,c_2,c_3,....,c_k]
C=[c1,c2,c3,....,ck]为A和B的最长公共子序列。
如果
a
m
=
b
n
a_m=b_n
am=bn,则
c
k
=
a
m
=
b
n
c_k=a_m=b_n
ck=am=bn,从而可以得出
C
k
−
1
C_{k-1}
Ck−1是
A
m
−
1
,
B
n
−
1
A_{m-1},B_{n-1}
Am−1,Bn−1的最长公共子序列。
如果
a
m
≠
b
n
a_{m} \neq b_{n}
am̸=bn,则
C
C
C是
A
m
A_{m}
Am与
B
n
−
1
B_{n-1}
Bn−1,或
A
m
−
1
A_{m-1}
Am−1与
B
n
B_{n}
Bn的最长公共子序列。
因此,求解最长公共子序列的问题则变成递归求解的两个子问题。但是,上述的递归求解的办法中,重复的子问题多,效率低下。改进的办法——用空间换时间,用数组保存中间状态,方便后面的计算。这就是动态规划(DP)的核心思想了。
用D[m+1][n+1]大小的数组记录A与B的LCS长度,并存储中间结果,可以确定状态转移方程组:
D
[
i
]
[
j
]
=
{
0
i=0 or j=0
D
[
i
−
1
]
[
j
−
1
]
+
1
i
,
j
>
0
,
a
i
=
b
j
m
a
x
(
D
[
i
]
[
j
−
1
]
,
D
[
i
−
1
]
[
j
]
)
i
,
j
>
0
,
a
i
≠
b
j
D[i][j]= \begin{cases} 0& \text{i=0 or j=0}\\ D[i-1][j-1]+1 & i,j>0 ,a_i=b_j \\ max(D[i][j-1], D[i-1][j]) &i,j>0, a_i \neq b_j \end{cases}
D[i][j]=⎩⎪⎨⎪⎧0D[i−1][j−1]+1max(D[i][j−1],D[i−1][j])i=0 or j=0i,j>0,ai=bji,j>0,ai̸=bj
输入描述:
输入包含两行,分别为A,B两个字符串
输出描述:
第一行输出一个整数表示最长公共子序列长度
第二行输出最长公共子序列
输入:
样例一:
dakjdakjks
iaskmapdiwldosf
样例二:
ABCBDA
BDCABA
输出:
样例一:
4
akas
样例二:
4
BCBA
#include<iostream>
#include<vector>
#include<string>
using namespace std;
//该方法可以得到任意位置的最长公共子序列
void getLength(string A, string B, int m, int n, vector<vector<int> > &D, vector<vector<int> > &path) {
D.resize(m + 1);
path.resize(m + 1);
//全部初始化为0
for (int i = 0; i <= m; ++i) {
D[i].resize(n + 1, 0);
path[i].resize(n + 1, 0);
}
//计算长度并记录搜索路径
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (A[i - 1] == B[j - 1]) {
D[i][j] = D[i - 1][j - 1] + 1;
//向左上
path[i][j] = 1;
}
else if (D[i][j - 1] >= D[i - 1][j]) {
D[i][j] = D[i][j - 1];
//向左
path[i][j] = 0;
}
else {
D[i][j] = D[i - 1][j];
//向上
path[i][j] = -1;
}
}
}
}
//该方法可以得到任意位置的最长公共子序列
void getStr(vector<vector<int> > path, string A, int i, int j, string &result) {
if (i ==0 || j ==0) {
return;
}
if(path[i][j] == 1){
//左上
getStr(path, A, i-1, j-1, result);
result = result + A[i - 1];
}
else if (path[i][j] == 0) {
//向左
getStr(path, A, i, j - 1, result);
}
else if (path[i][j] == -1) {
//向上
getStr(path, A, i - 1, j, result);
}
}
int main() {
string A,B;
getline(cin,A);
getline(cin, B);
//记录每个位置公共子序列的长度
vector<vector<int> >D;
//记录搜索路径
vector<vector<int> >path;
int m, n;
m = A.length();
n = B.length();
//计算得到最长公共子序列长度
getLength(A, B, m, n, D, path);
//输出最大长度
cout << D[m][n]<<endl;
//cout << "输出D[][]" << endl;
//for (int i = 0; i <= m; ++i) {
// for (int j = 0; j <= n; ++j) {
// cout << D[i][j]<<" ";
// }
// cout << " " << endl;
//}
//cout << "输出path[][]" << endl;
//for (int i = 0; i <= m; ++i) {
// for (int j = 0; j <= n; ++j) {
// cout << path[i][j] << " ";
// }
// cout << " " << endl;
//}
//回溯字符串,得到子序列
string result = "";
getStr(path, A, m, n, result);
cout << result << endl;
system("pause");
return 0;
}
最长公共子串
子串是要求更严格的一种子序列,要求在母串中连续地出现。如ho是hope与hold的最长公共子串。
因此最长公共子串的状态转移方程也与上述类似,只是更加严格。如下:
D
[
i
]
[
j
]
=
{
0
i=0 or j=0
D
[
i
−
1
]
[
j
−
1
]
+
1
i
,
j
>
0
,
a
i
=
b
j
0
i
,
j
>
0
,
a
i
≠
b
j
D[i][j]= \begin{cases} 0& \text{i=0 or j=0}\\ D[i-1][j-1]+1 & i,j>0 ,a_i=b_j \\ 0 &i,j>0, a_i \neq b_j \end{cases}
D[i][j]=⎩⎪⎨⎪⎧0D[i−1][j−1]+10i=0 or j=0i,j>0,ai=bji,j>0,ai̸=bj
输入描述:
输入包含两行,分别为A,B两个字符串
输出描述:
第一行输出一个整数表示最长公共子串长度
第二行输出最长公共子串,如果有多个子串,请分行输出(重复子串只用输出一次),不考虑输出顺序。
输入:
样例一:
dakjiaskdakjks
iaskmapdiwldosf
样例二:
ABCDBCAABC
BCDABCABCBCA
输出:
样例一:
4
iask
样例二:
3
ABC
BCA
BCD
#include<iostream>
#include<vector>
#include<string>
#include<algorithm>
using namespace std;
//该方法可以得到任意位置的最长公共子串长度
void getLength(string A, string B, int m, int n, vector<vector<int> > &D, int &maxlength) {
maxlength = 0;
D.resize(m + 1);
//全部初始化为0
for (int i = 0; i <= m; ++i) {
D[i].resize(n + 1, 0);
}
//计算长度
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (A[i - 1] == B[j - 1]) {
D[i][j] = D[i - 1][j - 1] + 1;
maxlength = max(maxlength, D[i][j]);
}
else {
D[i][j] = 0;
}
}
}
}
//该方法可以得到从任意位置开始的任意长度公共子串
void getStr(vector<vector<int> > D, string A, int m, int n, int length, vector<string> &result) {
string temp = "";
vector<string>::iterator ret;
for (int i = m; i >= 0; --i) {
for (int j = n; j >= 0; --j) {
if (D[i][j] == length) {
for (int k = 1; k <= length; ++k) {
temp = temp + A[i-1 - length + k];
}
//如果该子串已经存在,则不添加
ret = std::find(result.begin(), result.end(), temp);
if (ret == result.end()) {
result.push_back(temp);
}
temp = "";
}
}
}
}
int main() {
string A,B;
getline(cin,A);
getline(cin, B);
//记录每个位置公共子串的长度
vector<vector<int> >D;
int m, n;
m = A.length();
n = B.length();
int maxlength;
//计算得到最长公共子串长度
getLength(A, B, m, n, D, maxlength);
//输出最大长度
cout << maxlength <<endl;
//cout << "输出D[][]" << endl;
//for (int i = 0; i <= m; ++i) {
// for (int j = 0; j <= n; ++j) {
// cout << D[i][j]<<" ";
// }
// cout << " " << endl;
//}
//回溯字符串,得到最大长度的公共子串
vector<string> result;
getStr(D, A, m, n, maxlength, result);
for (int i = 0; i < result.size(); i++) {
cout << result[i] << endl;
}
system("pause");
return 0;
}