Make it work, make it right, make it fast. —— Kent Beck
此处前两句说的就是递归,后一句说的便是迭代。不得不承认递归大法确实简单易写,但是迭代法更加高效,快捷。
动态规划,可能听起来很陌生,但是换句话说就是递归得出初步结论后再用迭代的方式写出来。
就拿经典的Fibonacci数列来说吧
long long fibonacci(int n){
if(n == 0){ return 1; } else if(n == 1){ return 1; } else{ return fibonacci(n - 1) + fibonacci(n - 2); } }//T(n) = pow(2, n - 1) = O(pow(2, n))
大家不妨据此进行递归跟踪:
我们可以看到在这个递归树下有很多重复的分支,因此我们是否能删减去重复的分支?
改进一:记忆化递归
long long fibonacci(int n){ long long f[100] = {0}; if(n == 0){ return f[0] = 0; } else if(n == 1){ return f[1] = 1; } else if(f[n]){ return f[n]; } else{ return f[n] = fibonacci(n - 1) + fibonacci(n - 2); } }//T(n) = pow(2, n - 2) = O(pow(2, n))
虽说删去了重复的分支,但是复杂度上似乎并没有下降多少
改进二:动态规划(非递归)
long long fibonacci(int n){ long long f[100]; f[0] = 0; f[1] = 1; for(int i = 2; i <= n; i++){ f[i] = f[i - 1] + f[i - 2]; } return f[n]; }//T(n) = O(n)
这种算法改变了上面两种自上而下的思路,而采取了自底向上的思路,更倾向于我们通常解决问题的思维(迭代)。
因此也应照了我开始所说的动态规划是用递归得出初步结论,再转化为迭代的形式。
提及动态规划,一个经典的例子就是:
LCS (Longest Common Subsequence)//最长公共子序列
子序列:就是从一个字符串中抽出几个字母按照抽出时的顺序排列而成的字符串
最长公共子序列:两个公共子序列中的最长者
eg: d i d a c
a d v a n
最长公共子序列:d a
面对这样的问题,我们的常规思路是:
1.找出公共子序列
2.再在其中找出最长的公共子序列
动态规划的思路是:
将所有的子问题想象成一个矩阵
矩阵满足:
(1) if( i == 0 || j == 0 ) A[i][j] = 0
(2) if( i,j > 0 && Ci == Rj ) A[i][j] = A[i - 1][j - 1] + 1
(3) if( i,j > 0 && Ci != Rj ) max( A[i][j - 1], A[i - 1][j] )
#include <iostream> #include <cstdio> #include <algorithm> #include <cmath> #include <string> using namespace std; int main(){ string str1 = "didac"; string str2 = "advan"; int A[7][7] = {0}; int longest = 0; for(int i = 0; i <= str1.length(); i++){ for(int j = 0; j <= str2.length(); j++){ if(i == 0 || j == 0){ A[i][j] = 0; } else if(i > 0 && j > 0 && str1[i - 1] == str2[j - 1]){ A[i][j] = A[i - 1][j - 1] + 1; } else if(i > 0 && j > 0 && str1[i - 1] != str2[j - 1]){ A[i][j] = max(A[i - 1][j], A[i][j - 1]); } } } for(int i = 0; i <= str1.length(); i++){ for(int j = 0; j <= str2.length(); j++){ cout << A[i][j]; } cout << endl; } return 0; }
总结:无论是从表格来看,还是从矩阵规则来看,公共子序列的长度即矩阵最大元素,而且行数最小列数最小的最先增加的元素即为公共序列中的元素。
long long fibonacci(int n){
if(n == 0){ return 1; } else if(n == 1){ return 1; } else{ return fibonacci(n - 1) + fibonacci(n - 2); } }//T(n) = pow(2, n - 1) = O(pow(2, n))
大家不妨据此进行递归跟踪:
我们可以看到在这个递归树下有很多重复的分支,因此我们是否能删减去重复的分支?
改进一:记忆化递归
long long fibonacci(int n){ long long f[100] = {0}; if(n == 0){ return f[0] = 0; } else if(n == 1){ return f[1] = 1; } else if(f[n]){ return f[n]; } else{ return f[n] = fibonacci(n - 1) + fibonacci(n - 2); } }//T(n) = pow(2, n - 2) = O(pow(2, n))
虽说删去了重复的分支,但是复杂度上似乎并没有下降多少
改进二:动态规划(非递归)
long long fibonacci(int n){ long long f[100]; f[0] = 0; f[1] = 1; for(int i = 2; i <= n; i++){ f[i] = f[i - 1] + f[i - 2]; } return f[n]; }//T(n) = O(n)
这种算法改变了上面两种自上而下的思路,而采取了自底向上的思路,更倾向于我们通常解决问题的思维(迭代)。
因此也应照了我开始所说的动态规划是用递归得出初步结论,再转化为迭代的形式。
提及动态规划,一个经典的例子就是:
LCS (Longest Common Subsequence)//最长公共子序列
子序列:就是从一个字符串中抽出几个字母按照抽出时的顺序排列而成的字符串
最长公共子序列:两个公共子序列中的最长者
eg: d i d a c
a d v a n
最长公共子序列:d a
面对这样的问题,我们的常规思路是:
1.找出公共子序列
2.再在其中找出最长的公共子序列
动态规划的思路是:
将所有的子问题想象成一个矩阵
矩阵满足:
(1) if( i == 0 || j == 0 ) A[i][j] = 0
(2) if( i,j > 0 && Ci == Rj ) A[i][j] = A[i - 1][j - 1] + 1
(3) if( i,j > 0 && Ci != Rj ) max( A[i][j - 1], A[i - 1][j] )
#include <iostream> #include <cstdio> #include <algorithm> #include <cmath> #include <string> using namespace std; int main(){ string str1 = "didac"; string str2 = "advan"; int A[7][7] = {0}; int longest = 0; for(int i = 0; i <= str1.length(); i++){ for(int j = 0; j <= str2.length(); j++){ if(i == 0 || j == 0){ A[i][j] = 0; } else if(i > 0 && j > 0 && str1[i - 1] == str2[j - 1]){ A[i][j] = A[i - 1][j - 1] + 1; } else if(i > 0 && j > 0 && str1[i - 1] != str2[j - 1]){ A[i][j] = max(A[i - 1][j], A[i][j - 1]); } } } for(int i = 0; i <= str1.length(); i++){ for(int j = 0; j <= str2.length(); j++){ cout << A[i][j]; } cout << endl; } return 0; }
总结:无论是从表格来看,还是从矩阵规则来看,公共子序列的长度即矩阵最大元素,而且行数最小列数最小的最先增加的元素即为公共序列中的元素。