算法学习笔记(八) 动态规划的一般求解方法

1 一个问题:换零钱方式的统计

SICP 第一章 1.2.2 树形递归中,有这么一问题:给了半美元,四分之一美元,10美分,5美分和1美分的硬币,将1美元换成零钱,一共有多少种不同方式?更一般的问题是,给定了任意数量的现金,我们能写一个程序,计算出所有换零钱方式的种数吗?


2 动态规划的基本模型

动态规划(Dynamic programming,DP),是研究一类最优化问题的方法,通过把原问题分解为相对简单的子问题的方式求解复杂问题。动态规划处理的也就是是多阶段决策最优化问题,这一类问题可将过程分成若干个互相联系的阶段,在每一阶段都作出决策,从而使整个过程达到最好的结果。因此各个阶段决策的选取不能任意确定,它依赖于当前面临的状态,又影响以后的发展。当各个阶段决策确定后,就组成一个决策序列,从而也就确定了整个过程的一条活动路线。这种把一个问题看做是一个前后关联具有链状结构的多阶段过程称为多阶段决策过程。动态规划著名的应用实例有:求解最短路径问题,背包问题,项目管理,网络流优化等。动态规划的基本模型如下:
  1. 确定问题的决策对象
  2. 对决策过程划分阶段
  3. 对各阶段确定状态变量
  4. 根据状态变量确定费用函数和目标函数
  5. 建立各阶段状态变量的转移过程,确定状态转移方程

3 使用动态规划的一般前提

3.1 满足动态规划的最优化原理

作为整个过程的最优策略具有如下性质:无论过去的状态和决策如何,对前面的决策所形成的当前状态而言,余下的诸决策必须构成最优策略。

通俗理解就是子问题的局部最优将导致整个问题的全局最优,即问题具有最优子结构的性质,也就是说一个问题的最优解只取决于其子问题的最优解,非最优解对问题的求解没有影响。


3.2 满足动态规划的无后效性原则

所谓无后效性原则,指的是这样一种性质:某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响。也就是说,“未来与过去无关”,当前的状态是此前历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。

具体地说,如果一个问题被划分各个阶段之后,阶段 I 中的状态只能由阶段 I+1 中的状态通过状态转移方程得来,与其他状态没有关系,特别是与未发生的状态没有关系,这就是无后效性。从图论的角度去考虑,如果把这个问题中的状态定义成图中的顶点,两个状态之间的转移定义为边,转移过程中的权值增量定义为边的权值,则构成一个有向无环加权图,因此,这个图可以进行“拓扑排序”,至少可以按他们拓扑排序的顺序去划分阶段。


4 动态规划设计方法

4.1 一般方法

一般 由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线。步骤为:
  1. 划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
  2. 确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
  3. 确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两段各状态之间的关系来确定决策。
  4. 寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
  5. 程序设计实现:动态规划的主要难点在于理论上的设计,一旦设计完成,实现部分就会非常简单。

4.2 逆向推导

逆向思维法是指从问题目标状态出发倒推回初始状态或边界状态的思维方法。如果原问题可以分解成几个本质相同、规模较小的问题,很自然就会联想到从逆向思维的角度寻求问题的解决。动态规划与分治法最大的不同在于分解出来的各个子问题的性质不同:
  • 分治法要求各个子问题是独立的(即不包含公共的子问题),因此一旦递归地求出各个子问题的解后,便可自下而上地将子问题的解合并成原问题的解。如果各子问题是不独立的,那么分治法就要做许多不必要的工作,重复地解公共的子问题。
  • 动态规划与分治法的不同之处在于动态规划允许这些子问题不独立(即各子问题可包含公共的子问题),它对每个子问题只解一次,并将结果保存起来,避免每次碰到时都要重复计算。这就是动态规划高效的一个原因。
动态规划的逆向推导步骤:
  1. 分析最优值的结构,刻画其结构特征;
  2. 递归地定义最优值;
  3. 按自底向上或自顶向下记忆化的方式计算最优值;

4.3 正向推导

正向思维法是指从初始状态或边界状态出发,利用某种规则不断到达新的状态,直到问题目标状态的方法。动态规划的正向思维法,正是从已知最优值的初始状态或边界状态开始,按照一定的次序遍历整个状态空间,递推出每个状态所对应问题的最优值。
正向思维法中,不再区分原问题和子问题,将动态规划的过程看成是从状态到状态的转移。将所有的状态构造出一个状态空间,并在状态空间中设想一个状态网络,若对两个状态i,j,存在决策变量di使t(i,di)=j,则向状态网络添加有向边。给定己知最优值的初始状态或边界状态,可以沿著有向边推广到未知最优值的新状态,利用状态转移方程得到新状态的状态变量的最优值。我们可以用这种方式遍历整个状态空间,得到每个状态的状态变量的最优值。
动态规划的正向推导步骤:
  1. 构造状态网络;
  2. 根据状态转移关系和状态转移方程建立最优值的递推计算式:
  3. 按阶段的先后次序计算每个状态的最优值;

动态规划需要按阶段遍历整个状态空间,因此动态规划的效率取决于状态空间的大小和计算每个状态最优值的开销:如果状态空间的大小是多项式的,那么应用动态规划的算法就是多项式时间的;如果状态空间的大小是指数的,那么应用动态规划的算法也是指数时间的。因此,找一个好的状态划分对动态规划的效率是至关重要的。


5 小实验换零钱问题求解

逆推状态转移方程:数量为 a 的钱换成 n 种硬币的不同方式等于:
  • 数量为 a 的钱换成除第一种硬币外的 n-1 种硬币(必不包含第一种硬币)的不同方式数目加上,
  • 数量为 a-d 的钱(必包含第一种硬币)换成所有硬币种类的不同方式数目,d 为第一种硬币币值
逆推到初始状态:
  • 如果钱为 0 ,说明正好换完毕,是一种换零钱方法,
  • 如果钱为负数,或者种类已经递归到 0 种,则说明没有正好换完,不是一种换法,返回0;

还可以正向推导,打表记录已经计算出的值。

Python实现

NUM = 0


def count_change(amount, money, kinds):
    ''' 树形递归存在冗余'''
    global NUM
    if amount == 0:
        NUM+=1
        return 1
    if amount < 0 or kinds == 0:
        NUM+=1
        return 0
    NUM+=1
    return count_change(amount, money, kinds - 1) + count_change(amount - money[kinds - 1], money, kinds)

def count_dy(amount,money,kinds):
    '''动态规划,打表记录已经计算的值'''
    table = [[0 for col in range(kinds)] for row in range(amount+1)]
    table[0] = [1]*kinds
    for i in range(1,amount+1):
        for j in range(kinds):
            # 包括 money[j]
            x = table[i - money[j]][j] if i-money[j] >= 0 else 0
            # 不包含 money[j]
            y = table[i][j-1] if j>=1 else 0
            table[i][j] = x+y
    return table[amount][kinds-1]

if __name__ == '__main__':
    money = [1, 5, 10, 25, 50]
    print(count_change(100, money, len(money)),'time:',NUM)
    print(count_dy(100, money, len(money)),'time:',100*len(money))

'''
292 time: 15499
292 time: 500
'''

SICP中的Scheme实现(Racket)算法学习笔记(八) 动态规划的一般求解方法_第1张图片

【地址:http://blog.csdn.net/thisinnocence/article/details/41073275】


你可能感兴趣的:(算法,动态规划)