一些浅显的 dp 优化策略
前缀和优化
这个优化的方法还是显然的,就是当遇到形如 当前状态是由先前的连续状态转移, 则考虑将这种 连续 用前缀和维护,一般便会将这一部分的复杂度由
对于我最近遇到的题来说是这样思考的,当然,更重要的还是优化前的转化,比如通过推式子形成可以前缀和处理的求和式
[ABC179D] Leaping Tak
考虑定义
朴素地想,如果设给定的这些不重合区间的数的集合为
但是这样的复杂度是
进一步,发现这样算没有用到
这就好了, 发现
启发我们可以搞一个前缀和数组
注意
,所以 会更大些在右边,我一开始就搞反了
这样就把里边那层求和式消掉了,复杂度降到
code
#include <bits/stdc++.h> #define re register int #define max(x, y) (x > y ? x : y) using namespace std; typedef long long LL; const int N = 2e5 + 10, mod = 998244353; int n, k, L[20], R[20]; LL f[N], s[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n >> k; for (re i = 1; i <= k; i ++) cin >> L[i] >> R[i]; s[1] = f[1] = 1; for (re i = 2; i <= n; i ++) for (re j = 1; j <= k; j ++) { f[i] = (f[i] + s[max(i - L[j], 0)] - s[max(i - R[j] - 1, 0)] + mod) % mod; s[i] = (s[i - 1] + f[i]) % mod; } cout << f[n] % mod << '\n'; return 0; }
练习:
[ABC222D] Between Two Arrays
[ABC183E] Queen on Grid
AT_dp_m Candies
UVA1650 / hdu4055 数字串 Number String
hdu6078 Wavel Sequence
单调队列优化
看别人博客新学的一个词,1d / 1d dp,单调队列就是对这种 dp 进行优化的
tip:所谓 xd / yd dp 指的是状态数有
种,每个状态的转移数有 种的 dp
显然就是针对线性 dp 的策略(当然也可能是多维的),不扯了 ......
其实就是通过维护一个单调队列 及时排除不可能成为最优转移的位置,将寻找决策点的复杂度均摊成
具体的,就是通过三个步骤:及时将队头出队、与当前值比较不断删去队尾更劣的值、加入当前值。
tip:在这一部分的实现过程中,我就遇到了一些很小的,但是至关重要的细节处理问题,比如:
- 队列指针 h,t 初值是 h = 1, t = 0 还是 h = t = 1。
要从含义出发,h < t 说明队列是空的,而当有的问题需要调用 0 这个位置的元素时,就需要提前入队一个 q[h] = 0 的元素,这个不好说,要具体分析- 决策与队尾入队的顺序。
这个就是看当前转移是否允许可以由自己转移咯
实现维护决策单调性,从而使队头元素总是当前的最优决策
而每个元素至多入队出队一次,每个元素的决策便是
P3572 [POI2014] PTA-Little Bird
首先考虑朴素 dp,
很容易想到定义一个当前位置的最小劳累值
从第一个位置飞,所以初始化
这样就得到了一个
50pts code
#include <bits/stdc++.h> #define re register int using namespace std; const int N = 1e6 + 10; int n, q, k, d[N], f[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n; for (re i = 1; i <= n; i ++) cin >> d[i]; cin >> q; while (q --) { cin >> k; memset(f, 0x3f, sizeof(f)); f[1] = 0; for (re i = 2; i <= n; i ++) for (re j = k; j >= 1; j --) { if (i - j >= 1) { int w = (d[i] >= d[i - j] ? 1 : 0); f[i] = min(f[i], f[i - j] + w); } } // for (re i = 1; i <= n; i ++) cout << i << ' ' << f[i] << '\n'; cout << f[n] << '\n'; } return 0; }
而瓶颈就在于,每次对于当前位置
但显然,有的位置是重复扫描过多次的,浪费了时间
我们希望每个位置都只扫一遍,同时维护当前状态的最优决策,考虑用单调队列维护,其实这就是一个滑动窗口的简单变式嘛,板
于是降到
注意:
- 因为要从第一个位置转移(要不然从 f[0] 转移显然是不符合含义的),所以要在队列中放一个 1
- 当前位置不能由自己转移,所以决策要先于当前位置入队
- 这题维护的对象的比较规则中不只有一个元素,这是比较特殊的地方,因为这题有两个东西影响到最优性决策,f[] 和 d[]
我们可以简单分类讨论一下,现有队尾元素
当
当
当
第三种情况可能成为最优决策保留,前两种决策可以看出第一种总是不会优于第二种,所以就不用再讨论 d[],当然第二种贡献为 0 也可能成为最优决策保留
code
#include <bits/stdc++.h> #define re register int using namespace std; const int N = 1e6 + 10; int n, T, k, d[N], f[N]; int q[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n; for (re i = 1; i <= n; i ++) cin >> d[i]; cin >> T; while (T --) { cin >> k; int h = 1, t = 1; q[h] = 1; for (re i = 2; i <= n; i ++) { if (h <= t && q[h] < i - k) h ++; int w = (d[q[h]] <= d[i] ? 1 : 0); f[i] = f[q[h]] + w; while (h <= t && (f[q[t]] > f[i] || (f[q[t]] == f[i] && d[q[t]] <= d[i]))) t --; q[++ t] = i; } cout << f[n] << '\n'; } return 0; }
练习:
CF1077F2 Pictures with Kittens (hard version)
数据结构优化
通常是用线段树、树状数组(甚至平衡树等,不过太难了对我 qwq)来维护,众所周知,这些数据结构可以很高效地维护区间和、最值等操作,
而 dp 转移 中,对决策集合也经常要涉及这样的操作,
所以,数据结构优化就是从转移的角度,将朴素地循环枚举决策的时间变成维护数据结构的时间,一般就可以实现复杂度由
tip:所以,必须保证决策集合的下标是连续的,或是限制条件
LIS
惊喜?意外?我觉得例题还是不能放有太多弯弯绕绕的转化的,这题正好
求 LIS,朴素 dp 人尽皆知咯:
显然这样做是
考虑优化
主要是内层循环,要实时对 1 ~ i - 1 的决策集合取 max,同时要更新 i 决策的值
发现这两种操作,不就是求区间 min,单点修改吗?用线段树直接维护它
考虑转移方程共有两个限制,
那就直接 按值建线段树 -> 限制条件值域线段树(当然,这样做就经常需要结合离散化) 就好啦,对
这样用线段树替换掉循环,复杂度优化到
code
#include <bits/stdc++.h> #define re register int #define lp p << 1 #define rp p << 1 | 1 using namespace std; const int N = 5e3 + 10, M = 1e6 + 10; struct Tree { int l, r, mx; }t[M << 2]; int n, a[N], f[N], R; inline void push_up(int p) { t[p].mx = max(t[lp].mx, t[rp].mx); } inline void build(int p, int l, int r) { t[p].l = l, t[p].r = r; if (l == r) return; int mid = (l + r) >> 1; build(lp, l, mid); build(rp, mid + 1, r); } inline void update(int p, int x, int k) { if (t[p].l == x && t[p].r == x) { t[p].mx = k; return; } int mid = (t[p].l + t[p].r) >> 1; if (x <= mid) update(lp, x, k); if (x > mid) update(rp, x, k); push_up(p); } inline int ask(int p, int l, int r) { if (l <= t[p].l && t[p].r <= r) return t[p].mx; int res = 0; int mid = (t[p].l + t[p].r) >> 1; if (l <= mid) res = max(res, ask(lp, l, r)); if (r > mid) res = max(res, ask(rp, l, r)); return res; } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n; for (re i = 1; i <= n; i ++) { cin >> a[i]; R = max(R, a[i]); } build(1, 0, R); // 这里不需初始化 update(1, a[i], 1),可以举出反例 2 1 for (re i = 1; i <= n; i ++) { f[i] = ask(1, 1, a[i] - 1) + 1; update(1, a[i], f[i]); } int res = 0; for (re i = 1; i <= n; i ++) res = max(res, f[i]); cout << res << '\n'; return 0; }
练习:
P2418 yyy loves OI IV(值域线段树优化)
[ARC159D] LIS 2(贪心结论,维护两个线段树)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步