目录
引言
https://www.zhihu.com/question/23995189感觉说的非常全面,下面是我的个人理解
两个动规特性
- 最优子结构:问题能写成状态转移方程的形式,从而可以翻译成等价的递归函数,虽然这个函数往往不能实际使用(因为复杂度太高)
- 重叠子问题:递归树中含有许多重复的结点
实现步骤
- 关键是找到合适的描述问题的状态写出状态转移方程。对于线性动规一般从最后一个状态往前推,从而得到状态转移方程;
- 建立动态规划表dp(我认为用vector比较方便),根据状态转移方程,以根据状态转移方程填dp表为目的写出函数;
- 函数的返回值就是所求结果!以什么作为函数的返回值呢?对于线性动规,一般要么是dp表的最后一格,要么是填表过程中通过查找得出的指定格。
最长公共子串
问题描述
- 即Longest Common Substring,子串需要在原字符串中是连续的
状态转移方程
- 设 ,即C[i, j]表示序列X[1...i]和Y[1...j]的最长公共子串的长度,则可得到式(1)的状态转移方程,
(1)
若定义
#define MAX(x,y) ((x)>(y)?(x):(y))
则由(1)得
int LongestCommomSubstring(string x, string y, vector<vector<int> >& dp) {
int maxlen = 0;
for (unsigned int i = 1; i <= x.length(); i++) {
for (unsigned int j = 1; j <= y.length(); j++) {
if (x[i - 1] == y[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
maxlen = MAX(maxlen, dp[i][j]);
}
else
dp[i][j] = 0;
}
}
return maxlen;
}
最长公共子序列
问题描述
- 与最长公共子串相比,最长公共子序列(Longest Common Subsequence)只需保持相对位置而不需要是连续的
状态转移方程
- 设 ,即C[i, j]表示序列X[1...i]和Y[1...j]的最长公共子序列的长度,则可得到式(2)的状态转移方程,
(2)
由(2)得
int LongestCommomSubsequence(string x, string y, vector<vector<int> >& dp) {
for (unsigned int i = 1; i <= x.length(); i++) {
for (unsigned int j = 1; j <= y.length(); j++) {
if (x[i - 1] == y[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = MAX(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[x.length()][y.length()];
}
最大子段和
问题描述
- 给出一序列a,选出其中连续且非空的一段使得这段和最大,求这个和。
状态转移方程
- 设c[i]表示a[0...i]的最大子段和,则有
(3)
由(3)得
int MaxSubSeqSum(int a[], int size) {
int tmp = 0, sum = 0;
for (int i = 0; i < size; i++) {
if (tmp > 0)
tmp += a[i];
else
tmp = a[i];
if (tmp > sum) // 又是查找与打表过程放一起了
sum = tmp;
}
return sum;
}
输出所有最大子串
另一个技术:回溯。 需要分析dp表包含的状态信息,从而逆向得出结果。
问题描述
- 因为最大子串往往有多个,现在要输出所有的最大子串
- 比如设得出的最大子串长度为len_of_lcs,之前为了计算len_of_lcs判断条件是x[i] == y[j],现在需要反过来,判断一下dp[i][j] == len_of_lcs,如果true即说明这个位置的x[i - 1]==y[j - 1](why ?), 然后把x[i - 1](或y[j - 1])这个字符填进结果字符串
- 这样得出的最大字串(之一)是逆序的,注意要字符串反转一下。之后,便可加入结果集。
示例
/*******************************************************************************
*可以看到函数有5个参数:上一步求出的最大子串长度len_of_lcs,字符串x和y,动态规划表dp*
*以及最大子串结果集lcs_str_all。函数体为回溯的过程:若dp表某个格的值等于lcs_str_all*
*说明这里的x[i-1]等于y[j-1],又因为字串是连续的,所以沿着对角线往左上角走都是子串, *
* 从而写出一个while循环,把字符都填入结果字符串;填完以后,放入结果集 *
******************************************************************************/
void GetAllLCString(int len_of_lcs, string x, string y, vector<vector<int> > dp, set<string>& lcs_str_all) {
string str;
for (unsigned int i = 1; i <= x.length(); i++) {
for (unsigned int j = 1; j <= y.length(); j++) {
if (dp[i][j] == len_of_lcs) {
int dx = i, dy = j;
while (dp[dx][dy] > 0) {
str.push_back(x[dx - 1]);
dx--;
dy--;
}
string tmp(str.rbegin(), str.rend());
if (tmp.length() == len_of_lcs) {
lcs_str_all.insert(tmp);
str.clear();
}
}
}
}
}
输出所有最大子序列
问题描述
- 与输出所有最大子串类似,输出所有最大子序列;
- 分两种情况考虑:x[i - 1] == y[j - 1]。当然也可以用上个问题里的那个判断条件。
- 不管怎么样,需要用到递归。Why ? 当x[i - 1] != y[j - 1] 时,回溯的路线有3种情况:
- dp[i - 1][j] > dp[i][j - 1],这时往i方向走,即i--;
- dp[i - 1][j] < dp[i][j - 1],这时需要往j方向走,即j--;
- dp[i - 1][j] == dp[i][j - 1],这时需要分叉,因为两条路线都有可能找到符合题意的解,从而需要递归。
示例
void GetAllLCSequence(int i, int j, int len_of_lcs, string x, string y, string str, vector<vector<int> > dp, set<string>& lcs_str_all) {
while (i > 0 && j > 0) {
if (x[i - 1] == y[j - 1]) {
str.push_back(x[i - 1]);
i--;
j--;
}
else {
if (dp[i - 1][j] > dp[i][j - 1])
i--;
else if (dp[i - 1][j] < dp[i][j - 1])
j--;
else {
GetAllLCSequence(i - 1, j, len_of_lcs, x, y, str, dp, lcs_str_all);
GetAllLCSequence(i, j - 1, len_of_lcs, x, y, str, dp, lcs_str_all);
return;
}
}
}
string tmp(str.rbegin(), str.rend());
if (tmp.length() == len_of_lcs)
lcs_str_all.insert(tmp);
}
最大子矩阵(方阵)的大小
AKA面积最大的全1子矩阵
问题描述
在一个M * N的矩阵中,所有的元素只有0和1,从这个矩阵中找出一个面积最大的全1子矩阵,所谓最大是指元素1的个数最多。
示例
- 比如这个矩阵(*),面积最大的全1子矩阵就是(**),(**)的边长就是2;
(*)
不妨先一行行的看,拿第二行的向量[1, 0, 1, 1, 1]来说,对这个向量的五个分量,如果是1,就往同一列的上面看有多少连续的1,然后记录在数组histgram[]里。所以这轮迭代后histgram[] = { 1, 0, 2, 2, 1};
void GenHistgram(char mat[101][101], int histgram[], int i, int size) {
for (int k = 0; k < size; k++) {
if (mat[i][k] == '0')
continue;
for (int j = i; j >= 0; j--) {
if (mat[j][k] != mat[i][k])
break;
histgram[k]++;
}
}
}
求histgram[]的最大值:如果找到一个一般意义下的最大值m,看一下往右的连续子串长度len是不是大于等于最大值m(为了确保子矩阵的宽要大于等于高);若为真,则m即为一个全1子矩阵的边长(不一定是最终的面积最大的全1子矩阵的边长)。以这种DeformedMax方法查找出histgram[]的最大值m1。
int DeformedMax(int arr[], int size) {
int max = -1, tmpmax = -1;
for (int i = 0; i < size; i++) {
if (arr[i] > tmpmax)
tmpmax = arr[i];
for (int j = i; j < i + tmpmax; j++) {
if (arr[j] < arr[i]) {
tmpmax = -1;
break;
}
}
if (tmpmax != -1)
max = tmpmax > max ? tmpmax : max;
}
return max;
}
同样的方法处理下一个向量[0, 0, 0, 0, 0],查找出histgram[]的最大值m2;
最后从mi里查找出最大值M即为面积最大的全1子矩阵的边长。
int main() {
/* 维度 */
int m, n;
while (cin >> m >> n) {
/* Initialize */
char mat[101][101];
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
cin >> mat[i][j];
int max = -1;
/* 从第二行的向量开始 */
for (int i = 1; i < n; i++) {
/* 每个向量打一次表 */
/* histgram[]初始化 */
int histgram[500] = { -1 };
for (int j = 0; j < n; j++)
histgram[j] = 0;
/* 打表 */
GenHistgram(mat, histgram, i, n);
/* 求当前向量下的最大值 */
int tmpmax = DeformedMax(histgram, n);
/* 求全局最大值 */
max = tmpmax > max ? tmpmax : max;
}
cout << max << endl;
}
return 0;
}
1559 最大子矩阵
问题描述
- 给你一个m×n的整数矩阵,在上面找一个x×y的子矩阵,使子矩阵中所有元素的和最大。
状态转移方程
(4)
式(4)的原理就类似于图像积分,C[i, j]代表之前矩形里所有元素的和。
/* m、n、x、y同题目意义 */
int MaxSubMatrixSum(int m, int n, int x, int y) {
vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));
int ans = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
cin >> dp[i][j];
dp[i][j] += dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1];
if (i >= x && j >= y)
ans = MAX(ans, dp[i][j] - dp[i - x][j] - dp[i][j - y] + dp[i - x][j - y]);
}
}
return ans;
}