「学习笔记」动态规划 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 ...
随机推荐
- day3_python之函数基础知识
一 .为何要用函数之不用函数的问题 #1.代码的组织结构不清晰,可读性差 #2.遇到重复的功能只能重复编写实现代码,代码冗余 #3.功能需要扩展时,需要找出所有实现该功能的地方修改之,无法统一管理且维 ...
- win10如何关闭计算机设备和驱动器非硬盘图标
按win键+R,打开注册表regedit,找到这个路径: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\My ...
- canvas简单动画
实现场景:定义一个1000*800的方框,圆球在其中移动,碰撞到边框弹回的动画.方框背景是半径为10的小圆球组成.鼠标移动到移动圆球时,圆球停止运动. html代码: <div> < ...
- 2019-7-29-NetBIOS-计算机名称命名限制
title author date CreateTime categories NetBIOS 计算机名称命名限制 lindexi 2019-07-29 09:59:17 +0800 2018-12- ...
- 详解ThinkPHP支持的URL模式有四种普通模式、PATHINFO、REWRITE和兼容模式
URL模式 URL_MODEL设置 普通模式 0 PATHINFO模式 1 REWRITE模式 2 兼容模式 3 如果你整个应用下面的模块都是采用统一的URL模式 ...
- 谈谈数据库的 ACID(转)
一.事务 定义:所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位. 准备工作:为了说明事务的ACID原理,我们使用银行账户及资金管理的案例进行分析. 二.ACI ...
- Fragment开发实战(二)
由于在Android的实现机制中Fragment和Activity会被分别实例化为两个不相干的对象,他们之间的联系由Activity的一个成员对象Fragmentmanager来维护.Fragment ...
- 浅谈集合框架六——集合扩展:Arrays工具类、集合与数组相互转换方式;
最近刚学完集合框架,想把自己的一些学习笔记与想法整理一下,所以本篇博客或许会有一些内容写的不严谨或者不正确,还请大神指出.初学者对于本篇博客只建议作为参考,欢迎留言共同学习. 之前有介绍集合框架的体系 ...
- H3C 单区域OSPF配置示例二
- linux模块参数
驱动需要知道的几个参数因不同的系统而不同. 从使用的设备号( 如我们在下一章见到的 ) 到驱动应当任何操作的几个方面. 例如, SCSI 适配器的驱动常常有选项控制标记命令队列 的使用, IDE 驱动 ...