算法导论 第四部分——基本数据结构——第15章:动态规划

时间:2023-02-22 23:40:35

前言:动态规划的概念

  动态规划(dynamic programming)是通过组合子问题的解而解决整个问题的。分治算法是指将问题划分为一些独立的子问题,递归的求解各个问题,然后合并子问题的解而得到原问题的解。例如归并排序,快速排序都是采用分治算法思想。本书在第二章介绍归并排序时,详细介绍了分治算法的操作步骤,详细的内容请参考:http://www.cnblogs.com/Anker/archive/2013/01/22/2871042.html。而动态规划与此不同,适用于子问题不是独立的情况,也就是说各个子问题包含有公共的子问题。如在这种情况下,用分治算法则会重复做不必要的工作。采用动态规划算法对每个子问题只求解一次,将其结果存放到一张表中,以供后面的子问题参考,从而避免每次遇到各个子问题时重新计算答案。

动态规划与分治法之间的区别:
(1)分治法是指将问题分成一些独立的子问题,递归的求解各子问题
(2)动态规划适用于这些子问题不是独立的情况,也就是各子问题包含公共子问题

  动态规划通常用于最优化问题(此类问题一般有很多可行解,我们希望从这些解中找出一个具有最优(最大或最小)值的解)。动态规划算法的设计分为以下四个步骤:

(1)描述最优解的结构

(2)递归定义最优解的值

(3)按自低向上的方式计算最优解的值

(4)由计算出的结果构造一个最优解

  动态规划最重要的就是要找出最优解的子结构。书中接下来列举4个问题,讲解如何利用动态规划方法来解决。动态规划的内容比较多,我计划每个问题都认真分析,写成日志。

一、钢条切割

已知不同长度的钢条的价格,如下图.给定一个长度为n的钢条,如何切分才能使卖出去的价格最高。

  算法导论 第四部分——基本数据结构——第15章:动态规划

方法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算法中也用到了这种思路的解法: 

                               算法导论 第四部分——基本数据结构——第15章:动态规划

 

程序接口及实现如下:

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问题的最优子结构的递归式如下所示:

算法导论 第四部分——基本数据结构——第15章:动态规划

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 s1s2s3, 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

总结:

  动态规划问题的难点是 找到 递推的关系,从而构造适当的数组。