动态规划问题

动态规划是一种解决最优化问题的有效方法,常用于钢铁切割、矩阵链乘法、最长公共子序列和最优二叉搜索树等问题。通过递归、备忘录或自底向上的策略,动态规划能避免重复计算,降低时间复杂度。本文详细介绍了这些问题的动态规划解法及其优化策略。
摘要由CSDN通过智能技术生成

动态规划问题


动态规划方法通常用来求解最优化问题。问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值的解。这样的解为问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都达到最优值。

一、钢铁切割问题

给定一段长度为 n 英寸的钢条和一个价格表pi(i=1,2,,n),求切割钢条方案,使得销售收益 rn 最大。注意,如果长度为 n 英寸的钢条的价格pn足够大,最优解可能就是完全不需要切割。

  • 解法一:

为了求解规模为 n 的原问题,可以先求解形式完全一样,但规模更小的子问题。即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题。

通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。

称钢条切割问题满足最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。

  • 解法二:

递归求解:将钢条从左边切割下长度为i 的一段,只对右边剩下的长度为 ni 的一段继续进行切割(递归求解),对左边的一段则不在进行切割。

即问题分解的方式为:将长度为 n 的钢条分解为左边开始一段,以及剩余部分继续分解的结果。

公式:

rn=max1in(pi+rni)

在此公式中,原问题的最优解只包含一个相关子问题(右端剩余部分)的解,而不是两个。

1. 自顶向下递归实现–解法二

Cut_rod(p, n)
    if n == 0
        return 0
    q = -inf
    for i = 1 to n
        q = max(q, p[i] + Cut_rod(p, n-i))
    return q
  1. 随着n的增大,程序运行时间越来越长,效率很低,工作量会爆炸性地增长;
  2. Cut_rod反复用相同的参数值对自身进行递归调用,即反复在求解相同的子问题;
  3. 如图:

    这里写图片描述

  4. 运行时间: 2n ,为n的指数函数。

2. 动态规划方法求解

动态规划方法仔细安排求解顺序,对于每个子问题只求解一次,并将结果保存下来。

动态规划方法是付出额外的内存空间来节省计算时间,是典型的时空权衡的例子。

两种等价的实现方法:

带备忘的自顶向下法

仍然按照自然的递归形式编写过程,但过程中会保存每个子问题的解。

Memoized_cut_rod(p, n)
    let r[0..n] be a new array
    for i = 0 to n
        r[i] = -inf
    return Memoized_cut_rod_aux(p, n, r)

Memoized_cut_rod_aux(p, n, r)
    if r[n] >= 0
        return r[n]
    if n == 0
        q = 0
    else q = -inf
        for i = 1 to n
            q = max(q, p[i] + Memoized_cut_rod_aux(p, n-i, r))
    r[n] = q
    return q
自底向上法

将子问题按规模排序,按由小到大的顺序进行求解。

Bottom-up-cut-rod(p,n)
    let r[0..n] be a new array
    r[0] = 0
    for j = 1 to n
        q = - inf
        for i = 1 to j
            q = max(q, p[i]+r[j-i])
        r[j] = q
    return r[n]
  • 两种方法得到的算法具有相同的渐进运行时间;
  • 自顶向下的方法并未真正地递归考察所有可能的子问题;
  • 由于没有频繁的递归函数调用的开销,自底向上的方法的时间复杂性函数通常具有更小的系数。

3. 重构解

对2中的动态规划算法进行扩展,使之对每个子问题不仅保存最优收益值,还保存对应的切割方案。
其中 sj 保存钢条的切割长度;最大收益值 rj

Extended-bottom-up-cut-rod(p,n)
    let r[0..n] and s[0..n] be new arrays
    r[0] = 0
    for j = 1 to n
        q = - inf
        for i =1 to j
            if q < p[i] + r[j-i]
                q = p[i] + r[j-i]
                s[j] = i
        r[j] = q
    return r and s 

长度为n的钢条的完整的最优切割方案:

Print-cut-rod-solution(p,n)
    (r,s) = extended-bottom-up-cut-rod(p,n)
    while n > 0
        print s[n]
        n = n - s[n]

二、矩阵链乘法

矩阵链乘法中,在相乘的过程中加括号会对乘积运算的代价产生巨大的影响。

