DP 的凸优化

稍微浅浅地总结一下目前认识到的几种利用凸性优化 DP 的方法。

单调队列

考虑对于 \(f_i=\max_{j} f_j+v_j+p_iq_j\) 的形式。把 \(j\) 考虑成一个 \((q_j,v_j+f_j)\) 的点,然后 \(f_j+v_j+p_iq_j\) 即过点的斜率为 \(-p_i\) 的直线的截距,容易发现,最优解的点必然处于上凸壳(如果是 \(min\) 就是下凸壳)。所以维护上凸壳,我们相当于用这条直线平移去尝试与这个凸壳相切。

APIO2010 特别行动队

https://www.luogu.com.cn/problem/P3628

设朴素的状态 \(f_i\) 和前缀和数组 \(s_i\)

\(f_i=\max_{j} f_j+a(s_i-s_j)^2+b(s_i-s_j)+c\) 然后推一下得到

\[f_i=\max_{j} (f_j+s_j^2a-s_jb)+(-2s_is_ja)+(s_i^2a+s_ib+c) \]

发现 \(s_j\) 单增,并且有斜率 \(2s_ia\) 单减,于是可以用单调队列维护凸包。对于队首的维护,如果队首的斜率比 \(2s_ia\) 小那么就舍弃掉(无法相切),队尾需要保证斜率递减。

https://www.luogu.com.cn/record/54723476

ZJOI2007 仓库建设

https://www.luogu.com.cn/problem/P2120

(自己补充一道练手)

\[f_i=c_i+\min_{j<i} f_j+\sum_{k=j+1}^{i}(x_i-x_j)p_k \]

设前缀和 \(s=\sum p, t=\sum px\),则有

\[f_i=c_i+x_is_i-t_i+\min_{j<i} (f_j+t_j)-x_is_j \]

取每个点 \((s_j,f_j+t_j)\),然后用斜率 \(x_i\) 去切这个维护的下凸壳(斜率 \(>0\) 且维护最小值)。

https://www.luogu.com.cn/record/54726061

CDQ分治

针对上述问题但加入的点或者斜率不单调或者要求变多的情况。

考虑一开始先按照斜率排序,然后分治时按照 DP 枚举的顺序分成左右两半,先处理左边,然后左边按 \(x\) 排序,用单调队列/栈维护凸壳,进行从左半边到右半边的转移,随后处理右半边。

NOI2007 货币兑换

https://loj.ac/p/2353

必然存在一种最优的买卖方案满足每次买进操作使用完所有人民币,并且每次卖出操作卖出所有的金券。

对于 \(i\),设 \(f_i\) 表示第 \(i\) 天的最大收益,则第 \(i\) 天若买金券,则得到两种金券数分别为

\[x_i=\frac{f_ir_i}{a_ir_i+b_i}, y_i=\frac{f_i}{a_ir_i+b_i} \]

则我们对于转移,考虑枚举第 \(i\) 天卖出的是在哪一天买入的,则有 \(f_i=\max x_ja_i+y_jb_i\)。对于每个点 \(i\),我们将其看作平面上的点 \(x_i,y_i\),方程看成一条直线 \(y_j=-\frac{a_i} {b_i}x_j+\frac{f_i}{b_i}\),即拿一条斜率为 \(-\frac{a_i}{b_i}\) 尝试去切点,然后最大化截距。这种题很容易想到维护上凸包,但是问题在于加入的点的第二维是非单调的,斜率也是非单调的,很类似二维平面的一些问题,考虑 cdq 分治。

二分决策的时间(这是有序的维度),对于左半区间分治求出 DP 值,维护一个上凸壳,按照 \(x\) 排序,然后计算左半区间转移到右半区间的贡献,右半区间按照斜率排序,然后普通地去转移,完毕后分治右半区间。一开始按 \(k\) 排序。

https://loj.ac/s/1215011

BZOJ3963 [WF2011] Machine Works

考虑 DP 转移方程(已经按时间排过序)

\[f_i=\max_j{f_j-p_j+r_j+g_j(d_i-d_j-1)} \]

稍微转化一下

\[f_i=\max_j d_ig_j+(f_j-p_j+r_j-g_jd_j-g_j) \]

每个点转化成 \((g_j,f_j-p_j+r_k-g_jd_j-g_j)\)。考虑斜率 \(-d_i\) 从小到大排序了,于是 cdq 分治维护上凸壳,用单调栈弄转移即可。

https://www.luogu.com.cn/blog/forever-captain/solution-uva1106

WQS 二分

考虑这样一个函数 \(g_x\),表示选择 \(x\) 个物品并且满足一些要求的最大值,题目需要你求出一个特定的 \(g_m\)。你发现不考虑 \(m\) 的限制会非常好求,更重要的,你发现 \((x,g_x)\) 形成了一个凸壳,那么就可以用 wqs 二分来解决此类问题。

以最大值+上凸壳为例。我们用斜率 \(k\) 的直线去尝试切这个凸壳,设切点为 \((p_k,g_{p_k})\)。可以发现,\(p_k\)\(k\) 减小而增加(是单调的),所以考虑去二分这个斜率 \(k\)。我们知道,斜率为 \(k\) 的直线切上凸壳于使得其在 \(y\) 轴上的截距最大的一个点,即切点满足 \(b=g_{p_k}-kp_k\) 最大。发现右边的式子相当于给每个物品的价值减去 \(k\),然后求出使得目标函数最大的值(取的物品的数量)\(p_k\) 即可。

要注意,切的时候,若存在 \(i,j\) 然后发现两点间斜率为 \(k\)(二分的斜率),则会出问题,为了保证我们求出的是正确的解,我们计算答案的时候用的应该是题目限制中的 \(m\) 而不是 \(k\)

LG5633 最小度限制生成树

