浅谈动态规划问题的优化

动态规划(Dynamic programming)是一种常见而具备很大开发潜力的算法,若要追奔朔源,所谓的动态规划其实就是“借助表格”来记录过去的“状态”,以减小探索未来状态过程中不必要的重复求解步骤(实际上programming在这里就是指表格,而不是编程...).至于该在什么时候用动态规划,这里推荐LeetCode 123. 买卖股票的最佳时机 III 的题解 “一个通用方法团灭 6 道股票问题” ,其中很明确地阐述了动态规划的使用方法。最为重要的部分莫过于“状态、选择和转移”。因本篇侧重于叙述动态规划的优化问题,关于何时应当使用动态规划不多做阐述,简单来说,其中经验性的部分也占据很大比重---“看到一个问题,感觉能够使用动态规划”此类,比如往往在字符串问题上,动态规划的效能要好于深度优先搜索,等等。

 

优化方法

为了比较具体地叙述动态规划的优化步骤,这里以实际的题目为例。题源为LeetCode 97. 交错字符串

题目描述如下:

给定三个字符串 s1s2s3, 验证 s3 是否是由 s1 和 s2 交错组成的。

比如输入 s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac" ,输出为true;

再比如输入 s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc",输出为false;

如上文所言,遇到字符串问题首先想一想能否用动态规划解决?是否还有其他方法?

本人曾使用过dfs来解决,但无法通过全部测试用例(99/101),而在处理字符串问题时,如果dfs行不通,那摆在眼前的选项也很明确了----动态规划。

 

经过一番思考,我们大概很快能确定这个问题的“转移方程”:

dp[j][k][i] = ( dp[j-1][k][i-1]  && s1[j-1] == s3[i-1]  ) || ( dp[j][k-1][i-1]  && s2[k-1] == s3[i-1] )

其中,j表示s1的字串长度,k表示s2的字串长度,i表示s3的字串长度。所以dp[j][k][i]的含义是,s1的长度为j的字串和s2的长度为k的字串能否交错拼接成s3的长度为i的字串。

随后,我们可以得到原始的代码:

提交后通过,但是时间消耗60ms,仅仅击败15%的测试用例;空间消耗15.5Mb,仅仅击败5%的测试用例。Wooops,好像结果不太理想呀。看来可以优化的空间还很大!

 

第一步优化:通过约束关系降低空间复杂度

让我们重新理解一下题意。如果s1和s2要拼接成s3,那么对于字符串s1和字符串s2而言,其长度j + k一定等于i!

所以我们可以得到约束关系式:k = i - j;这样,原本要用三维表示的dp数组可以降低为2维,因为k所在的维度实际是“没有必要”的,因为k的取值仅仅与j有关!所以我们可以直接省略k的维度,这样得到了如下代码:

好了,现在我们成功让dp空间从三维降低为二维,提交后,时间消耗8ms击败90%用户,空间消耗8.7Mb击败50%的用户。呜,看上去优化得不错?是否还有进一步优化的空间呢?

 

第二步优化:从“时序性”入手

所谓的时序,也就是“状态所处的时间点”,让我们重新审视状态转移方程:

dp[j][k][i] = ( dp[j-1][k][i-1]  && s1[j-1] == s3[i-1]  ) || ( dp[j][k-1][i-1]  && s2[k-1] == s3[i-1] )

其中,我们让i不断递增,因此,我们可以把i视为“时序”。也就是说,i表示的维度现在成为了“时序维度”,但我们是否真的需要添加这个“时序维度”呢?

假设dp[j][k][i]表示的是在i时刻的状态dp[j][k],我们可以看到,当前状态仅仅与上一时刻的状态(dp[j-1][k][i-1]、dp[j][k-1][i-1])有关,既然我们的i是递增的,也就是说,当时刻行进到i时,如果我们还没有更新dp[j][k]的状态,那么,这时候的dp[j][k]不就是上一时刻的dp[j][k]了吗?乍一看好像很容易做到?

等等,注意一下,如果我们让j也是顺序递增的,当要更新dp[j][k][i]时,我们其实已经更新过了dp[j-1][k][i],那么这时候的dp[j-1][k]就不再是“上一时刻的”dp[j-1][k]了。

那如果采用倒序呢?

如果j采取倒序遍历,我们可以看到,dp[j][k][i]应当是在dp[j-1][k][i]之前更新的,也就是说,当我们需要在i时刻更新dp[j][k]的时候,dp[j-1][k]存取的还是上一时刻的值(也就是dp[j-1][k][i-1]),到这里是不是豁然开朗了呢?

在第一步优化的基础上,如果我们对时序性进行优化,可以得到转移方程为:

dp[j] = (dp[j] && s1[j-1] == s3[i-1]) || (dp[j-1] && s2[k-1] == s3[i-1])

最终的优化代码如下:

提交结果,时间消耗8m击败90%用户,空间消耗8.3Mb击败89%的用户,大功告成!

 

 

总结

一般来说,动态规划问题上,涉及空间复杂度的优化比较常见,我们不妨通过约束关系与时序性这两个角度切入,看看当前的动态规划空间是否存在降维的可能。尤其是时序性优化,这是一种巧妙的优化方式,仅仅改变遍历顺序就能做到空间优化是不是很神奇?本质上这其实利用了状态时序的性质,像类似的时序性优化,01背包问题也是个非常典型的案例。实际上,很多时候当我们看见当前状态仅仅与上一时刻有关时,便可以从时序性角度入手,优化当前的动态规划空间。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值