您的位置:首页 > 理论基础 > 数据结构算法

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

2016-07-12 21:47 429 查看
前言:动态规划的概念

  动态规划(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
>= 0
return r

if n==0
q=0
else
for i 1 to n
q=max(q,p[i]+memoized-cut-aux(p,n-i,r))
r
= 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


具体实现:  

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]
中,另外引入一个二维数组B[M]
用来保存最优解的构造过程。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

总结:

  动态规划问题的难点是 找到 递推的关系,从而构造适当的数组。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: