动态规划入门

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划入门相关的知识,希望对你有一定的参考价值。

动态规划入门

 

什么是动态规划?

动态规划(dynamic programming)是求解决策过程(decision process)最优化的数学方法。把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。

动态规划可以分为几类:

线性动规:拦截导弹,合唱队形,挖地雷,建学校,剑客决斗等;

区域动规:石子合并, 加分二叉树,统计单词个数,炮兵布阵等;

树形动规:贪吃的九头龙,二分查找树,聚会的欢乐,数字三角形等;

背包问题:01背包问题,完全背包问题,分组背包问题,二维背包,装箱问题,挤牛奶等;
 
同时,动态规划需要满足一定的条件,主要有两个:
 
1.最优化原理(最优子结构性质)
最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
 
2.无后效性
将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
 
什么是状态转移方程?

给定k阶段状态变量x(k)的值后,如果这一阶段的决策变量一经确定,第k+1阶段的状态变量x(k+1)也就完全确定,即x(k+1)的值随x(k)和第k阶段的决策u(k)的值变化而变化,那么可以把这一关系看成(x(k),u(k))与x(k+1)确定的对应关系,用x(k+1)=Tk(x(k),u(k))表示。这是从k阶段到k+1阶段的状态转移规律,称为状态转移方程。

(以上摘自百度百科)
 
动态规划的搜索实现
动态规划其实可以用我们很常见的搜索方法来实现。为什么搜索这么慢?因为在搜索过程中,有很多数据被重复计算了很多次,极大地浪费了时间。如果我们定义一个数组vis,表示该数据有没有被计算过,如果被计算过,那就直接return;没有计算过就计算。这样一来搜索时间有了质的飞跃,因为如果说以前的搜索是一个健忘症,计算一次忘一次,那么现在每个数被算出来的同时,就被牢牢记住了,不会再算。这种搜索方法因此得名——记忆化搜索。记忆化搜索其实就是动态规划,是动态规划的一种实现方式。记忆化搜索的时间复杂度与用状态转移方程的dp的时间复杂度相差不大,编码较寻常dp较长,但是思维难度低,在个别题目中甚至更优于寻常dp,可以排除一些无效状态。更重要的是搜索还可以剪枝,可能剪去大量不必要的状态,因此在空间开销上往往比动态规划要低很多。所以当你不会打dp时,记忆化搜索是一个很好的选择。
 
例题:

题目描述

观察下面的数字金字塔。

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

         7 
      3   8 
    8   1   0 
  2   7   4   4 
4   5   2   6   5 

在上面的样例中,从7 到 3 到 8 到 7 到 5 的路径产生了最大

输入输出格式

输入格式:

 

第一个行包含 R(1<= R<=1000) ,表示行的数目。

后面每行为这个数字金字塔特定行包含的整数。

所有的被供应的整数是非负的且不大于100。

 

输出格式:

 

单独的一行,包含那个可能得到的最大的和。

 

输入输出样例

输入样例#1:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5 
输出样例#1:
30

说明

题目翻译来自NOCOW。

USACO Training Section 1.5

 

这个题可以算是动态规划的开山鼻祖了,如果我们不知道dp这种东西,那你会怎么做?

 

方法一:爆搜

我们可以爆搜,搜索从顶向下的每一条路径

#include<cstdio>

using namespace std;

const int maxn = 1010;

int r,a[maxn][maxn];

int max(int x,int y) {
    return x > y ? x : y;
}

inline void input() {
    scanf("%d",&r);
    for(int i = 0; i < r; i++)
        for(int j = 0; j <= i; j++)
            scanf("%d",&a[i][j]);
    return;
}

inline int maxnumber(int i,int j) {
    if (i == r)
        return a[i][j];
    return max(maxnumber(i + 1,j),maxnumber(i + 1,j + 1)) + a[i][j];
}

int main() {
    input();
    printf("%d",maxnumber(0,0));
    return 0;
}

 

这样一来我们的搜索总数达到了2^(d-1),其中d是三角形总层数

肯定是不可取的

方法二:记忆化搜索

定义一个数组vis,表示该数据有没有被计算过,如果被计算过,那就直接return;没有计算过就计算。

#include<cstdio>
#include<cstring>

using namespace std;

const int maxn = 1010;

int r,a[maxn][maxn];
int vis[maxn][maxn];

int max(int x,int y) {
    return x > y ? x : y;
}

int maxnumber(int i,int j) {
    if (vis[i][j] == -1) {
        if (i == r)
            vis[i][j] = a[i][j];
        else 
            vis[i][j] = max(maxnumber(i + 1,j),maxnumber(i + 1,j + 1)) + a[i][j];
    }
       return vis[i][j];
}

inline void input() {
    scanf("%d",&r);
    memset(vis,-1,sizeof(vis));
    for(int i = 0; i < r; i++)
        for(int j = 0; j <= i; j++) 
            scanf("%d",&a[i][j]);
    maxnumber(0,0);
}

int main() {
    input();
    printf("%d",vis[0][0]);
    return 0;
}

时间复杂度降成了O(n) !

方法三:动态规划

动态规划问题需要设置状态(一维或多维数组)和转移方程

还是以数字三角形问题为例

我们设f[i][j]表示第i行第j列的点走到底层的最优答案。

这时的f[i][j]就是状态

f[i][j] = a[i][j] + max(f[i+1][j], f[i+1][j+1])

由子问题推导原问题的转移式就是状态转移方程

#include<cstdio>

using namespace std;

const int MAXN = 1010;

int r,a[MAXN][MAXN],f[MAXN][MAXN];

int max(int x,int y) {
    return x > y ? x : y;
}

int main() {
    scanf("%d",&r);
    for (int i = 1; i <= r; i++) 
        for (int j = 1; j <= i; j++) {
            scanf("%d",&a[i][j]);
            f[i][j] = a[i][j];
        }
    for (int i = r - 1; i > 0; i--)
        for (int j = 1; j <= i; j++) 
            f[i][j] += max(f[i + 1][j],f[i + 1][j + 1]);
    printf("%d",f[1][1]);
    return 0;
}

dp代码相对来说要更简洁,但思维难度较高。

入门篇就讲到这里!

(完)

 

 

 

 

 

以上是关于动态规划入门的主要内容,如果未能解决你的问题,请参考以下文章

leetcode专项 动态规划入门

leetcode专项 动态规划入门

leetcode专项 动态规划入门

动态规划入门:01背包问题

递归?这次真不行 —— “动态规划” 入门理解(下)

详细实例说明+典型案例实现 对动态规划法进行全面分析 | C++