写在前面

注意:此文章仅供参考,如发现有误请及时告知。

更新日期:2018/3/16,2018/12/03


动态规划介绍

动态规划,简称DP(Dynamic Programming)

简介1 简介2

动态规划十分奇妙,它可以变身为记忆化搜索,变身为递推,甚至有时可以简化成一个小小的算式。

动态规划十分灵活,例如 NOIP2018 PJ T3 摆渡车 ,写法有很多很多,但时间、内存却各有差异。

动态规划十分简单,有时候一个小小的转移方程就能解决问题。

动态规划十分深奥,有时你会死也想不出合适的转移方程,有时你会被后效性困扰,有时动态规划的同时还有许多蜜汁优化。

动态规划在NOIP中十分重要,我目前为止参加的\(NOIP_{2017 PJ} \& NOIP_{2018PJ}\)都有一道动态规划,而且都是\(T3\)。(估计普及考纲比较窄,要出难题只有DP了)


问题引入

还是这道题...... 数塔问题!!!

这里我们选择动态规划来解决.

我们不难理解,对于每一个元素,它到顶层的最大值是确定的,也就是说,从顶层到任何一个元素的最大值都是确定的.比如,对于第3层的第2个元素6,顶层到它的最大值只有一个(9 + 15 + 6 = 30)(但不代表路径只有一条),不会改变.

所以,我们用一个数组dp来存储从元素(i, j)到底层的最大值.

#define MAXN 100
int dp[MAXN + 5][MAXN + 5];

仔细观察分析,不难发现,对于每一个元素dp[i][j],都存在

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

即每一个元素到(1, 1)的最大值都是上一层与它相连的两个元素中较大的一个,再加上这个元素本身的值. 最后的答案即为dp[1][1].

不过,我们自顶向下分析,但是却要自底向上实现,即从最顶层开始分析,写代码时却要注意for语句要倒过来写:

for ( int i = N; i >= 1; --i )
for ( int j = 1; j <= i; ++j )
dp[i][j] = max( dp[i + 1][j], dp[i + 1][j + 1] ) + a[i][j];

为什么会这样呢?其实不难分析,在算dp[i][j]时,你必须确保dp[i + 1][j] dp[i + 1][j + 1]已经完成,如果没有完成,dp[i + 1][j] dp[i + 1][j + 1]的值就是错误的,算出的dp[i][j]也是错误的,这样结果就不对了。而反过来做,你就会发现i从大的开始,在做dp[i][j]的时候dp[i + 1][1 ~ N]都已经做过了。还有,要注意,动态规划的初始化很重要,有时初始化就会决定你结果对不对。这里的初始化很简单,现在给出两种方法:

memset( dp[N + 1], 0, sizeof( dp[N + 1] ) );//即把dp[N + 1][0...]全部初始化为0.
for ( int i = 1; i <= N; ++i )
dp[i] = a[i];
//下面这个与上面等价:
copy( a[N] + 1, a[N] + N + 1, dp[N] );// copy( 开始地址, 结束地址, 放到的数组 ); copy( a, a + n, b );即为把a数组下标为0~n按次序复制到b数组.
//当然,这样写,实现时要注意少一层循环:(下面这个是修改后的)
for ( int i = N - 1; i >= 1; --i )
for ( int j = 1; j <= i; ++j )
dp[i][j] = max( dp[i + 1][j], dp[i + 1][j + 1] ) + a[i][j];
//至于为什么这样,这里不再赘述,请自己思考.

这里再完整地放一放代码,实在不会写的可以参考.

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100 int C, N;
int a[MAXN + 5][MAXN + 5];
int dp[MAXN + 5][MAXN + 5]; void solve(){
scanf( "%d", &N );
memset( dp, 0, sizeof dp );
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= i; ++j )
scanf( "%d", &a[i][j] );
for ( int i = N; i >= 1; --i )
for ( int j = 1; j <= i; ++j )
dp[i][j] = max( dp[i + 1][j], dp[i + 1][j + 1] ) + a[i][j];
printf( "%d\n", dp[1][1] );
} int main(){
scanf( "%d", &C );
while( C-- ) solve();
return 0;
}

事实上,可以做一个优化:去掉dp数组,直接用a数组来做:(节约空间,人人有责)

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100 int C, N;
int a[MAXN + 5][MAXN + 5]; void solve(){
scanf( "%d", &N );
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= i; ++j )
scanf( "%d", &a[i][j] );
for ( int i = N - 1; i >= 1; --i )
for ( int j = 1; j <= i; ++j )
a[i][j] += max( a[i + 1][j], a[i + 1][j + 1] );
printf( "%d\n", a[1][1] );
} int main(){
scanf( "%d", &C );
while( C-- ) solve();
return 0;
}

至于为什么,请诸位自己理解(很好理解的,选个小一点的数据自己算一算就知道了)。


总结

怎么样,找到些感觉了吧?现在我们来学习怎么写动态规划的程序.

