动态规划法

1.动态规划的递归写法和递推写法

(1)什么是动态规划

动态规划(Dynamic Programming,DP)是一种用来解决一类最优化问题的算法思想,简单来说,动态规划将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个求解过的子问题的解记录下来,这样当下一次碰到同样的子问题是时,就可以直接使用之前记录的结果

(2)动态规划的递归写法

先看看斐波那契数列

int F(int n)
{
	if(n==0 ||n==1)
		return 1;
	else
		return F(n-1)+F(n-2);
}

这是正常的递归算法,可以发现,当n=5
时,F(5) = F(4)+F(3),当n=4时,F(4) = F(3) + F(2) ,可以看到重复计算了两遍F(3),如果n很大,重复计算的次数将难以想象。

为了避免重复计算,可以开一个一维数组dp,用来保存已经计算的结果,这样第二次计算F(3)可以直接使用dp[3]的值

int dp[MAXN];
int F(int n)
{
	if(n==0 ||n==1)
		return 1;
	if(dp[n]!=-1)
		return dp[n];
	else
		dp[n] = F(n-1)+F(n-2)
		return dp[n];
}

在这里插入图片描述
通过上面的例子可以引申出一个概念:如果一个问题可以被分解为若干个子问题。且这些子问题会重复出现,那么称这个问题拥有重叠子问题。动态规划通过记录重叠子问题的解,来使下次碰到相同的子问题时使用之前记录的结果

(2)动态规划的递推写法

以经典的数塔问题为例。数塔问题就是将一些数字排成数塔的形状,其中每一层有一个数字,第二层有两个数字…第n层有n个数字。现在要从第一层走到第n层,每次只能走向下一层连接的两个数字中的一个,问:最后将路径上所有数字相加后得到的和最大是多少?
在这里插入图片描述
如果尝试穷举所有路径,然后记录路径上数字和的最大值,那么时间复杂度回答到O(22),产生这么大复杂度的原因是什么?
一开始,从第一层的5出发,按5->8->7的路径来到7,并枚举从7出发的到达最底层的所有路径。但是,之后当按5->3->7的路线再次来到7,又会去枚举从7出发的到达最底层的所有路径,事实上,可以在第一次枚举从7出发的到达最底层的所有路径,这就导致了从7出发的到达最底层的所有路径反复地被访问, 做了许多多余的运算。

事实上,可以在第一次枚举从7出发的到达最底层的所有路径就把路径上能产生的最大和记录下来,这样再次访问可以直接获取最大值。
我们不妨设dp[i][j]为从第i行第j个数字的最大和。那么dp[1][]1]就是最终想要的答案
通过分析我们可以得到
dp[i][j] = max { dp[i+1][j],d[i+1][j+1] } + f[i][j]
把dp[i][j]称为问题的状态,而把上面的式子称作状态转移方程,可以发现,状态dp[i][j]只与第i+1层的状态有关,而与其它层状态无关。而什么时候到头呢,可以发现数塔的最后一层dp值总是等于元素本身,把这种直接确定结果的部分称为边界,而动态规划的递推写法总是从这些边界出发,通过状态转移方程扩散到整个dp数组

递归是上一层的值由下一层得到(关键在最底层的值),而动态规划是上一层的最优值由下层的最优值得到(关键在每次的比较求最优值)

int DPNumTower(int f[][100] , int n)
{
	int dp[100][100];

	//对边界赋值
	for (int i = 1; i <= n; ++i)
		dp[n][i] = f[n][i];
	for (int i = n - 1; i >= 1; --i)
		for (int j = 1; j <= i; ++j)
			dp[i][j] = max(dp[i+1][j], dp[i+1][j + 1]) + f[i][j];
	return dp[1][1];
}

通过上面的例子再引申出一个概念:如果一个问题的最优解可以由其子问题的最优解有效地构造出来,那么称这个问题拥有最优子结构
一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决。

2.动态规划经典题目

(1)最大连续序列和

最大连续子序列和问题如下:
给定一个数字序列A1,A2,…,An,求i,j(1≤≤i≤j≤n),使Ai+…+Aj最大,输出这个最大和

