線性動態規劃:最長公共子*問題和實例——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;
}

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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