动态规划到底是什么鬼?dp数组到底是什么?
Posted 日常学习杂谈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划到底是什么鬼?dp数组到底是什么?相关的知识,希望对你有一定的参考价值。
今天在leetcode上面做了一道很有意思的题目,虽然挂的是动态规划的标签,但是我做完这道题之后却觉得它其实很“不”动态规划。做完之后,拿它与别的题目比较了一下,有了一点个人体会,在这里记录一下,分享一下。
1. Leetcode838 ---《推多米诺骨牌》
一开始,我们当然还是按照以往的思路,找子问题 -> 状态定义 -> 状态转移
首先,让我们看看这道题是如何把大问题拆分成小问题的。其实,这道题这一步个人觉得是整个步骤最繁琐的,如果你能想清楚这个过程,那这道题其实就很简单。
整个题目有一个很重要的地方需要强调:
每过一秒,倒向一边的骨牌会推倒相应一侧相邻的骨牌。时间的限制,导致了这道题整体的复杂性。但我们要想解决一道所谓动态规划的题目,无非就是穷举所有状态,再想办法记录一些已经保存的状态从而达到时间性能上的优化。
你看最左边和最右边都是
'.' ,那我们可以把这两个地方直接去掉吗?其实是不行的,因为你如果去掉最右边的多米诺骨牌,从上帝视角看,倒数第二个骨牌其实是会把最后一个骨牌推倒的,所以不能就这么直接去掉。
其实这道题就是这里很棘手,你不能随意地把某个位置的骨牌给切掉,
因为别的位置的骨牌可能会影响到你当前切掉的这个。这就是所谓的
子问题之间不相互独立。如果你现在返回去看之前我们做过的所有题,你会发现各个子问题之间必然是独立的,也就是说,
各个子问题之间是不可能相互影响的,这又是一个动态规划的大前提。
那这道题究竟如何定义状态,才能使各个子问题之间相互独立呢?
最左边是
R,最右边是
L,这样我们可以很简单地从左右两边同时往中边走,走一步推一步,一直到:
所以我想,能不能在原数组中,把所有这样的情况给提取出来(即
R....L)?你看,
如果是这种情况的话,那么从(R....L)这个区间上无论怎么推怎么倒,就都不会影响到两边了不是吗?这不就把原先的大问题切割出来,变成一个个独立的子问题了吗?
end = dominoes.length
left = 0
for i in range(0, end):
if dominoes[i] == '.':
continue
elif dominoes[i] == 'R':
left = i
elif dominoes[i] == 'L':
push_domino(left, i)
其中,pushDomino() 函数表示我们推倒
(R...L)这个区间上骨牌的过程,在
R的时候开始记录,遇到
'.' 我们就直接跳过,直到遇到了
L
,表示我们找到了一个这样的区间,这个时候就可以开始推了。
当然,并不是只有这种情况,我们还需要把其它可能会出现的问题解释一下:
你可能已经注意到了,遇到
R
的时候,我们会直接把当前区间的最左边更新为当前的位置,那我们如果连续遇到好几个
R
之后才遇到
L
怎么办呢?
很明显,这时候如果不去处理一下第2个
R之前的多米诺骨牌,就会产生错误,因为从上帝视角看,最后第二个和第三个骨牌必然也是倒向右边的。怎么处理呢?其实也很简单,只需要在更新
left之前,把前面的骨牌推一下就可以了,
(R....R)这样一个区间内的骨牌怎么倒,它左边和右边部分的骨牌是影响不了的,这也是一个独立的子问题!
end = dominoes.length
left = 0
for i in range(0, end):
if dominoes[i] == '.':
continue
elif dominoes[i] == 'R':
push_domino(left, i)
left = i
elif dominoes[i] == 'L':
push_domino(left, i)
还有一种情况是,在遇到L之前,我们没有遇到任何一个
R,其实这时候,也跟处理第二个
R的情况一样,只需要把
(....L)这个区间单独处理一下就可以了,在上面的代码已经隐含了这种情况,这里也不再赘述。
到此为止,我们已经把这个循环该处理的都处理了。但是还有最后一个小问题,需要我们注意一下。
如果我们压根就遇不到
L呢?其实也很容易分析:那整个循环就会直接走到完,我们只需要在循环结束之后,重新对当前的
(R....)区间进行一次
pushDomino() 就可以了。最后的伪码是这样的:
end = dominoes.length
left = 0
for i in range(0, end):
if dominoes[i] == '.':
continue
elif dominoes[i] == 'R':
push_domino(left, i)
left = i
elif dominoes[i] == 'L':
push_domino(left, i)push_domino(left,
push_domino(left, end - 1)
到此为止,如何将问题转化为多个独立子问题,我们已经讲清楚了。那接下来要干什么,状态定义?状态转移方程?仔细想想,有必要吗?在我们这种定义方式下,
有可能出现重叠子问题吗?
在转化过程中,我一直都在强调,各个子问题之间是相互独立的,并且我们
只从左向右扫描了一遍数组,这个过程怎么会产生重叠子问题呢?
这也是我说这个问题很"不"动态规划的原因,因为它其实跟一般的动态规划题目很不一样,
我们不需要使用额外的数据结构记忆一些什么,就可以把所有情况无重复的一次性列举出来。虽说你硬要把这个过程叫做动态规划,也不是不行吧,毕竟
Dynamic Programming,按个人理解就是动态编程,将大问题转化为多个子问题来解决,这个过程其实也是动态的。
最后,我们来实现一下
pushDomino的过程。
其实也很简单,回顾上一部分,我们要处理的情况无非也就那几种:
使用双指针的技巧,
i指向头,
j指向尾,不断往中间边走边推,就可以更新完最后的状态了。
public class PushDomino {
private char[] cdominoes;
public String pushDominoes(String dominoes) {
int n = dominoes.length();
cdominoes = dominoes.toCharArray();
int curLeft = 0;
for(int i = 0; i < n; i ++)
if(cdominoes[i] != '.'){
pushDominoes(curLeft, i);
curLeft = (cdominoes[i] == 'R' ? i : i + 1);
}
pushDominoes(curLeft, n - 1);
return String.valueOf(cdominoes);
}
private void pushDominoes(int left, int right){
if(left >= right) return;
if(cdominoes[left] == '.' && cdominoes[right] == '.') return;
if(cdominoes[left] == 'R' && cdominoes[right] == 'L'){
int i = left + 1;
int j = right - 1;
while(i < j){
cdominoes[i ++] = 'R';
cdominoes[j --] = 'L';
}
if(i == j) cdominoes[i] = '.';
}else if(cdominoes[left] == 'R')
for(int i = left; i <= right; i ++)
cdominoes[i] = 'R';
else if(cdominoes[right] == 'L')
for(int i = right; i >= left; i --)
cdominoes[i] = 'L';
}
}
到此为止,整道题目已经讲解完了,但是我下面还想再说一个跟这道题情境很相像的一个算法, 就是大家都非常熟悉的归并排序。
归并排序这个算法, 相信大家都很熟悉,下面我直接给出归并排序分解过程的树,我们来看看归并排序与这道题之间到底有什么相似的地方
归并排序的分解过程,相比我们上面做的这道题,其实容易很多。我们上面还要逐个去分析
(R .... L)的存在性,才能放心地将这个区间从整个区间切出来从而形成独立子问题。
而归并排序不用,直接对半分,对数组两边进行排序,就是两个独立的子问题。这也就是所谓的分治思想。
其实你仔细地回想以前做过的动态规划题目,你会发现无非是在分治的思想基础上,加入一步记忆化罢了。自底向上的分治
(由循环迭代实现),就是我们常说的动态规划。而自顶向上
(由递归实现)的分治,就是所谓的回溯记忆化搜索。
归并排序是单纯的分治,快速排序其实也是分治,只不过快速排序要分解出独立子问题稍微有些麻烦,需要经过一步额外的partition罢了,跟我们这道题类似。所以,我更倾向于称这道《推多米诺骨牌》为一道纯粹分治的题目而不是动态规划。
ps:其实这题也可以用dp数组记录状态做,但是那个做法其实挺难想到的,而且从时间性能上讲,这篇文章给出的解法并不会比那个算法差。有兴趣的朋友可以去题目相应位置看题解,这里也顺便给出我看的一个解释得比较清楚的题解。https://leetcode-cn.com/problems/push-dominoes/solution/xiang-xi-jie-shi-dong-tai-gui-hua-jie-fa-by-ripple/
好了,今天的分享就到这里。希望读者们如果有什么问题或意见,可以直接在下面回复提出来。第一次做公众号,排版方面有一些朋友可能看着不舒服,还请各位多多见谅。内容方面个人绝对是精心考虑过的,如果读者们看完觉得还可以,可以转给身边对算法或者java有兴趣的朋友看看。本公众号虽然是从算法开始,但是之后也打算写一些java方面的文章。希望大家多多包涵。
以上是关于动态规划到底是什么鬼?dp数组到底是什么?的主要内容,如果未能解决你的问题,请参考以下文章
linux下的动态链接库和静态链接库到底是个什么鬼?动态链接库的编译与使用
动态规划答疑篇
动态规划答疑篇
肝!动态规划
Java虚拟机到底是什么鬼,小白看这里!
算法面试课程笔记001 算法面试到底是什么鬼