动态规划

       自己搭的服务器炸了,把写的几篇博客再挪过来。

      这几天在看动态规划,算法基础不是很好,颇费脑力,也走了不少弯路。学习过程是按照《动态规划:从新手到专家》这篇文章的思路来的。本文以几个简单的demo来描述我所认识的动态规划,如有不妥之处,欢迎各位coder斧正。

      首先我们需要强调的是,所有动态规划的核心,都是围绕着两个点展开的:一个或者多个初始状态(临界值),状态转移方程。

      我个人认为,动态规划算法中的初始状态,类似于我们在递归的时候的临界值,因为这二者在求解问题是都是一种,通过其他过程结果来得到自身结果的过程,必须有一个条件来退出或者开始这一过程,对于递归,是临界值,而对于动态规划,是初始状态。

      例子:

      三种硬币,面值:1,3,5。求其组成某一面值以及比该面值小的面值需要的最少个数。

     比如 

0元,0个;

1元,1个1;

2元,1个1+1个1;

3元,1个3;

4元,1个3+1个1

       以此类推。

      设  凑够i元  需要 d(i)个硬币

       初始状态。

       0元,0个,这是一个很特殊的值,他不与任何面值有关,肯定是一个初始状态了(也就是在代码中写死)。1元,1个1,他是第一个与面值交互的的点,其后的2元,1个1+1个1已经开始调用1元,1个1的结果了。

  

       初始状态,可多,不可少(个人理解),在此题中,我们需要的最少的初始状态也就是0元,0个;1元,1个1;即可。

        d(0) = 0;

        d(1) = d(1-1)+1 = 1;



       状态转移方程

       在此题中(面值1,3,5)

       当你需要凑够2元时  没有 面值2元的硬币(比2大的不用考虑),只能1元1元的来凑,所以d(2)= d(2-1)+1=2

       (+1表示用1个1元来凑,硬币数+1,   2-1表示,已经使用了1个1元,面值-1,所以,该方程的含义就是:d(2)相当于,拿1个1来凑 再加上  d(1)所需要的硬币数量。)比较绕口,没理清可以先向下看。


       当你需要凑够3元时,恰好有3元面值的硬币。所以,此时你面临了两种选择:

            选择1:

                   类似于 d(2) = d(1)  + 1;

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

                          

           选择2:

                   d(3)   =  d(3-3) +1  = d(0) + 1


           当你需要凑够10元时,这时的选择就有两个:

              

             选择1:

                   类似于 d(2) = d(1)  + 1;

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

                              d(10) = d(9) +1;

                          

             选择2:

                         d(10) = d(10-5)+1 = d(5) +1;(这块有点类似贪心算法了-.-,用两个五来凑10 肯定比  3*3+1要少)

                    

           tips:

                     1.   几乎所有的值,都可以理解为他前面的值+1,比如 9 = 8+1,  10 =9+1,这就是为什么我们把1设为初始状态的原因;

                      2.   对于恰好有对应面值硬币的值,此题中是1,3,5. 我们可以理解为1 = 1+0 ,3 = 3+0, 5 = 5 +0,这就是为什么 我们把0设为初始状态的原因;

 

    例子举完,我们来总结一下:

              d(i)=min{ d(i-vj)+1 },其中i-vj>=0,vj表示第j个硬币的面值

           (是不是有点突兀?是不是毫无头绪?正常,我花了好长时间才理解了这个式子,但还是没想出来怎么把它解释清楚......)


talk is cheap,show me the code:

  private int[] dp(int v,int[]c){
        
       int[] results = new int[v];
       int[] preResults = new int[v];
        	 
        	
          	 for (int i = 0; i < v; i++) {
  				for (int j = 0; j < c.length; j++) {
  					 if(i==0){
         			                preResults[i]=0;
 					}else if(i==1){
 						preResults[i]=1;
 					}else if(i-c[j]>=0){
 						 results[i] = preResults[i-c[j]]+1;
 						 
 					}
 							     											
  			        }
  				preResults = results;        	 
                  }         	 
          	return results;
       }

这是我测试0-12元的结果

在这个代码中,我用两个数组,preResults 和 results。 r记录了 i 元所需的最小硬币数量,而p则记录了 i 元 在当前硬币面值,之前的 i-c[j]元的  最小硬币数量。 比如 i = 10 , c[j] = 3,  p[i-c[j]] = r[7]  而 r[10] = r[7]+1, 当然这一结果会被 c[j] = 5所产生的结果替换掉。

   该方法,两个辅助数组,空间复杂度为o(n),两层for循环 ,时间复杂度o(n*n)


入门

     当然了,刚刚那个,只是一个带大家了解DP的例子,现在正式入门。

      LIS问题

      一个序列有N个数:A[1],A[2],…,A[N],求出最长非降子序列的长度。 (讲DP基本都会讲到的一个问题LIS:longest increasing subsequence)

        

如果我们要求的这N个数的序列是:

5,3,4,8,6,7

根据上面找到的状态,我们可以得到:(下文的最长非降子序列都用LIS表示)

前1个数的LIS长度d(1)=1(序列:5)

前2个数的LIS长度d(2)=1(序列:3;3前面没有比3小的)

前3个数的LIS长度d(3)=2(序列:3,4;4前面有个比它小的3,所以d(3)=d(2)+1)

