动态规划详解(第一讲)

第一讲内容来自《算法问题实战策略》【韩】具宗万著

重复子问题与制表

动态规划(DP,dynamic programming)和分治法具有类似的处理方式,都是先把问题分割成若干子问题,然后求出这些子问题的解,再利用这些解得到整个问题的最后答案。
不过,动态规划和分治法在分割子问题的方式上存在不同。DP中,有些子问题的计算结果会用于多个问题的求解过程(如果不理解参考下面的示例)。因此,在对子问题进行一次计算的情况下,重复利用其结果就能提高运算的速度。为了实现这种方法,需将子问题的解预先保存到内存中,保存的方法称为“制表”,能够重复利用两次以上的子问题称为重复子问题(overlapping subproblems)

##典型示例

应用动态规划最有名的示例是二项式系数(binomial coefficient)的计算,二项式系数 ( n r ) \dbinom{n}{r} (rn)表示在n个互不相等的元素中无顺序的挑选出r个元素的方法的总数。二项式系数具有如下递归式:
( n r ) = ( n − 1 r − 1 ) + ( n − 1 r ) \dbinom{n}{r}=\dbinom{n-1}{r-1}+\dbinom{n-1}{r} (rn)=(r1n1)+(rn1)
利用此递归式就可以编写出对给定的n和r返回 ( n r ) \dbinom{n}{r} (rn)结果值的函数bino(n,r),如代码1-1所示:

//代码1-1
int bino(int n,int r){
//递归边界:n=r(选择完所有元素的情况)
//或r=0(没有可选元素的情况)
if(r==0||r==n)return 1;
return bino(n-1,r-1)+bino(n-1,r);

下图表示计算bino(4,2)过程中函数调用的流程。
去重前
值得注意的是,bino(2,1)将会被调用两次,因为计算bino(3,1)和bino(3,2)都需要先调用bino(2,1)。不仅如此,bino(1,0)和bino(1,1)也会调用两次。由此可见,有些子问题会被重复计算多次,并且,重复调用的次数会随着n和r的增大而呈几何级数增长。下表给出了计算bino(n,n/2)而调用的函数次数.
n | 2 | 3|4|5|6|…|18|19|…|24|25
—|–|--|–|--|–|--|–|--|
调用bino()的次数|3|5|11|19|39|…|97239|184755|…|5408311|10400599
那么有没有办法避开那些重复的计算呢?输入值n和r一定时,bino(n,r)的返回值也是一定的,利用该原理就可以去掉重复计算。
首先,定义一个缓存数组,查询有没有相应的结果值。如果有,就返回数组中的值,否则直接计算;计算结果先存储到数组中,然后再返回。这种“先定义保存函数结果值的空间,然后重复使用结果值”的优化方法称为制表(memorization)

	int cache[30][30];//初始化为-1
	int bino2(int n,int r){//我们用把经过优化的函数称为bino2
	//初始部分
	if(r==0||n==r)return 1;
	//如果不是-1,则说明这个值是之前已计算过的结果值,直接返回。
	if(cache[n][r]!=-1)
		return cache[n][r];
	//直接计算并保存到数组中。
	return cache[n][r]=bino2(n-1,r-1)+bino2(n-1,r);

去重后bino2()的调用的情况如下图:

这里写图片描述
##适用制表方法的情况
有些函数的返回值只依赖于输入值,这种特性称为引用透明性(referential transparency);而有的函数不具备这种特性,因为函数的执行不仅依赖于输入值,而且会受到全局变量、输入文件、类的成员变量等诸多因素的影响。当然,制表的方法只适用于具有引用透明性的函数。

##实现制表的范式
范式,即模板。对于一个算法,掌握一种范式是非常好的习惯。不必要求每个人都必须按照下面的范式编写,只是有一种适合自己的固定格式。

//把所有的数组元素初始化为-1
int cache[2500][2500];
//a和b是[0,2500)区间的整数
//返回值总是int类型的非负整数
int someObscureFunction(int a,int b){
//先处理初始化部分
if(...)return...;
//已求解过(a,b),直接返回解过的值
int& ret=cache[a][b];
if(ret!=-1)return ret;
//在此求解
...
return ret;
}
int main(){
//利用memset()初始化cache数组
	memset(cache,255,sizeof(cache));
	//小技巧:用memeset函数想赋值-1的话,写255



##分析制表的时间复杂度
分析这种"分治思想"的算法的复杂度有一个简单的公式:

(子问题的个数)x(解一个子问题时循环语句的执行次数)

利用上述公式计算bino2()的时间复杂度:r的最大值是n,所以计算bino2(n,r)时能够分割的最大子问题个数是O( n 2 n^2 n2)(注意并不严格等于 n 2 n^2 n2)。求解子问题时,因为没有循环语句,所以时间复杂度为O(1),所以bino2()耗费的时间复杂度为O( n 2 n^2 n2)xO(1)=O( n 2 n^2 n2).

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值