数据结构与算法自学系列之动态规划(一)
在刷了几次算法题都发现了动态规划的影子,一时之间竟无从下手,遂去网上查各种资料,希望能够慢慢弄懂这个算法思想。
首先从最简单的台阶问题来看,有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法?
从题目来看,以小白的想法,每次走台阶都有两种选择,如果用step(i)表示每一步走的步数,那么n = step(1) + step(2) + ... + step(i),step(i)可能值为1和2,首先考虑极值情况,全走1,需要走n次;如果n为偶数,全走2,需要走n/2次,所以i的范围在n/2到n之间,然而对n比较小的情况下,举个例子,n=5,i的可能情况就有8种,显然n越大值成几何倍数增加,这种情况,小白几乎就陷入瓶颈了,难受。
动态规划就是为了应对复杂状态情况下出现的算法,从名字就可以看出,为了应对动态的情况,对应这题,每次选择走1步还是2步,每次都需要我们做出动态抉择,直至到达题目给的限制条件,这里是到达n级台阶,动态规划分为从下至上或者从上至下两个角度,这题爬阶梯,我们先试着倒着分析,第n级台阶只能由n-1级台阶或者n-2级台阶上来,列出公式即为dp[n] = dp[n-1] + dp[n-2]。
C++代码如下:
#include<iostream> #define N 20 using namespace std; int dp[N]; int fun(int n) { if (n == 1 || n == 2) { return n; } dp[n - 1] = fun(n - 1); dp[n - 2] = fun(n - 2); dp[n] = dp[n - 1] + dp[n - 2]; return dp[n]; } int main() { fun(N); cout << dp[15] << endl; return 0; }
其实这也是一个递归问题,不过这里用动态规划分析,下面看最短路径问题,这种问题求最值极值的,就要考虑一下是否可以用动态规划了。
给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0
那么路径1,3,1,0,6,1,0就是最小路径和,返回12。
同第一个题的分析方法,走到第(i,j)位置时,只可能是从(i-1,j)或是(i,j-1)过来的,状态转移方程即为dp[i][j] = a[i][j] + min(dp[i-1][j] + dp[i][j-1])。
C++代码如下:
#include<iostream> #include<algorithm> using namespace std; int dp[4][4] = {}; int main(int argc,char** argv) { int a[4][4] = { 1,3,5,9,8,1,3,4,5,0,6,1,8,8,4,0 }; for (int i = 0; i < 4;i++) { for (int j = 0; j < 4;j++) { if (i==0&&j==0) { dp[i][j] = a[i][j]; //出发初始位置 } else if(i==0&&j!=0){ dp[i][j] = a[i][j] + dp[i][j - 1];//上边界 } else if (i != 0 && j == 0) { dp[i][j] = a[i][j] + dp[i - 1][j];//左边界 } else { dp[i][j] = a[i][j] + min(dp[i - 1][j], dp[i][j - 1]);//动态规划选择右走还是往下走 } } } cout << dp[3][3] << endl; return 0; }
从代码中可以看到,两层循环遍历了整个数组,一层层把所有点到出发点的最短距离找到,接着看下剑指offer上的剪绳子问题,
给定一根长度为n的绳子,剪成m段(m,n>1都是整数),每段绳子长度记为k[0],k[1],k[2],…,k[m-1],求k[0]*k[1]*k[2]*…*k[m-1]乘积的最大值,例如:n=8,则可以得到剪出3段时,分别为2,3,3,乘积最大为18.
首先动态规划就是看某一步有多少种可能性,之前说了动态规划有从上到下和从下到上两种,这里只能选择从下到上,因为我们尚不清楚最后会剪成什么样,那么第一步剪绳子,有n-1种可能,f(n)=max(f(i)xf(n-i)),首先我们得到f(2)=1,f(3)=2,这两个作为基本(至于为什么是基本,大家可以自己推推后面几个),一直可以推出f(n)来,现在假如输入n=1000,我们不能从1000开始分解成m段,但是我们可以从第一段开始往上求,通过一层一层往上算,并将算的结果存在数组中,求到1000为止,最后就能知道结果了。
C++代码如下:
#include<iostream> #include<algorithm> using namespace std; int maxProduct(int length) { if (length < 2) return 0; if (length == 2) return 1; if (length == 3) return 2;//初始绳子长度的特殊情况 int product = 1; int max = 0; int *products = new int[length + 1];//存算好的值的数组 products[0] = 0; products[1] = 1; products[2] = 2; //这里是剪出的绳子两个基本单位 products[3] = 3; for (int i = 4; i <= length; ++i) { //这一层循环是将length以下的product[i]存到数组中 for (int j = 1; j <= i/2; ++j) {//这一层循环是求剪一刀乘积的最大值 product = products[j] * products[i - j]; if (product > max) max = product; } products[i] = max; } return products[length]; } int main() { int n = 7; cout << maxProduct(n) << endl; }
当然这不是最优解法,只是在动态规划场景中运用所学的知识,有兴趣的可以深入学习。