第一步,我们要观察题目是否可以用动态规划实现。怎么判断呢?我们要看它是否可以分成几个阶段,如上题,可以分成1~N层共N个阶段,每个阶段还可以分成1~i个元素共i个小阶段。然后,我们要看看每个阶段的答案是不是确定的,上题中,每一个元素到底层的最大值就是确定的。再看看每个阶段是不是有关联,如果有,还要确定有什么关联,是否对于每一个阶段都满足。

第二步,就是确定关联啦。怎么确定呢?我们要仔细分析题目,观察每两个阶段之间的关系。动态规划的重点也就在这里,关联确定了,动态规划基本上就可以写下来了。

第三步,确定边界条件,比如,上题就要把dp[N+1][...]全部赋值为0,否则就会出错。

除此之外,还要确定完成的顺序,要做某个阶段,它需要用到的阶段必须先做完。

当然,有时还要添加滚动数组、优化等。

这样,一个动态规划程序就完成啦。


尾声

当然,动态规划还有许多分支(背包DP、区间DP等),以上讲的都是最表皮的。那些难一点的,都只好下次再讲吧。

最好拿点题目来练一下:洛谷的DP

「学习笔记」动态规划 I『初识DP』的更多相关文章

  1. 「学习笔记」Min25筛

    「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}​\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...

  2. 「学习笔记」FFT 之优化——NTT

    目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...

  3. 「学习笔记」FFT 快速傅里叶变换

    目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...

  4. 「学习笔记」Treap

    「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...

  5. 「学习笔记」字符串基础:Hash,KMP与Trie

    「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...

  6. 「学习笔记」wqs二分/dp凸优化

    [学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...

  7. 「学习笔记」ST表

    问题引入 先让我们看一个简单的问题,有N个元素,Q次操作,每次操作需要求出一段区间内的最大/小值. 这就是著名的RMQ问题. RMQ问题的解法有很多,如线段树.单调队列(某些情况下).ST表等.这里主 ...

  8. 「学习笔记」递推 & 递归

    引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...

  9. 「学习笔记」min_25筛

    前置姿势 魔力筛 其实不看也没关系 用途和限制 在\(\mathrm{O}(\frac{n^{0.75}}{\log n})\)的时间内求出一个积性函数的前缀和. 所求的函数\(\mathbf f(x ...

随机推荐

  1. oralce 分离表和索引

    总是将你的表和索引建立在不同的表空间内(TABLESPACES). 决不要将不属于ORACLE内部系统的对象存放到SYSTEM表空间里. 同时,确保数据表空间和索引表空间置于不同的硬盘上.   “同时 ...

  2. python不得不知的几个开源项目

    1.Trac Trac拥有强大的bug管理 功能,并集成了Wiki 用于文档管理.它还支持代码管理工具Subversion ,这样可以在 bug管理和Wiki中方便地参考程序源代码. Trac有着比较 ...

  3. js获取当前时间戳以及前一天时间戳

    js获取当前时间戳以及前一天时间戳(毫秒) var timestamp = (new Date()).getTime(); console.log(timestamp);//打印当前时间戳 conso ...

  4. OpenCV 安装与调试

    Visual Studio 是微软提供的面向任何开发者的同类最佳工具. OpenCV(开源计算机视觉库)是一个开源的计算机视觉和机器学习软件库. 目前最新版本:Visual Studio 2019.O ...

  5. Python--day67--Django的路由系统

    原文:https://www.cnblogs.com/liwenzhou/articles/8271147.html Django的路由系统 Django 1.11版本 URLConf官方文档 URL ...

  6. torch中的copy()和clone()

    torch中的copy()和clone() 1.torch中的copy()和clone() y = torch.Tensor(2,2):copy(x) ---1 修改y并不改变原来的x y = x:c ...

  7. codeforces 615A

    题意:给你m个编号为1到m的灯泡:然后n行中每一行的第一个数给出打开灯泡的个数xi 然后是yij是每个灯泡的编号: 题目中有一句话. 我愣是没看,因为我英语真的是一窍不通,看了也白看,直接看数据做的, ...

  8. java声明异常(throws)

    在可能出现异常的方法上声明抛出可能出现异常的类型: 声明的时候尽可能声明具体的异常,方便更好的处理. 当前方法不知道如何处理这种异常,可将该异常交给上一级调用者来处理(非RuntimeExceptio ...

  9. P1095 水仙花数

    题目描述 春天是鲜花的季节,水仙花就是其中最迷人的代表,数学上有个水仙花数,他是这样定义的:"水仙花数"是指一个三位数,它的各位数字的立方和等于其本身,比如:153=1^3+5^3 ...

  10. Postman:非专业的并发测试

    Postman是开发中常用的接口测试工具,也可以用来进行并发测试. 使用方法如下: 1. 直接输入url 选择GET方法,点击Send. 结果打印一个"test",如下: 2. 使 ...