(1)发现重复子问题:我们的想法应该是从头到尾遍历该数字序列,遍历到i就求从开始到i的序列的最大和,显然遍历到i+1又要将i以前的序列和算出来,所以我们可以得到dp[i]来记录i以前的最大和
(2)得到状态转移方程:dp[i] = max{A[i],dp[i-1]+A[i]}
(3)确定边界:dp[1] = A[1]
(4)确定结果:结果不是dp[n],而是dp中的最大值

int FoundMaxArray(int A[],int n)
{
	int dp[100],maxArray=0;
	//定义边界
	dp[1] = A[1];
	//动态规划
	for (int i = 2; i <= n; ++i)
	{
		dp[i] = max(A[i], A[i] + dp[i - 1]);
		if (dp[i] > maxArray)
			maxArray = dp[i];
	}
	return maxArray;
}

状态的无后效性:当前状态记录了历史信息,一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或若干状态的基础上进行。
对动态规划可解的问题来说,总会有很多设计状态的方式,但并不是所有状态都具有无后效性,因此必须找到无后效性的状态以及相应的状态转移方程。

(2)最长不下降子序列(LIS)

最长不下降子序列问题如下:
在一个数字序列中,找到一个最长的子序列,使得这个子序列是不下降(非递减)的。

(1)发现重复子问题:想法应该是从头开始遍历序列,遍历到第i个序列就应该计算i前面的最大序列长度,而i+1又会计算到第i个序列前面的最大序列的长度,造成重复,所以应该设dp[i]为i前面的最大序列长度
(2)得到状态转移方程:dp[i] = max{1,dp[j]+1} 1<=j<=i-1,A[i] >=A[j]
(3)确定边界:dp[1] = A[1]
(4)确定结果:结果不是dp[n],而是dp中的最大值

int FoundLIS(int A[],int n)
{
	int dp[100],maxIndex=0,maxCount=0,ans=0;
	for (int i = 1; i <= n; ++i)
	{
		//记录边界
		dp[i] = 1;
		for (int j = 1; j < i; ++j)
		{
			if (A[i] >= A[j] && dp[i]<dp[j]+1)
				dp[i] = dp[j] + 1;
		}
		ans = max(ans, dp[i]);
	}
	return ans;
}

(3)最长公共子序列(LCS)

最长公共子序列的问题描述为:
给定两个字符串A和B,求一个字符串,使得这个字符串是A和B的最长公共部分

(1)发现重复子问题:首先这个问题是二维的,必然有i和j,我们不妨看遍历到A[i]和B[j]的时候,它首先会判断A[i]是否等于B[j] ,如果等于,则等于i-1和j-1之前计算过的最长公共子序列加1,如果不等于的话,他是等于i-1和j之前的最大公共子序列和j-1和i之前的最公共子序列两者的最大值,所以我们可以设dp[i][j]为i和j之前的最大公共子序列
(2)得到状态转移方程:
A[i] == B[j] dp[i][j] = dp[i-1][j-1]
A[i] != B[j] dp[i][j] = max{ dp[i-1][j] , dp[i][j-1] }
(3)确定边界
dp[i][0] = dp[0][j] = 0
(4)确定结果: dp[n][m]

int FoundLCS(char* str1,char* str2)
{
	int len1 = strlen(str1 + 1);
	int len2 = strlen(str2 + 1);
	int i, j,dp[100][100],ans=0;
	//确定边界
	for (i = 0; i <= len1; ++i)
		dp[i][0] = 0;
	for (j = 0; j <= len2; ++j)
		dp[0][j] = 0;
	for (i = 1; i <= len1; ++i)
	{
		for (j = 1; j <= len2; ++j)
		{
			if (str1[i] == str2[j])
				dp[i][j] = dp[i - 1][j - 1]+1;
			else
				dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
			ans = max(ans, dp[i][j]);
		}
	}
	return ans;
}

(4)DAG最长路

DAG就是有向无环图
在这里插入图片描述

第一个问题:求整个DAG中的最长路径#

