动态规划技巧

一些对于动态规划的技巧,与优化进行区分。

技巧学过之后是简单的,但是不学基本上写不出来,这些技巧一般只是解题的一小步,或者状态的设计与优化,但其实是至关重要的。

1. 费用提前计算

当 DP 中当前决策会影响未来的费用/贡献,且该费用/贡献当前决策相关,这样我们可以提前计算所影响的费用。

栗子:我们有一些机器,存在一个值表示该机器的价值,选出某个机器可以增加其他所有机器的价值 \(up\),则对于选该机器的决策,我们就有固定增加 \(up \times (n - 1)\) 的贡献,可以提前计算。

什么叫呢?再举一个例子,若选择 \(i,j\) 两个机器会造成额外 \((w_i + w_j)^2 = w_i^2 + w_j^2 + 2w_i w_j\) 的贡献,则对于该式子就不仅与 \(i\) 相关,因为 \(2w_iw_j\) 与两项皆有关。

例题:P2466 [SDOI2008] Sue 的小球

首先按照 \(x\) 排序,这样走一定更优,我们设 \(f_{0/1,i,j}\) 表示当前已经拿到 \([i,j]\) 区间内的彩蛋,且当前在 \(i/j\) 的最高分数,若我们要花费 \(t\) 时间进行移动,则未选择的彩蛋的贡献会减少,即当前选择去某个彩蛋,则对于未来将要造成的贡献是有影响的,且与该决策,或者说花费的时间相关,若是暴力弄的话需要加一维状态,是非常不优的。

考虑费用提前计算,假设我们选了 \([i,j]\) 的点,将要选择 \(i-1\) 点,则有 \(y_{i-1} - t \times \left(\sum_{k=1}^{i-1}v_k + \sum_{k=j+1}^n v_k\right)\) 的贡献,右边是对未来贡献造成的影响,即所有未选择的彩蛋都下降 \(t\) 时间,其中 \(t\) 为移动所花费的时间,即 \(x_i - x_{i-1}\)

所以我们可以得到转移方程:

  • \(f_{0,i,j} = \max(f_{0,i+1,j} - (x_{i+1} - x_i) \times s_{i+1,j},f_{1,i+1,j} - (x_j - x_i) \times s_{i+1,j}) + y_i\)
  • \(f_{1,i,j} = \max(f_{0,i,j-1} - (x_j - x_i) \times s_{i,j-1},f_{1,i,j-1} - (x_j - x_{j-1}) \times s_{i,j-1}) + y_j\)

其中 \(s_{i,j}\) 表示除去 \([i,j]\) 区间内 \(v\) 的和,即上文的式子。

复杂度 \(\mathcal{O}(n^2)\)

例题

I P2466 [SDOI2008] Sue 的小球

代码

II P4870 [BalticOI 2009 Day1] 甲虫

双倍经验,该题不用喝完所有露水,我们需要枚举所选个数,复杂度 \(\mathcal{O}(n^3)\)

代码

III UVA1336 Fixing the Great Wall

三倍经验。

代码

IV CF441E Valera and Number

好题。

首先对于 \(\times 2\) 相当于在其二进制下加了个 \(0\),而 \(+ 1\) 操作相当于进位,这个进位不是很好处理,因为其会大幅影响结尾 \(0\) 的个数,我们思考一下,最后的结果都是形如 \(\dots 1000 \dots\),后面全是 \(0\),而前面我们不关注,也就是说我们只关心其最低位 \(1\) 在哪里,所以我们考虑从后往前 DP。

\(f_{i,j,k}\) 表示后 \(i\) 个操作后,相当于加 \(j\) 后,有 \(k\)\(\times 2\)概率,则有:

  • 若上个操作为 \(+ 1\),则因为后面跟着 \(k\)\(\times 2\),相当于 \(+ 1\),即 \(f_{i+1,j+1,k} \gets f_{i,j,k} \times \frac {100 - p} {100}\)
  • 若上个操作位 \(\times 2\)
    • \(2\mid j\),我们可以相当于加 \(\frac j 2\) 后,有 \(k+1\)\(\times 2\),即 \(f_{i+1,\frac j 2,k+1} \gets f_{i,j,k} \times \frac p {100}\)
    • \(2\nmid j\),则其末尾 \(1\) 就固定了,而前面我们不关注,所以我们可以直接统计答案,即 \(ans \gets f_{i,j,k} \times \frac p {100} \times k\)