矩阵链乘法问题: 给定 n 个矩阵的链<A1,A2,...,An>,矩阵 Ai 的规模为 pi1×pi(1in) ,求完全括号化方案,使得计算乘积 A1A2An 所需乘法次数最少。

利用动态规划的方法寻找最优括号化方案。

步骤1:最优括号化方案的结构特征

假设 AiAi+1Aj 的最优括号化方案的分割点在 Ak Ak+1 之间。那么继续对“前缀”子链 AiAi+1Ak 进行括号化时,我们应该直接采用独立求解它时的最优化方案。

步骤2:一个递归求解方案

m[i,j] 来表示计算矩阵 Ai,j 所需标量乘法的最小值。

矩阵 Ai 的大小为 pi1×pi

AiAI+1Aj 最小代价括号化方案的递归求解公式:

这里写图片描述

步骤3:计算最优代价

自底向上方法实现:

Matrix_chain_order(p)
    n = p.length - 1
    let m[1..n, 1..n] and s[1..n-1,2..n] be new tables
    for i = 1 to n
        m[i,j] = 0
    for l = 2 to n
        for i = 1 to n-l+1
            j = i + l - 1
            m[i,j] = inf
            for k = i to j-1
                q = m[i,k] +m[k+1,j] + p_{i-1}p_{k}p_{j}
                if q < m[i,j]
                    m[i,j] = q
                    s[i,j] = k
    return m and s

时间代价: O(n3)
空间代价: Θ(n2)
算法图解

这里写图片描述

步骤4:构造最优解

输出最优括号化方案:

Print_optimal_parens(s,i,j)
    if  i == j
        print "A"
    else print "("
        Print_optimal_parens(s,i,s[i,j])
        Print_optimal_parens(s,s[i,j]+1,j)
        print ")"

调用 Print_optimal_parens(s,1,6)输出括号化方案: ((A1(A2A3))((A4A5)A6))

带备忘的自顶向下法:

Memoized_matrix_chain(p)
    n = p.length - 1
    let m[1..n,1..n] be a new array
    for i = 1 to n
        for j = 1 to n
            m[i,j] = inf
    return Lookup_chain(m,p,1,n)

Lookup_chain(m,p,1,n)
    if m[i,j] < inf
        return m[i,j]
    if i == j
        m[i,j] = 0
    else for k = i to j-1
        q = Lookup_chain(m,p,i,k) + Lookup_chain(m,p,k+1,j) + p_{i-1}p_{k}p_{j}
        if q < m[i,j]
            m[i,j] = q
    return m[i,j]

时间复杂度 O(n3)

三、最长公共子序列

Longest-common-subsequence problem.

最长公共子序列问题:给定两个序列 X=<x1,x2,,xm> Y=<y1,y2,,yn> ,求 X Y长度最长的公共子序列。

步骤1:刻画最长公共子序列的特征

如果用暴力搜索方法求解LCS问题,就要穷举X的所有子序列,对每个子序列检查它是否也是Y的子序列,运行时间为 2m 即为指数阶。

这里写图片描述

步骤2:一个递归解

定义 c[i,j] 表示 Xi Yj 的LCS的长度。

根据LCS问题的最优子结构性质,可得如下公式:

这里写图片描述

步骤3:计算LCS的长度

使用动态规划方法自底向上计算。

过程Lcs_length 接受两个序列 X=<x1,x2,,xm> Y=<y1,y2,,yn> 为输入。

它将 c[i,j] 的值保存在表c[0..m,0..n]中,并按行主次序计算表项。过程还维护一个表 b[1..m,1..n] ,帮助构造最优解。

b[i,j] 指向的表项对应计算 c[i,j] 时所选择的子问题最优解。过程返回表b和表c,c[m,n]保存了X和Y的LCS的长度。

Lcs_length(X,Y)
    m = X.length
    n = Y.length
    let b[1..m,1..n] and c[0..m,0..n] be new tables
    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}
                c[i,j] = c[i-1, j-1]+1
                b[i,j] = "^\" #表示图中指向左上的箭头
            elseif c[i-1,j] >= c[i,j-1]
                c[i,j] = c[i,j-1]
                b[i,j] = "^|" #表示图中指向上的箭头
            else c[i,j] = c[i,j-1]
                b[i,j] = "<-" #表示图中指向左的箭头
    return c and b

