目錄
引言
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;
}