发现满足凸性,直接二分即可,比较板。

这无解判的让人骂人。

https://www.luogu.com.cn/record/55484025

CF739E Gosha is hunting

首先设转移方程 \(f(i,j,k)\) 表示对于前 \(i\) 个 Pokemons,用了 \(j\) 个 Poke 和 \(k\) 个 Ultra 的最佳期望值。这样转移显然是不行的。但是我们发现 \(f(i,j,k)\) 关于 \(k\) 形成一个凸包,并且 \(i,j\)\(k\) 独立。考虑 \(i,j\) 保持一样,然后 \(k\) 这维直接用 wqs 二分去求(即最后要求横坐标为 \(b\)\(f(i,j,k)\)),具体操作即每次用 Ultra 都有一个二分出来的代价。这样就 \(O(n^2\log n)\) 了。然后我们发现 \(j\) 这维实际上和 \(k\) 这维是一样满足凸性的,于是我们 \(j\) 也可以 wqs 二分(wqs 二分套 wqs 二分),然后就能 \(O(n\log^2 n)\) 了。

https://codeforces.com/contest/739/submission/125451013

SDOI2016 征途

发现答案实际上是 \(\min -sum^2+m\sum x^2\),即最小化平方和。有两种做法。设 \(f_{i,j}\) 表示前 \(i\) 个分 \(j\) 段的方案数,则有 DP 转移(设 \(s\) 为前缀和):

\[f_{i,j}=\min f_{k,j-1}+(s_i-s_k)^2 \]

  • 法一:斜率优化。

    拆一下式子,\(f_{i,j}=s_i^2+\min -2s_is_k+(f_{k,j-1}+s_k^2)\),发现是斜率优化的形式(并且有斜率 \(-2s_i\) 单调,横坐标 \(s_k\) 单调)。
    https://loj.ac/s/1219724

  • 法二:wqs 二分。

    发现 \(f_{i,j}\) 关于 \(j\) 满足凸性(每个 \(f_{i}\) 都形成一个下凸壳)。于是我们可以考虑用 wqs 二分求出其在 \(m\) 处的值。二分斜率的切线的截距 \(g_x=f_x-km\),即每切一段都要有 \(-k\) 的代价(\(k<0\))。然后内部直接 DP,\(f_i=\min f_j+(s_i-s_j)^2+k\),最后统计一下即可。
    https://loj.ac/s/1219777

  • 我们发现实际上 \(i,j\) 两维都是凸的,然后两个算法结合一下即可达到 \(O(n\log n)\)

https://loj.ac/s/1219779

通过比对,我们发现斜率优化主要是对决策转移的优化(转移满足凸性),wqs 二分主要是对状态的优化(状态满足凸性)。而合理利用凸性能带来很好的优化。

Min/Max 卷积

穿越时空

一个带点权的树,你可以从任意节点开始,每次可以往任意一个祖先跳或者往任意一个子树节点跳,一个节点只能被跳一次,最大化经过的节点的点权和。\(n\le 10^5\)

必然是从 \(u\) 跳到一个祖先,才能继续转化到另一个子树中。设 \(f(u,j)\) 表示在 \(i\) 的子树内,有 \(j\) 次重新选择起始点(然后跳到子树中)的次数机会。考虑用类似树形背包的转移。\(f(u,j)=w_u+\sum_{\sum c=j+1} \sum_{v\in son_u} f(v,c_v)\)

发现这是若干个子树值的一个 max 卷积,所以 \(f(i,j)\) 是关于 \(j\) 的一个凸函数【凸函数做max卷积还是凸函数】。【两个凸函数做max卷积的差分数组相当于两个凸函数的差分数组做归并排序】。考虑用数据结构维护凸函数的差分数组。然后我们每次的时候要把第一个和第二个给加起来(因为 \(sum=j+1\) 所以要向左移动)(即pop掉第一个第二个,然后加起来再加上 \(w_i\) 再 push 进去),对于 \(+w_i\),只要给第一个加一个 \(w_i\) 即可。可并堆/启发式合并即可。

BZOJ3252 攻略

我们把转移写的奇怪一点:

\[f_{u,i}=[i>0]w_u+\max_{\sum c_v=i} \sum_{v} f_{v,c_v} \]

我们发现 \(f_{u}\) 即所有 \(f_{v}\) 的 max 卷积,然后最后 \(i>0\) 的位置加上 \(w_u\)
https://www.luogu.com.cn/blog/forever-captain/solution-bzoj3252

拐点集合

https://codeforces.com/blog/entry/77298 这篇 blog 把维护拐点的技巧描述的很清晰。

对于斜率为整数的凸壳,可以用一个拐点集合和一个直线来表示。
然后是一个非常重要的维护手段——维护拐点来维护凸壳。一个下凸壳(其实上凸壳也是类似的)可以按照如下维护(维护一个集合和一条直线):对于一个凸壳上的点 \(s_i\),如果它两端的斜率不一样,就放拐点。设两边斜率差为 \(p\),那就在集合中放入 \(p\)\(s_i\) 的横坐标。由于最后一段必然是无限延申下去的一条支线,我们考虑用一个直线来描述一条直线。容易发现,用一个一次函数和一个拐点集合能够描述一个这样的斜率为整数的凸壳。

设凸函数 \(C_1,C_2\) 的集合为 \(S_1,S_2\),并且直线分别为 \(f_1,f2\),则 \(C=C_1+C_2\) 的集合为 \(S=S_1\cup S_2\),一次函数是 \(f=f_1+f_2\)(斜率相加,截距相加)。

CF713C

https://www.luogu.com.cn/blog/forever-captain/solution-cf713c 我写的 slope-trick。

posted @ 2021-08-14 17:56  LarsWerner  阅读(647)  评论(1编辑  收藏  举报