最后当结束 \(k\) 次操作后,其答案与 \(x\) 也相关,即 \(ans \gets \sum\sum f_{k,i,j} \times (s_{x + i} + j)\),其中 \(s_x\) 表示 \(x\) 最低位 \(0\) 的个数。

我们最多有 \(k\)\(+ 1\) 操作,总复杂度 \(\mathcal{O}(k^3)\),已经可以过了。

可是这与费用提前计算有什么关系呢?我们发现 \(k\) 这维是用来计算答案的,而若操作为 \(\times 2\) 时,其对于后面一定有 \(1\) 的贡献,即其二进制末尾一定会多个 \(0\),所以我们可以直接对答案造成 \(f_{i,j} \times \frac p {100}\) 的贡献,这样我们减少一维,复杂度 \(\mathcal{O}(k^2)\)

代码

V CF626F Group Projects

一个技巧

首先可以排序,这样对于我们选择的一组学生,其最大值即为最右边的值,则对于每个人有三种情况,最小值/最大值/中间值,所以我们设计 \(f_{i,j,k}\) 表示前 \(i\) 个学生,还有 \(j\) 组没有最大值,所造成贡献为 \(k\) 的方案数,有:

  • 若第 \(i\) 个人为新的一组最小值,则 \(f_{i,j,k} \gets f_{i-1,j-1,k+a_i}\)
  • 若第 \(i\) 个人为中间值,则 \(f_{i,j,k} \gets f_{i-1,j,k} \times j\)
  • 若第 \(i\) 个人为某一组最大值,则 \(f_{i,j,k} \gets f_{i-1,j+1,k-a_i} \times (j + 1)\)
  • 若第 \(i\) 个人单独一组,则 \(f_{i,j,k} \gets f_{i-1,j,k}\)

这里我们将 \(|max - min|\) 的贡献拆开来计算,\(k\) 可以为负数,最大可能开到 \(\sum a_i\),复杂度 \(\mathcal{O}(n^3V)\),无法通过。

可以发现 \(k\) 是较小的,我们考虑将 \(a_i - a_j\) 拆分成一些数的和,这样不会造成负数的情况,可以考虑差分,对于状态 \(f_{i,j,k}\),有对于所有没有最大值\(j\) 组,一定有 \(j \times (a_{i+1} - a_i)\) 的贡献,因为其最大值在 \(i\) 之后,一定大于 \(a_i\),这里相当于费用提前计算了。

\(c_i = a_i - a_{i-1}\),有转移:

  • 若第 \(i\) 个人为新的一组最小值,则 \(f_{i,j,k} \gets f_{i-1,j-1,k-c_i\times (j-1)}\)
  • 若第 \(i\) 个人为中间值,则 \(f_{i,j,k} \gets j \times f_{i-1,j,k-c_i\times j}\)
  • 若第 \(i\) 个人为某一组最大值,则 \(f_{i,j,k} \gets (j+1) \times f_{i-1,j+1,k-c_i\times (j+1)}\)
  • 若第 \(i\) 个人单独一组,则 \(f_{i,j,k} \gets f_{i-1,j,k-c_i\times j}\)

这样复杂度是 \(\mathcal{O}(n^2k)\)

代码

VI AT_abc219_h Candles

II 的加强,唯一的区别是本题的蜡烛长度会不同,这样会导致我们选择的熄灭的蜡烛会不是一段连续的区间,我们就不可以枚举区间,考虑到我们费用提前计算的是未熄灭的蜡烛,即这些蜡烛都会变短,这样我们在其基础上加一维 \(k\) 表示 \([l,r]\) 区间外还剩 \(k\) 只蜡烛未被熄灭。

转移类似,多了两种不熄灭该蜡烛的情况需要讨论,复杂度 \(\mathcal{O}(n^3)\)

代码

VII P7650 [BalticOI 2007 Day 1] Ranklist Sorting

神题。

