五大算法之动态规划法

能采用动态规划求解的问题一般具有以下3个性质:
(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,则称该问题具有最优子结构,即满足最优化原理。
(2)无后效性:某状态一旦确定,不受该状态以后决策的影响。即某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(这个性质是动态规划节省时间复杂度的原因)

动态规划求解的基本步骤

动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

     初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

(1) 划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题无法求解。

(2) 确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

(3) 确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

(4) 寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

(1)分析最优解的性质,并刻画其结构特征。
(2)递归定义最优解
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算优值时得到的信息,构造最优解

动态规划最重要的两个概念:状态和状态转移方程。

1、01背包问题

问题描述:
假设有n件物品,分别编号为P1, P2, …Pn。其中编号为i 的物品价值为Vi,重量为Wi;假设有一个背包,能够承载的重量是C。现在,希望往背包里装这些物品,使得包里装的物品价值最大化,那么该如何选择装的物品?

动态规划解决问题:
选择n个元素中的若干个来形成最优解,假定为k个。那么对于这k个元素P1, P2, …Pk来说,它们组成的物品组合必然满足总重量<=背包重量限制,而且它们的价值是最大的。假定Pk是我们按照前面顺序放入的最后一个物品,它的重量为Wk,它的价值为Vk。既然前面选择的这k个元素构成了最优选择,如果把这个Pk物品拿走,对应于剩下的k-1个物品来说,它们所涵盖的重量范围为0~(C-Wk)。假定最终的价值是V,则剩下的物品所构成的价值为V-Vk。那么这剩下的k-1个元素是不是构成了一个这种C-Wk的最优解呢?

可以用反证法来推导。假定拿走Pk这个物品后,剩下的k-1 个物品没有构成C-Wk重量范围内的最佳价值选择。那么肯定有另外k-1个元素,他们在C-wk重量范围内构成的价值更大。如果这样的话,我们用这k-1个物品再加上第k个,他们构成的最终C重量范围内的价值就是最优的。这就和我们前面假设的k个元素构成最佳矛盾了。所以可以肯定,在这k个元素里拿掉最后那个元素,前面剩下的元素依然构成一个最优解。

动态规划算法解01背包问题的时间复杂度、空间复杂度都是O(n*C)
贪心算法解解01背包问题的时间复杂度是O(nlogn)

分析:一件一件的考虑是否加入背包,假设value[k][m] 表示前k 件物品,在不超过重量m 的情况下达到的最大价值,枚举一下第k 件物品的情况:

情况1:如果选择了第k 件物品,则前k-1件物品的重量不能超过 m-Wk。

情况2:如果没选择第k 件物品,则前k-1件物品的重量不超过m。

所以value[k][m] 可能等于value[k-1][m],也就是不取第k 件物品,价值和之前一样;也可能是value[k-1][m-Wk]+V[k],也就是拿第k 件物品的时候,当然会获得第k件物品的价值。两种可能的选择中,应该选择符合条件的价值较大的那个,即:

value[k][m] = max{ value[k-1][m], value[k-1][m-Wk]+Vk }

考虑初始情况value[0][m] 表示一个物品都不选择,此时不管m 是多少,价值一定为0;同理value[i][0] 表示总重量限制为0,价值也一定为0。综上,初始条件为:value[0][m] = value[i][0] = 0

因此,对于value 矩阵来说,行数是物品的数量k,列数是k个物品的重量m,根据以上初始条件和递归关系(状态转移方程)从左到右,从上到下,依次计算出value 即可。

示例:物品个数n = 5,物品重量W[n] = {0, 2, 2, 6, 5, 4},物品价值V[n] = {0, 6, 3, 5, 4, 6},C = 10。

代码:

#define max(a,b) ((a)>(b)?a:b)   //获取a,b的最大值

int n=5;                         //物品个数
int C = 10;                      //背包承重
int W[6] = {0, 2, 2, 6, 5, 4};   //物品重量
int V[6] = {0, 6, 3, 5, 4, 6};   //物品价值
// dp矩阵(二维动态规划表),记录前k个物品在重量不超过m 的条件下所能达到的最大价值,初始化为0。
int value[6][11] = {0};   
  
for(k=1; k<6; k++)   //枚举物品
	for(M=0; M<=C; M++)    //枚举重量
	{
		//判断枚举的重量和当前选择物品的重量关系
		if( M>=W[k] )  //如果重量大于等于当前物品重量,判断是否选择当前物品
			value[k][M] = max( value[k-1][M-W[k]]+V[k], value[k-1][M] );
		else    
			value[k][M] = value[k-1][M];
		printf("value[%d][%d]: %d \n", k, M, value[k][M]);
	}
 

k 表示物品个数,m 表示k个物品的重量,dp 矩阵如下:
在这里插入图片描述

最优解:选择的3个物品重量为2,2,4。
注意:以上dp矩阵的解中只有最后一列是最优解。即value[4][6],value[3][4]均不是相应子问题的最优解。

2、最大子段问题

问题描述:
给定两个字符串str1和str2,返回两个字符串的最长公共子序列,例如:str1=“1A2C3D4B56”,str2=“B1D23CA45B6A”,"123456"和"12C4B6"都是最长公共子序列,返回哪一个都行。

分析:假设str1的长度为M,str2的长度为N,则生成M*N的二维数组dp,dp[i][j]的含义是str1[0…i]与str2[0…j]的最长公共子序列的长度。

dp值的求法如下:

dp[i][j]的值必然和dp[i-1][j],dp[i][j-1],dp[i-1][j-1]相关,结合下面的代码来看,我们实际上是从第1行和第1列开始计算的,而把第0行和第0列都初始化为0,这是为了后面的取最大值在代码实现上的方便,dp[i][j]取三者之间的最大值。

int findLCS(string A, int n, string B, int m) {
	// n表示字符串A的长度,m表示字符串B的长度
    int dp[500][500] = {0};    //dp矩阵初始化为0
    for (int i = 0; i < n; i++)
    	for (int j = 0; j<m; j++)
       {
       		if (A[i]==B[j])
       			dp[i+1][j+1] = dp[i][j]+1;
           else
               dp[i+1][j+1] = max(dp[i+1][j],dp[i][j+1]);
        }
     return dp[n][m];
}

3、走台阶问题

问题描述:有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。

分析:动态规划的实现的关键在于能不能准确合理的用动态规划表来抽象出 实际问题。在这个问题上,我们用f(n)表示走上n级台阶的方法数。

当n为1时,f(n) = 1;n为2时,f(n) =2。就是说当台阶只有一级的时候,方法数是一种;台阶有两级的时候,方法数为2。那么当我们要走上n级台阶,必然是从n-1级台阶迈一步或者是从n-2级台阶迈两步,所以到达n级台阶的方法数必然是到达n-1级台阶的方法数加上到达n-2级台阶的方法数之和,即f(n) = f(n-1)+f(n-2)。我们用dp[n]来表示动态规划表,dp[i], i>0,i<=n, 表示到达i 级台阶的方法数。

裴波那契数列:f(1)=1, f(2)=1, 当n>=3时:f(n)=f(n-1)+f(n-2)。

/*dp是全局数组,大小为n,全部初始化为0,是题目中的动态规划表*/
dp[n] = {0};
int fun(int n){
	if (n==1||n==2)
		return n;
	/*判断n-1的状态有没有被计算过*/
	if (!dp[n-1])
		dp[n-1] = fun(n-1);
	if(!dp[n-2])
		dp[n-2]=fun(n-2);
	return dp[n-1]+dp[n-2];
}

4、走方格问题

问题描述:
给定一个矩阵M,从左上角开始每次只能向右走或向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的M如下,那么路径1,3,1,0,6,1,0 就是最小路径和,返回12.

1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0

分析:对于这个题目,假设M是m行n列的矩阵,那么我们用dp[m][n]来抽象这个问题,dp[i][j]表示的是从原点到[i, j]位置的最短路径和。首先计算第一行和第一列,直接累加即可,那么对于其他位置,要么是从它左边的位置达到,要么是从上边的位置达到,我们取左边和上边的较小值,然后加上当前的路径值,就是达到当前点的最短路径。然后从左到右,从上到下依次计算即可。

#include <iostream>
#include <algorithm>   //min函数
// #define min(a,b) ((a<b)?a:b)
int dp[4][4] = {0};    //dp矩阵初始化为0
int main(){
	int arr[4][4] = {1,3,5,9,8,1,3,4,5,0,6,1,8,8,4,0};
	for (int i = 0; i < 4;i++)
		for (int j = 0; j<4; j++)
		{
				if ( i==0 && j==0 )
					dp[i][j] = arr[i][j];
				else if ( i==0 && j!=0 )
					dp[i][j] = arr[i][j] + dp[i][j-1];
				else if( i!=0 && j==0 )
					dp[i][j] = arr[i][j] + dp[i-1][j];
				else
					dp[i][j] = arr[i][j] + min(dp[i-1][j],dp[i][j-1]);
		 }
}

在这里插入图片描述

5、最长公共子序列数

问题描述:
给定数组arr,返回arr的最长递增子序列的长度,比如arr=[2,1,5,3,6,4,8,9,7],最长递增子序列为[1,3,4,8,9],返回其长度为5。

分析:
首先生成dp[n]的数组,dp[i]表示以arr[i]这个数结束的情况下产生的最大递增子序列的长度。对于第一个数来说,很明显dp[0]为1,计算dp[i]的时候,我们去考察i 位置之前的所有位置,找到i 位置之前的最大的dp值,记为dpj,dp[j]代表以arr[j]结尾的最长递增序列,而dp[j]又是之前计算过的最大的那个值,我们在来判断arr[i]是否大于arr[j],如果大于,则dp[i]=dp[j]+1。计算完dp之后,我们找出dp中的最大值,即为这个串的最长递增序列。

伪代码:

#include <stdio.h>
/*动态规划表初始化为0*/
int dp[9] = {0};    
int main(){
	int arr[9] = {2, 1, 5, 3, 6, 4, 8, 9, 7};   
	dp[0] = 1;   
	for (int i = 1; i<9; i++)
	{
		j = max_index(dp, i-1);   //数组dp[0...i-1] 中最大值的下标
		if( arr[i] > arr[j] )   
			dp[i] = dp[j] + 1;
		else
			dp[i] = dp[j];
	}
	
	int maxlist=0;
	maxlist = dp[8];  //最长公共子序列的长度等于dp数列的最后一个值
}

输出结果:
i=1, max_index=0 dp[1]=1
i=2, max_index=0 dp[2]=2
i=3, max_index=2 dp[3]=2
i=4, max_index=2 dp[4]=3
i=5, max_index=4 dp[5]=3
i=6, max_index=4 dp[6]=4
i=7, max_index=6 dp[7]=5
i=8, max_index=7 dp[8]=5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值