(1)发现重复子问题:假如从点i出发能获得的最长路径长度,那么它就等于i的邻接点j的最长路径长度+edge(i,j)中的最大值。所以可以设dp[i]为从i号顶点出发能获得的最长路径长度
(2)状态转移方程:dp[i] = max{d[j] + edge[i][j]} (i,j)∈E
(3)确定边界:dp[i] = 0
(4)确定结果: max{d[i]}

int DP(int i)
{
	if (dp[i] > 0)  return dp[i];
	for (int j = 0; i < num;++j) {
		if (edge[i][j] != INT_MAX)
		{
			if (DP(j) + edge[i][j] > dp[i])
			{
				dp[i] = DP(j) + edge[i][j];
				//记录i的后继结点为j
				choice[i] = j;
			}
		}
	}
	maxcount = max(dp[i], maxcount);
	return dp[i];
}

void PrintPath(int i)
{
	while (i != -1)
	{
		cout << i << "-->";
		i = choice[i];
	}
}

其中的choice记录了选择的路径信息。
对一半的动态规划问题而言,如果想要得到具体的最优方案,可以采用类似的方法。
即记录每次决策所选择的策略,然后在dp数组计算完毕后根据具体情况进行递归或者迭代来获取方案

第二个问题:固定终点,求DAG的最长路径

基本和第一个问题差不多 ,主要不同的是固定了路径,这样边界就不同了,只需要将dp[dest]= 0,而其余的dp应该设置为大负数,这样的话还应该设置一个额外数组用来记录dp[i]是否已经计算

int DP(int i)
{
	if (vis[i] > 0)  return dp[i];
	for (int j = 0; i < num;++j) {
		if (edge[i][j] != INT_MAX)
		{
			if (DP(j) + edge[i][j] > dp[i])
			{
				dp[i] = DP(j) + edge[i][j];
				//记录i的后继结点为j
				choice[i] = j;
			}
		}
	}
	maxcount = max(dp[i], maxcount);
	return dp[i];
}

void PrintPath(int i)
{
	while (i != -1)
	{
		cout << i << "-->";
		i = choice[i];
	}
}

(5)背包问题

① 01 背包问题

01背包问题是这样的:
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包物品的总价值最大。其中每个物品都只有一件。

(1)发现重复子问题:现在考虑对第i个物品的选择问题,如果不放第i个物品,则最大价值为前i-1个物品装入容量为v的背包中获得的最大价值,如果放第i个物品,则转换成前i-1个物品装入容量为v-w[i]的背包中的最大价值加上c[i],所以设dp[i][v]为第i个物品放入时背包容量为v时的最大价值
(2)状态转移方程:dp[i][v] = max{dp[i-1][v],dp[i-1][v-w[i]]+c[i]}
(3)确定边界: dp[0][v] = 0
(4)确定结果:max{ dp[n][v] }

int bb(int n,int V)
{
	int i, v,ans;
	for (i = 0; i <= V; ++i)
	{
		dp[0][i] = 0;
	}
	for (i = 1; i <= n; ++i)
	{
		for (v = w[i]; v <= V; ++v)
		{
			dp[i][v] = max(dp[i - 1][v], dp[i - 1][v - w[i]] + c[i]);
			ans = max(ans, dp[i][v]);
		}
	}
	return ans;
}

② 完全背包问题

01背包问题是这样的:
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包物品的总价值最大。其中每个物品都有无穷件。

与0/1背包的唯一区别就是每个物品可以无限取,所以唯一改变的应该是dp[i][v],当第i件物品选择取的话
dp[i][v] = max{dp[i-1][v],dp[i][v-w[i]]+c[i]}

```cpp
int bb(int n,int V)
{
	int i, v,ans;
	for (i = 0; i <= V; ++i)
	{
		dp[0][i] = 0;
	}
	for (i = 1; i <= n; ++i)
	{
		for (v = w[i]; v <= V; ++v)
		{
			dp[i][v] = max(dp[i - 1][v], dp[i][v - w[i]] + c[i]);
			ans = max(ans, dp[i][v]);
		}
	}
	return ans;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值