线性动态规划:最长公共子*问题和实例——1559 最大子矩阵

目录

引言

两个动规特性

实现步骤

 

最长公共子串

问题描述

状态转移方程

最长公共子序列

问题描述

状态转移方程

最大子段和

问题描述

状态转移方程

输出所有最大子串

问题描述

示例

输出所有最大子序列

问题描述

示例

最大子矩阵(方阵)的大小

问题描述

示例

1559 最大子矩阵

问题描述

状态转移方程


 

引言

https://www.zhihu.com/question/23995189感觉说的非常全面,下面是我的个人理解

 

两个动规特性

  • 最优子结构:问题能写成状态转移方程的形式,从而可以翻译成等价的递归函数,虽然这个函数往往不能实际使用(因为复杂度太高)
  • 重叠子问题:递归树中含有许多重复的结点

 

实现步骤

  • 关键是找到合适的描述问题的状态写出状态转移方程。对于线性动规一般从最后一个状态往前推,从而得到状态转移方程;
  • 建立动态规划表dp(我认为用vector比较方便),根据状态转移方程,以根据状态转移方程填dp表为目的写出函数;
  • 函数的返回值就是所求结果!以什么作为函数的返回值呢?对于线性动规,一般要么是dp表的最后一格,要么是填表过程中通过查找得出的指定格。

 


 

最长公共子串

 

问题描述

  • 即Longest Common Substring,子串需要在原字符串中是连续的

 

状态转移方程

  • C[i, j] = |LCS(x[1...i],y[1...j])|,,即C[i, j]表示序列X[1...i]和Y[1...j]的最长公共子串的长度,则可得到式(1)的状态转移方程,

C[i, j] = \left\{\begin{matrix} C[i-1,j-1]+1 & \ \ if\ x[i]=y[j] & \\ 0 & \ \ if\ x[i]\neq y[j] & \end{matrix}\right.{\color{Red} }       (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] = |LCS(x[1...i],y[1...j])|,,即C[i, j]表示序列X[1...i]和Y[1...j]的最长公共子序列的长度,则可得到式(2)的状态转移方程,

\dpi{120} C[i, j] = \left\{\begin{matrix} C[i-1,j-1]+1 & \ \ if\ x[i]=y[j] & \\ max(c[i-1,j],c[i,j-1]) & \ \ if\ x[i]\neq y[j] & \end{matrix}\right.{\color{Red} }       (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]的最大子段和,则有

    C[i] = max(0,\ C[i - 1]+a[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种情况:
  1. dp[i - 1][j] > dp[i][j - 1],这时往i方向走,即i--
  2. dp[i - 1][j] < dp[i][j - 1],这时需要往j方向走,即j--;
  3. 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子矩阵就是\begin{bmatrix} 1 &1 \\ 1 &1 \end{bmatrix}(**),(**)的边长就是2;

\begin{bmatrix} 0 &0 &1 & 1 &0 \\ 1& 0 &1 &1 &1 \\ 0 & 0 & 0 & 0 &0 \\ 0 & 0 & 0 & 0 &0 \\ 0 & 0 & 0 & 0 &0 \end{bmatrix}(*)

 

不妨先一行行的看,拿第二行的向量[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的子矩阵,使子矩阵中所有元素的和最大。

 

状态转移方程

C[i,j] = C[i,j] + C[i - 1, j] + C[i, j - 1] - C[i - 1,j - 1] (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;
}

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章