下图显示了Lcs_length对输入序列 X=<A,B,C,B,D,A,B> Y=<B,D,C,A,B,A> 生成的结果。过程的运行时间为 Θ(mn) ,因为每个表项的计算时间为 Θ(1)

这里写图片描述

这里写图片描述

步骤4:构造LCS

可以用Lcs_length返回的表b快速构造 X=<x1,x2,,xm> Y=<y1,y2,,yn> 的LCS,只需简单地从 b[m,n] 开始,并按照箭头方向追踪下去即可。

当在表项 b[i,j] 中,遇到一个“指向右上”的箭头时,意味着 xi=yj 是LCS的一个元素。

Print_lcs(b,X,i,j)
    if i == 0 or j == 0
        return
    if b[i,j] == "^\"
        Print_lcs(b,X,i-1,j-1)
    elseif b[i,j] == "^|" 
        Print_lcs(b,X,i-1,j)
    else
        Print_lcs(b,X,i,j-1)

四、最优二叉搜索树

optimal binary search tree.

最优二叉搜索树问题:给定一个n个不同关键字的已排序的序列 K=<k1,k2,..,kn> ,用这些关键字构造一棵二叉搜索树。

对每个关键字 ki ,都有一个概率 pi 表示其搜索频率。有些要搜索的值可能不在K中,因此我们还有n+1个“伪关键字” d0,d1,d2,,dn 表示不在K中的值。 d0 表示所有小于 k1 的值, dn 表示所有大于 kn 的值, di 表示所有在 ki ki+1 之间的值。

这里写图片描述

假定一次搜索的代价等于访问的节点数,即此次搜索找到的结点在T中的深度再加1。则一次搜索的期望代价为:

这里写图片描述

步骤1:最优二叉搜索树的结构

如果一棵最优二叉搜索树T有一棵包含关键字 ki,,kj 的子树T’,那么T’必然是包含关键字 ki,,kj 和伪关键字 di1,,dj 的子问题的最优解。

???没有明白这么表述的意义在哪里。

步骤2:一个递归算法

对于包含关键字 ki,,kj 的子树,所有概率之和为:
w(i,j)=jl=ipl+jl=i1ql

kr 为包含关键字 ki,,kj 的最优二叉搜索树的根结点,公式:

这里写图片描述

这里写图片描述

e[i,j]的值给出了最优二叉搜索树的期望搜索代价。

步骤3:计算期望搜索代价

e[1..n+1,0..n] 来保存 e[i,j] 的值。第一维的下标上界为 n+1 而不是 n ,原因在于对于只包含伪关键字dn的子树,我们需要计算并保存 e[n+1,n] 。第二维下标下界为0,是因为对于只包含伪关键字 d0 的子树,我们需要计算并保存 e[1,0] 。我们只使用表中满足 j>=i1 的表项 e[i,j] 。我们还使用一个表root,表项 root[i,j] 记录包含关键字 ki,,kj 的子树的根。

同时还需要一个w[1..n+1,0..n]的表来保存w(i,j)避免每次计算e[i,j]时重新计算w(i,j).

w[i,i1]=qi1(1<=i<=n+1) ,对 j>=i 的情况,

w[i,j]=w[i,j1]+pj+qj

Optimal_bst(p,q,n)
    let e[1..n+1, 0..n], w[1..n+1, 0..n], and root[1..n, 1..n] be new tables
    for i = 1 to n+1
        e[i,i-1] = q_{i-1}
        w[i,i-1] = q_{i-1}
    for l = 1 to n
        for i = 1 to n-l+1
            j = i+l-1
            e[i,j] = inf
            w[i,j] = w[i,j-1] + p_{j} + q_{j}
            for r = i to j
                t = e[i,r-1] + e[r+1,j] +w[i,j]
                if t < e[i,j]
                    e[i,j] = t
                    root[i,j] = r
    return e and root 

时间复杂度: O(n3)

这里写图片描述

作者: 大树先生
博客: http://blog.csdn.net/koala_tree
2017 年 09 月 06 日
本文为博主原创文章,未经本人允许不得转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值