全新的动态规划入门——从维度谈起

转载请注明出处:http://www.cnblogs.com/WABoss/p/DP.html

概论

动态规划(Dynamic Programming, DP)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法……
(先忘了这个吧)允许我从另一个角度去理解并解释动态规划,那么开始吧。

一、维度

首先说明维度(Dimension)这个概念。
这儿有一个很好很有启发意义的回答:http://www.guokr.com/question/584012/?answer=760222#answer760222。
里面提到所谓的维度其实就是允许某种东西自由变化的范围

比如说,人,人在地球上就有无数个维度,包括人当前所在的经度、纬度和高度,还有人的身高、体重和头发颜色之类的,甚至还有时间这个维度。

因此,世间万物都具有无数个维度。事实上在具体的问题中显然并不需要考虑这无数个维度,只需分析具体问题所需要的维度。

比如说,一个导航的程序,人的身高、体重和头发颜色是无所谓的,而经度和纬度是导航程序最关心的两个基础维度,此外还有方向、速度等等维度是和导航相关的,总之具体问题具体分析。

现在联想一下高级语言里数组,如果把多维数组各个维度拿出来看,是不是与物理上的“维度”的概念有种异曲同工之妙?

  • int dp [ n0 ][ n1 ][ n2 ];

比如上面这个多维数组每个维度n0、n1和n2都可以自由在0到ni-1变化,这正是维度的概念!只不过这些维度是些表面没有太多意义的连续非负整数。

如果我们赋予它们意义呢?请看下一节,【状态】。


二、状态

事物具有多个维度,反之多个维度就能共同描述一个事物,而多个维度的值就描述了事物的状态。

比如说,导航程序,经度和纬度这两个维度的值就能描述当前某用户的状态,即这个用户正在某经度某维度的地方。

对应于数组,经度、纬度两个维度对应了二维数组:

  • int dp[MaxLng][MaxLat];

如果多维数组各个维度的值确定后,那么就相当于确定了在多维数组中具体哪一个单元格。因此可以这么说,各个单元格就对应了各个状态,数组就是状态的集合,而状态就是通过确定数组各个维度的值定位的!

  • dp[x][y]所对应的单元格就表示人在经度x纬度y的状态。

不过,单元格可不仅仅是单元格,因为数组可以是bool类型、int类型等等,即这个单元格有值的。这个值就是状态的值,而状态该是什么类型的值,取值是多少这取决于具体的问题。

注意,上面说到值有两个,红色字(加粗),一个是维度的值,一个是状态的值,其中维度的值描述了具体的状态。

比如说,上面的导航程序的数组是int类型,这样dp[x][y]就可以拿来表示人在到达经度x纬度y这个状态所需的时间(分钟数)。

再比如说,如果数组是bool类型,这样dp[x][y]就可以拿来表示人能否到达经度x纬度y这个状态。

在动态规划问题里,状态的值经常是最小值、最大值和方案数等。一般就是从已知的初始状态的值,通过状态的转移,得出最终目标状态的值。

不过,上面提到的这个“状态”还不是动态规划里的“状态”,因为还少了一样东西——请看下一节,【无后效性】。


三、无后效性

动态规划的状态必须是无后效性的。下面我用一道题目具体说明什么是无后效性。
HDU2041 超级楼梯

有一楼梯共M级,刚开始时你在第一级,若每次只能跨上一级或二级,要走上第M级,共有多少种走法?

这个问题里面很容易找到一个维度:楼梯的级数。那么到达第i级台阶就是一个状态,简称状态i,问题所需要求的是到达状态i的方案数,即:

  • dp[i]表示到达第i级台阶的方案数

i的取值是从1到M,也就是说有M个状态。这M个状态之间是有关系的。比如可以从第1级的台阶直接移动到第2级或者第3级,或者第5级台阶可以从第3级或者第4级台阶直接移动到。

下面就M取5画一张状态图
在这里插入图片描述

这张图上面的顶点表示的是各个状态,有向边(弧)就表示状态的转移。事实上这张图是一张有向无环图DAG, Directed Acyclic Graph),即从任何一点出发不可能回到自身。

也就是说在那张图中状态i是通过状态i-1和状态i-2确定的,并且和状态i-3、i-4、i-5…没有一点关系,之后状态i也不会影响到状态i-3、i-4、i-5…,i-3、i-4、i-5状态的值已经是确定好的
这就是无后效性。动态规划的状态必须是无后效性的状态。

