「学习笔记」动态规划 I『初识DP』
写在前面
注意:此文章仅供参考,如发现有误请及时告知。
更新日期: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』的更多相关文章
- 「学习笔记」Min25筛
「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...
- 「学习笔记」FFT 之优化——NTT
目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...
- 「学习笔记」FFT 快速傅里叶变换
目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...
- 「学习笔记」Treap
「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...
- 「学习笔记」字符串基础:Hash,KMP与Trie
「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...
- 「学习笔记」wqs二分/dp凸优化
[学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...
- 「学习笔记」ST表
问题引入 先让我们看一个简单的问题,有N个元素,Q次操作,每次操作需要求出一段区间内的最大/小值. 这就是著名的RMQ问题. RMQ问题的解法有很多,如线段树.单调队列(某些情况下).ST表等.这里主 ...
- 「学习笔记」递推 & 递归
引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...
- 「学习笔记」min_25筛
前置姿势 魔力筛 其实不看也没关系 用途和限制 在\(\mathrm{O}(\frac{n^{0.75}}{\log n})\)的时间内求出一个积性函数的前缀和. 所求的函数\(\mathbf f(x ...
随机推荐
- uva 11665 Chinese Ink (几何+并查集)
UVA 11665 随便给12的找了一道我没做过的几何基础题.这题挺简单的,不过uva上通过率挺低,通过人数也不多. 题意是要求给出的若干多边形组成多少个联通块.做的时候要注意这题是不能用double ...
- 支付宝防并发方案之"一锁二判三更新"
每年支付宝在双11和双12的活动中,都展示了绝佳的技术能力.这个能力不但体现在处理高TPS量的访问,更体现在几乎不会出错,不会出现重复支付的情况,那这个是怎么做到的呢? 诚然,为了实现在高并发下仍不会 ...
- 可运行的js代码
canrun <html> <head> <title>测试博客园HTML源码运行程序</title> <meta http-equiv=&quo ...
- phpStudy中升级MySQL版本到5.7.17的方法步骤
http://www.jb51.net/article/120263.htm 前言 今天在工作发现一个错误,在往本地导数据表的时候老是报错: ? 1 [Err] 1294 - Invalid ON U ...
- codeforce 379(div.2)
A.B略 C题 ——贪心,二分查找: 对于每一个a[i], 在d中二分查找 s-b[i],注意不要忘记计算速度为x时需要花费的最小时间,以及整数范围为64位整数 1 #include <cstd ...
- windows命令行下redis读取中文字符乱码
我在eclipse上对redis进行了一个操作,添加了一个中文字符串进去,可以看到是添加成功了的 但是在命令行中读取的时候却成了乱码,如下图所示 这是因为windows命令行的编码是gbk 可以通过如 ...
- P1023 活动安排
题目描述 某个人可以在n个活动中选择一些出来参加.每个活动都有起止时间.而且每个时间段只能参加一个活动.问,这个人最多能加参加几个活动. 可以在活动结束时,立即开始新的活动. 输入格式 第一行是一个整 ...
- 中文javadoc
大量的中文注释,其实最大的目的是想生成中文的Javadoc.但是中文注释实在太不高级了,在各种编码下还会变成乱码.本着高端大气国际化的思路,还是想把它换成英文,但是又不想放弃中文的Javadoc,怎么 ...
- 解决netcore在docker容器中连接oracle报错(timezone region not found)
错误提示: timezone region not found错误原因:docker 容器内时区不是 CST 导致解决办法:1.在dockerfile 中增加一下命令ENV TZ=Asia/Shang ...
- .Net Core解除文件上传大小限制
一共要修改两处地方: \Startup.cs public void ConfigureServices(IServiceCollection services) { services.AddMvc( ...