事情发生在今天这个夜黑风高的夜晚。
我遇到一道题目
62. Unique Paths
A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).
The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).
How many possible unique paths are there?
走方格,只能向下和向右。
首先,一看就是有数学规律的,于是我开始上演一部大戏。(m,n)= x 代表m行,n列,x种走法。
(4,1)=1(4,2)=4(4,3)=10(4,4)=20(4,5)=35(4,6)=56(4,7)=84
强迫症的我似乎找到了一点点规律。
1,4,10,20,35,56,84 一种平方式增长。
(3,1)=1(3,2)=3(3,3)=6(3,4)=10(3,5)=15(3,6)=21
1,3,6,10,15,21 一种线性增长。
当m=2时那就是非零常数式增长,
当m=1时就是常数。
逆推得到当m值越大随n的增长方式为 x^(m-1)式。
而且总结归纳出,高层次的相邻值相加结果等于低成次的结果。
即 a(m,n)-a(m,n-1)=a(m-1,n)
从而得到 a(m,n)=a(m-1,n)+a(m,n-1);
有了递推公式我就开始想装逼了!!!!
谁都不要拦我!!
直接递归搞定,
然而,事与愿违。时间超时,超时!!
是的啊,这个和斐波那契数列一样啊,使用递归的时候函数调用会成指数式增长,而且这个肯定比斐波那契更厉害。
好吧,那就想斐波那契那样从前往后加吧。
从哪开始加呢???无从下手啊!!
再推倒看看
a(m,n)-a(m,n-1)=a(m-1,n)
这样能写出 a(m,n-1)-a(m,n-2)=a(m-1,n-1),
我靠这样写过之后就可以推出来什么了
a(m,n)-a(m,n-1)=a(m-1,n)
a(m,n-1)-a(m,n-2)=a(m-1,n-1)
a(m,n-2)-a(m,n-3)=a(m-1,n-2)
a(m,n-3)-a(m,n-4)=a(m-1,n-3)
...
a(m,2)-a(m,1)=a(m-1,2)
相加之后得到:
a(m,n)=a(m-1,n)+a(m-1,n-1)+a(m-1,n-2)+a(m-1,n-3)......... a(m-1,2)+a(m-1,1)
好像很有用了,等等,好像陷入一个超级爆炸的递归中了。右边的a(m-1,n)等还能分身很多,每个分身又能分身,最后会变成一个明确的数,但是分身太爆炸了,m分成n个m-1,每个又能分成n-x个m-2;同时n能分成m个n-1,每个又能分成m-x个n-2。双变量的指数增长!!大学里的高等数学没有学过这种。。。。
好吧不解数学了,怎么写成代码呢?
首先必要的a(m,n)=a(m-1,n)+a(m,n-1)实质是不变的,
在代码中直接 return a(m-1,n)+a(m,n-1);但是这样会带来爆炸式的函数调用开销。
其实类似斐波那契数列一样,递归的时候计算了太多已经计算过的内容了。
我们如果能够在第一次计算某个数时保存值,这样下次再需要的时候就不需要计算了,直接提取就好了。
if( 已经计算过){ 从某个地方拿出来}
这样就ok了啊,这样计算值最多就是m*n个,剩下的就是每当需要的时候拿出来相加就可以了。
肯定避免不了一个m*n的二维数组,我们记作dp[m][n],
数组有了,怎么放进去呢?
寻找初始值,无论是任何(m,1)、(1,m),都是有一种可能,一直走。就是d[x][1]=1,d[1][x] =1;
其他的数都是这两加上去的。
如下,最简单的递归,就是加了dp的递归!!当我们已经有了值时直接return值,不用再调用函数了!!!
int helper(int m, int n, vector< vector<int> > &dp){
if(m==1 || n==1){
return 1;
}
if(dp[m][n]==0){
dp[m][n]= helper(m-1, n, dp) + helper(m, n-1, dp);
}
return dp[m][n];
}
在主函数中调用
int uniquePaths(int m, int n) {
vector< vector<int> > dp(m+1, vector<int> (n+1, 0) );
return helper(m, n, dp);
}
就这样超时变成accept!!!成功解决了问题!
在这整个的函数推导中,我们试图得到一个直接答案,却陷入一个爆炸增长危机中,我们每一步去分解问题总是出现很多很多小的子问题,这些小问题存在量多、重复的问题。
为了解决大量的重复问题,我们使用数组对每个子问题存储值和分辨是否为重复计算。解决问题!
能使用动态规划的问题:
可由子问题构成的,能拆解成子问题,且子问题总体上在缩小问题,最终能拆成一个可预知的值。
这样我们使用递归+数组就解决了问题。
然而这不是彻底的解决问题的办法,递归和动态规划并不是一种类型。
重点!!递归是由大分小,动态规划是由小构成大。
我们由一个最小子问题得到一个中型子问题,再慢慢构成一个大问题,最后达到目标。这就是动态规划!,每向前走的一步(小问题)都可能对将来的步伐(大问题)带来解决方法,动态的自我走向答案。
解决代码,简洁优雅!
class Solution {
public:
int uniquePaths(int m, int n) {
vector< vector<int> > dp(m, vector<int> (n, 1) );
for(int i=1; i<m; i++)
for(int j=1; j<n; j++)
dp[i][j] = dp[i][j-1] + dp[i-1][j];
return dp[m-1][n-1];
}
};
虽然是个比较简单的问题,但是对理解动态规划有很大的帮助,其他的动态规划都是万变不离其中。
在这之前我都是一个迷糊的状态,说清楚也不太清楚,说不明白也知道解决动态问题的方法。通过自己对公式的推导(虽然没推导出来),熟悉了问题分成子问题的过程。正是由于分成子问题后数量过大无法解决大量子问题,促使我的解决问题的方法由“化大为小”转化成了“积小成大”。
一切算法的学习皆该如此,深入的剖析,返璞归真,从外看到里,再从里看到外。