博主太菜,可能会炸联赛,于是恶补一下 QAQ
题目比较基础,动态更新
Tags
仅包含 提高组 内容。
- 类型:
区间 dp
,背包 dp
,树形 dp
,状压 dp
,计数 dp
,数位 dp
,概率/期望 dp
,环形 dp
,基环树 dp
。
- 优化:
单调栈/单调队列 优化
,其他数据结构 优化
,斜率 优化
,倍增 优化
Summarize
简单总结了部分基础类型的 dp 以及一些优化。
区间 dp
基础状态: (f(l, r)) 表示区间 ([l, r]) ……
基础转移模型示例:(f(l,r) = minlimits_{k}{f(l, k) + f(k+1, r)+cdots})。
基本实现:枚举区间长度,枚举左端点并计算出右端点,枚举断点。或者直接记搜。
树形 dp
树上动态规划。
基础状态:(f(x, cdots)) 表示以 (x) 为根的子树……
由于树的递归性质,基本用 Dfs 实现。
状压 dp
常见于元素个数较少的情况。
基础状态:(f(S, cdots)) 表示以 (x) 为根的子树……
集合使用二进制、位运算的思想压缩成一个整数并进行判断或转移。
计数 dp
常用于统计方案数。
设计状态是重点,转移看题意。
可能有一些组合计数的知识。
数位 dp
常见情况:统计值域在 ([a, b]) 中满足……条件的数的个数/数位数字和/……((a, b) 通常很大)
一般拆成 ( ext{ans}(b) - ext{ans}(a - 1)) 计算,以记忆化搜索实现。
搜索函数的参数一般有当前位置,前面是否有前导零,枚举时是否受当前位大小限制。但大多数题目还会有其他限制。
数位 dp 的复杂度和位数有关。
概率/期望 dp
一般情况下,解决概率问题需要顺序循环,而解决期望问题使用逆序循环,如果定义的状态转移方程存在后效性问题,还需要用到转化成求解方程组的解并使用高斯消元。
部分题目还会牵扯到状压、矩阵加速等。
对分数取模记得逆元。
环形 dp
环上的 dp,一般的处理方法有断环为链然后 dp 两次或 (n) 次,或者复制一遍。
基环树 dp
环形 dp + 树形 dp。据说很烦,不会。
背包 dp
主要是挖出经典模型。
单调栈/单调队列 优化
一般会出现滑动最值差不多的模型,如:(f(i) = minlimits_{j<i}{f(j)+ ext{val}(i, j) }),其中 ( ext{val}(i, j)) 为一个仅存在关于 (i) 或关于 (j) 的项的多项式,不存在同时关于 (i, j) 的项。
此时可以提出关于 (i) 的项,用单调队列维护 (f(j)) 和关于 (j) 的项,加速转移。
斜率 优化
上述 单调栈/单调队列 优化 的拓展。适用于 ( ext{val}(i, j)) 中含有同时关于 (i, j) 的项时的情况。
一般可以假设来两个决策点 (a, b(a<b<i)),并设 (a) 劣于 (b)。我们可以得到一个不等式,然后变形为 (dfrac{y_a - y_b}{x_a - x_b} = ext{slope}(a, b)) 的形式。最后用单调队列维护上/下凸包即可。
其他数据结构 优化
一般根据转移的特征,使用合适的数据结构优化转移。
倍增 优化
也许会有类似与 (2^k) 这种明显的提示。
一般把“转移 (j) 次”换成“转移 (2^k) 次”降下复杂度。
Content
- 「eJOI2017」Experience
树形 dp
- 「eJOI2018」山
- 「CSP2019-J」纪念品
背包 dp
- 「NOIp2018-普及」摆渡车
- 「AHOI2009」中国象棋
计数 dp
- 「LA 4394」String painter
区间 dp
- 「CSP2019-S」Emiya 家今天的饭
- 「Codeforces」Coloring Brackets
区间 dp
- 「USACO19DEC」Greedy Pie Eaters
区间 dp
- 「NOIp2018-提高」愤怒的小鸟
状压 dp
- 「Codeforces 372C」Watching Fireworks is Fun
单调栈/单调队列 优化
- 「Codeforces 16E」Fish
状压 dp
概率/期望 dp
- 「POI2014」Little Bird
单调栈/单调队列 优化
- 「POJ 1746」Minimizing maximizer
其他数据结构 优化 - 线段树
- 「POJ 2836」Rectangular Covering
状压 dp
- 「POJ 1821」Fence
单调栈/单调队列 优化
- 「HDU 3507」Print Article
斜率 优化
- 「HNOI2008」玩具装箱
斜率 优化
- 「HDU 2829」Lawrence
斜率 优化
- 「THUSC 2016」成绩单
区间 dp
- 「Codeforces 148D」Bag of mice
概率/期望 dp
- 「Luogu P1654」OSU!
概率/期望 dp
- 「Luogu P3413」SAC#1 - 萌数
数位 dp
- 「SDOI2016」征途
斜率 优化
- 「Luogu P1613」跑路
倍增 优化
「eJOI2017」Experience
update - 2020.7.27
设 (f(x, 0), f(x, 1)) 分别表示以 (x) 为根的子树,(x) 所在链为向下(非严格)递减、递增时的答案。
题目中要求求极差,那么结点 (x) 可能为最大值或最小值。
以向下递减为例,我们在所以满足 (W_x ge W_y) 的所有子结点 (y) 中,将原本 (y) 的贡献用 (x) 作为新的最大值替换,并更新 (f(x, 0)) 的答案。向下递增也是同理,不过这里是作为最小值,因此符号会改变。
在做结点 (x) 是,我们设 (p = sumlimits_{y in ext{son}(x)} max(f(y, 0), f(y, 1))),并令 (p) 为 (f(x, 0), f(x, 1)) 的初始值。
那么状态转移方程((leftarrow) 表示最(大)值操作):
时间复杂度 (O(n))。
「eJOI2018」山
update - 2020.7.27
设 (f(n, k, 0), f(n, k, 1), f(n, k, 2)) 表示 ([1, n]) 中,建造了 (k) 座房子,且第 (n-1),第 (n) 个位置的状态分别为 ((0,0),(1, 0), (0, 1)) 时((0) 表示没有房子,(1) 表示有)的最小花费时间。
状态转移方程:
时间复杂度 (O(n^2))。
「CSP2019-J」纪念品
update - 2020.7.27
直接存物品的个数显然原地升天。但仔细想一下发现,转换成统一在第 (i) 天买,然后在第 (i+1) 天卖,其实等价。因为即使对某种物品实际上并不操作,换成全卖了然后买回来也未尝不可。
不难设计出一个比较暴力的状态:(f(i, j, k)) 表示第 (i) 天,对于前 (j) 个物品,剩余现金为 (k) 时,下一天手头上的最大现金数。转移方程显然。
为了不空间爆炸,我们尝试优化。第一维显然可以缩掉——我们只要上一轮的最优值即可。剩下了两维看着像一个背包,于是倒序以下就只剩最后一维了。方程:
时间复杂度 (O(TN imes max P))。
「NOIp2018-普及」摆渡车
update - 2020.7.28
(t) 数组升序排序,显然的。设 (f(i, j)) 表示第 (i) 个人等了 (j) 分钟,送走前 (i) 个人等待总时间的最小值。
直接看 (j) 的范围貌似可以达到 (max t),但其实不然——一个人最多等 (< 2m) 分钟。
- 相比在起点停着 (m) 分钟,啥也不管直接开一趟(尽管空车)总不会更劣,那么停车一次不超过 (T_1 < m)。
- 一个人等一辆离开的车回来的时间 (T_2 le m)。
于是一个人等待的时间 (j = T_1 + T_2 < 2m)。
那么状态的规模差不多是 (O(n m))。考虑设计转移:
- 若 (t_i + j ge t_{i + 1}),那么第 (i+1) 个人赶得上本趟车,则:(f(i+1, j+t_i-t_{i+1}) leftarrow f(i,j)+j+t_i-t_{i+1})。
- 若 (t_i+j+mge t_{i+1}),那么第 (i+1) 个人可以在车回来前等车,则枚举车回来的时间 (k),转移为: (f(i+1, j+t_i+m+k-t_{i+1}) leftarrow f(i, j)+j+t_i+m+k-t_{i+1})
- 若 (t_i + j + m < t_{i +1}),那么第 (i+1) 个人在车回到起点后到,则枚举第 (i+1) 个人的等待时间 (k),转移为: (f(i+1, k) leftarrow f(i, j)+k)。
时间复杂度:(O(nm^2))。
「AHOI2009」中国象棋
update - 2020.7.28
会设状态基本就做完了。设 (f(i, j, k)) 表示考虑到第 (i) 行,有 (j) 列存在一个棋子,(k) 列有两个棋子的方案数。
转移非常显然:
- 不放:(f(i, j, k) leftarrow f(i-1, j, k))。
- 放在空列:
-
- 放一个:(f(i, j, k) leftarrow f(i - 1, j - 1, k) imes (m - (j - 1) - k))
-
- 放两个:(f(i, j, k) leftarrow f(i - 1, j - 2, k) imes C_{m - (j - 2) - k}^2)
- 放在已经有一个的列:
-
- 放一个:(f(i, j, k) leftarrow f(i-1, j+1, k-1) imes (j+1))
-
- 放两个:(f(i, j, k) leftarrow f(i - 1, j + 2, k - 2) imes C_{j+2}^2)
- 在已有一个的和空列各放一个:(f(i, j, k) leftarrow f(i - 1, j, k - 1) imes j imes (m - j - (k - 1)))
时间复杂度 (O(nm^2))。
「LA 4394」String painter
update - 2020.7.28
直接算 (A, B) 串不好处理,不如先考虑空串到 (B) 的步数。设 (f(i, j)) 表示从空串到 (B[i,j]) 的最小步数。
那么显然有:
我们令答案为 (g),(g(i)) 表示 ([1, i]) 部分的答案。那么:
答案为 (g(n)),复杂度 (O(n^3))。
「CSP2019-S」Emiya 家今天的饭
update - 2020.7.28
分析题目性质,发现不可能有两列或以上占去一半,不合法的只可能是一列。
那么容斥以下,答案等于 总方案数 减去 某一列不合法的方案数。
合法的方案不难处理:(g(i, j)) 表示前 (i) 行选了 (j) 个的方案数,那么显然 (g(i, j) = g(i - 1, j) + g(i - 1, j - 1) imes sum_j a_{i,j})。可以在 (O(n^2)) 时间内解决。
对于不合法的方案,有一个比较暴力的思路,枚举这个不合法列 (c),设 (f(i, j, k)) 为前 (i) 行,在第 (c) 列选了 (j) 个,其他列选了 (k) 个的方案数。那么有转移:
不合法的总数即为所有 (sum_{j>k} f(n, j, k)) 之和。然而这个需要 (O(n^3 m)) 的时间,只有 84 分。
突然发现我们只需要 (j, k) 之间的差值大小,并不需要具体大小,于是又砍掉一维—— (f(i, j)) 表示当不合法列为 (c) 时,考虑到前 (i) 行,第 (c) 列的个数比其他列多 (j) 个的方案数。转移方程:
这样时间复杂度 (O(n^2m))。
「Codeforces 149D」Coloring Brackets
update - 2020.7.29
显然不能直接用 (f(l, r)) 表示区间 ([l, r]) 的方案数,因为相邻位置也有影响。
于是带上 (l, r) 的颜色:(f(l, r, x, y)) 表示区间 ([l, r]) 中,位置 (l) 颜色为 (x),(r) 为 (y) 的方案数。颜色 (0/1/2) 分别表示无色,蓝色,红色。
那么分三种情况讨论:
- (l + 1 = r):边界条件,(f(l, r, 0, 1) = f(l, r, 1, 0) = f(l, r, 0, 2) = f(l, r, 2, 0) = 1)。
- 位置 (l, r) 为一组配对的括号:我们可以由区间 ([l + 1, r - 1]) 的 dp 值推出,转移显然,根据题意模拟即可。
- 位置 (l, r) 为不配对:那么我们可以由两组配对的 dp 值推出。具体地,设位置 (l) 的右括号的位置为 (k),那么答案就由区间 ([l, k]) 和 ([k + 1, r]) 得出。
推荐使用记忆化搜索实现,复杂度 (O(|s|^2))。
「USACO19DEC」Greedy Pie Eaters
update - 2020.7.29
设 (f(i, j)) 表示区间 ([i, j]) 被全部吃完后可得的最大 (sum w_i)。那么有
其中 (p(i, j, k)) 表示所有满足 (i le l_xle kle r_xle j) 的 (k) 中 (w_k) 的最大值。求 (p) 数组可以用向外拓展的方式:
在计算 (p) 时应先枚举 (k)。时间复杂度 (O(n^3))。
「NOIp2018-提高」愤怒的小鸟
update - 2020.7.29
众所周知三点确定一条抛物线。那么设 (s(i, j)) 表示点 ((x_i, y_i), (x_j, y_j), (0, 0)) 构成的抛物线上的点集。显然可以 (O(n^3)) 预处理。
设计状态:(f(S)) 表示覆盖点集 (S) 所需的最小抛物线数。转移:
这个方法是 (O(T imes 2^ncdot n^2)) 的,非常悬。
考虑一个优化:设 (x) 为点集 (S) 中没有出现过的最小编号的点的编号,即 (x = min{x| exttt{S&(1<<(x-1))}=0, xin mathbb N^+})。那么可以限定由 (S) 拓展的转移的线必定经过第 (x) 个点。
这相当于限定了顺序,因为原本乱序枚举与现在可以枚举到的范围其实是一样的。时间复杂度 (O(T imes 2^ncdot n))。
「Codeforces 372C」Watching Fireworks is Fun
update - 2020.7.29
设 (f(i, j)) 表示前 (i) 个烟花放完后,当前在位置 (j) 处,得到的最大快乐值。那么有:
显然这个 dp 是 (O(n^2m)) 的,需要优化。
发现这个 (b) 可以提出来,那么重新定义 (f):
答案即为 (sum b_i - min f(m, j))。又发现可以把 (|a_i-j|) 提出来,于是我们发现一个类似于 (f(i)) 一段滑动区间的最值问题,显然可以单调队列优化。发现 (k) 的范围其实有两段,于是正反各做一次, 时间复杂度 (O(nm))。
直接开数组炸空间,于是滚动数组滚掉第一维。
「Codeforces 16E」Fish
update - 2020.7.30
首先要知道 (x) 个鱼中任意一对相遇的机率 (P(x) = frac{1}{x(x-1)/2})。
(n) 很小,考虑状压——(f(S)) 表示存活下来的鱼的集合为 (S) 的概率。转移:
后面的 (-1) 改成了 (+1) 是因为原本的集合大小为 (|S| + 1)。
时间复杂度 (O(2^n imes n^2))。
「POI2014」Little Bird
update - 2020.7.30
设 (f(i)) 为到达第 (i) 个位置的最小消耗。那么有:
时间复杂度 (O(qnsum k_i)),直接爆炸。
考虑它是一个滑动最值,那么用单调队列优化这个过程。这里的比较关键字有两个:第一为 (f) 的大小,第二为 (d) 的大小。
优化后复杂度为 (O(qn)),STL 的话可能有点卡常。
「POJ 1746」Minimizing maximizer
update - 2020.7.30
有一个比较朴素的想法:(f(i, j)) 表示前 (i) 个 Sorter 中,位置 (j) 到位置 (1) 所需 Sorter 数量的最小值。那么有一个非常显然的转移:
但 (O(nm)) 的时空复杂度根本无承受。我们发现第一位明显可以缩掉:
然而最坏仍然是 (O(nm)) 的,于是使用线段树优化区间最值的过程,得到了 (O(mlog n)) 的优秀算法。
「POJ 2836」Rectangular Covering
update - 2020.7.30
首先预处理以所有点对为对顶点的矩形的面积和覆盖点集,分别即为 (s(i, j), p(i, j))。
设 (f(S)) 为覆盖点集 (S) 所需的最小面积。状态转移方程:
注意共线的特判处理。时间复杂度 (O(2^n imes n^2))。
「POJ 1821」Fence
update - 2020.7.31
先将工人按位置 (S) 排序。
设 (f(i, j)) 表示前 (i) 个工人,处理前 (j) 块木板,可得的 (sum P_i) 的最大值。朴素的状态转移:
显然第三个转移是 (O(n)) 的,考虑优化。
我们把第三个拆一下:(f(i, j) = maxlimits_{j - L_ile kle S_i - 1} {f(i-1, k) - P_i imes k}+P_i imes j)。
发现中间是一个上界固定为 (S_i - 1),下界逐渐上升的区间最值问题。于是单调队列优化。
时间复杂度 (O(nm))。
「HDU 3507」Print Article
update - 2020.7.31
第一道斜率优化 /cy
若令 (f(i)) 表示前 (i) 个的最小花费。那么有一个显然的转移:
(O(n^2)) 的时耗承受不了,考虑优化。
设 (k<j<i),若决策 (j) 优于 (k),那么满足不等式
化简得
我们将 (j, k) 抽象成一个二维点 ((x_j, y_j), (x_k, y_k)),那么设 (x_t = s(t),y_t = f(t)-s^2(t)),于是有:
我们使用斜率优化,用单调队列维护这些点,斜率递增。
当做到第 (i) 个时,先弹出队首不符合上述不等式的元素,再将队首直接转移至 (f(i)),然后将队尾违背斜率单调性的弹出,最后在尾部插入 (i)。弹出时须保证队中存在两个及以上的元素。
时间复杂度 (O(n))。
「HNOI2008」玩具装箱
update - 2020.7.31
设前 (i) 个的最小花费为 (f(i)),则:
为简化计算,设 (a_t = t + s(t),b_t=t+s(t)+L+1),于是:
对于某个 (j),我们将原转移化为:
设 (x_t = b_t, y_t = f(t) + b_t^2),那么这可以被视为一个直线的表达式,为斜率即为 (2a_i)。
斜率优化,维护一个斜率递增的单调队列即可,第一个满足斜率 ( ext{slope}(j, j+1) >2a_i) 的 (j) 直接转移。
时间复杂度 (O(n))。
「HDU 2829」Lawrence
update - 2020.8.1
显然的状态:(f(i, j)) 表示前 (i) 个站点,炸 (j) 次产生的最小值。
显然的方程:(f(i, j) = minlimits_{k<i} { f(k, j - 1) + c(i)-c(k)-s(k) imes (s(i)-s(k)) }),其中 (s(i) = sum_{j=1}^i a_i, c(i)) 表示前 (i) 个的两两乘积和,(=c(i-1)+a(i)s(i-1))。
单次转移 (O(n)),需要优化。设 (a<b),且决策 (a) 劣于 (b),那么有 (f(a, j-1)-c(a)-s(a)s(i)+s^2(a) > f(b, j-1)-c(b)-s(b)s(i)+s^2(b)),即:
设 (x_i = s(i), y_i = f(i, j-1)-c(i)+s^2(i)),那么带入上式:
于是斜率优化。时间复杂度 (O(nm))。
「THUSC 2016」成绩单
update - 2020.8.1
考虑区间动规,但显然不是 (f(i, j)),因为代价与最值有关,而我们这样是无法知道的。
正确的设法应该是,(f(i, j, l, r)) 表示区间 ([i, j]) 消去部分数,剩下数在值域 ([l, r]) 中的最小花费。
转移?分类讨论:
- (l = r = 0)。说明我们需要取完,分为直接取完整个区间,或先取这个区间中某段值域的数字:
- (l e 0) 或 (r e 0)。考虑分割这个区间。
-
- 当然如果 (i=j) 的话只要判断当前值 (w_i) 是否 (in [l, r]) 即可。
-
- 否则枚举断点 (k),左右两部分共有 3 中情况:左边取完,右边取完,都不取完:
可以使用记忆化搜索实现,总复杂度 (O(n^5)),但由于状态实际并没有这么多,跑不满。
「Codeforces 148D」Bag of mice
update - 2020.8.3
设 (f(i, j)) 表示轮到公主时,袋中剩下 (i) 只白,(j) 只黑,公主胜的概率。
首先 (f(i, j) leftarrow frac{i}{i + j}),即一下就抓到白的的情况。
两人都抓到黑的,中途跑出一只黑的:
两人都抓到黑的,中途跑出一只白的:
其他败的情况不计算内,复杂度 (O(w imes b))。
「Luogu P1654」OSU!
update - 2020.8.3
考虑这样一个等式:((x+1)^3 = x^3 + 3x^2 + 3x +1),相比 (x^3) 多出了 (3x^2 + 3x +1)。
设 (f(i)) 为前 (i) 个的期望分数。除此之外,还需维护一次方,二次方的期望值 ( ext{E}(x)) 和 ( ext{E}(x^2))。
对于一、二次方,有:
对于 (f(i)) 的计算存在两种情况,当前位置取或不取:
(O(n)) 递推即可。
「Luogu P3413」SAC#1 - 萌数
update - 2020.8.3
比较复杂的数位 dp。首先不能直接设 (f( ext{pos}, ext{isPa}, v_0, v_1)),而应设为 (f( ext{pos}, ext{isPa}, ex_0, v_0, ex_1, v_1))。其中从左至右分别为当前位置,之前是否已经构成回文,前二位是否有效(若为前导 0 则无效),前二位是什么数字,前一位是否有效,前一位是什么数字。多加了两维是为了防止形如 ( exttt{000041}) 这种前导 0 构成回文的实际非法的串混进来。
考虑用记忆化搜索实现。在枚举当前位的数字时,需要多计算一个 bool
值:下一次的回文情况(下一个 ( ext{isPa}))。
由于数位 dp 的重点根本不在方程而是实现,下面给出搜索部分的代码:
LL f[N][2][2][D][2][D];
LL solve(int pos, bool isPa, bool ex0, int val0, bool ex1, int val1, bool lim, bool zero) {
if (!pos) return LL(isPa);
if (!lim && !zero && (~f[pos][isPa][ex0][val0][ex1][val1]))
return f[pos][isPa][ex0][val0][ex1][val1];
LL ret = 0ll; int maxD = lim ? num[pos] - '0' : 9;
for (int d = 0; d <= maxD; d++) {
bool nxtPa = (val0 == d && ex0) || (val1 == d && ex1) || isPa;
bool nxtZr = zero && (d == 0);
bool nxtLm = lim && (d == maxD);
(ret += solve(pos - 1, nxtPa, ex1, val1, !nxtZr, d, nxtLm, nxtZr)) %= mod;
}
if (!lim && !zero) f[pos][isPa][ex0][val0][ex1][val1] = ret;
return ret;
}
「SDOI2016」征途
update - 2020.8.4
首先推式子,化简方差:(v = dfrac{1}{m}sumlimits_{i=1}^m v_i^2 - dfrac{1}{m^2}left(sumlimits_{i=1}^m v_i ight)^2),那么 (v imes m^2 = msumlimits_{i=1}^m v_i^2 - left(sumlimits_{i=1}^m v_i ight)^2)。
这个式子的第二项显然不变,着重考虑第一项中的平方和部分。
设 (f(t, i)) 表示走了 (t) 段到位置 (i) 的最小平方和。那么朴素的转移:
这个 (O(n)) 一次的转移显然可以斜率优化。
具体的,我们设 (a < b) 且 (a) 劣于 (b),那么:
然后用单调队列维护一个斜率递增的下凸包即可。复杂度 (O(nm))。
「Luogu P1613」跑路
update - 2020.8.4
这是一个最短路问题,但真正的图并不是原图。我们设 (f(u, v)) 为结点 (u, v) 的联通情况,现在需要处理出这个图。
看到 (2^k) 就很显然是倍增。设 (g(u, v, t)) 为是否存在连接 (u, v) 长度为 (2^t) 的路径。那么可以用 dp 的方法处理出来:
对于 (f(i, j)) 的值,若存在一个 (tin[0, log_2( ext{maxlongint})]),使得 (g(i, j, k)= exttt{true}),那么 (f(i, j) = 1),反之则为 (infty)。
最后在 (f) 上跑 Floyd 即可。时间复杂度 (O(n^3 log m))。
后记
- 原文地址:https://www.cnblogs.com/-Wallace-/p/13385430.html
- 本文作者:@-Wallace-
- 转载请附上出处。