经典动态规划问题总结

14 篇文章 0 订阅
3 篇文章 0 订阅

动态规划引入

首先我们以一个最基本的例子来分析——菲波那切数列。

我们都知道,菲波那切数列的递推公式f(n) = f(n-1)+f(n-2) (这里我就说明一般情况,不列举边界条件了),很简单,如果我们用递归的方法来求解f(n),两三行代码就出来了。那么我们深入分析一下这样有什么问题?

f(2) = f(1) + f(0);

f(3) = f(2) + f(1);

f(4) = f(3) + f(2);

f(5) = f(4) + f(3);

......

计算一个f(5)我们需要计算一个f(4)和一个f(3),而一个f(4)又需要一个f(3)和一个f(2),这其中就有了一个重复的f(3),那么在继续往下推导,会发现有越来越多的重复。当我们在计算机中计算f(40)并输出时,我们会发现已经有相当长时间的延时了,为什么?因为这样的递归重复计算太多了,导致整个算法效率非常低。

由于上述过程存在着大量的重复计算,我们可以用一个数组保存所有已经计算过的项,这样便可以达到用空间换时间的目的,在这种情况下,时间复杂度为O(N),而空间复杂度也为O(N)。事实上,我们所述的这种算法就是利用了动态规划的思想。


动态规划算法的思想与分治法类似,也是通过组合子问题的解而解决整个问题。其基本思路是利用一个表来记录所有已解的子问题的答案,不管该子问题以后是否被用到,只要它被计算过,就将结果填入表中,这样就可以避免重复计算问题。

动态规划算法的设计可以分为如下几个步骤:

1)描述最优解的结构;

2)递归定义最优解的值;

3)按自底向上的方式计算最优解的值;

4)由计算出的结果构造一个最优解;

其中第1~3步构成问题的动态规划解的基础。

适合动态规划方法的最优化问题的两个要素:最优子结构,重叠子问题


例1:求用1*2的瓷砖覆盖2*M的地板有几种方式?

分析:假设所求问题的解为F(M),有下面两种情况:


当第一块瓷砖竖着放的时候,问题转换成求用1*2的瓷砖覆盖剩下的2*(M-1)的方式,即F(M-1)。

当第一块瓷砖横着放的时候,则必有另一块瓷砖横着放在其下面,问题转换成求用1*2的瓷砖覆盖 剩下的2*(M-2)的方式,即F(M-2)。

在求F(M-1)和F(M-2)时,由于第一列地板的覆盖方式已经不同,故F(M-1)种覆盖方式和F(M-2)中覆盖方式没有重叠,故:

F(M) = F(M-1)+F(M-2)

其中,F(1) = 1,F(2) = 2。可见我们能够将问题规模缩小。

仔细看递推式,其实就和菲波那切数列是一样的,既然这样,我们就不要用上面的递归方式进行求解F(M)了,而是用动态规划的方式,建立一个表格,存储每一步骤的F(i).

//...
int a[100];
int func(int M){}
	a[1] = 1;
	a[2] = 2;
	for(int i = 3; i <= M; ++i)
		a[i] = a[i-1] + a[i-2];
	return a[M];
}
//...

例2:LCS(最长公共子序列问题)

注:LCS问题不要求所求得的字符在所给的字符串中是连续的。

分析:假设X={x1,x2,...,xm}和Y={y1,y2,...,yn}的一个最长公共子序列为Z={z1,z2,...,zk},

1)若x(m) = y(n),则必然有z(k) = x(m) = y(n),且Z(k-1)是X(m-1)和Y(n-1)的最长公共子序列;

2)若x(m) != y(n)且z(k) != x(m),则Z是X(m-1)和Y的最长公共子序列;

3)若x(m) != y(n) 且z(k) != y(n),则Z是X和Y(n-1)的最长公共子序列;

也就是说,

当x(m) = y(n)时,LCS(X(m),Y(n)) = LCS(X(m-1),Y(n-1)) + 1;

x(m) != y(n)时,LCS(X(m),Y(n)) = max{LCS(X(m-1),Y(n)), LCS(X(m),Y(n-1))};

若用一个二维表格c来存储LCS,则c[i][j]表示X和Y长度分别为i和j时的LCS,显然,当X或Y为空时,LCS为0.


下面给出动态规划算法的代码:

/* 动态规划:最长公共子序列问题LCS */
const int INF = 99999;
int c[100][100];
int LCS_Memo(string A, string B, int i, int j){
	if(c[i][j] < INF)
		return c[i][j];
	if(i == 0 || j == 0)
		c[i][j] = 0;
	else if(A[i-1] == B[j-1])
		c[i][j] = c[i-1][j-1] + 1;
	else{
		int p = LCS_Memo(A, B, i-1, j);
		int q = LCS_Memo(A, B, i, j-1);
		if(p >= q)
			c[i][j] = p;
		else
			c[i][j] = q;
	}
	return c[i][j];
}

int LCS_Length(string A, string B){
	int m = A.length();
	int n = B.length();
	memset(c, INF, sizeof(c));
	return LCS_Memo(A, B, m, n);
}


下面再给出一种非递归的方法。

//dp[i][j]存放的是长度分别为i、j的字符串A、B的LCS
int LCS(string A, string B, int m, int n){
	int dp[300][300];
	memset(dp, 0, sizeof(dp));
	
	for(int i = 1; i <= m; ++i){
		for(int j = 1; j <= n; ++j){
			if(A[i-1] == B[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[m][n];
}


例3: 01背包问题:一个背包有一定的承重cap,有n件物品,每件都有自己的价值,记录在数组v中,也都有自己的重量,记录在数组w中,每件物品只能选择要装入背包还是不装入背包,要求在不超过背包承重的前提下,选出物品的总价值最大,给定物品的重量w价值v及物品数n和承重cap。请返回最大总价值。

分析:这里以行n,列cap建立二维表格dp[n+1][cap+1],其中dp[i][j]表示重量不超过j时的最大价值。那么这里就有两种情况:

1)选择第i件物品,则前i-1件物品的重量不能超过j-w[i];

2)不选择第i件物品,则前i-1件物品的重量不能超过j;

即dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);

int getMaxValue(vector<int> w, vector<int> v, int n, int cap){
	int dp[n+1][cap+1];
	for(int i = 0; i <= n; ++i)
		dp[i][0] = 0;
	for(int j = 0; j <= cap; ++j)
		dp[0][j] = 0;
	
	for(int i = 1; i <= n; ++i){
		for(int j = 1;j <= cap; ++j){
			if(j >= w[i-1])
				dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]]+v[i-1]);
			else
				dp[i][j] = dp[i-1][j];
		}
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值