动态规划典型例题解析

什么是动态规划

动态规划的主要思想是把问题划分为一个个子状态,一个状态的最优解往往是基于其前一个状态的最优解。两个状态之间的关系,我们就称之为状态转移方程。这里引出了状态和状态转移方程的概念:状态是一个当前的值,这个值是通过前一个值以及状态转移方程推得的。在解决动态规划问题的时候,我们往往会把问题建模为一个一维数组或是二维数组,处理完边界值之后,就可以通过前一个状态和后一个状态的递推关系循环解出轴上的一个个状态值。如果说贪心算法是为了达到目的追求局部最优,简单粗暴,那么动态规划就是一种相对严密,统筹全局的算法。

动态规划的特点

1、空间换时间:动态规划把最终问题的求解分解为一个个子结构,我们可以称之为最优子结构,在求解出最终结果前,我们把之前的每一个状态的最优解存储在了数组中,后面的状态的求解基于之前的结果,这样可以节省时间,但需要一些存储空间来放这些值,这就是空间换时间。
2、题目提供的往往是一些具有一定属性的对象,然后用他们去达成某种总体最优的目标。
3、动态规划问题的求解数组可以是一维的也可以是二维的。
4、重叠子结构:在求解一个大问题的过程中需要多次求解某个小问题,而小问题的解我们已经得到,直接取出来使用即可。
5、无后效性:某个状态的求解只与它之前的状态有关,而与它后面的状态没有关系。

动态规划解题步骤

1、确定状态:首先找到题目中变化的量,和求解目标对应的变量,一般我们可以基于这些量构建一个一维或者二维数组。
2、确定递推方向:分析题目要求,确定求解数组状态的循环初始位置和递推方向。
3、处理数组边缘值:定义数组后,往往我们会把边缘值置0。若目标值为0,对应的状态的解自然也是0,边界的0的部分需要我们主动加上去,大家也要格外注意一下dp下标和数组下标因为引入0导致的偏移。
4、在循环中求解数组:即通过循环,去递推一个个子状态的值。这个时候需要注意状态转移方程,状态之间的关系变化。
5、选出需要的最优解。

动态规划题型1:钱币选择问题

问题描述:给你若干面值为1,3,5,数量不限的钱币,给一个目标值,需要用最少的钱币数量凑齐目标值。
解题思路:使用动态规划,先确定凑0元需要0个钱币,凑1元至少需要一个1元钱币;凑2元至少两个1元钱币;凑3元时选择一个3元硬币比选择3个1元硬币更优,故选择一个3元硬币;凑四元的时候我们选择用到前面的结果,只要再加上一元硬币即可,当然在选择这个方案前我们比对过(4-1)元、(4-3)元、(4-5)元的几种情况。由于之前的计算结果都已经最优,所以我们只要考虑最后一张钱币拿的是一元、三元、还是五元即可。这样一来,我们可以递推得到任意状态的解。
算法实现:

	/**
	 * 动态规划1:钱币选择问题
	 */
	public void dp1()
	{
		int[] money={1,3,5,7};
		int sum=24;
		int result = DP1(money,sum);
		System.out.println(result);
	}
	/**
	 * 动态规划1:钱币选择问题
	 * @return
	 */
	private int DP1(int[] money,int sum)
	{
       int[] number = new int[sum+1];
       for(int m =0;m<number.length;m++)//初始化一个较大的值
       {
    	   number[m]=m;
       }
       
       for(int i=1;i<=sum;i++) //循环获取不同面值对应的最小数量
       {
    	   for(int j=0;j<money.length;j++) //循环拿可能情况进行比对
    	   {
    		   if(i>=money[j]&&number[i-money[j]]+1<number[i]) //满足更小条件则赋值
    		       number[i]=number[i-money[j]]+1;  //如果发现有更小的情况就更新数值		
    	   }
       }
       return number[sum];
	}

动态规划题型2:求三角形路径数字最大和(自顶至底)

题目描述:给你一个类似二叉树的结构,每个节点都有相应的值,现求自顶至底的路径的最大数字和。
解题思路:其实这题自顶至底或者自低至顶都是可以的,我们这里选择前者来求解。我们只要把数据存入一个二维数组,然后从顶端往下依次推得到达每个节点的最大数字和。
算法实现:

	/**
	 * 动态规划2:求三角形路径数字最大和(自顶至底)
	 */
	public void dp2()
	{
		int[][] a = new int[5][5];
		Scanner sc = new Scanner(System.in);
		System.out.println("请输入数组的每一个数:");
		for(int i=0;i<5;i++)
		{
			for(int j=0;j<i+1;j++)
			{
				a[i][j]=sc.nextInt();
			}
		}
		int sum = DP2(a);
		System.out.println("最大和为:"+sum);
	}
	/**
	 * 动态规划2:求三角形路径最大数字和(自顶至底)
	 * @param a
	 * @param i
	 * @param j
	 * @return
	 */
	private int DP2(int[][] a) {
		int[][] sum = new int[a.length][a.length];
		sum[0][0]=a[0][0];
		int max=0;
		for(int i=1;i<a.length;i++)
		{
			for(int j=0;j<i+1;j++)
			{
				if(j>0&&j<i&&sum[i-1][j-1]+a[i][j]>sum[i-1][j]+a[i][j])
					sum[i][j]=sum[i-1][j-1]+a[i][j];
				else if(j>0&&j<i&&sum[i-1][j-1]+a[i][j]<=sum[i-1][j]+a[i][j])
					sum[i][j]=sum[i-1][j]+a[i][j];
				else if(j==0)
					sum[i][j]=sum[i-1][j]+a[i][j];
				else if(j==i)
					sum[i][j]=sum[i-1][j-1]+a[i][j];
				System.out.println(i+","+j+"最大和:"+sum[i][j]);
				if(i==a.length-1&&max<sum[i][j])
					max=sum[i][j];
			}
		}		
		return max;
	}