动态规划本质上可以理解成在一个DAG上的递推:入度0的状态点就是初始的状态,有向边说明了状态转移的方向,通过状态的转移按着DAG的拓扑序推出最终目标状态的值。而由于是DAG,状态不会重复经过,最多就经过所有的状态。

具体如何转移请看下一节,【状态转移】。

四、状态转移

前面提到了,动态规划的过程就是从初始状态转移到最终的目标状态,而初始状态的值是知道的,目标状态的值就是问题需要的。

还是那一题【超级楼梯】,状态(的值)是这么表示的:

  • dp[i]表示到达第i级台阶的方案数

而转移的表示,通常当然不是画图,而是写出状态转移方程

  • dp[i] = 1 (i=1)
  • dp[i] = dp[i-1] (i=2)
  • dp[i] = dp[i-1] + dp[i-2] (i>2)

为什么是这样呢?可以从之前画的状态图中理解。

首先dp[1]就表示到达台阶1的方案数,一开始就在台阶1,这个状态的值当然就是1了,即dp[1]=1,而事实上这个也正是初始状态;

然后dp[i]=dp[i-1](i=2),也就是说dp[2]=dp[1],因为走到台阶2只有一种策略就会从台阶1走来,因而到达台阶2的方案数就等于达到台阶1的方案数,也就是1;

最后dp[i]=dp[i-1]+dp[i-2](i>2),这个从语义上理解是这样的:走到i-1的方案数是dp[i-1],然后向上走一步就到了i,因而状态i的方案数就有dp[i-1];而走到i-2的方案数是dp[i-2],然后向上走2步就到了i,因而状态i的方案数又可以有dp[i-2]种;总共就是dp[i-1]+dp[i-2]个方案数。

那么有了状态转移方程,就可以动手写程序实现了。具体程序的实现多种多样,但要注意的是状态的值一定要按拓扑序依次求出。下面我讲讲讲三种方式:“人人为我”、“我为人人”和记忆化搜索。其中“人人为我”和“我为人人”这两个是我从北大ACM-ICPC暑期课的课件中学到的。

“人人为我”

这个很常见,就是上面转移方程表示的那样。
  
状态图大概可以这么看:
  
在这里插入图片描述
比如可以看到状态5就是从3和4转移过来的。

写成程序就是这样:

#include<cstdio>
using namespace std;

int dp[41];

int main(){

    dp[1]=1; dp[2]=1;
    for(int i=3; i<=40; ++i){
        dp[i]=dp[i-1]+dp[i-2];
    }

    int N,M;
    scanf("%d",&N);
    while(N--){
        scanf("%d",&M);
        printf("%d\n",dp[M]);
    }
    return 0;
}

“我为人人”

这个是从当前状态顺着推过去,更新能到达状态的值。

这个实现起来挺直观的,因为是顺着往前推。

状态图可以这么看:
在这里插入图片描述
比如可以看到状态1更新到2和3。

写成程序是这样的:

#include<cstdio>
using namespace std;

int dp[44];

int main(){

    dp[1]=1;
    for(int i=1; i<40; ++i){
        dp[i+1]+=dp[i];
        dp[i+2]+=dp[i];
    }

    int N,M;
    scanf("%d",&N);
    while(N--){
        scanf("%d",&M);
        printf("%d\n",dp[M]);
    }
    return 0;
}

记忆化搜索

一般都是用DFS实现,就是用搜索当前状态能从哪儿转移过来。

另外开一个数组记录搜索过的状态的值,避免重复搜索,时间复杂度就不会是指数级的了,而是多项式级。

有时候用记忆化搜索很直观,而且记忆化搜索还可以避免搜索无意义、不需要的状态,从而使效率提升。

这个直接上代码吧:

#include<cstdio>
using namespace std;

int dp[44];

int dfs(int i){
    if(dp[i]!=0) return dp[i];
    return dp[i]=dfs(i-1)+dfs(i-2);
}

int main(){
    dp[1]=1; dp[2]=1;

    int N,M;
    scanf("%d",&N);
    while(N--){
        scanf("%d",&M);
        printf("%d\n",dfs(M));
    }
    return 0;
}

五、最后

如果有什么地方说得不好或者不对的欢迎交流。

博客地址:http://www.cnblogs.com/WABoss

或者QQ联系:2322271369

转载者注:

本文由于为跨平台转载,所以在板式上与原文略有不同,为转载者根据本平台特性所做修改。
本文版权全部归原作者所有。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值