动态规划技巧
一些对于动态规划的技巧,与优化进行区分。
技巧学过之后是简单的,但是不学基本上写不出来,这些技巧一般只是解题的一小步,或者状态的设计与优化,但其实是至关重要的。
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\) 与两项皆有关。
首先按照 \(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)\)。
例题
II P4870 [BalticOI 2009 Day1] 甲虫
双倍经验,该题不用喝完所有露水,我们需要枚举所选个数,复杂度 \(\mathcal{O}(n^3)\)。
III UVA1336 Fixing the Great Wall
三倍经验。
好题。
首先对于 \(\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)\)。
一个技巧。
首先可以排序,这样对于我们选择的一组学生,其最大值即为最右边的值,则对于每个人有三种情况,最小值/最大值/中间值,所以我们设计 \(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)\)。
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