DP与DP优化选讲笔记

DP

DP 是什么

从解决问题角度上来讲,dp 具有一些状态,每个状态可能通过决策(转移)到达其他状态。所以 dp 更像是一个自动机。那么就有了 dp 的几个关键性质:

  • 相同子问题:小的问题与大的问题类似,每个状态信息的形式类似。
  • 最优子结构:问题的最优解一定是通过子问题的最优解堆叠得到的。
  • 无后效性:某个状态的决策确定后,后续的决策只与大当前状态有关,而不能与之前的决策有关。

通常我们的目标是判定、计数和最优化。设计和优化 dp 的目标是使用尽量少的状态来表达过程中所有可能用到的结构与转移。

上面的描述与自动机是十分相似的。

例如,我们需要计数满足某条件的排列。我们令 \(f_{i,s}\) 表示长度为 \(i\),排列是 \(s\) 的方案数,转移时在后面新增一个数并检查是否满足条件,这也是一种 dp,只是复杂度无法接受。如果此问题能够使用状压 dp 解决,我们可能会写出 \(f_s\) 表示当前填的数集合为 \(s\) 的方案数,然后枚举下一个数转移。相比前一个状态设计就有了明显优化。

所以 dp 的设计和优化本质是合并同类结构的过程。

完成一道 dp 题目的大致流程

  • 首先需要像做任何一道题一样认真读题并整理出题目的大致模型。
  • 经过一系列转化,确定要进行 dp 的对象。
  • 设计 dp 状态来刻画问题。
  • 优化 dp。

状态设计策略

此部分内容比较细碎,需要更多积累。这里可以给出大体的思考流程:

  1. 从问题入手,先整体列出限制量和所求量。这是状态与转移中需要的关键信息。

  2. 尝试确定主要 dp 顺序。为达到此目的,可以观察限制量在某维度下的变化情况,更进一步地可以直接观察限制量之间的关系。对于一个看起来比较合理的 dp 顺序,限制量与所求量在此维度下应该依赖较少信息。
    此部分可能有维度较多、维度需要转化等难点。在过程中要重点关注哪些变化难以求出的量。

  3. 在此顺序下添加需要的状态。如果发现某个量无法完整记录(例如排列信息等),尝试找到其他量与其的变化关系并优化记录方式。

  • 如果实在无从下手,可以从暴力 dp 或特殊情况的 dp 入手进行扩展。

检验 dp 的正确性

主要可以从以下角度思考:

  • 从单个可行解的角度入手,是否正确处理了每个解的贡献(尤其是计数问题中是否漏算、重算),不合法的解是否没有被计入答案。
  • 从整体自动机考虑,是否有被错误合并的状态、是否有某个转移需要依赖其他信息。

trick

题目

[ABC134F] Permutation Oddness \(\checkmark\)

题目大意

求 \(1\sim n\) 的排列 \(p\) 中,满足 \(\sum_{i=1}^n |i-p_i|=k\) 的排列个数,对 \(10^9+7\) 取模。

\(1\le n\le 50,\ 0\le k\le n^2\)。

解题思路

对于这种带绝对值的题目,有一种比较好的切入角度:拆贡献。

事实上我们要考虑两个的贡献,\(i\) 和 \(p_i\) 的贡献。所以我们要将 \(i\) 与 \(p\) 一起考虑。

\(i,p_i\) 他们之间的地位是等价的,这启发我们对值 dp,同时填 \(i,p_i\)。

稍微转化一下:原问题相当于给你两个排列 \(a,b\),你需要将他们的元素两两配对。你需要求出 \(|a_i-b_i|\),我们把值相等的 \(a_i,b_j\) 放在一起考虑。我们从小到大枚举权值 \(i\),分讨转移 \(a,b\) 的匹配情况。

思考一下我们需要把什么记录进状态,首先权值 \(i\) 是一定要有的(他相当于一个阶段划分);然后是 \(k\),也就是当前的 \(\sum |a_i-b_i|\);还有什么,我们还要知道匹配情况,我们要知道 \(<i\) 的有多少没有配对,\(>i\) 的有多少没有配对,事实上,我们只要知道 \(<i\) 的有多少便可以自然推出 \(>i\) 的数量。

