1. 动态规划简介 分享至QQ空间

发布时间: 2018-09-04 10:00:26.0 点击: 169

动态规划用于解决一类复杂问题,它将问题分解为子问题进行求解先,并存储子问题结果以避免反复求解。一般具有以下两个重要性质的问题都可以通过动态规划求解:

(1)重叠子问题:类似分治法,动态规划也是利用子问题进行求解,如果很多子问题需要反复求解,那么存储起来就可以避免重复计算,但如果没有重叠子问题,动态规划就失去意义。比如在二分查找中,各个子问题彼此不重叠,不需要动态规划。但在求斐波那契数时,很多子问题需要反复计算:

int fib(int n)

{

   if ( n <= 1 )

      return n;

   return fib(n-1) + fib(n-2);

}

QQ截图20180904100301.jpg

比如在求解fib(5)时,fib(3)需要调用2次,最好存储起来避免反复调用。

有两种方法用来存储中间变量,分别是造表和记忆化。

(2)最优子结构:即问题的最优解可以通过各个子问题中求最优解得到,这种性质称为最优子结构。比如在求解最短路径时,如果x位于从顶点u到顶点v的最短路径上,那么这条最短路径必然是从ux的最短路径和从xv的最短路径组合而成。其中Floyd-WarshallBellman-Ford算法都是典型例子。

反过来,求最长路径(不含回路)就不满足最优子结构性质,比如在下图中,从qt的最长路径有两条:q->r->tq->s->t,其中第一条它不是qr的最长路径(q->s->t->r)和rt的最长路径(r->q->s->t)组成的。

1536026269484054018.gif 

(3)造表VS记忆化

在求解动态规划过程中,为了存储计算过的中间值(为了重用),有两个方法:造表(自底向上)和记忆化(自顶向下)。

在状态转换时,如果我们以dp[0]为基础推导出dp[x],再推导出dp[x+1],最终求得我们的目标dp[n],我们就称之为自底向上。比如当我们求解斐波那契数列时:

// 造表求斐波那契数列

int dp[MAXN];

int dp[0] = 1;//基础状态

for (int i = 1; i< =n; i++)

{

    dp[i] = dp[i-1] * i;

}

这里的状态转换是线性的,因此只要一维存储。

如果,我们在求dp[n]时,不是从基础状态(如dp[0])推导,而是去寻求能够产生dp[n]的前导状态,那么这种方法就称为自顶向下的。

// 为了提高效率,存储中间值(-1表示未求过),记忆化求斐波那契数列,

int dp[MAXN]

int solve(int x)

{

    if (x==0)

        return 1;

    if (dp[x]!=-1)

        return dp[x];

    return (dp[x] = x * solve(x-1));

}

同样,这里记忆的变量是一维数组,今后会碰到二维情况。

最后总结一下它们的特点:


造表

记忆化

状态转换

不容易找

容易找

代码

相对复杂

简单

速度

递归调用,相对慢

子问题求解

所有子问题必须先求解一遍才能进行查表

边查表边求解,可能跳过一些求解

填表

从初始状态按顺序挨个填

根据实际求解填,不一定有顺序

 

(4)如何求解动态规划问题

根据动态规划的两个性质,步骤如下:

1)确定是否是动态规划问题:一般求最小值或最大值或者计数这些问题可能会是动态规划问题,再检查子问题是否重叠,是否满足最优子结构性质,如果满足一般就是动态规划问题。

2)使用最少的参数来定义状态:DP的主要步骤就是状态的变换,因此状态定义很关键,状态用来表达问题中间某种值,参数要尽可能少以用来节省状态空间。比如在求解背包问题时,使用索引(index)和背包容量(weight)两个参数来定义状态DP[index][weight],即最优解可以通过第0index之间容量为weight的不同值得到。

3)使用公式表示状态之间的关系:这是相对来说是最为复杂的一步,需要多加观察和实践。举个例子:

给定3个整数组成的集合{1, 3, 5}和一个整数N,求集合里若干个元素(可以使用0~无穷多次)之和等于N的方法数(顺序不同属于不同的方法)。当N=6时答案为8

1+1+1+1+1+1

1+1+1+3

1+1+3+1

1+3+1+1

3+1+1+1

3+3

1+5

5+1

使用动态规划进行思考:首先我们使用参数n来确定一个状态,表示为state(n),即state(n)是指使用{1, 3, 5}来分解n的方法数。接下来要思考状态之间的关系,假设我们已经求得了n=1,2,3,4,5,6时的状态即state(n=1)state(n=2)state(n=3)...state(n=6),考虑求解state(n=7),因为我们只能添加135,因此我们分三种方法添加:

1) state (n = 6)基础上加1
Eg : [ (1+1+1+1+1+1) + 1]
[ (1+1+1+3) + 1]
[ (1+1+3+1) + 1]
[ (1+3+1+1) + 1]
[ (3+1+1+1) + 1]
[ (3+3) + 1]
[ (1+5) + 1]
[ (5+1) + 1]

2) state (n = 4)基础上加3;

Eg : [(1+1+1+1) + 3]
[(1+3) + 3]
[(3+1) + 3]

3) state(n = 2)基础上加5
Eg : [ (1+1) + 5]

以上已经全部覆盖了和为7的情况,因此得到状态之间关系:

state(7) = state (6) + state (4) + state (2)

state(7) = state (7-1) + state (7-3) + state (7-5)

推广到n的情况则:
state(n) = state(n-1) + state(n-3) + state(n-5)

递归的代码如下:

int solve(int n)

{

   if (n < 0)

      return 0;

   if (n == 0)  

      return 1;  

   return solve(n-1) + solve(n-3) + solve(n-5);

}

4)造表或者记忆化

上述递归代码很容易改造成记忆化(你也可以自行通过造表进行递推解决):

int dp[MAXN];//需要事先初始化为-1

int solve(int n)

{

  if (n < 0)  

      return 0;

  if (n == 0)  

      return 1;

   if (dp[n]!=-1) //检查是否已经求解

      return dp[n];

    return dp[n] = solve(n-1) + solve(n-3) + solve(n-5);// 存储结果并返回

}



|返回 |   | 转到页头|
Copyright @ 2008-2024(浙ICP备2022001332号), TZOJ. All Rights Reserved.