单调队列优化
P2627 [USACO11OPEN]Mowing the Lawn G
通常优化形如 dp[i]=minji=1(dp[j]+val(j)) ,且 val(j) 只与 j 有关的方程。
考虑用单调队列维护:在 可以转移 到当前状态 且 用其转移最优 的状态。于是类似滑动窗口,分析可以转移到当前状态的取值范围并更新队列,每次只需取出队首更新即可。
注意是否能将 val(j) 转化成只与 j 有关的式子。
单调队列中的比较运算可能需要比较多个因素。
优化多重背包
P1776 宝物筛选
记多重背包中共有 n 个物品,背包容量为 V。对于第 i 个物品,其价值为 wi,体积为 ci,数量为 mi
普通多重背包的状态转移方程:
f[i][j]=max(f[i−1][j−k⋅ci]+k⋅wi
其中 0≤k≤min(⌊Vci⌋,mi)
发现实际上可能无法取到 mi 个第 i 个物品,所以实际上最多能取的个数为:
m′i=min(⌊Vci⌋,mi)
为方便起见,下文以 mi 表示 m′i
观察状态之间的转移关系,我们容易发现:
f[i][j] 可以从 f[i−1][j−ci],f[i−1][j−2⋅ci],...,f[i−1][jmodci] 转移。
f[i][j−ci] 可以从 f[i−1][j−2⋅ci],f[i−1][j−3⋅ci],...,f[i−1][jmodci] 转移。
以此类推,即只有背包容积对 ci 取模余数相同的状态之间才会发生转移,并且余数不同的状态之间转移互不影响。因此,我们可以考虑把状态按余数分类,分别考虑转移。
接下来考虑把状态转移方程写成可以单调队列优化的形式。
令 a=⌊jci⌋,b=jmodci,即 j=a⋅ci+b
观察原本的状态转移方程:
f[i][j]=max(f[i−1][j−k⋅ci]+k⋅wi
其中 0≤k≤min(⌊Vci⌋,mi)
显然有 j−k⋅ci=(a⋅ci+b)−k⋅ci=(a−k)⋅ci+b
令 k′=a−k,则 j−k⋅ci=k′⋅ci+b
因为 0≤k≤mi,k′=a−k
所以 a−mi≤k′≤a
又易得 k⋅wi=a⋅wi−(a−k)⋅wi=a⋅wi−k′⋅wi
所以原状态转移方程等价于:
f[i][j]=max(f[i−1][k′⋅ci+b]+a⋅wi−k′⋅wi)
其中 a−mi≤k′≤a
发现可以用单调队列维护方程中 f[i−1][k′⋅ci+b]−k′⋅wi 的最大值。于是可以考虑先枚举余数 b,然后再升序枚举 a 以枚举 j(j=a⋅ci+b),显然 k′ 取值范围的上下界都随 a 的增大而增大,直接单调队列维护即可。
注意代码中先更新单调队列,再更新状态,因此使用的值总是前 i−1 个物品的答案,即不需要倒序枚举 a
容易发现对于当前物品,所有分组的状态总数为 V。单调队列维护转移的复杂度是 O(V),故而总时间复杂度为 O(nV),优于二进制优化和朴素算法。
注意当 mi=0 时需要特判(有用全拿)
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 105;
const int maxv = 1e5 + 5;
struct item
{
int p, v;
item(int _p = 0, int _v = 0) : p(_p), v(_v) {}
} dq[maxv];
int n, v;
int f[maxv];
int w[maxn], c[maxn], m[maxn];
int main()
{
scanf("%d%d", &n, &v);
for (int i = 1; i <= n; i++)
{
scanf("%d%d%d", &w[i], &c[i], &m[i]);
m[i] = min(m[i], v / c[i]);
}
for (int i = 1; i <= n; i++)
{
for (int j = 0; j < c[i]; j++)
{
int l, r;
l = 1, r = 0;
for (int k = 0; k <= (v - j) / c[i]; k++)
{
int val = f[k * c[i] + j] - k * w[i];
while ((l <= r) && (dq[r].v <= val)) r--;
dq[++r] = item(k, val);
while ((l <= r) && (dq[l].p < k - m[i])) l++;
if (l <= r) f[k * c[i] + j] = dq[l].v + k * w[i];
}
}
}
printf("%d\n", f[v]);
return 0;
}
数据结构优化
朴素优化
若转移方程形如 f[i]=max(f[j]+val(j,i)),可以考虑用线段树优化。
用线段树维护从第 i 个位置转移的贡献,每次向右移动的时候把当前位置的贡献添加到线段树上即可。
例:CF833B The Bakery
整体 dp
有一类状态设计形如 dp[i][j],且通常有很多第一维相同并且值相同的状态。
可以考虑用一棵动态开点线段树去维护第二维。
可以和线段树合并一类的技巧一起使用。
例:CF1455G Forbidden Value
矩乘优化
每个状态能转移到的状态集合一定时可以使用矩乘优化,例如方程为 f[i]=a⋅f[i−2]+b⋅f[i−1]
通常考虑根据定义设计出初始矩阵,再根据方程设计出用以转移的矩阵,然后通过矩阵快速幂快速转移。
有时特征是可以用 dp 解决并且数据范围极大(log 可过)
斜率优化
单调队列优化 plus pro max ultra x 至尊奢华黄金尊享版
优化形如 f[i]=a(i)b(j)+c(i)+d(j) 的方程
考虑从两个位置 j,k 转移且 j 比 k 的条件,列不等式。
参变分离,整理式子时将含有 i 的项放在一边,将剩下的部分放在另一边。
于是得形如直线斜率的不等式,单调队列维护凸包即可。
求最小值维护的是下凸包,求最大值为上凸包。
线段树合并优化
优化一类对于每个结点都维护值域所有信息的树形 dp
【题解】P5298 [PKUWC2018]Minimax
杂项
- 求最值 -> 观察每一项的单调性
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】