所以定义出 \(f(i,j,k)\) 来计数,状态转移方程如下:

  • \(a,b\) 两个自己配对 \(f(i,j,k)\gets f(i-1,j,k)\)

  • \(a,b\) 两个都和前面的配对 \(f(i,j,k) \gets f(i-1,j+1,k-2i)\times(j+1)^2\)。

  • \(a,b\) 两个都准备和后面的配对 \(f(i,j,k)\gets f(i-1,j-1,k+2i)\)。

  • \(a,b\) 一个在前一个在后,由于 \(i\) 单调枚举,所以 \(<i+1\) 的未配对数不变 \(f(i,j,k)\gets 2\times f(i-1,j,k)\times j\)。

注意,由于 \(k\) 这个可能会从负数转移过来,所以集体偏移 \(n^2\) 即可。

启发:

  1. 绝对值拆贡献是老生常谈了。
  2. 这道题的转化与设计是非常妙的。
  3. 按位置 dp 不行可以换个角度。

P7967 [COCI 2021/2022 #2] Magneti \(\checkmark\)

题目大意

在 \(l\) 个连续空位上放置 \(n\) 个磁铁(每个空位最多一个),所有磁铁必须放置。第 \(i\) 个磁铁有吸引半径 \(a_i\),若两个磁铁之间的距离小于 \(a_i\),则它们会相互吸引。求使得任意两个磁铁都不相互吸引的方案数,对 \(10^9+7\) 取模。
\(1\le n\le 50,\ n\le l\le 10000,\ 1\le r_i\le l\)。

解题思路

这道题明显没有这么直接了当了,他多加了一个覆盖限制,不过还是好做的,我们还是记录连续段个数,同时也要记录这些连续段总的覆盖长度,最后拿插板法算一算。

首先按照 \(a_i\) 从小到大排序(这样就使得在转移中的方便考虑很多),定义 \(f_{i,j,k}\) 表示添加了 \(1\sim i\) 个磁铁,连续段个数为 \(j\),总覆盖长度(不延伸出去)为 \(k\) 的方案数。

转移方程:

  • 新开:\(f_{i,j,k}\gets j\times f_{i-1,j-1,k-1}\)。
  • 加入:\(f_{i,j,k}\gets 2j\times f_{i-1,j,k-a_i}\)。
  • 合并:\(f_{i,j,k}\gets j\times f_{i-1,j+1,k-2a_i+1}\)

答案:\(\sum_{k=1}^l f_{n,1,k}\times {l-k+n\choose n}\)。

复杂度:\(O(n^2 l)\)。

其实不是这道题,但是几乎一样。只是多了个模数不一定为质数的情况而已。

[AGC009E] Eternal Average

题目大意

黑板上初始有 \(n\) 个 \(0\) 和 \(n\) 个 \(1\)。每次选择 \(k\) 个数,用它们的平均数替换。保证 \(n+m-1\) 被 \(k-1\) 整除,最终只剩一个数。求最终数可能的不同取值个数 \(\bmod 10^9+7\)。

\(1\le n,m\le 2000,\ 2\le k\le 2000,\ (k-1)\mid(n+m-1)\)。

解题思路

这种若干个数合并的过程其实可以抽象成树的结构,然后通过树的结构性质来计数。

这道题相当于一个 \(k\) 叉树,考虑每个数的贡献:初始权值乘上 \(1\over k^d\),\(d\) 是该数在该树中的深度。对所有 \(k\) 叉树直接计数,这会算重,我们发现我们只关注每一层 1 的个数,它的形态并不重要。

但是直接对每一层 1 的个数计数又会有一个问题:如果 1 层存在 \(\ge k\) 个数,那么就会算重。因为当前层 \(k\) 个 1 相当于上一层 1 个 1,所以这 \(k\) 个 1 其实可以接在任意 1 个 1 下面,把它替换掉。

定义 \(D\) 为树的深度,为 \(n+m-1\over k-1\)。我们可以考虑枚举进行了 \(t\) 次 \(k\) 个 1 的合并,那么令 \(m'=m-t(k-1),d'=d-t\),那当前的答案就是 \(f(m',d')\)。其中 \(f(i,j)\) 就是简单背包,表示考虑到第 \(i\) 层目前一共选了 \(j\) 个 1。转移还要加上每一层最多选 \(k-1\) 个数的限制,由于有 \((k-1)\mid(n+m-1)\) 所以不用多其他的限制。

时间复杂度就是背包的 \(O(n^2)\)(假设 \(n,m,k\) 同阶),类似树背包的分析方法?

启发:

  1. 若干个数合并的过程其实可以抽象成树的结构,然后通过树的结构来计数、求最优化问题。

[ARC106E] Medals \(\checkmark\)

题目大意

有 \(N\) 名员工,第 \(i\) 名员工从今天起连续工作 \(A_i\) 天、连续休息 \(A_i\) 天,如此循环。每天从当天出勤的员工中选一人发一枚奖牌(无人出勤则不发)。求使每名员工都至少获得 \(K\) 枚奖牌所需的最少天数。

\(N\le 18,\ K\le 10^5,\ A_i\le 10^5\)。

解题思路

容易发现答案具有单调性,并且上下界是可以确定的,上界是 \(2nk\),下界是 \(nk\)。于是我们可以进行二分。

其实不从单调性出发,也应该从判定开始去考虑,去思考问题。

现在来想一想判定:我们可以建出二分图,左部点便是员工,右部点是天数,有解就相当于要求二分图存在 \(k-\) 完美匹配。也可以拆点考虑,把左部点每个点拆成 \(k\) 份,要求存在完美匹配,运用 Hall 定理。再合起来就是要求 \(|N(S)|\ge k|S|\)。

左部点和右部点之间有边要求右边天数的上班集合包含左部点。可以先预处理出右边每天的上班集合,我们现在要对于 \(S\) 统计出右边与它有交的上班集合,我们有交这个东西并不好优化来做。正难则反,考虑反过来,统计出右边与它无交的上班集合,就相当于统计 \(S\) 的补集的子集和。这个是可以用高维前缀和提前处理出来的。

高维前缀和的时间复杂度是 \(O(n2^n)\),枚举子集的复杂度是 \(O(2^n)\),二分套上一个 \(\log\),还要考虑预处理上班集合的复杂度是 \(O(n^2k)\),总复杂度就是 \(O(n2^n\log (nk)+n^2k)\)。

启发:

  1. 图论建模思想,建出二分图,然后运用 Hall 定理。
  2. 正难则反,高维前缀和运用。

DP 优化

优化策略

主要可以分为状态设计优化和转移优化两部分。

下面有一些在优化开始之前常用的思考方式。

  • 用得到的 dp 式子重新审视题目,观察是否有更简单的表达方式?
  • 观察 dp 式子本身,寻找是否有组合意义?有没有冗余、重复、可以合并的状态或转移,以及是否有单调性?

状态设计优化

可以大概理解为对已经设计好的状态进行一些变换,使得最终状态数更少。此部分的大多数优化都较为明显,所以可能在状态设计的时候已经做过了。

  • 压缩维度
  • dp 值域状态换维:值域较小但某维度较大,且交换后能够得到答案。
  • 调整下标记录信息:例如将 \(i\) 更换为 \(\frac{n}{i}\) 等。
  • 放宽限制:dp 的维度可能用于记录限制,若不满足某限制一定更劣(最优化问题)或不符合其他条件(计数问题),则可以删去该限制,如此可以减少 dp 维度。

转移优化

在状态数已经比较优的情况下,可能由于转移过多导致复杂度过高。

  • 分步转移:在决策需要考虑多步过程(有些单步也可能能够强行拆成多步),多步的复杂度可能是每步复杂度的积。如果能够拆出中间层以过度,可能能降低复杂度。
  • 数据结构优化:主要有三种形式,一种是用来快速找决策点(例如 LIS 问题),一种是维护 dp 值(例如整体 dp),一种是快速完成多次转移(例如矩阵快速幂)。
  • 结构优化:问题结构可能带来单调性等特殊性质,包括单调队列、斜率优化、决策单调性、凸优化等。
  • 数学方法优化:这类有很强的数学指向性。常见的有矩阵快速幂、多项式优化等。

trick

  1. 观察出重要性质对于做题有着非常必要的帮助。

题目

CF1768F Wonderful Jump \(\checkmark\)

题目大意

有 \(n\) 个位置,第 \(i\) 个位置有权值 \(a_i\)。从位置 \(i\) 向右跳到位置 \(j\)(\(i\le j\))的花费为 \(\min(a_i,\dots,a_j)\times(j-i)^2\)。对每个 \(k=1,\dots,n\),求从位置 \(1\) 跳到位置 \(k\) 的最小总花费。

\(n\le 4\times 10^5,\ 1\le a_i\le n\)。

解题思路

首先由非常显然的 \(O(n^2)\) 最短路,但这样显然是过不了的。

这道题有非常妙的观察,可以辅助优化。

性质 1

最简单的,\(f_i\) 显然不可能从后面转移过来,但他不一定单调。

性质 2

首先,最小值一定要放在跳的区间两侧才是最优的。比如说,\(\min=a_k\),\(a_i\to a_j\) 不如 \(a_i\to a_k\to a_j\),这个用基本不等式便可以证出。

性质 3

每次从最小值 \(a_i\) 跳的步长不超过 \(n\over a_i\),跳到 \(a_i\) 也同理。我们从 \(a_i\) 跳到其他格子有两种方法,一步一步跳和一步跳。
一步一步跳最劣也就是 \(nd\),而一步跳到是 \(a_id^2\),我们枚举步长 \(d\) 一步跳到比一步一步跳要求 \(a_id^2\le nd\),也就是 \(d\le {n\over a_i}\)。

我们根据以上重要性质可以优化转移过程,我们对每个 \(i\),以 \(a_i\) 作为最小值去转移,每次转移到的点不超过 \(2n\over a_i\)。

分析一下时间复杂度 \(O(n\sqrt n)\),考虑根号分治:

  • \(a_i\ge \sqrt n\),步长不超过 \(\sqrt n\),所以总的是 \(O(n\sqrt n)\)。
  • \(a_i<\sqrt n\),考虑将相同的 \(a_i\) 合在一起考虑,以 \(a_i\) 作为最小值的并起来长度为 \(O(n)\),而不同的 \(a_i\) 个数为 \(\sqrt n\),总的也是 \(O(n\sqrt n)\)。

实现的时候,枚举 \(2\le i\le n\),枚举步长,看看 \(f_i\) 能从哪里转移而来,然后能转移到哪里,就做完了。

启发:

  1. 先列出朴素的 DP 转移式子,观察重要性质。

  2. 通过一些特殊的情况限制答案范围,比如这道题用一步一步跳这种跳法筛选掉很多不合法的步长,缩小枚举的范围。

  3. 根号分治分析复杂度太牛了。

P17129 [ICPC 2025 Shanghai R] Round screws \(\checkmark\)

题目大意

有长度为 \(n\) 的非负整数序列 \(a\),定义价值 \(V=a_1+a_n+\sum_{i=1}^{n-1}(a_i\oplus a_{i+1})\)。可任意次修改任意元素为任意非负整数,每次花费 \(C\),求最小化修改总花费加修改后序列价值。多组数据。

\(T\le 100,\ n\le 10^5,\ C<2^{19},\ 0\le a_i<2^{18}\),所有测试用例 \(n\) 之和 \(\le 2\times 10^5\)。

解题思路

每次找到增量最大的贪心是错误的,因为这样子会有有后效性。

那么我们可以从最终结构来考虑,考虑对于一段 \([l,r]\) 里面的 \(a_i\) 全都要修改,那么全部改成 \(a_{l-1}\) 是最优的。因为无论如何,\(a_{l-1}\oplus a_{r+1}\) 的每一个二进制位一定要在 \(\sum_{i=l}^r a_i\oplus a_{i+1}\) 贡献至少一次,那么全部改成 \(a_{l-1}\) 或者 \(a_{r+1}\) 一定是最优的了。

对于这种问题,我们可以考虑保存哪些位置,这个其实就是经典的序列分段问题,套路化的,定义 \(f_i\) 表示考虑到当前位置 \(i\) 的最优方案。实现是可以现在头尾补 0(避免边界讨论),转移方程简单写出来:

\[f_i=\min_j(f_j-C+(a_i\oplus a_j)) \]

答案就是 \(f_{n+1}+(n+1)C\)。

看到这种方程,很让人想到四边形不等式优化、斜率优化之类的,但是这个有一个 \(\oplus\) 就很烦人。

一般带 \(\oplus\) 要么用线性基,要么用 trie,但是这道题这两个都没法套上去。

接下来介绍一个 trick,听别人说这个叫做二进制平衡规划:

比如这道题中,\(a_i\le V=2^{18}\),\(V\) 并不大。我们拆成高低位,分块。具体来说,共有 \(2^9\) 个块,高 9 位作为块的编号,每个块长度为 \(2^9\),维护低 9 位的答案。

维护 \(g_{h,l}\) 表示高 9 位为 \(h\),低 9 位为 \(l\) 的最优答案,\(H_i\) 表示 \(a_i\) 的高 9 位,\(L_i\) 同理。这样 \(f_i=\min_h(g_{h,L_i}+2^9\times (H_i\oplus h)-C)\),这样复杂度是 \(O(n\sqrt V)\)。

考虑怎么计算 \(g_{h,l}\),其实就是每次计算完 \(f_i\),然后 \(g_{h,l}\gets f_i+(L_i\oplus l)\),这个复杂度也是 \(O(n\sqrt V)\) 的。

总时间复杂度是 \(O(n\sqrt V)\)。

启发:

  1. 非常妙的一道 trick 题,知道这个高低位分块的 trick 就很好做。

P13580 [CCPC 2024 重庆站] Median Replacement \(\checkmark\)

题目大意

给定每个位置 \(i\) 的取值区间 \([l_i,r_i]\),对所有可能的初始序列 \(a\)(\(l_i\le a_i\le r_i\)),可以反复选择长度大于 \(1\) 的奇数区间,将该区间内所有数替换为它们的中位数。最终序列所有数相等,设最终值为 \(x\),求所有可能初始序列对应的 \(x\) 的最大值之和,模 \(10^9+7\)。

\(T\le 10,\ 3\le n\le 150,\ 1\le l_i\le r_i\le 10^9\)。

解题思路

先考虑判定,也就是给定序列 \(a\),它的最终值能否 \(\ge x\)。经典地,将 \(\ge x\) 的替换成 1,\(<x\) 的替换成 0。容易发现,只要存在相邻两个 1 间隔 \(\le 1\),那么就一定可以替换成 \(\ge x\) 的数。

接下来计算值 \(\ge x\) 的序列个数,记之为 \(c_x\)。运用补集转换的思想,我们可以计算出不存在两个 1 的下标差 \(\le 2\)(即任意两个 1 的下标差 \(> 2\))的序列个数 \(c'_x\),则 \(c_x = \prod_{i=1}^n (r_i - l_i + 1) - c'_x\)。

考虑使用动态规划求解 \(c'_x\)。 令 \(f_{i,0/1/2}\) 表示仅考虑序列中前 \(i\) 个数,且第 \(i\) 位和第 \(i-1\) 位分别 \((< x, < x) / (< x, \ge x) / (\ge x, < x)\) 的 \(c'_x\) 的值,则转移方程为:

\[\begin{cases} f_{i,0} = (f_{i-1,0} + f_{i-1,1}) \cdot \max(0, \min(x - 1, r_i) - l_i + 1) \\ f_{i,1} = f_{i-1,2} \cdot \max(0, \min(x - 1, r_i) - l_i + 1) \\ f_{i,2} = f_{i-1,0} \cdot \max(0, r_i - \max(x, l_i) + 1) \end{cases} \]

则 \(c'_x = f_{i,0} + f_{i,1} + f_{i,2}\),\(c_x = \prod_{i=1}^n (r_i - l_i + 1) - (f_{i,0} + f_{i,1} + f_{i,2})\)。

(当然这里也可不用补集转换直接动态规划求解 \(c_x\),但状态会多出一维,常数也会增加。)

现在我们所要求的答案就是 \(\sum_x (c_x-c_{x+1})\times x=\sum_x c_x-c_{x+1}\times (x+1)=\sum_x c_x\)。

令 \(g(n)=\sum_{1\le i\le n} c_i\),我们最终的答案就是 \(\sum_i g(i)\)。这样就得到了 \(O(TnV)\)。

接下来考虑优化,\(n\) 很小,但是值域很大,我们能做什么优化呢?拉格朗日插值?矩阵乘法?显然矩阵乘法不可行。拉格朗日插值很难处理 \(\min \max\) 的情况下。怎么办呢?

我们把 \(l_i,r_i\) 全部拿出来一起放在 \(p\) 里,排一下序,答案其实就是 \(\sum f(p_i+1,p_{i+1})\),其中 \(f(l,r)=\sum_{i=l}^r g(i)\)。为什么这样做,因为 \([p_i+1,p_{i+1}]\) 在做 DP 的时候有很多类似的地方,它们在上方 DP 中 \(\min\max\) 这些操作的取值相同,而每次到了分界点 DP 值都会有所变化。这样一旦 \(\min\max\) 固定下来,我们就可以在一段区间中用拉格朗日插值了,因为 \(g\) 最多是 \(n+1\) 次多项式, \(f\) 也就是 \(g\) 的前缀和也是 \(n+1\) 次,然后用拉格朗日差值跑完 \(n+2\) 个点,然后就可以算出其他的了。

复杂度 \(O(Tn^3)\)。

posted @ 2026-10-06 16:02  lcx_OIer  阅读(11)  评论(0)    收藏  举报