DP 详解
DP 概述
DP(Dynamic programming,全称动态规划),是一种基于分治,将原问题分解为简单子问题求解复杂问题的方法。
动态规划的耗时往往远少于朴素(爆搜)解法。
动态规划 and 递归
之前说过,动态规划也是分治思路,而递归更是传统的分治思路,但时间复杂度却大相径庭,为什么呢?
动态规划是 自顶向上 思想,而递归是 自顶向下 解法。
自顶向上 and 自顶向下?
自顶向上
意思很简单,从下往上推导:\(f(1) \rightarrow f(2) \rightarrow \dots \rightarrow f(n - 1) \rightarrow f(n)\)。
这也是为什么 动态规划算法 脱离了 递归 的函数,改用循环迭代推到的原因。
自顶向下
反过来,自顶向下就是从上往下推,触底后在将结果返回回来。
\(f(n) \rightarrow f(n - 1) \rightarrow \dots \rightarrow f(2) \rightarrow f(1) \rightarrow f(2) \rightarrow f(3) \rightarrow \dots \rightarrow f(n - 1) \rightarrow f(n)\)
这也是为什么递归比动态规划时间复杂度高的多的原因。
我们可以看出,动态规划更像是递推算法的 plus 版。
状态转移方程
状态转移方程,就是如何将子问题转移至父亲问题的公式。
在简单 DP 中,转移方程可以直接套用至 dfs, bfs 等爆搜算法。
DP 最难的部分就是列出状态转移方程,如果没有状态转移方程,一切都白搭。
例:设 \(f_i\) 为数列第 \(i\) 为的数,斐波那契数列的状态转移方程为 \(f_i = f_{i - 1} + f_{i - 2}\)。
DP 如下:
f[1] = 1;
f[2] = 1;
for (int i = 3; i <= n; i++)
f[i] = f[i - 1] + f[i - 2]; // 转移方程
cout << f[n];
同样的,我们可以将转移方程套用在递归暴力上:
int f(int n)
{
if (n == 1 || n == 2)
return 1;
return f(n - 1) + f(n - 2); // 转移方程
}
动态规划要素
最优子结构:问题的最优解 包含 子问题最优解。即为:局部最优解 = 全局最优解。
无后效性:
在推导后面状态时,仅在意前面状态数值,不在意是如何推导出来的。
某状态确定后,不会因为后面的决策而改变前面的决策。
重叠子问题:不同的决策到达相同的状态时可能产生重复的状态,为了避免不必要的计算,我们通常使用 记忆化搜索(在计算出新状态时将它存储起来一遍下次使用)来解决,这也是最经典的 空间换时间。
不满足这三点你还想 DP?想 peach 呢?
状态的定义
前言:空间换时间
很简单的名字,即为使用空间的代价来确保不会超时。
状态?
状态,通俗来讲就是你 \(f_{xxx}\) 代表的是什么。比如斐波那契数列中 \(f_i\) 代表的就是第 \(i\) 为是什么。
对于状态:
状态越多,表示的信息越多,空间越大。
反之,状态越少,表示的信息越少,空间越小。
在我们状态定义时,可能有这些情况:
\(部分情况 \begin{cases} 状态太少?\begin{cases} 信息量太少 & 无解 \\\\ 信息量太少 & 不满足动态规划要素 \end{cases} \\\\ 状态太多? \begin{cases} 空间太大 & MLE \\\\ 需要太多时间更新状态 & TLE \end{cases} \end{cases}\)
所以,状态 and 状态转移方程时整个动态规划中最最最难的部分,想清楚这两点,这题也就解出来了。
参考资料
https://zh.wikipedia.org/wiki/动态规划
五大基本算法之动态规划算法 DP dynamic programming
例题
例题一思路
纯 DP
没看数据:好一个 dfs!
注:两种情况
拿本物品
3 倍奖金?
1 倍奖金?
不拿本物品
ll dfs(int i, int now, ll cnt)
{
if (i == n + 1)
return cnt;
if (!((now + 1) % 3) && ((now + 1) >= 3))
return max(dfs(i + 1, now + 1, cnt + (a[i] * 3)), dfs(i + 1, now, cnt));
else
return max(dfs(i + 1, now + 1, cnt + a[i]), dfs(i + 1, now, cnt));
}
我们看题面,一眼看出的状态为:\(f_i\) 表示前 \(i\) 个物品获得的最大奖金。
但是,我们发现不满足无后效性。
根据上述方法,我们尝试使用空间的代价来优化。
将状态改为:\(f_{i, j}\) 表示前 \(i\) 个物品,当前物品数取余 \(3\) 为 \(j\) 时获得的最大奖金。
\(f{i, j} = \begin{cases} j = 0 \begin{cases} i \ge 3 \begin{cases} f_{i - 1, 0} & 不拿 \\\\ f_{i - 1, 2} + a_i \times 3 & 拿 \end{cases} \\\\ f_{i - 1, 0} & 没有到 3 个,不存在这种情况。 \end{cases} \\\\ j = 1 \begin{cases} f_{i - 1, 1} & 不拿 \\\\ f_{i - 1, 0} + a[i] & 拿 \end{cases} \\\\ j = 2 \begin{cases} i \ge 2 \begin{cases} f_{i - 1, 2} & 不拿 \\\\ f_{i - 1, 1} + a_i & 拿 \end{cases} \\\\ f_{i - 1, 2} & 没有至少 2 个物品,没有这种情况。 \end{cases} \end{cases}\)
完整代码为:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n;
ll a[100005];
/*
20PTS
ll dfs(int i, int now, ll cnt)
{
if (i == n + 1)
return cnt;
if (!((now + 1) % 3) && ((now + 1) >= 3))
return max(dfs(i + 1, now + 1, cnt + (a[i] * 3)), dfs(i + 1, now, cnt));
else
return max(dfs(i + 1, now + 1, cnt + a[i]), dfs(i + 1, now, cnt));
}
*/
ll f[100005][3];
ll ans;
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
// cout << dfs(1, 0, 0) << "\n";
for (int i = 1; i <= n; i++)
{
f[i][0] = f[i - 1][0];
f[i][1] = f[i - 1][1];
f[i][2] = f[i - 1][2];
if (i >= 3)
f[i][0] = max(f[i][0], f[i - 1][2] + (a[i] * 3));
f[i][1] = max(f[i][1], f[i - 1][0] + a[i]);
if (i >= 2)
f[i][2] = max(f[i][2], f[i - 1][1] + a[i]);
ans = max(ans, f[i][0]);
ans = max(ans, f[i][1]);
ans = max(ans, f[i][2]);
}
cout << ans << "\n";
return 0;
}
首先,我们欣赏一下原出题人的提示。

