zoukankan      html  css  js  c++  java
  • 浅谈动态规划

    浅谈动态规划

    动态规划问题经常出现在各种算法竞赛和面试中。需要说明的问题是,这里的动态规划比较广义,也包括一部分组合计数问题。最近感觉对这类问题有了一些认识和心得。从认识方面上,我试图找到一种对动态规划问题的通用解释。在心得上,尽管限于经验和练习量有限,不是次次都能解出,我暂时探索出了一套比较好的思路。

    在这里我们以一个经典问题为例:

    POJ 1163

    给出一个 n 行 n 列的三角形,形如:

          7
        3   8
      8   1   0
    2   7   4   4
    

    从顶端出发,每次可以选择向左或者向右走,求一条路径,从顶走到底,使路径上的和最大。

    问题的解法大家都知道:设 ( DP_{i,j} ) 表示走到第( i )行,第( j )列的最优解,( Tri_{i, j} )表示第( i )行,第( j )列的数字,则有 [ DP_{i,j} = max(DP_{i-1,j}, DP_{i,j-1}) + Tri_{i,j} ] 问题的解为 [ max{DP_{n,i}} ] 暂时忘记结论,我的想法如下:

    首先,考虑状态的编码。我们知道,搜索问题和动态规划问题,都需要设计状态。显然同一个状态有不同的编码方法。对于这个问题的一个状态:

    {
      访问序列 = 7,8,1
      当前位置 = (3,2)
      当前和 = 16
    }
    

    我们有若干方法编码:

    • 记录决策,L 代表左,R 代表右:RL => 16,使用二进制可以编码成:( (10)_{2} )
    • 记录当前位置:第 3 行第 2 列:(3, 2) => 16

    直接搜索产生可行解集的过程如下:

    {} ->
    {L,R} ->
    {LL, LR, RL, RR} ->
    {LLL, LLR, LRL, LRR, RLL, RLR, RRL, RRR}

    问题的复杂度是指数级。

    我们考虑动态规划的性质:最优子结构和无后效性。所以对于当前问题,我们只关心当前位置,而不关心历史信息。编码方法 RL 保留了历史信息,而 (3, 2) 没有。由于信息损失,多个不同的状态,可能有相同的编码。我们进行演算:

    {(1,1) => {}} ->
    {(2,1) => {L}, (2,2) => {R}} ->
    {(3,1) => {LL}, (3,2) => {LR, RL}, (3,3) => {RR} } ->
    {(4,1) => {LLL}, (4,2) => {LLR, LRL, RLL, }, (4,3) => {LRR, RLR, RRL}, (4,4)=>{RRR}}

    根据最优子结构,位置(4, 2) 和 (4, 3) 的最优解,只可能由位置 (3, 2) 的最优解产生。具体来说,因为 7 + 8 + 1 > 7 + 3 + 1,所以 7 + 8 + 1 + X > 7 + 3 + 1 + X。于是对于所有编码同为 (3, 2) 的方案,我们只保留最优的,这样做相当于实现了搜索的剪枝,计算步骤如下:

    {} ->
    {(2,1) => {L}, (2,2) => {R}} ->
    {(3,1) => {LL}, (3,2) => {LR, RL}, (3,3) => {RR} } 剪枝 ->
    {(3,1) => {LL}, (3,2) => {RL}, (3,3) => {RR} } ->
    {(4,1) => {LLL}, (4,2) => {LLR, RLL}, (4,3) => {RLR, RRL}, (4,4)=>{RRR}} 剪枝 ->
    {(4,1) => {LLL}, (4,2) => {LLR}, (4,3) => {RLR}, (4,4)=>{RRR}}

    于是,问题的状态空间,从指数级映射到了 ( O(n^{2}) ),且没有丢解。

    于是,我认为动态规划的本质是在搜索法的前提下,对状态重新编码,将编码相同的状态组织起来,并利用最优子结构的性质剪枝(或加法、乘法原理计数)。而解决动态规划的核心就是考虑用恰当的方法编码状态,保留解决问题需要的全部信息,且尽可能的简单。

    其他一些例子和想法:

    石子合并

    • 记忆化搜索:适合数学苦手,推不出显示的公式。
    • 记得某次TC 500,我逼急了,直接用字符串编码当前状态,记忆化搜索,居然也过了,之后看题解,很类似石子合并。这也算是我写本文的一个动机吧,只要找到了一个正确的编码,能够将状态重新组织起来,方式可能有很多,甚至这么搞,不卡常数也是可以的。。。
    • 本文只是讨论了如何设计问题的状态。对于优化转移,一些情况下可以使用一些数据结构的支持,一些情况下可以通过四边形不等式,单调队列等,技能暂时没有 get,暂不讨论。

    0-1背包问题

    • 动态规划做法,本质上是另一种编码方式。当背包体积为整数且范围不大的时候,这种编码方法是优于暴力枚举的。反过来说,如果背包体积的取值范围很大,这样做相当于把一个一一映射的解空间,映射到一个更大的稀疏的解空间中了,效果可能还不如暴力搜索。

    旅行商问题

    • 显然可以直接记录访问顺序,状态空间是 ( O(N!) )。考虑问题的性质,我们只关心当前位置和还可以访问的点集合,于是,可以以( O(N*2^{N}) ) 编码状态。问题的规模少许得到了简化。举这个例子也是因为我个人的看法,“状态压缩”就是可以直接编码集合,本质还是一样的。
  • 相关阅读:
    多维数组的理解
    C++ STL 中erase()的使用需要小心
    大整数相加和大整数相乘
    project1_calculator(使用tkinter实现python计算器,含有具体过程与注释)
    (转)在Source Insight中看Python代码
    class12_pack_grid_place 放置位置
    (转)AttributeError: module 'tkinter' has no attribute 'messagebox'
    class10_Frame 框架
    class9_Menubar 菜单
    class8_Canvas 画布
  • 原文地址:https://www.cnblogs.com/sweetsc/p/3533473.html
Copyright © 2011-2022 走看看