前4个数的LIS长度d(4)=3(序列:3,4,8;8前面比它小的有3个数,所以 d(4)=max{d(1),d(2),d(3)}+1=3)

前5个数的LIS长度d(5)=3(序列:3,4,6;6前面比它小的有3个数,所以 d(5)=max{d(1),d(2),d(3)}+1=3)

前6个数的LIS长度d(6)=4(序列:3,4,6,7;7前面比它小的有4个数,所以 d(6)=max{d(1),d(2),d(3),d(5)}+1=4)

OK,分析到这,我觉得状态转移方程已经很明显了,如果我们已经求出了d(1)到d(i-1), 那么d(i)可以用下面的状态转移方程得到:

d(i) = max{1, d(j)+1}

用大白话解释就是,想要求d(i),就把i前面的各个子序列中, 最后一个数不大于A[i]的序列长度加1,然后取出最大的长度即为d(i)。 当然了,有可能i前面的各个子序列中最后一个数都大于A[i],那么d(i)=1, 即它自身成为一个长度为1的子序列。


talk is cheap ,show me the code


public int[] dp(int[] source){
		
		int[] results = new int[source.length];
		
		for (int i = 0; i < source.length; i++) {
			if(i==0){
				results[i] = 1;
			}else if(source[i]>=source[i-1]){
				results[i] += results[i-1]+1;
			}else{
				int j = i;
				//这个while循环用于寻找 当前值 之前的 第一个比他小的值。
				while(j>0){
					j--;
			        if (source[j]<source[i])break;
				}
				//临界点判断,因为前一个while循环 遇到j==0也会退出,可能导致source[i]比source[0]要小
				if(j ==0){
					if(source[i]>=source[j]){
						results[i] = results[j]+1;
					}else{
						results[i] = 1;
					}
				}else{
					results[i] = results[j]+1;
				}
				
			}
		}
		return results;
	}

5,3,4,8,6,7的运行结果:


由于《动态规划:从新手到专家》这篇文章后面的例子条件不足,只能用伪码描述,所以在本文的后面,我将转而用我们最熟知的背包问题来描述动态规划。


01背包问题。

        注:因为问题描述都差不多,我也就没必要在自己写一遍。原文:http://www.cnblogs.com/rhythmic/p/5285437.html

      代码肯定自己写-.-,不能懒得太过分。



      在这个问题中,对于每个物体,只允许存在两个状态:不装入背包或者仅仅装入1个,从这里其实也能够理解01背包这个名称的由来,因为物体只对应这0或1这2种状态。
 

  下面我们开始分解决策过程以寻求各阶段之间的关系并得到状态转移方程。我们从决策的过程中开始分析,假设我们当前正在选择第i个物品,那么我们决策的结果有如下两种情况。


  ①将第i个物品装入背包当中。


  ②不将第i个物品装入背包当中。


  针对①情况,我们必须基于前i-1件物品装入背包当中的方案是最优的,如果我们用数组dp[i]表示装入背包i件物品最大的价值,这就找到了前后两个阶段之间的关系,那么针对①情况,dp[i] = dp[i - 1] + w[i]。


  针对情况②,显然有dp[i] = dp[i - 1]。


  那么dp[i],到底应给等于多少呢?我们要找的是价值最大的方案嘛,显然dp[i] = max(dp[i-1] , dp[i-1] + v[i])。


  可能有人会问了,max(dp[i-1] , dp[i-1] + w[i])显然等于dp[i - 1] + w[i]啊,那这个方程还有什么意义?


  别急,我们还有另外一个重要因素——重量,没有考虑。我们设j为当前方案下的背包的容量,那么此时dp[i][j]就表示把i件物体装入容量为j的空间中可以得到的最大价值数。


  针对①,有dp[i][j] = dp[i][j-w[i]] + v[i],这里相当于在大的背包(容量为j)里面套了一个小的背包(容量为j - w[i]),这样一来,分阶段之间的重量关系也建立起来了。 同样。


  针对②,有dp[i][j] = dp[i-1][j]。


  综合来看,即dp[i][j] = max(dp[i-1][j] ,  dp[i][j-w[i]] + v[i])。有个该状态转移方程,我们就可以从第一件物品开始依次选择,最终得到最优方案。


  我们通过一个具体的题目体会一下这个模型的应用。(Problem source : hdu 2602)


 


Problem Description
Many years ago , in Teddy’s hometown there was a man who was called “Bone Collector”. This man like to collect varies of bones , such as dog’s , cow’s , also he went to the grave … The bone collector had a big bag with a volume of V ,and along his trip of collecting there are a lot of bones , obviously , different bone has different value and different volume, now given the each bone’s value along his trip , can you calculate out the maximum of the total value the bone collector can get ?
 
 背包容量、骨头数量、每个骨头的价值自行决定。

 
  题目大意:标准的01背包问题。

  编程实现:基于我们讨论的状态转移方程,我们首先枚举这n个物体,这与上文中我们分析问题的过程是相呼应的。假设枚举到第i个物体,我们要抉择选还是不选,此时我们再枚举此时的背包空间,并利用状态转移方程记录下当前最优的方案,这里其实也是上文中我们提到的构造“小背包”的过程

 copy 完毕。

整个01背包问题的求解,其实就是在填一张d[i][j]表,然后在该表里面求找最优解。i 代表物品数量,j代表背包容量

talk is cheap ,show me the code


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值