例题二前言:分类讨论
在看了许多不当人的讲解后,我浓缩出:分类讨论就是分类 --> 讨论!分类讨论就是将问题通过不同的结果 / 形式 / 不同点分成几类逐个解决。
例题二思路
既然说到分类讨论我们先来分个类。
\(\max(\sum_{i = 1}^{N} A_i) = \begin{cases} C > 0 & \max(\sum_{i = L}^{R} A_i) \times C \\\\ C < 0 & \min(\sum_{i = L}^{R} A_i) \times C \end{cases}\)
最大最小怎么使用 \(O(N)\) 求?Bingo!最大 / 最小 子段和即可。
最后比一下就好了。
完整 Code:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n;
ll c;
ll a[100005];
ll solve()
{
ll original_sum = 0;
for (int i = 1; i <= n; ++i)
original_sum += a[i];
ll dp_max[100005], dp_min[100005];
dp_max[1] = a[1];
dp_min[1] = a[1];
ll maxx = dp_max[1];
ll minn = dp_min[1];
for (int i = 2; i <= n; i++)
{
dp_max[i] = max(a[i], dp_max[i - 1] + a[i]);
dp_min[i] = min(a[i], dp_min[i - 1] + a[i]);
maxx = max(maxx, dp_max[i]);
minn = min(minn, dp_min[i]);
}
ll res = max((c - 1) * maxx, (c - 1) * minn);
ll ans = original_sum + res;
return ans;
}
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; ++i)
cin >> a[i];
cout << solve() << endl;
return 0;
}
DP 详解的更多相关文章
- 数位DP 详解
序 天堂在左,战士向右 引言 数位DP在竞赛中的出现几率极低,但是如果不会数位DP,一旦考到就只能暴力骗分. 以下是数位DP详解,涉及到的例题有: [HDU2089]不要62 [HDU3652]B-n ...
- 动态规划晋级——HDU 3555 Bomb【数位DP详解】
转载请注明出处:http://blog.csdn.net/a1dark 分析:初学数位DP完全搞不懂.很多时候都是自己花大量时间去找规律.记得上次网络赛有道数位DP.硬是找规律给A了.那时候完全不知数 ...
- 数位DP详解
算法使用范围 在一个区间里面求有多少个满足题目所给的约束条件的数,约束条件必须与数自身的属性有关 下面用kuangbin数位dp的题来介绍 例题 不要62 题意:在一个区间里面求出有多少个不含4和6 ...
- 状压DP详解(位运算)
前言: 状压DP是一种非常暴力的做法(有一些可以排除某些状态的除外),例如dp[S][v]中,S可以代表已经访问过的顶点的集合,v可以代表当前所在的顶点为v.S代表的就是一种状态(二进制表示),比如 ...
- 状态压缩dp 状压dp 详解
说到状压dp,一般和二进制少不了关系(还常和博弈论结合起来考,这个坑我挖了还没填qwq),二进制是个好东西啊,所以二进制的各种运算是前置知识,不了解的话走下面链接进百度百科 https://baike ...
- 线性DP详解
顾名思义,线性DP就是在一条线上进行DP,这里举一些典型的例子. LIS问题(最长上升子序列问题) 题目 给定一个长度为N的序列A,求最长的数值单调递增的子序列的长度. 上升子序列B可表示为B={Ak ...
- 数位dp详解&&LG P2602 [ZJOI2010]数字计数
数位dp,适用于解决一类求x~y之间有多少个符合要求的数或者其他. 例题 题目描述 杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除 ...
- 状态压缩动态规划(状压DP)详解
0 引子 不要999,也不要888,只要288,只要288,状压DP带回家.你买不了上当,买不了欺骗.它可以当搜索,也可以卡常数,还可以装B,方式多样,随心搭配,自由多变,一定符合你的口味! 在计算机 ...
- 状压DP详解+题目
介绍 状压dp其实就是将状态压缩成2进制来保存 其特征就是看起来有点像搜索,每个格子的状态只有1或0 ,是另一类非常典型的动态规划 举个例子:有一个大小为n*n的农田,我们可以在任意处种田,现在来描述 ...
- 树形DP详解+题目
关于树形dp 我觉得他和线性dp差不多 总结 最近写了好多树形dp+树形结构的题目,这些题目变化多样能与多种算法结合,但还是有好多规律可以找的. 先说总的规律吧! 一般来说树形dp在设状态转移方程时都 ...
随机推荐
- SMU Summer 2023 Contest Round 6
SMU Summer 2023 Contest Round 6 A. There Are Two Types Of Burgers 从0枚举到汉堡的最大个数,取最大值 #include <bit ...
- 新员工一口气写完了这些C语言例子,领导给他转正了!
持续更新中... 很多想从事嵌入式Linux开发的老铁问一口君,有没有快速提升自己编程水平的小例子? 一口君根据自己多年工作经验,整理了一些基于Linux的c语言的非常实用的小例子, 这些例子在嵌入式 ...
- flink + iceberg 快速搭建指南
flink + iceberg 快速搭建 the environment includes: minio iceberg flink Centos 更换 tencent 的yum源 备份系统旧配置文件 ...
- 通过 ob-operator 部署 OceanBase 数据库
本文介绍如何通过 ob-operator 来部署 OceanBase 数据库. 背景信息 ob-operator 与其他 operator 一样,旨在让 OceanBase 以容器的方式,无缝运行在 ...
- 讲讲Java的序列化反序列化?
序列化:把对象转换为字节序列的过程称为对象的序列化. 反序列化:把字节序列恢复为对象的过程称为对象的反序列化. 什么时候会用到 当只在本地 JVM 里运行下 Java 实例,这个时候是不需要什么序列化 ...
- 注册中心Nacos集群搭建
一提到注册中心,大家往往想到Zookeeper.或者Eureka.今天我们看看阿里的一款配置中心+注册中心的中间件--Nacos.有了它以后,我们的项目中的配置就可以统一从Nacos中获取了,而且Sp ...
- 走进Docker的世界--(库存学习笔记)
第一天 走进Docker的世界 介绍docker的前世今生,了解docker的实现原理,以Django项目为例,带大家如何编写最佳的Dockerfile构建镜像.通过本章的学习,大家会知道docker ...
- Serilog文档翻译系列(一) - 入门指南
保持优质文档是 Serilog 的优先事项.如果你发现文档中有缺失或不准确的内容,或者希望通过添加主题或教程来扩展 wiki,请通过问题追踪系统告知我们. 为什么选择Serilog? 与许多其他 .N ...
- mybatis springboot多数据源,根据使用的数据库不同,执行不同的sql语句
springboot 多数据源配置就不说了,百度太多的用例, 这里只说下在多数据源下切换执行sql逻辑 1.xml sql嵌套,通过<if>标签来判断,用的是mybatis自己sql动态拼 ...
- 全网最适合入门的面向对象编程教程:46 Python函数方法与接口-函数与事件驱动框架
全网最适合入门的面向对象编程教程:46 Python 函数方法与接口-函数与事件驱动框架 摘要: 函数是 Python 中的一等公民,是一种可重用的代码块,用于封装特定的逻辑:事件驱动框架是一种编程模 ...