DP动规复习梦忆版
DP 大复习!!
这边 HZOI2026普及组 的学弟学妹们刚刚讲完 DP,被贬的我只好陪他们一起学了咕~
不对啊 Emoji_Happy 不是说学完了吗 等等你怎么也单飞了QAQ
呜呜呜含泪复习
前言
本人行文略有些意识流,属于梦见啥说啥(梦忆版),还请见谅~
本人也是 DP 蒟蒻,不保证思路完全正确,如有错谬还请诸位 Dalao 多多指正咕~
什么是 DP?
Dynamic Programming,简称 DP,即 动态规划。
动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法
———— OI wiki
根据定义,一般情况下只有
- 原问题具有可拆分性,拆分后的子问题能够保持性质(最优)
- 一个子问题不会影响到之前已经求出解其他的子问题;
- 会重复计算很多次同个子问题。
三个条件同时满足时,我们才可以使用 DP。
学术上讲,想要使用 DP 算法,原问题需要满足 最优子结构,无后效性 和 子问题重叠。
如果你想不到 DP 思路,看到题解短短十几到五十几行代码请不要失落,DP 和递归一样,都是思维量极大而代码量极小的。
DP 要怎么做?
首先,你需要根据原题目的细节找到 状态定义,状态转移方程 和 初始化。
一般情况下,我们用数组来定义一个状态,要求能够在可接受范围内简明地表示出每一种状态。
状态转移方程是在状态定义的基础上推导的,所以如果状态定义错了,转移方程也会直接炸掉……
状态转移方程 就是指根据现有的一个状态,要如何推出另一个状态。
一般 由大手子直接从题面里面瞪出来 从题面中的信息推导出来。我们可以先钦定我们已经知道其他所有状态(不要深究这个状态是怎么知道的),唯独有一个状态不知道,我们尝试求这个状态。这个思维过程就是状态转移方程啦。
初始化 是大家经常忘记但非常重要的一件事。不进行初始化的 DP 一定是错误的。如果你发现程序的解不正确,请检查你的初始化。
这一部分同样也是根据题面得到。由于在定义状态转移方程时我们钦定我们已知一个状态,实际上这个状态是什么我们并不知道,初始化就是用来解决这个问题的。
DP 有什么类型?
一般有两种分类方式,第一种是按照是否进行优化来分,有:
- 朴素 DP
- 数据结构优化 DP
- 倍增优化 DP
- 斜率优化 DP
- 决策单调性优化 DP
等等。
也可以 按照适用场合来分,有:
- 线性 DP
- 背包 DP
- 区间 DP
- 树形 DP
- 换根 DP
- 虚树 DP
- 数位 DP
- 状压 DP
- 概率 DP
- DAG 上 DP(图上 DP)
在本篇博客中,作者仅更新作者会的 DP(将在名称上显示链接) 咕……
线性 DP
顾名思义,这类 DP 一般用来解决线性序列上的动规问题。
例题:最长上升子序列
给出一个由 \(n\) 个正整数组成的序列 \(A\),第 \(i\) 个数为 \(a_i\)。请输出这个序列的最长上升子序列的长度。
最长上升子序列是指,从原序列中按顺序且尽可能多地取出一些数字排在一起,这些数字是逐渐增大的。
形式化地,最长上升子序列 \(S\) 中的数应满足 \(\forall i,\space S_i<S_{i+1}\)。
真·线性 DP 典中典。
一般有两种做法,这里只讲 DP 做法,贪心按下不表。
考虑定义 \(f_i\) 表示以 \(a_i\) 结尾的最长上升子序列的长度。
那么用人类的智慧可以想到:
好的那么直接遍历所有的 \(j<i\),寻找满足条件的最大的 \(f_j\) 转移即可。
时间复杂度:\(\mathcal{O}(n^2)\),空间复杂度:\(\mathcal{O}(n)\)。
#include<bits/stdc++.h>
using namespace std;
const int N=5020;
int f[N],a[N];
int n, ans;
int main(){
cin>>n;
for(int i=1; i<=n; i++) cin>>a[i];
for(int i=1; i<=n; i++) f[i]=1;//初始化:对于每个单个节点,单独一个节点也是上升子序列。
for(int i=2; i<=n; i++){
for(int j=1; j<i; j++){
if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
}
}
for(int i=1; i<=n; i++) ans=max(ans, f[i]);
cout<<ans<<endl;
return 0;
}
例题:最大字段和 By Luogu
给出一个长度为 \(n\) 的序列 \(a\),选出其中连续且非空的一段使得这段和最大。
啥这题是线性 DP?
要求什么我们就定义什么。定义 \(f_i\) 表示以 \(i\) 结尾的最大子段和。
考虑对于每一个数 \(a_i\),我们有两种选择:
- 把这一个数接到已有的、以 \(a_{i-1}\) 结尾的段上;
- 新开一段。
即:
好的直接上 Code!
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+7;
int a,f[N],n,ans=INT_MIN;
int main(){
cin>>n;
for(int i=1; i<=n; i++){
cin>>a;
f[i]=max(f[i-1]+a,a);
ans=max(f[i],ans);
}
cout<<ans;
return 0;
}
当然,发现这个 \(f_i\) 的值只和 \(f_{i-1}\) 有关,那么即使这一维我们也可以直接压掉:
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+7;
int a,pre,ans=INT_MIN,n;//pre 是压维后的 f
int main(){
cin>>n;
for(int i=1; i<=n; i++){
cin>>a;
pre=max(a,a+pre);
ans=max(pre,ans);
}
cout<<ans;
return 0;
}
时间复杂度:\(\mathcal{O}(n)\),空间复杂度:\(\mathcal{O}(1)\)。
通过上面的这些例题不难发现,我们一般只要瞪出状态定义和状态转移方程,基本上就能得到解了。
但是考试的时候可不会这么仁慈 Ψ(▼w▼)
练习题:
这道题涉及到偏序问题了,不过还能做,正好了解一下~
后记:
不定时更新咕。
特别鸣谢
感谢 Luogu 提供习题与链接。
感谢 HUGE 教练。
感谢 OI wiki 提供标准知识定义。
感谢 每一位看到这里的读者!

浙公网安备 33010602011771号