动态规划思想与常见问题解决

动态规划思想

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 &lt; n A = [a_1,a_2,a_3,...,a_m],B=[b_1,b_2,b_3,...,b_n], m&lt;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} Ck1 A m − 1 , B n − 1 A_{m-1},B_{n-1} Am1,Bn1的最长公共子序列。
如果 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} Bn1,或 A m − 1 A_{m-1} Am1 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 &gt; 0 , a i = b j m a x ( D [ i ] [ j − 1 ] , D [ i − 1 ] [ j ] ) i , j &gt; 0 , a i ≠ b j D[i][j]= \begin{cases} 0&amp; \text{i=0 or j=0}\\ D[i-1][j-1]+1 &amp; i,j&gt;0 ,a_i=b_j \\ max(D[i][j-1], D[i-1][j]) &amp;i,j&gt;0, a_i \neq b_j \end{cases} D[i][j]=0D[i1][j1]+1max(D[i][j1],D[i1][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 &gt; 0 , a i = b j 0 i , j &gt; 0 , a i ≠ b j D[i][j]= \begin{cases} 0&amp; \text{i=0 or j=0}\\ D[i-1][j-1]+1 &amp; i,j&gt;0 ,a_i=b_j \\ 0 &amp;i,j&gt;0, a_i \neq b_j \end{cases} D[i][j]=0D[i1][j1]+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;
}

递增子序列

最长递增公共子序列

最长递增公共子串

组合可能性问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值