Loading

动态规划优化

单调队列优化

P2627 [USACO11OPEN]Mowing the Lawn G

通常优化形如 \(dp[i] = \min_{i = 1}^j (dp[j] + val(j))\) ,且 \(val(j)\) 只与 \(j\) 有关的方程。

考虑用单调队列维护:在 可以转移 到当前状态 且 用其转移最优 的状态。于是类似滑动窗口,分析可以转移到当前状态的取值范围并更新队列,每次只需取出队首更新即可。

注意是否能将 \(val(j)\) 转化成只与 \(j\) 有关的式子。

单调队列中的比较运算可能需要比较多个因素。

优化多重背包

P1776 宝物筛选

记多重背包中共有 \(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\) 个位置转移的贡献,每次向右移动的时候把当前位置的贡献添加到线段树上即可。

例:CF833B The Bakery

整体 dp

有一类状态设计形如 \(dp[i][j]\),且通常有很多第一维相同并且值相同的状态。

可以考虑用一棵动态开点线段树去维护第二维。

可以和线段树合并一类的技巧一起使用。

例:CF1455G Forbidden Value

矩乘优化

每个状态能转移到的状态集合一定时可以使用矩乘优化,例如方程为 \(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

【题解】P5298 [PKUWC2018]Minimax

决策单调性优化

杂项

  1. 求最值 -> 观察每一项的单调性
posted @ 2022-07-01 14:37  kymru  阅读(52)  评论(0编辑  收藏  举报