Atcoder Educational DP Contest A~Z
To begin
本文章将从头分析 dp 阶段、状态、转移的设计,简单的话可以倒序查看。
其中,阶段的存在可以使我们找到状态之间的拓扑序,使状态之间的转移满足无后效性,从而可以正常进行转移。
而状态则需要我们将题目中给定的所有条件、属性完美涵盖,使得转移时状态不漏但也不重。
再根据题目要求想出来决策是什么,就可以列出状态转移方程了。
列出后可以考虑优化状态的设计或是根据决策之间的性质、状态的改变对于决策贡献的影响等方面对于转移进行优化,进一步优化时间复杂度。
对于空间复杂度,则可以通过对于阶段一维进行滚动数组优化;对于阶段数据范围非常大,其它状态数据范围非常小的情况,则可以将 dp 写成矩阵进行快速转移。
A. Frog 1
阶段显然可以按照当前石子的位置进行划分,而我们对于答案的计算也只关心这个,所以状态无需增加其他什么。
设 为到达 的方案数,则由题意,转移方程为:
按照方程转移即可。
B. Frog 2
将 A 的决策扩展到 个即可。
C. Vacation
阶段和上题类似,都是典型线性 dp 的阶段划分,即用当前所在的天数表示阶段。
但本题除了阶段之外,由于不能连续两天做相同的事,所以我们还会关心上一天我们做了什么,而这个则可以在转移的时候被我们放到状态里。
于是可以设 表示对于前 天,且第 天做了某一件事时最大的快乐值。
转移时只需要不和上一天选择相同的事情即可。
最后的答案为 。
D. Knapsack 1
普通 0/1 背包板子。
阶段显然可以是【考虑了前 个物品的选取】,除此之外,由于题目中还有【体积】的属性,所以我们还会关心当前已经使用的容量是多少。
于是可以设 表示考虑了前 个物品,且当前已经使用了 个体积(不一定用完)时的最大价值。
转移时可以考虑是否选择第 个物品,则有转移方程
时空复杂度 。
一个经典的降低空间复杂度的方法是滚动数组。我们发现可以更新 的只有 ,也就是对于第 个阶段,我们只会用第 个阶段更新它。所以我们可以将 的维度开成 ,使用时滚动使用即可。
进一步地,对于本题,由于可以更新 的决策一定在 前面,所以我们可以考虑将 这一维删掉,然后倒序枚举 进行转移。倒序的枚举可以保证更新 的决策一定属于上一个阶段而非当前阶段,如此即可保证转移时复杂度的正确。也即:
E. Knapsack 2
本题是上一题的变式问题。
观察到相比上题,体积的上限变大,而价值的上限变小。
我们显然不可能像上题一样再将体积设进状态之中,所以考虑将体积作为答案维,对应地,将价值设进状态。
也就是说,我们可以设 表示对于前 个物品,当价值为 时,最小的体积。则有转移方程:
最后的答案即为最小体积小于 的最大价值。
F. LCS
题意让求两个字符串的最长公共子序列的一个方案。
我们可以将第一个字符串的前 位作为阶段,我们关心的是第二个字符串考虑到了第几位,于是可以将它设进状态。
即设 表示第一个字符串考虑完前 位,第二个字符串考虑完前 位时的最长公共子序列。
又由于我们只有【让 配对】和【不让 】配对两种决策,所以有转移方程:
dp 结束之后,我们从最终状态倒序向前寻找路径,将选到的字符输出递归即可。
G. Longest Path
拓扑排序+dp.
题目中要求的是一张 DAG 上的最长路径,所以可以考虑拓扑排序。
阶段显然可以是考虑到了哪个节点。我们可以设 表示以 号节点为结尾的最长路径长度,决策即为 号节点的所有入点。
而 DAG 的性质显然就保证了 dp 的无后效性,用拓扑排序辅助转移即可,状态转移方程为:
H. Grid 1
我们只会关心当前到达了哪个位置,由于只会向右走和向下走,所以状态的转移图显然是一个 DAG,满足无后效性。
不妨设 表示以 结尾的路径个数。则显然有:
时间复杂度 。
I. Coins
简单线性概率 dp。
阶段很明显,为【考虑到了前几个硬币】。除此之外,由于题目还对于已抛硬币的正反面关系进行了限制,所以我们同时还需要将正反面硬币个数之差设进状态。即我们可以设 表示考虑了前 个硬币,且当前抛到正面的硬币比反面多 个时的概率。则有转移方程:
又由于 可能小于 ,此时数组就会爆炸,所以我们可以将 都加上一个合适的数,再进行正常转移即可。
J. Sushi
阶段显然还是当前吃了多少次寿司,除此之外,我们还会关心当前各个位置都有多少个寿司,而这个东西显然不太好设进状态。
不过观察到 ,而对于相同数量寿司的盘子,显然都是等价的!所以其实我们只需要关心每一种数量的盘子的个数即可。
考虑设 表示吃了 次寿司后,四种寿司数量的盘子各有 个时的次数期望。此时我们发现阶段已经在我们设计状态时被模糊了,所以我们可以考虑将 省去。而我们完全可以通过三种数量的盘子算出第四种数量的盘子的个数,所以我们可以再将记录【有多少个盘子没有寿司】的状态删去。
设 ,转移时有状态转移方程(采用期望 dp 常用的倒序转移写法):
可以将式子右侧的 放到左边去,再将系数除到右边,即可得到真正的状态转移方程。
注意倒序转移时初态为 ,终态为 ,其中 分别为初始寿司个数为 个的盘子的数量。
K. Stones
简单博弈论 dp。
这是一个公平组合游戏,每一个状态都必为必胜态或必败态。其中一个经典结论是:
必胜态存在一个后继状态是必败态。
必败态的任意后继状态都是必胜态。
显然我们可以用 dp 来解决这个题目,阶段就是当前剩余的石子数量。
不妨设 为当前还剩余 个石子时的胜败态,则首先 一定为 。
其余 dp 值根据公平组合游戏的结论转移即可。
L. Deque
博弈论区间 dp。
两个人的目的都是让自己的值尽可能的大,让对方的值尽可能的小,且只能取序列的左侧或右侧。
所以状态其实就是当前剩余的石子的区间,我们可以设 表示两个人取完 中的所有石子后,最后一个取的人和另一个人差值的最大值。转移时有两个决策——取当前区间的左侧或是右侧。
那阶段是什么呢?我们发现长度更长的区间一定是由长度较短的区间转移而成的,所以我们可以将阶段设为区间的长度,转移方程为:
M. Candies
阶段显然可以是当前分到了第几个孩子,除此之外,我们还会关心当前一共分出了多少蛋糕,这个也需要设进状态。
所以我们可以设 表示当前分了前 个孩子,且一共分出了 个蛋糕时的方案数,决策有 个,转移方程为:
但是这样的方程直接转移是 的,而状态刚好覆盖了所有我们需要关注的东西,显然是无法再继续优化的。于是考虑优化转移。
注意到我们的 其实是一段编号连续的区间,而这个转移方程其实是一个类似于区间求和的方式。
所以我们不妨在处理完所有阶段 的状态之后,将当前阶段的每一个状态 单点加进一个数据结构,转移时做一个区间查询即可。
时间复杂度 。
N. Slimes
经典区间 dp——石子合并。
阶段和状态的设计与上面的 L 相似,我们可以将区间长度设为阶段,将当前区间所在位置设为状态。
那么决策是什么呢?对于当前区间 ,它的所有子区间的答案显然是已经被计算过了的。所以我们可以考虑将最后一次合并的位置作为决策,代价就是该位置两侧子区间的答案之和加上当前区间的总和。
设 表示区间 的区间和,状态转移方程为:
O. Matching
可以将当前配对的男生数量作为阶段,除了阶段之外,我们还会关心当前剩余的女生都有哪些,结合数据范围,考虑状态 dp。
我们设 表示当前已经给前 个男生配对,且还未配对的女生集合为 的配对方案数量。
那么决策其实也很好找了——所以和第 个男生可以配对的女生都可以成为这个男生的决策。
设 表示可以和第 个男生配对的女生集合,于是有:
P. Independent Set
简单树形 dp。
阶段显然可以是当前处理到的以某个节点为根的子树,除此之外,由于题目中对于【颜色】的限制,我们显然还可以设置另一维状态 表示当前根节点上所涂的颜色。根据题目要求,如果当前节点的颜色为黑色,则子节点必须都为白色,否则子节点可以是任意颜色。
按照决策即可列出转移方程:
Q. Flowers
也就是让我们求最大价值上升子序列。
阶段与普通线性 dp 一样,还是可以为当前处理完的花的数量。
我们可以设状态 表示处理完前 朵花,且强制选择第 朵花时的最大价值上升子序列。
那么决策显然可以是第 朵花前面所有高度更小的花,即状态转移方程为:
时间复杂度为 。状态已经十分简洁,考虑优化转移。
我们发现浪费我们转移时间复杂度的其实是前面那个 式子。而这个东西其实就是一个被转移阶段省去一维的二维偏序。
还剩下的一维偏序我们用树状数组等数据结构优化一下即可。
时间复杂度 .
R. Walk
观察到 奇特的数据范围——矩阵快速幂优化 dp!
考虑从长度为 的路径一直 dp 到长度为 的路径,也就是将当前路径的长度设为阶段。
而假设我们已经确定了一条长度为 的路径的起点,已经当前路径的起点 和终点 ,那么对于 的所有相邻节点 ,都可以由当前路径扩展成一条长度为 ,起点为 ,终点为 的路径,且很容易证明,这些路径都是不重的。
于是我们可以将我们关心的另一个东西——当前的节点编号——也设进状态。
即我们设 表示长度为 、以 为终点的路径的条数,则有转移方程:
但是本题特殊的数据范围显然是不允许我们正常转移的。
注意到节点之间的连边关系可以表示成一个矩阵的形式,且满足以下条件:
- 某一维状态可以写作一个矩阵的形式。
- 节点数量很少,即矩阵的规模很小。
- 阶段规模很大,即我们需要将阶段之间转移的时间内复杂度缩小为一个 。
考虑用矩阵快速幂进行优化转移。
初始矩阵中,每一个元素都为 ,即长度为 的路径中,以每一个点为起点的路径都为一条。
然后通过连边矩阵进行矩阵快速幂即可。
S. Digit Sum
模板数位 dp。
阶段可以是当前从高到底处理到的位数,除此之外,我们还关心当前各个数位的加和取模 的值,也可以将这个设进状态。
但是仅仅这样好像无法转移。我们不知道之前的数位是否严格小于上界——如果严格小于,则当前数位上的数可以是任何值;否则,只能是小于等于当前数位上限的某一个值。
于是可以在加一个 维表示前 位是否以及该严格小于了上界。这样以来转移就会方便许多了。
对于当前第三维为 的状态,决策只能是上一位第三维也为 ,且当前位为上界时的情况。
而对于当前第三维为 的状态,若上一位第三维时 ,决策可以是 到 的任何一个数;若上一位第三维为 ,则当前维最高只能为上界。枚举决策转移即可。
T. Permutation
典中典排列 dp(?
阶段显然依旧可以是处理完了前面的哪些数,且由于题目中规定了 是个排列,所以我们也可以设 表示用 到 组成的排列填充不等号序列的方案数。
那么对于 ,我们考虑的其实就是数 应该放在前 个数组成的排列中的哪里。
但其实插入一个数时不好插的,一次插入就会使得插入位置后面的所有不等关系发生改变。所以我们考虑在第 个位置放哪个数。
注意到如果对于前 个数,将大于某个值 的所有数都加上 后,是不会影响不等关系的。(不信可以自己列举一下各种情况)
而如果我们将大于某个值 的数都加上 ,那么其实在将 放进前面的同时,我们还空出了 可以放进第 个位置。
本质的来讲,我们将两者做了一个映射,方案数其实是相同的。
于是考虑第 个不等号的朝向以及第 个数的大小,后者显然可以被我们设进状态记录,对于前者,如果是小于,则可以选择所有末位数小于当前数的状态进行转移;如果是小于则相反。所以状态转移方程为:
观察到转移方程中的求和形式,利用前缀和优化转移即可。
U. Grouping
我们关心的其实只有每一组的兔子是否重复以及现在已经分了哪些兔子。所以我们可以考虑将已经选择了的兔子压进状态,作为阶段。
那么很显然我们对于 ,我们可以枚举 的子集 ,并有转移方程:
其中 数组显然是可以预处理的。考虑对 数组也作一个类似于状压 dp 的转移,对于 ,我们可以任意选择它的一个元素 并找出 中没有 时的集合 ,则有转移方程:
其中 表示 和 中所有元素的匹配贡献。
预处理的复杂度是 的,转移时需要枚举子集,时间复杂度为 。
故总时间复杂度为 。
V. Subtree
简单来说,题目需要我们求出以每个节点为根时的与包含根节点的连通块数量。
看到【以每个节点为根】的条件,第一反应应该是换根 dp。
考虑先以 号节点为根,作一遍树形 dp,则我们可以设 表示以 号节点为根的子树中,包含 号节点的连通块数量。那么决策是什么呢?对于每一个子节点 ,我们对于这个子树的连通块方案可以是所有 中包含的方案,也可以不选这个子树,那么根据乘法原理,我们有:
于是我们便可以在 的时间复杂度内完成一次 dp,算出以 号节点为根时的答案。
但我们显然不可能对于每一个节点都作一边这样的 dp,不然复杂度将达到 。
那么怎么优化呢?我们发现在我们进行一次 dp 之后,除了 号节点的答案,我们还得到了这棵树以 号节点为根时,以每一个节点为根的子树的答案。那么根节点之外的每一个节点,我们差的其实只有该节点到根节点的那棵子树对于该节点答案的贡献。
考虑再对答案数组 做一遍 dp,对于 ,其实答案就是 号节点的 值,与父亲节点删去 号节点之后的 值加上 之后的乘积。即:
这样的时间复杂度就是 的。然而本题还有一个陷阱就是,模数 并不一定是一个质数,所以不能计算逆元。
那该怎么办呢?考虑在第一次 dp 的时候,如果将每一个子树 dp 值都看作是序列中的一个元素,则除去 其实就相当于是求序列中删去一个元素之后的所有元素之积,而这个东西除了直接用除法计算之外,显然还可以使用前缀积乘上后缀积计算。于是我们预处理出每一个节点子树 dp 值的前缀积和后缀积,然后用这些辅助转移即可。
W. Intervals
考虑对于字符串的每一位逐位考虑,那么处理到的位数显然就是本题 dp 的阶段。
考虑设 表示处理了前 位,且第 位强制选 时的方案数。则决策就是上一次选 的贡献,设上一次选 的位置为 ,则当前决策的额外贡献就是左端点在 ,右端点在 之间的所有区间的贡献。
这个东西是可以用二维数点来做的,时间复杂度为 。
怎么优化呢?状态显然是没有办法继续优化的,只能考虑优化转移。
注意到所有状态的决策空间是在单调递增的,即当状态向右移动一位时,决策只会多一个而不会变少。那么是否可以高效维护每个决策的贡献以及它们的最大值呢?
对于一个固定的决策 ,考虑当当前状态从 扩展到 时对于决策贡献的改变——此时,所有左端点在 且右端点为 的区间的贡献都被删去了,而所有左端点为 ,右端点在 的区间又被加进贡献。如果我们将贡献和决策“反演”,考虑每一个贡献对于哪些决策会产生贡献,那么这两个东西是可以转化为区间加,用数据结构维护的。
这些区间的一个端点都是固定的,这就意味着我们如果将这些区间挂在它们的左右端点上,那么每一个区间只会被算两次贡献(左端点和右端点各算一次)。
此时算法流程大概已经有些明晰了,考虑用一个数据结构动态维护每个决策的贡献及其最大值,则对于当前状态 :
- 对于右端点为 的所有给定区间 ,设区间权值为 ,则我们将决策 的贡献都减去 。
- 对于左端点为 的所有给定区间 ,设区间权值为 ,则我们将决策 的贡献都加上 。
- 对于所有决策区间查询最大值即为 的值。
- 将 加进第 个决策。
时间复杂度为 。
本题的本质其实还是利用了状态的改变对于决策贡献的影响关系,通过用数据结构快速解决贡献的改变,来达到快速判断最优决策进行转移的目的。
优化 dp 问题的转移时也应多去考虑 决策的改变对于固定状态的贡献变化(即决策之间的性质,如单调性),状态的改变对于决策的贡献变化,并判断这些贡献是否可以快速处理,来达到快速判断当前最优决策的目的。
X. Tower
这道题的状态不难想,设计 表示当前最低层为第 个方块、且当前总重量为 时的最大贡献即可。难点就在于 dp 的阶段十分不明晰,使得我们的每一个状态都可能会用到其它任何一个决策进行转移,这显然是行不通的。
那如果我们可以将所有方块按照某种依据进行排序,使得后面的方块只会通过前面的方块进行转移,是否可以做呢?不妨先考虑在这种情况下我们应该如何解决这个问题。显然我们可以将第 个方块之前的,总重量为 的每一个状态作为决策。即有状态转移方程:
注意到 是一个定值,所以我们可以设 表示当前重量为 时的所有决策的最大值,此时时间复杂度即为 ,可以解决本题。
这一看就很对啊,但是问题来了,该怎么排序呢?
假设我们现在已经对于所有方块排好了一个序,那么对于两个相邻的方块 ,我们考虑应该满足什么条件才能使 放下面比 更优。
设 之前已经选择了重量为 的方块,那么如果 放在下面,则需要满足 ,即 ;若 在下面,则需要满足 ,即 。
那么按照最优考虑,我们显然是希望 尽可能地大,所以当 时,我们显然是希望选择 方块放在下面的。
变换一下就是,当 时,我们更青睐 这个方块。所以我们按照 从小到大将所有方块排序后再进行上述 dp 即可。
Y. Grid 2
网格图变大了,但是 却很小。 复杂度的算法显然时行不通了,我们需要思考一种复杂度和 有关的算法。
考虑若图中没有障碍,我们应该怎么快速计算从左上角到达右下角的答案。
dp 吗?其实不用。由于没有障碍物的干扰,从左上角到达右下角的方案数其实用组合数就可以算出来——设右下角的坐标为 ,则从左上角到右下角只能走 步,且其中要选择 步向下走,所以方案其实一共就是 种。
那么对于这道题呢?正着做算不经过障碍的路径数显然时不好做的,我们把它反过来,先算出所有路径数,再减去至少经过一个障碍的路径数。
前者显然有 种,考虑后者的计算。我们不妨规定假设有一条路径需要经过障碍,那么这条路径只会被最靠左上角的障碍计算到。我们设 表示从左上角到达第 个障碍不经过任何其他障碍的路径总数。那么对于当前状态 ,以及左上角的一个决策 ,则根据我们的状态设置, 中的计入所有路径显然都以 为左上角的第一个障碍。故我们枚举每一个 ,将 乘上 到 的路径总数即为 对 的负贡献。
状态转移方程为:
Z. Frog 3
斜率优化 dp 板子题。
阶段和状态都很好设置。我们不妨设 表示前 个格子的最小花费,则有:
但是这样显然是会 TLE 的,需要考虑优化转移。
考虑将贡献拆开,即为 。
将只与状态 有关的贡献去掉,还剩下 ,如果将 和 看作关于决策的常数,把 看作是变量,则这个式子其实就是一个关于 的一个一次函数。
所以对于所有状态集合,其实就等价于坐标系中的若干条直线,我们需要找到当 时, 值最小的一个决策。
观察发现,由于 是单调递增的,所以这些直线的斜率 其实是单调递减的,也就是说,对于两个决策 ,若 ,则我们可以认为 的发展潜力是更大的,即当 足够大的时候, 一定会比 更优。我们定义 取代 的第一个 为决策 的”临界值“。
发展潜力?是不是很熟悉?我们可以考虑用一个单调队列维护一个当前贡献单调递增,且临界值也单调递增的一个集合,
每次转移时:
- 若队首决策已经劣于下一个决策,则删除掉队首决策;重复删除队首直到队首决策更优或队列中只剩下这一个决策为止。
- 此时队首决策一定最优,去队首决策转移即可。
- 转移过后,考虑将当前状态作为决策加入单调队列。若队尾决策和当前状态的临界值比队尾前面的决策和队尾决策的临界值更小,则删除队尾,直到变得更大为止。
- 此时将当前决策加进队列。
那么临界值怎么计算呢?其实就是两个一次函数求解交点的过程。
若当前 比 优了,则有:
左侧式子即为决策 的”临界值“。
由于每一个决策只会进出队列一次,所以如此一来我们便通过一个单调队列实现了均摊 转移。
这道题其实是可以和 W 放到一块总结的,也是利用了决策之间的性质——即【发展潜力的单调性】。我们通过一个单调队列,利用决策之间的单调性,实现了快速选取当前最优决策的过程,从而优化了决策之间选取的冗余复杂度。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通