「学习笔记」动态规划 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 ...
随机推荐
- @hdu - 5503@ EarthCup
目录 @description@ @solution@ @accepted code@ @details@ @description@ n 个队伍两两之间比赛,保证没有平局. 现在给出 n 个队伍分别 ...
- @游记@ CQOI2019(十二省联考)
目录 @day - 0@ @day - 1@ @day - 2@ @后记@ 我只是来打酱油哒-- 顶多能进个 E 类继续打酱油. 原本还在互奶 A 队,结果现在--铁定进不了队啦. 对初中生的歧视啊 ...
- Bert系列(三)——源码解读之Pre-train
https://www.jianshu.com/p/22e462f01d8c pre-train是迁移学习的基础,虽然Google已经发布了各种预训练好的模型,而且因为资源消耗巨大,自己再预训练也不现 ...
- iptables在我们的网络机房实现NAT共享上网
工作环境:上层代理192.168.60.6(4480),只授予教师机(192.168.62.111)使用该代理的权限 目标:不使用squid代理上网,而是使用NAT的方式上网 方法: 1) 确保停止教 ...
- Python--day43--补充之主键和外键
主键只有一个,但是可以用两列不为空的值组成:
- H3C 被动方式建立连接过程
- 用Xshell连接谷歌云
谷歌云服务器,默认用浏览器进行SSH链接,而且也不告知密码.以Centos为例,先使用浏览器连接 1,给root修改密码 1 sudo passwd root 2,编辑ssh配置文件 sudo nan ...
- vue项目安装scss,以及安装scss报错(this.getResolve is not a function)
1.安装scss: npm install node-sass sass-loader vue-style-loader --save-dev //安装node-sass sass-loader vu ...
- 51nod 1307绳子和重物
1307 绳子与重物 题目来源: Codility 基准时间限制:1 秒 空间限制:131072 KB 分值: 40 难度:4级算法题 收藏 关注 有N条绳子编号 0 至 N - 1,每条绳子后 ...
- vue-learning:7-template-v-bind-with-class-and-style
绑定元素样式的指令v-bind:class 和v-bind:style 在HTML元素结构中,class和style特性(attribute)是非常突出的,可以为元素添加样式属性(property). ...