分数各不相同,先离散化好处理,这里将其从大到小排序(意思是最大值为 \(1\),次大值为 \(2\) ...),令 \(id_i\) 表示数 \(i\) 在原数列所在下标,\(a_i\) 表示下标为 \(i\) 中的数。

所以最后我们会得到 \((1,2,....n-1,n)\) 的数列。

首先观察题目,可以感性发现一些性质。

  • 若要使某个人 \(x\) 移动,最多让其移动 \(1\) 次,即移动到 \(x+1\) 前面,移动多次一定不如只移动一次优。
  • 对于某个人 \(x\),若其向后移动,会使一些人的下标减少,可以减少费用。

可以得出一个 结论:我们的操作应该是从编号大的到编号小的进行操作,且每个人最多操作一次

对于某个人 \(i\),我们假设所有 \(> i\) 的数已经聚到一起,则我们可以设计 \(f_{i,j}\) 表示到第 \(i\) 个人,且所有 \(\geq i\) 的人聚到 \(j\) 的位置的最小费用。

分为两种情况。

  • 若其要向右移动,设其要到达的位置为 \(j > id_i\)

    因为每次所造成的贡献是当前下标,所以我们需要考虑求出,因为我们从大到小操作,根据结论可知,所有小于 \(i\) 的数都没有动,即 \(i\) 所在位置前面小于 \(i\) 的数是可知的,而所有大于 \(i\) 的数一定在右边,因为 \(j > id_i\),所以 \(i\) 所在的真实位置是下标小于 \(id_i\) 且大小小于 \(i\) 的数目 \(+1\),设 \(c_{i,j}\) 表示前 \(i-1\) 个数中 \(< j\) 的数目,则当前 \(i\) 所在真实位置即为 \(c_{id_i,i} + 1\),同理我们可以得出 \(i+1\) 的真实位置为 \(c_{j,i+1} + 1\)

    • 则若移动至 \(j\) 前面的贡献\(c_{id_i,i} + c_{j,i+1} + 1\)

      所以有 \(f_{i,j} = f_{i+1,j} + c_{id_i,i} + c_{j,i+1} + 1 = f_{i+1,j} + c_{id_i,i} + 1 + c_{j,i} + 1,j > i\)

    • 因为 \(j > id_i\),所以其实我们可以不移动,让在 \((id_i,j)\) 中间的数向前移动,但是该决策会影响向左移动的人的费用,我们考虑费用提前计算,对于 \(k \in (id_i,j)\),我们计算 \(k\) 只计算了下标小于 \(k\)大小小于 \(a_k\),的值,所以若 \(a_k < i\)\(k\) 还需要跳过 \(i - a_k\) 个数,因为我们已经假设所有 \(> i\) 的数已经聚到一起,即所有 \(> a_k\) 的数都在 \(k\) 之前。

      所以有 \(f_{i,id_i} = \displaystyle\min_{j=i+1}^n(f_{i+1,j} + \displaystyle\sum_{k=i+1,i>a_k}^{j-1}i - a_k)\)

  • 若其要向左移动,设其要到达的位置为 \(j < id_i\)

    去除掉不移动的人的影响,这种情况必须移动,所以贡献为 \(c_{id_i,i} + 1 + c_{j,i} + 1\)

总转移有:

  • \(f_{i,j} = f_{i+1,j} + c_{id_i,i} + 1 + c_{j,i} + 1\)
  • \(f_{i,id_i} = \displaystyle\min_{j=i+1}^n(f_{i+1,j} + \sum\limits_{k=i+1,i>a_k}^{j-1}i - a_k)\)

答案即为 \(\displaystyle\min_{i=1}^n f_{1,i}\)

这里为了方便,添加了一个 \(n+1\) 的数放到数列末尾,对于输出方案我们记录来源,找出下标在其之前的数的个数,求出真实下标即可。

复杂度 \(\mathcal{O}(n^2)\)

代码

2. 假设未来决策

参考文章

对一类动态规划问题的研究 - 徐源盛 2009

浅谈一类优化思想:费用提前计算 - LgxTpre

posted @ 2024-11-12 10:34  oXUo  阅读(12)  评论(0编辑  收藏  举报
网站统计