前言:动态规划的概念
动态规划(dynamic programming)是通过组合子问题的解而解决整个问题的。分治算法是指将问题划分为一些独立的子问题,递归的求解各个问题,然后合并子问题的解而得到原问题的解。例如归并排序,快速排序都是采用分治算法思想。本书在第二章介绍归并排序时,详细介绍了分治算法的操作步骤,详细的内容请参考:http://www.cnblogs.com/Anker/archive/2013/01/22/2871042.html。而动态规划与此不同,适用于子问题不是独立的情况,也就是说各个子问题包含有公共的子问题。如在这种情况下,用分治算法则会重复做不必要的工作。采用动态规划算法对每个子问题只求解一次,将其结果存放到一张表中,以供后面的子问题参考,从而避免每次遇到各个子问题时重新计算答案。
动态规划与分治法之间的区别:
(1)分治法是指将问题分成一些独立的子问题,递归的求解各子问题
(2)动态规划适用于这些子问题不是独立的情况,也就是各子问题包含公共子问题
动态规划通常用于最优化问题(此类问题一般有很多可行解,我们希望从这些解中找出一个具有最优(最大或最小)值的解)。动态规划算法的设计分为以下四个步骤:
(1)描述最优解的结构
(2)递归定义最优解的值
(3)按自低向上的方式计算最优解的值
(4)由计算出的结果构造一个最优解
动态规划最重要的就是要找出最优解的子结构。书中接下来列举4个问题,讲解如何利用动态规划方法来解决。动态规划的内容比较多,我计划每个问题都认真分析,写成日志。
一、钢条切割
已知不同长度的钢条的价格,如下图.给定一个长度为n的钢条,如何切分才能使卖出去的价格最高。
方法1: 自顶向下的递归实现 : 该方法的弊端是: 复杂度为 2^n 因为每次回次调用都会递归调用子序列,而不会将重复计算的数据保存起来
cut_rod(p,n) if n==0 return 0 q=int_min for i= 1 to n q=max(q,p[i]+cut_rod(p,n-i)) return q;
int cut_rod(int *p, int len, int n) { if (n == 0) return 0; int q = MIN; *p = 0; for (int i = 1; i <= n; i++) { q = max(q, *(p + i) + cut_rod(p, len, n - i)); } return q; }
方法2: 带备忘录的自顶向下的递归实现 : 是对方法一的改进
memoized-cut-rod(p,n) let r[0...n] be new array for i=0 to n r[i] = int_min return memoized-cut-aux(p,n,r) memoized-cut-aux(p,n,r) if r[n] >= 0 return r[n] if n==0 q=0 else for i 1 to n q=max(q,p[i]+memoized-cut-aux(p,n-i,r)) r[n] = q return q
int mem_cut_rod(int *p, int len, int n) { vector<int> result; result.push_back(0); for (int i = 0; i<n + 1; i++) result.push_back(MIN); return mem_cut_rod_r(p, len, n, result); } int mem_cut_rod_r(int *p, int len, int n, vector<int> &result) { if (result.at(n) >= 0) return result.at(n); int q; for (int i = 1; i<n + 1; i++) { q = MIN; for (int j = 1; j <= i; j++) { q = max(q, *(p + j) + mem_cut_rod_r(p, len, i - j, result)); } result.at(i) = q; } return q; }
方法三、自底向上的实现
伪代码:
bottom-cut-rod(p,n) let r[0....n] be a new array r[0] =0 for j=1 to n q=int_min for i= 1 to j q=max(q,p[i]+r[j-i]) r[i] = q return r[n]
具体实现:
int bottom_up_rod(int *p, int len, int n) { if (n == 0 || n == 1) return n == 0 ? 0 : *(p + 1); int *result = new int[n + 1]; int q; *result = 0; for (int i = 1; i<n + 1; i++) *(result + i) = MIN; for (int i = 1; i <= n; i++) { q = MIN; for (int j = 1; j <= i; j++) { q = max(q, *(p + j) + bottom_up_rod(p, len, i - j)); } *(result + i) = q; } int temp = *(result + n); delete[]result; return temp; }
二、矩阵链乘法
矩阵的成法有结合律,那么问题来了,怎么结合才能使 运算的次数最小。书上的例子: 三个举证ABC,大小分别为 10x100,100x5 ,5x50
那么那种结合(AB)C 和 A(BC)的运算次数分别为 7500 ,25000 。那么如何给一个算法知道矩阵想乘的最小运算次数?
输入为 vector<pair<int ,int>> a; // pair 存放一个矩阵的行和列
输出 为最小计算次数(也可以额外输出如何进行结合的s[i][j](表示i~j相乘中划分为 i~s[i][j] 与 s[i][j]+1~j两个最优数组时,它的运算次数最小)
动态规划的设计思路:
如果暴力求解的话 时间复杂度为 2^n ,但是可以把为题划分为求两个最优解的和,划分成两部分。p[i][j] = p[i][k] + p[k+1][j] + a[i].first*a[j].second*a[k].second
这里用一个二维数组存放计算的结果如下 。 p[i][j] 代表 A[i]*A[i+1]*....*A[j] 的最优运算次数。
计算次序为: 对号标记部分 小红旗标记部分 上升箭头标记部分 最后是所要求的结果。
在kmp算法中也用到了这种思路的解法:
程序接口及实现如下:
int maxtrix_chain(vector<pair<int, int >> a,int s[6][6]){ int const len = a.size(); int **p = new int*[len]; for (int i = 0; i < len; i++){ p[i] = new int[len]; } for (int i = 0; i < len; i++){ for (int j = i; j < len; j++){ if (j == i + 1){ s[i][j] = i; //需要初始化的值 p[i][j] = a[i].first*a[i].second*a[j].second; //需要初始化的值 } else p[i][j] = 0; } } for ( int k = 2; k <len; k++){ for ( int i=0 ; i <len-k; i++){ int re = INT_MAX; for (int j = 0; j < k ; j++){ int temp=min(re,p[i][i+j] + p[i+j+1][k+i]+a[i].first*a[k+i].second*a[i+j].second); if (temp < re){ re = temp; s[i][k + i] = i+j; } } p[i][k+i] = re; } } int resu = p[0][len - 1]; for (int i = 0; i < len; i++) delete p[i]; delete p; return resu; }
根据s[6][6]输出结合方式的函数:
void print(int s[6][6],int i, int j){ if (i == j) cout << "A" << i; else{ cout << "("; print(s, i, s[i][j]); print(s,s[i][j]+1,j); cout << ")"; } }
三、最长公共子序列(LCS)
1)描述一个最长公共子序列
如果序列比较短,可以采用蛮力法枚举出X的所有子序列,然后检查是否是Y的子序列,并记录所发现的最长子序列。如果序列比较长,这种方法需要指数级时间,不切实际。
LCS的最优子结构定理:设X={x1,x2,……,xm}和Y={y1,y2,……,yn}为两个序列,并设Z={z1、z2、……,zk}为X和Y的任意一个LCS,则:
(1)如果xm=yn,那么zk=xm=yn,而且Zk-1是Xm-1和Yn-1的一个LCS。
(2)如果xm≠yn,那么zk≠xm蕴含Z是是Xm-1和Yn的一个LCS。
(3)如果xm≠yn,那么zk≠yn蕴含Z是是Xm和Yn-1的一个LCS。
定理说明两个序列的一个LCS也包含两个序列的前缀的一个LCS,即LCS问题具有最优子结构性质。
2)一个递归解
根据LCS的子结构可知,要找序列X和Y的LCS,根据xm与yn是否相等进行判断的,如果xm=yn则产生一个子问题,否则产生两个子问题。设C[i,j]为序列Xi和Yj的一个LCS的长度。如果i=0或者j=0,即一个序列的长度为0,则LCS的长度为0。LCS问题的最优子结构的递归式如下所示:
3)计算LCS的长度
采用动态规划自底向上计算解。书中给出了求解过程LCS_LENGTH,以两个序列为输入。将计算序列的长度保存到一个二维数组C[M][N]中,另外引入一个二维数组B[M][N]用来保存最优解的构造过程。M和N分别表示两个序列的长度。该过程的伪代码如下所示:
LCS_LENGTH(X,Y) m = length(X); n = length(Y); for i = 1 to m c[i][0] = 0; for j=1 to n c[0][j] = 0; for i=1 to m for j=1 to n if x[i] = y[j] then c[i][j] = c[i-1][j-1]+1; b[i][j] = ‘\‘; // 构造一个路径表,用于指示 。 \: 两边都可以 else if c[i-1][j] >= c[i][j-1] then c[i][j] = c[i-1][j]; b[i][j] = ‘|‘; // | : 上下 else c[i][j] = c[i][j-1]; b[i][j] = ‘-‘; // - : 左右 return c and b
四、[LeetCode] Interleaving String
Given s1, s2, s3, find whether s3 is formed by the interleaving of s1 and s2.
For example,
Given:
s1 = "aabcc"
,
s2 = "dbbca"
,
When s3 = "aadbbcbcac"
, return true.
When s3 = "aadbbbaccc"
, return false.
DP. 通过构造一个 二维数组 f[i][j] = (f[i-1]f[j] && s3[i+j-1]==s1[i]) || (f[i][j-1]&&s3[i+j-1]==s2[j])
class Solution { private: bool f[1000][1000]; public: bool isInterleave(string s1, string s2, string s3) { // Start typing your C/C++ solution below // DO NOT write int main() function if (s1.size() + s2.size() != s3.size()) return false; f[0][0] = true; for(int i = 1; i <= s1.size(); i++) f[i][0] = f[i-1][0] && (s3[i-1] == s1[i-1]); for(int j = 1; j <= s2.size(); j++) f[0][j] = f[0][j-1] && (s3[j-1] == s2[j-1]); for(int i = 1; i <= s1.size(); i++) for(int j = 1; j <= s2.size(); j++) f[i][j] = (f[i][j-1] && s2[j-1] == s3[i+j-1]) || (f[i-1][j] && s1[i-1] == s3[i+j-1]); return f[s1.size()][s2.size()]; } };
五、leetcode : unique bianry search tree
http://www.cnblogs.com/NeilZhang/p/5347667.html
总结:
动态规划问题的难点是 找到 递推的关系,从而构造适当的数组。