动态规划题型3:求最长非降子序列长度

题目描述:求某序列的最长非降子序列长度。
解题思路:设数组上每个值为一条非降子序列的末端,这条子序列的长度就是数组的状态。每个状态都与前一个状态有关。
算法实现:

    /**
     * 动态规划3:求最长非降子序列长度
     */
	public void dp3()
	{
		int[] a= {5,3,4,3,9,11,7};
		int length = DP3(a);
		System.out.println(length);
	}
	/**
	 * 动态规划3:求最长非降子序列长度(LCS)
	 * @param a
	 * @return
	 */
	public int DP3(int[] a)
	{
		int length=1;
		int[] len = new int[a.length];
		len[0]=1;
		for(int i=1;i<a.length;i++)
		{
			if(a[i]>a[i-1])
				len[i]=len[i-1]+1;
			else
				len[i]=1;
			if(length<len[i])
		       length=len[i];
		}						
		return length;
	}

动态规划题型4:求最长公共子序列

题目描述:给定两个序列,求它们的最长公共子序列,子序列不要求连续(这里的子序列可以理解为删除序列上任意节点后余下的部分组成的序列)
解题思路:这题也可以用动态规划来解,这里涉及到两条序列,我们使用二维数组,一个角标来表示第一个序列的某字符位置,另一个角标来表示另一个序列的某字符位置。状态值的表示即两个角标左侧的两个序列段的最长公共子序列长度。角标左移后得到的是当前状态的前一个状态,比如当前a[i]=b[j],那么如果令i和j都减一,那么最长公共子序列长度减一;若a[i]!=b[j],且数组a的角标左移后最长子序列会减小,那么不能移动这个角标,而应该移动j,若两段序列还有相同部分,那么a[i]还会再次等于b[j]。因此,在这个题型里我们依然可以找到子状态之间的递推关系。
算法实现:

    /**
     * 动态规划3:求最长非降子序列长度
     */
	public void dp3()
	{
		int[] a= {5,3,4,3,9,11,7};
		int length = DP3(a);
		System.out.println(length);
	}
	/**
	 * 动态规划3:求最长非降子序列长度(LCS)
	 * @param a
	 * @return
	 */
	public int DP3(int[] a)
	{
		int length=1;
		int[] len = new int[a.length];
		len[0]=1;
		for(int i=1;i<a.length;i++)
		{
			if(a[i]>a[i-1])
				len[i]=len[i-1]+1;
			else
				len[i]=1;
			if(length<len[i])
		       length=len[i];
		}						
		return length;
	}

动态规划题型5: 01背包问题

题目描述:给定若干物品,它们有一定的重量和价值,以及一个有一定容量上限的背包,物品每种类型只有一个且在装包时只可以选择装或者不装,不能装一部分。求背包所能容纳的最大价值。
解题思路:因为是物品只能选择装或者不装,那么就不适合采用贪心算法了,此题我们用动态规划来解。首先我们来找我们需要求解的状态,这里涉及到两个变化的量,一个是选择物品的种类,一个是背包的容量(背包的容量类似于我们之前求凑钱币的题的那个总金额)。这题和例题1的区别是这题背包可能会有一些空间剩余,而例题一中必须要凑齐指定金额。在这题中,我们要求出在不同背包容量下的最大价值,在某一定的容量下,还需要去考虑不同物品的选择。假定我们只选择物品A,然后求出在不同背包容量下的最大价值,这显然都是相同的。这时我们再加上物品B,再去推导不同背包容量下的最大价值,这是受到物品A的影响的,我们要选择是否放物品B。以此类推,我们可以得到在选择任意物品和任意背包容量下的最大价值。所以这题我们选择二维数组,表示不同种类物品数量的选择和背包的容量。
算法实现:

	/**
	 * 动态规划5:01背包问题(物品有重量和价值,且只有一个,去放置到定容的包中)
	 */
	public void dp5()
	{
		int[] w = {3,4,5}; //重量
	    int[] v = {4,5,6}; //价值
	    int m = 8; //背包最大总容量
	    int n = 3;  //物品种类数量
	    int value = DP5(w,v,m,n);
	    System.out.println(value);
	}
	/**
	 * 
	 * 动态规划5:01背包问题
	 * @param w:物品重量
	 * @param v:物品价值
	 * @param m:背包容量
	 * @param n: 物品种类数量
	 */
	public int DP5(int[] w , int[] v , int m ,int n)
	{
		int max=0;
		int[][] dp = new int[n+1][m+1];
		for(int i = 0 ;i<n+1;i++)
		    dp[i][0]=0;
		for(int i = 0 ;i<m+1;i++)
			dp[0][i]=0;		
		for(int i = 1;i<n+1;i++)   //以放的物品数量为尺度
		{
			for(int j=1;j<m+1;j++) 
			{
				if(w[i-1]>j) //不放
					dp[i][j]=dp[i-1][j]; //总价值和之前的情况相同,少了这个物品,用之前的物品去填充这个包
				else //放,继续分类
				{
					if(dp[i-1][j-w[i-1]]+v[i-1]>dp[i-1][j])//假设这个物品已经放了,然后推其总价值和不放这个物品的最优解比较,选大的
						dp[i][j]=dp[i-1][j-w[i-1]]+v[i-1]; 
					else
						dp[i][j]=dp[i-1][j];
				}
				if(dp[i][j]>max) //筛选最大值
				{
					max=dp[i][j];
				}
			}
		}
		return max;		
	}

猜你喜欢

转载自blog.csdn.net/mayifan_blog/article/details/85120512