动态规划优化
单调队列优化
P2627 [USACO11OPEN]Mowing the Lawn G
通常优化形如 \(dp[i] = \min_{i = 1}^j (dp[j] + val(j))\) ,且 \(val(j)\) 只与 \(j\) 有关的方程。
考虑用单调队列维护:在 可以转移 到当前状态 且 用其转移最优 的状态。于是类似滑动窗口,分析可以转移到当前状态的取值范围并更新队列,每次只需取出队首更新即可。
注意是否能将 \(val(j)\) 转化成只与 \(j\) 有关的式子。
单调队列中的比较运算可能需要比较多个因素。
优化多重背包
记多重背包中共有 \(n\) 个物品,背包容量为 \(V\)。对于第 \(i\) 个物品,其价值为 \(w_i\),体积为 \(c_i\),数量为 \(m_i\)
普通多重背包的状态转移方程:
\(f[i][j] = \max(f[i - 1][j - k \cdot c_i] + k \cdot w_i\)
其中 \(0 \leq k \leq \min(\lfloor \frac{V}{c_i} \rfloor, m_i)\)
发现实际上可能无法取到 \(m_i\) 个第 \(i\) 个物品,所以实际上最多能取的个数为:
\(m^{\prime}_i = \min(\lfloor \frac{V}{c_i} \rfloor, m_i)\)
为方便起见,下文以 \(m_i\) 表示 \(m^{\prime}_i\)
观察状态之间的转移关系,我们容易发现:
\(f[i][j]\) 可以从 \(f[i - 1][j - c_i], f[i - 1][j - 2 \cdot c_i], ..., f[i - 1][j \bmod c_i]\) 转移。
\(f[i][j - c_i]\) 可以从 \(f[i - 1][j - 2 \cdot c_i], f[i - 1][j - 3 \cdot c_i], ..., f[i - 1][j \bmod c_i]\) 转移。
以此类推,即只有背包容积对 \(c_i\) 取模余数相同的状态之间才会发生转移,并且余数不同的状态之间转移互不影响。因此,我们可以考虑把状态按余数分类,分别考虑转移。
接下来考虑把状态转移方程写成可以单调队列优化的形式。
令 \(a = \lfloor \frac{j}{c_i} \rfloor, b = j \bmod c_i\),即 \(j = a \cdot c_i + b\)
观察原本的状态转移方程:
\(f[i][j] = \max(f[i - 1][j - k \cdot c_i] + k \cdot w_i\)
其中 \(0 \leq k \leq \min(\lfloor \frac{V}{c_i} \rfloor, m_i)\)
显然有 \(j - k \cdot c_i = (a \cdot c_i + b) - k \cdot c_i = (a - k) \cdot c_i + b\)
令 \(k^{\prime} = a - k\),则 \(j - k \cdot c_i = k^{\prime} \cdot c_i + b\)
因为 \(0 \leq k \leq m_i, k^{\prime} = a - k\)
所以 \(a - m_i \leq k^{\prime} \leq a\)
又易得 \(k \cdot w_i = a \cdot w_i - (a - k) \cdot w_i = a \cdot w_i - k^{\prime} \cdot w_i\)
所以原状态转移方程等价于:
\(f[i][j] = \max(f[i - 1][k^{\prime} \cdot c_i + b] + a \cdot w_i - k^{\prime} \cdot w_i)\)
其中 \(a - m_i \leq k^{\prime} \leq a\)
发现可以用单调队列维护方程中 \(f[i - 1][k^{\prime} \cdot c_i + b] - k^{\prime} \cdot w_i\) 的最大值。于是可以考虑先枚举余数 \(b\),然后再升序枚举 \(a\) 以枚举 \(j\)(\(j = a \cdot c_i + b\)),显然 \(k^{\prime}\) 取值范围的上下界都随 \(a\) 的增大而增大,直接单调队列维护即可。
注意代码中先更新单调队列,再更新状态,因此使用的值总是前 \(i - 1\) 个物品的答案,即不需要倒序枚举 \(a\)
容易发现对于当前物品,所有分组的状态总数为 \(V\)。单调队列维护转移的复杂度是 \(O(V)\),故而总时间复杂度为 \(O(nV)\),优于二进制优化和朴素算法。
注意当 \(m_i = 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\) 个位置转移的贡献,每次向右移动的时候把当前位置的贡献添加到线段树上即可。
整体 dp
有一类状态设计形如 \(dp[i][j]\),且通常有很多第一维相同并且值相同的状态。
可以考虑用一棵动态开点线段树去维护第二维。
可以和线段树合并一类的技巧一起使用。
矩乘优化
每个状态能转移到的状态集合一定时可以使用矩乘优化,例如方程为 \(f[i] = a \cdot f[i - 2] + b \cdot 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
决策单调性优化
杂项
- 求最值 -> 观察每一项的单调性