决策单调性
警告:本篇中关于“二维凸性”的描述有误,四边形不等式并不意味着二维凸性,这种性质被称为矩阵的蒙日性,是极为高阶的内容,感兴趣者可以自行搜索。
但这个错误对本篇的应用价值没有损害,在怀疑一个问题有决策单调性时,更多地我们会去观察贡献形式,进而举反例或打表,而不是尝试去证明其满足四边形不等式。
决策单调性是一类统称,根据其名字可以直观感受到优化的思路,即把无用的决策点压缩起来,引起复杂度质的改变。
通常决策单调性体现在一维上(之后也有多维的例子),令 S 表示状态组,p(i) 表示 fS,i 的最优转移点在 i 这一维上的最小/最大数值,我们描述其为:
i<j⇒p(i)≤p(j)
决策单调性可以用来优化 dp。
注:斜率优化与 Smawk 算法也属于决策单调性范围,但前者有更便利的理解方式,后者绝大多数时候把 O(nlogn)→ 大常数 O(n),所以本文不涉及到这两种算法。
目录:
- 理论:四边形不等式
- 概述:实质是凸优化
- 最小后缀问题
- 区间拆分问题
- 区间合并问题
- 实现方法:分治
- 概述:最常用的技巧
- trick: 贡献难算
- 例题
- 实现方法:二分队列
- 带个数限制的区间拆分问题
- 概述:复杂度上的进展
- 解决方法:wqs 二分 + 决策单调性
- 例题
- 理论:反四边形不等式
- 实现方法:二分栈
- 实现方法:线段树分治 + 分治
理论:四边形不等式
概述:实质是凸优化
四边形不等式作为一种优化,绝大多数情况下都是用来判断决策单调性的。
假设 w(i,j) 是一种代价函数,其应用场景在下面我们会提到。
四边形不等式:若 ∀a≤b≤c≤d,w(a,c)+w(b,d)<w(a,d)+w(b,c),则称 w 这个二元函数满足四边形不等式,简记方法是**「交叉优于包含」**。
一些更深的理解(初学可以跳过)
使用更高的视角来看该式,我们可以变形得:
w(b,d)−w(a,d)−w(b,c)+w(a,c)≤0
若把 w 写到一个二维平面上,则若 w 是某个二元函数 g 的二维前缀和,该式描述的就是每个子矩形的和都非正,更清楚的描述是每个点上的值都非正。
换句话说就是 w 的二维混合差分 ΔiΔjw 在定义域中非正。
但请注意这个“在定义域内”,这并不能理解为这个矩阵的所有元素都非正,如果那样就可以推导出 a 不变 b 越大则 w(a,b) 越小。
实际上取 w(a,b)=b−a 就可以推翻上面的最后一个推导,原因是四边形不等式仅仅描述了一个非正矩形的二维前缀和,而这个“非正矩形”是可以作为一个大矩形的子矩形的!外面的东西不受我们控制,所以四边形不等式没有那般强悍的性质。
想象一下 w 的图像,实际上它应当是一个三维下凸包(z 轴是数值维),它的每个平行于 z 轴的二维截面都是凸函数,而不是一个单调函数。
所以四边形不等式以及反四边形不等式(下凸包变成上凸包,在“二分栈”部分会提到)能够优化的本质其实是二维凸优化。
这可以解释许多 w 的性质,在后文我们会提到。
四边形不等式可以用来判断一些情况下的决策单调性。
最小后缀问题
考虑一类 dp:fjmini≤jw(i,j),其中 w(i,j) 是一种代价函数(这甚至不能被称作 dp,但我们此处仅讨论决策点的关系)。
- 定理:若 w 满足四边形不等式,则 f 满足决策单调性。
证明
Proof:使用反证法,我们假设 c<d,且 a=p(d)<p(c)=b,此时必定满足 a<b<c<d,则:
w(a,d)w(b,c)≤w(b,d)≤w(a,c)
此时 (1)+(2):w(a,d)+w(b,c)≤w(b,d)+w(a,c),前者 < 后者时直接与四边形不等式矛盾,前者 = 后者时取等条件变为 (1),(2) 的取等条件,可知 a,b 都能转移到 c,d,没区别,与 p(c),p(d) 的最小/最大性矛盾。
你可能会说怎么会有 dp 式长这样呢,但其实这属于一个中间结论,可以证明满足四边形不等式的 w(a,b) 每维对另一维都有决策单调性。
这在更高维的视角来看其实就是三维凸包的一维 x(y) 增大,数值维 z 对另一维 y(x) 的极值点偏移是单调的。
区间拆分问题
考虑一类 dp:fjminfi+w(i,j)(或者 fjmingi+w(i,j))。
(可以理解为将 [1,n] 拆分为若干区间,每个区间都有代价。)
如果不给任何额外信息,这是一个 1D/1D 动态规划,需要 O(n2) 的时间复杂度。
- 定理:若 w 满足四边形不等式,则 f 满足决策单调性。
证明
Proof:类似上文,假设 c<d,且 a=p(d)<p(c)=b,此时必定满足 a<b<c<d:
fa+w(a,d)fb+w(b,c)≤fb+w(b,d)≤fa+w(a,c)
此时 (1)+(2) 也能导出上文的结论。
这个证明也告诉我们,f(a)+w(a,b)+g(b) 和 w(a,b) 是否具有四边形不等式的性质是相同的,证明就是在定义式中 f,g 都消掉了。
之后判断 w(i,j) 是否满足四边形不等式的时候,就可以忽略仅和 i 或者 j 有关的项了。
更深入的理解
这在更高维的视角其实就是把区间拆分成 w(a,b)+w(b,c)+w(c,d)+⋯,考虑任意两个相邻项的合并,其实是 r=b 的一个凸函数与 l=b 的一个凸函数的二维归并,其实质是对第一个凸函数的每个点复制一份第二个凸函数,并整体加上该点的 y,这自然得到了一个二维凸函数。
- 对它的证明可以看出,决策单调性不依赖 f 数组,只依赖 w 的四边形不等式性质,换句话说就算我给定 f 的初值不满足任何性质,也可以套用决策单调性。
对 fjminfi+w(i,j) 的优化是下文将要提到的“二分队列”,对 fjmingi+w(i,j) 的优化是下文将要提到的“分治”。
区间合并问题
考虑一类 dp:fl,rminfl,k+fk,r+w(l,r),此时记 p(l,r) 为最优且最小/最大的 k。
区间包含单调性:∀a≤b≤c≤d,w(b,c)≤w(a,d),即小区间优于大区间,其实质是 w(a,b) 关于 a 单减,关于 b 单增。
差分的视角来看就是 Δiw≤0,Δjw≥0。
- 引理:若 w 同时满足 区间包含单调性 和 四边形不等式,则 fl,r 也满足四边形不等式。
证明
Proof:设 a≤b≤c≤d,我们要证 f(a,c)+f(b,d)≤f(a,d)+f(b,c)。
令 e=p(a,d),讨论 e 的位置。
- e∈[b,c]。
- 此时考虑 E=p(b,c) 的位置,下处考虑 E≤e,E>e 是对称的,目前字母们的顺序是 a≤b≤E≤e≤c≤d:
f(a,d)+f(b,c)=[f(a,e)+f(e,d)+w(a,d)]+[f(b,E)+f(E,c)+w(b,c)]=[f(a,e)+f(b,E)]+[f(e,d)+f(E,c)]+[w(a,d)+w(b,c)]≥[f(a,E)+f(b,e)]+[f(e,d)+f(E,c)]+[w(a,c)+w(b,d)]=[f(a,E)+f(E,c)+w(a,c)]+[f(b,e)+f(e,d)+w(b,d)]≥f(a,c)+f(b,d)
-
- 解释:等号都是拆开或者分组,第一个 ≥ 中的第一组是一个归纳假设,即用小区间的四边形不等式证明大区间的四边形不等式。
- 第二个 ≥ 其实就是把两组分别找了两个转移点,必定不优于最优转移点。
- e∈[a,b)∪(c,d],假设 e∈[a,b),另一种情况是对称的,目前字母们的顺序是 a≤e≤b≤c≤d:
f(a,d)+f(b,c)=[f(a,e)+f(e,d)+w(a,d)]+f(b,c)=f(a,e)+[f(e,d)+f(b,c)]+w(a,d)≥f(a,e)+[f(e,c)+f(b,d)]+w(a,d)=[f(a,e)+f(e,c)+w(a,c)]+f(b,d)≥f(a,c)+f(b,d)
-
- 解释:第一个 ≥ 是归纳假设以及区间包含单调性,第二个 $\ge $ 是最优转移点。
证明比结论复杂不少,记住结论就好。
- 定理:若 f 满足四边形不等式,则 p(l,r−1)≤p(l,r)≤p(l+1,r),前后两个区间的长度比该区间少 1。
证明
Proof:f(l,k)+f(k,r)+w(l,r) 若 r 固定,将参数视为 (l,k),则其满足四边形不等式,因为与 (l,k) 同时有关的项是 f(l,k)。
根据“最小后缀问题”部分的推论,我们可以知道 k 对 l 满足决策单调性,也就是 p(l,r)≤p(l+1,r)。
类似的,l 固定时 (k,r) 满足四边形不等式,p(l,r−1)≤p(l,r)。
有了该结论我们就可以直接记录 pl,r,然后利用其优化转移,区间长度相同的每层总枚举次数是 n,复杂度 O(n2),利用该结论你可以通过经典例题:O(n2) 的石子合并。
在实际应用中,我们几乎完全不关心四边形不等式的成立性,通常打个表验证就可以开始做了,或者蒙一个上去然后拍。
实现方法:分治
概述:最常用的技巧
分治套路通常有一维是层数,每层仅转移到下一层。
每次处理 [l,r] 的时候,区间内的最优转移点必定 ∈[p(l),p(r)],我们暴力求出 p(m=2l+r),然后递归,总复杂度 O(nlogn)。
1
| void DP(int l,int r,int pl,int pr);
|
trick:贡献难算
如果我们的贡献难以 O(1) 求出,但是能快速移动左右端点,那也可以达到 O(nlogn) 的复杂度,这是一个震撼的结果。
左右端点分开考虑,右端点每次从 l 或 r 跳到 m=2l+r,左端点每次从 p(l) 或 p(r) 跳到 p(m),移动次数都是区间长级别的,故得证。
(该分析建立在每次还原端点的基础上,实际操作上不还原也不会劣于该算法。)
值得注意的是,类似回滚莫队,该算法也是可以支持端点单调变化的,只需要支持撤销,具体推导类似回滚莫队,略过了(CF1386C)。
数轴上有 n 个点,每个点有位置和权值 ai,还有一个已经确定的在所有点右边的洞,每个点会往右走直到遇到一个洞,此时向答案贡献权值与路程的乘积。
让你再在任意位置添加两个洞,使得答案最小。
n≤2×104。
显然洞一定修在某一个点脚下。
设状态 fi,k 表示前 i 个点有 k 个洞(只算前 i 个点)的总代价,转移枚举上一个洞在哪,O(n2),fi,k←fj−1,k−1+w(j,i)。
我们直接大胆猜测这玩意有决策单调性(这个洞往后移了它前一个洞显然没有理由往前移),开始分治。
快速求 w(j,i)=∑k=ji(xi−xk)ak,只需要维护区间 xkak 的和还有 ak 的和即可。
核心代码
1 2 3 4 5 6 7 8 9
| #define mid (L+R>>1) void DP(int L,int R,int pL,int pR) { if(L>R) return; int pM=pL;f[mid]=w(1,pM-1)+w(pM,mid); F(i,pL+1,min(mid,pR)) if(w(1,i-1)+w(i,mid)<=f[mid]) pM=i,f[mid]=w(1,i-1)+w(i,mid); DP(L,mid-1,pL,pM);DP(mid+1,R,pM,pR); }
|
数轴上有 n 个村庄,你要放置 m 个邮局,使得村庄到最近邮局的距离和最小。
n≤3000,m≤300。
顺便提一嘴:只放一个邮局是经典问题,放在中位数上即可,证明可以考虑往左挪一格和往右挪一格答案的变化量。
根据这个经典问题我们知道邮局放在某个村庄上肯定不劣。
w(l,r) 就是 2xl+xr 左边的村庄都去左边的邮局,右边的去右边的邮局,O(logV) 二分出分界点计算即可,计算方法同上题。
复杂度 O(nmlognlogV)。
本题村庄坐标很小,可以对每个坐标预处理出比它大的第一个村庄,可以省掉一个 logV。
w(l,r)=∑i<j,i,j∈[l,r][ai=aj],把整个序列拆分成 k 段,求所有段费用和的最小值。
n≤105,k≤min(n,20)。
首先盲猜有决策单调性(这个可以利用四边形不等式交叉优于包含来证),利用上文的 trick 可以求出贡献。
核心代码
1 2 3 4 5 6 7 8 9 10 11 12
| void DP(ll g[],ll f[],int L,int R,int pL,int pR) { if(L>R) return; while(r<mid) A(++r); while(l>pL) A(--l); while(r>mid) D(r--); while(l<pL) D(l++); int pM=pL;f[mid]=g[pL-1]+res; F(i,pL+1,min(mid,pR)) { D(l++); if(g[l-1]+res<=f[mid]) pM=i,f[mid]=g[l-1]+res; } DP(g,f,L,mid-1,pL,pM);DP(g,f,mid+1,R,pM,pR); }
|
w(l,r) 是区间颜色数,求 k 段价值和的最大值。
n≤35000,k≤50。
前面是同一个数越来越多则贡献爆炸增长,求总贡献最小,这里是同一个数第二次出现就没贡献,求总贡献最大。
同理可得本题也有决策单调性,跟上题一样做即可。
w(l,r) 是区间顺序对数,求 k 段价值和的最小值。
n≤25000,k≤25。
这种题你就要想如果你是一个大胖段,你把左右元素扔给左右的段会不会让全局答案少很多,显然是会的。
所以可以感性理解到决策单调性。
HDU2829 Lawrence
w(l,r)=∑i,j∈[l,r],i<jaiaj,求 k 段价值和的最小值。
n,k≤103。
不难发现交叉优于包含,决策单调性即可。
w(l,r)=∑i,j∈[l,r],i≤j[gcd(i,j)≥l],把 [1,n] 分成 k 段,每段非空,求 k 段价值和的最小值。
T≤3×105,1≤k≤n≤105,3 seconds。
按照套路首先盲猜 w 满足四边形不等式。
神级结论:若 n<2k,则令第 i 段为 [2i−1,2i),这样每段任意不同两数的 gcd 都 <2i−1,答案为 n,达到了理论最优。
然后 k 就变成 O(logn) 了。
倘若我们对每个前缀求出一个数组 fi,k,表示前 i 个人分成 k 段的总价值,则我们可以快速回答询问,这是一个 soft 1D/1D 动态规划。
现在的问题就是快速计算 w(l,r)。
我们看一下能否莫反,令 x/y=[yx]:
i=l∑rj=i∑r[(i,j)≥l]=k=l∑ri=1∑r/kj=i∑r/k[(i,j)=1]=k=l∑ri=1∑r/kφ(i)=k=l∑rSφ(r/k)
这是一个整除分块的形式,可以 O(r) 计算,那你总复杂度就是 O(nnlog2n),还是很恐怖的。
我们可以对每个 r 预处理出根号段相等数列,记录每段那些相等元素的值,记录后缀和,然后就可以 O(1) 查询了。
复杂度 O(nn+nlog2n)。
w(l,r)=∑i<j,i,j∈[l,r]ai,j,求分 k 段代价和最小。
n≤4000,k≤min(n,800),ai,j≥0。
先证一下决策单调性:交叉或包含时中间那块的元素都可以与本身贡献两次,与两边贡献一次。
但交叉时两边元素不能互相贡献,包含时可以,故交叉优于包含。
预处理 w(l,r),跑决策单调性,复杂度 O(nklogn+n2)。
将 n 段不能分割的路合并成 m 段,使方差最小,输出方差 × m2。
n≤3000。
化简一下发现其实输出的是 m∑x2−(∑x)2。
后面一项是已知的,只需要令前面那项最小即可。
w(l,r)=(∑i=lrai)2,这一看就很凸。
给定一个长度为 n 的序列 {an},对于每个 i∈[1,n] ,求出一个最小的非负整数 p ,使得 ∀j∈[1,n],都有 aj≤ai+p−∣i−j∣
1≤n≤5×105,0≤ai≤109 。
把式子变形:pi=maxj(aj+∣i−j∣)−ai,我们正反做一遍,把 ∣i−j 变成 i−j,显然 i−j 是凸的,所以把 a 视为原来的 dp 数组进行一次决策单调性转移即可,分治就可以解决。
需要注意的细节
很烦,决策单调性涉及到小数的时候必须要用浮点类型存,否则函数的凸性并不能保证。
最后的时候再取整。
沙东省集2024 D2T1
给定一个序列,对每个 k∈[0,n] 求出取反至多 k 个元素最大的最大前缀和是多少。
n≤2×105。
这个东西并不是凸的,也不是增量构造的。
考虑 k 对最终选择的前缀是具有决策单调性的,即 k 增大,最终选择的前缀必定不会减少。
确定前缀我们只需要反转绝对值最大的几个负数即可,这可以用主席树实现。
然后套上分治即可。
实现方法:二分队列
概述:在线的决策单调性
二分队列是(半)在线版本的决策单调性,相对的讲,分治是离线版本的决策单调性,二者的适用范围没有其他区别。
我们可以优化四边形不等式中提到的这个问题:fjminfi+w(i,j),i<j⇒p(i)≤p(j)。
具体来说,我们维护一个单调队列,每个元素都是一个三元组 (l,r,p),表示 [l,r] 的目前的最优决策点都相等,是 p。
- 初始时该队列中仅有 (1,n,1),我们枚举 k=(2→n) 作为决策点插入进队列中。
- 当插入决策点 k 时,我们首先将它与队尾 (l,n,p) 进行比较,若 k 决策 l 比 p 还优秀,那 p 肯定没用了,可以弹出队尾。
- 重复上述过程直到有一个决策点与他各有千秋,谁也不能完全代替谁,则把该决策点插进队尾,接下来我们需要求出两个人的 决策分界点,在 w 好求的前提下,这可以使用二分来求出。
由于决策点是从小到大插入的,故我们比较两个决策点的时候二者的贡献皆已确定。
上面这类 dp 的决策点贡献需要依赖 f,故需要边求决策点边更新 f,下面的口胡题是另一类不需要依赖 f 也不应当更新 f 的题目。
上面这种实现方法可以见下面例题的“核心代码”。
口胡题中使用了二分队列的另外一种实现方式,与二分栈十分相似,我们在二分栈部分介绍。
给定 n 个串,分成若干段,每个串不能切开,两个在同一段的串中间要加一个字符,给定 L,P,使 ∑∣x−L∣P 最小。并构造方案。
n≤105,T≤5,P≤10。
其实就是 w(l,r)=∣∑i=lrai+r−l−L∣P,令 ai←ai+1,L←L+1 就是 w(l,r)=∣∑i=lrai−L∣P。
然后 fj←fi−1+w(i,j),求 minfn。
二分队列板题。
需要注意的细节
本题答案超过 1018 时无需输出,但实际存储上我们不应将超过 1018 的量统一用 ∞ 存储,这会导致决策单调性无法比较两个值的大小关系。
正确的做法是使用 long double,它的精度足以表示 [1,1018]∩N。
还有就是本题是严格判等,字符串不要多输出空格和换行。
核心代码
1 2 3 4 5 6 7 8 9 10 11 12
| ld val(int l,int r) { return f[l]+w(l,r); } main: q[hd=tl=1]={1,n,0}; F(i,1,n) { while(hd<tl&&q[hd].r<i) ++hd;f[i]=val(pos[i]=q[hd].p,i); while(hd<tl&&val(i,q[tl].l)<=val(q[tl].p,q[tl].l)) --tl; int L=q[tl].l,R=n,mid,ans=n+1; while(L<=R) mid=L+R>>1,val(i,mid)<=val(q[tl].p,mid)?R=mid-1,ans=mid:(L=mid+1); if(ans<=n) q[tl].r=ans-1,q[++tl]={ans,n,i}; }
|
这里的结构体写法也可以变成队列中仅记录决策点,然后对每个决策点维护决策范围两个数组,是一样的。
口胡题
给定一个有向正权图,定义路径的权值 d(e1,e2,e3,…)=∑iw(ei,ei+1),其中 w(a,b) 是能快速计算的关于 a,b 的单增凸函数,例如 w(a,b)=∑i=05aib5−i。
求 1 到每个点的最短路。
n≤2×105。
看到这个条件我们第一反应肯定是把边当作点跑最短路,但那显然规模无法接受。
但我们知道长得像最短路的问题通常只能用最短路算法来做,所以我们顺着往下想。
每条边现在都有一个二元组 (di,wi),表示最短路和边权,你发现这可以决策单调性,将每个点出点按照 w 从小到大排序,则决策点必定 d 单调不升,w 单调不降(因为两个大一点的 w 就可以爆炸增长)。
用我们更熟悉的语言来表述:将决策点按 d 从小到大排序,则交叉优于包含。
你发现 dijkstra 的过程正好就是把 d 从小到大确定的,于是我们在每个点维护入点对出点的决策单调性,对每条边维护它的决策范围。
每条边要维护目前更新到决策范围中的哪条出边了,这条出边出队之后该点需要更新下一条出边,即下文代码中的 upd()。
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| const int N=2e5+5;vector<int> G[N],q[N]; ll a[N],b[N],c[N],d[N],Z[N],ans[N];int n,m,k,X[N],Y[N],st[N],ed[N],vis[N]; struct S { ll d;int x,pre;S(ll d=0,int x=0,int pre=0):d(d),x(x),pre(pre) {} bool operator<(S y)const{return d>y.d;} };priority_queue<S> Q; int fd(int x,int y) { int v=Y[x],L=0,R=sz(G[v])-1,mid; while(L<=R) mid=L+R>>1,d[x]+w(Z[x],Z[G[v][mid]])>=d[y]+w(Z[y],Z[G[v][mid]])?R=mid-1:(L=mid+1); return L; } void psh(int x) { int y=Y[x];auto &q=::q[y]; if(sz(q)&&Z[q.back()]<=Z[x]) return st[x]=1,ed[x]=0,void(); while(sz(q)>1&&fd(q.back(),x)<=fd(q[sz(q)-2],q.back())) st[q.back()]=ed[q.back()]+1,q.pop_back(); if(sz(q)) st[x]=fd(q.back(),x),ed[q.back()]=st[x]-1; else st[x]=0;ed[x]=sz(G[y])-1;q.pb(x); } void upd(int x) { if(st[x]>ed[x]) return;int y=G[Y[x]][st[x]]; d[y]=min(d[y],d[x]+w(Z[x],Z[y])); Q.push(d[y],y,x),st[x]++; } int main() {freopen("path.in","r",stdin);freopen("path.out","w",stdout); n=read();m=read();k=read(); F(i,1,k) a[i]=read(),b[i]=read(),c[i]=read(); F(i,1,m) { X[i]=read();Y[i]=read();Z[i]=read(); G[X[i]].pb(i); } F(i,1,n) sort(begin(G[i]),end(G[i]),[](int x,int y){return Z[x]<Z[y];}); memset(ans,0x3f,sizeof ans);memset(d,0x3f,sizeof d); for(int x:G[1]) Q.push(d[x]=0,x,0);F(i,1,m) st[i]=1; while(!Q.empty()) { auto x=Q.top();Q.pop(); if(x.pre) upd(x.pre); if(vis[x.x]) continue;vis[x.x]=1; psh(x.x);upd(x.x); } F(i,1,m) ans[Y[i]]=min(ans[Y[i]],d[i]); F(i,2,n) printf("%lld\n",ans[i]>1e18?-1ll:ans[i]); return 0; }
|
带个数限制的区间拆分问题
概述:复杂度上的进展
在上文的例题中,我们见到了很多这种把长为 n 的序列拆成 k 段的问题。
将其形式化描述,设 fi,k 表示 [1,i] 分为 k 段的最小代价,则有转移方程:
⎩⎨⎧f0,0=0fi,0=f0,k=∞fi,k=minj{fj,k−1+w(j,i)}i,k>0i,k>0
我们在之前的例题中都是用 O(nklogn) 的复杂度处理的。
看到这个问题形式我们就想到了 wqs 二分,那能否将二者联系起来呢?
有一个用不到的结论,类似上文的区间合并问题:
p(i,k−1)≤p(i,k)≤p(i+1,k)
解决方法:wqs 二分 + 决策单调性
很重要的结论:若 w(l,r) 满足四边形不等式,则 fn,k 关于 k 是凸的。
证明
只需证明 fn,k−1+fn,k+1≥2fn,k 即可。
考虑 fn,k−1 分成了 k−1 段,而 fn,k+1 分成了 k+1 段,那么在前一种分段方式中必然存在一段中有两个后者的分段点,将后半部分的第二种与前半部分第一种组合,就得到了两个 k 段的方案,类似上面的形式使用四边形不等式证明后者必定优于前者即可。
所以 fj,k←fi,k−1+w(i,j) 变成了 fj←fi+w(j,i)+C,利用上文的套路解决即可。
上文所有的固定段数 dp 都可以这么做,复杂度 O(nlognlogV)。
注意到这是一个半在线决策单调性,所以要使用二分队列。
套用上面非加强版的思路,换成二分队列写法即可。
前面所有的 k 段划分问题都可以利用该方法实现。
丑陋的核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| struct X { int l,r,p; } q[N]; struct V { ll f;int k; V operator+(ll x) {return {f+x,k};} V operator|(ll x) {return {f+x,k+1};} bool operator<=(V b) {return f==b.f?k<=b.k:f<=b.f;} } f[N]; auto ck=[](ll val) { q[hd=tl=1]={1,n,0}; auto cal=[&](int i,int j)->V {return f[i]+w(i,j)|val;}; F(i,1,n) { while(q[hd].r<i) ++hd;f[i]=cal(q[hd].p,i); while(hd<=tl&&cal(i,q[tl].l)<=cal(q[tl].p,q[tl].l)) --tl; if(hd>tl) {q[++tl]={1,n,i};continue;} int L=i,R=n,mid,ans=n+1; while(L<=R) mid=L+R>>1,cal(i,mid)<=cal(q[tl].p,mid)?R=mid-1,ans=mid:(L=mid+1); if(ans<=n) q[tl].r=ans-1,q[++tl]={ans,n,i}; } V ans={(ll)1e18,0}; F(i,1,n) { auto v=f[i]+((s[n]-s[i-1])-(n-i+1)*a[i]); if(v<=ans) ans=v; } return ans; }; ll L=-1e9,R=1e9,mid,ans; while(L<=R) ck(mid=(L+R)/2).k<=m?R=mid-1,ans=mid:(L=mid+1); cout<<ck(ans).f-ans*m<<endl;
|
理论:反四边形不等式
概述:另一种凸优化
仍然考虑该式:fjminfi+w(i,j)。
若此时 w 满足**「包含优于交叉」**,即反向的四边形不等式,那么不难发现:设 p(i)=s,则 ∀j>i,p(j)∈/(s,i)。
形象地表述这一结论:一个最优转移点的出现可以**「支配」所有比它大的候选转移点**,或者说弹掉它们。
需要注意的是:由于不断地有新的候选转移点加入,所有点的最优转移点并没有决策单调性,既不单增也不单减。
实际上反四边形不等式优化是另一种形式的凸优化,由于它凸的方向与优的方向并不一致,形成的转移并没有那么显然的决策单调性,但仍然可以在一定程度上优化。
实现方法:二分栈
概述:优化实现
二分栈可以解决 无转移范围限制(每个点都可以转移到之后的每个点) 的 反四边形不等式优化。
事实上二分栈并不是决策单调性,但与二分队列很像,用来启发一下思路。
- 有很多题解声称自己使用了二分栈,但其中有从栈底使用元素的情况,那实际上就是二分队列。
我们可以用栈来实现反四边形不等式的优化过程,每次取出栈顶用来转移,加入转移点时比较它与栈顶的决策分界点 p 和栈顶与次栈顶的决策分界点 q,若 p≤q 那么栈顶被该转移点和次栈顶支配,可以弹出。
Q:为何不能直接用单调栈来实现?每次到了新点就反复弹栈直到栈顶优于次栈顶,被弹掉的元素之后一定不会有用。
A:这主要是因为 次栈顶优于栈顶 的时间可能晚于 次次栈顶优于次栈顶 的时间,如果当前应该用次次栈顶,但你却仍然判断栈顶优于次栈顶,不去弹栈,这就爆了。
这里可以二分决策分界点的理论支撑就是**「包含优于交叉」**,虽然没有决策单调性,但对于两个决策点来说,左边的点总是会决策靠右边的一段区间,以及两点之间的这段区间。
把二分的边界设置妥当就可以排除掉后者的干扰,然后二分出分界点。
- 二分栈的重点是左边界的设立,需要找到一个对 i,j 都合法的左端开始二分。
代码实现
fj←fi−1+w(i,j),p(j)=i(这种情况下 w(i,j) 算的是 [i,j] 贡献,可以有 p(i)=i)写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #define sz(x) int(x.size()) #define tp s.back() #define se s[sz(s)-2] vector<int> s; int cal(int i,int j) {return f[i-1]+w(i,j);} int pos(int i,int j) { int L=j,R=n,mid,ans=n+1; while(L<=R) mid=L+R>>1,cal(i,mid)<cal(j,mid)?R=mid-1,ans=mid:(L=mid+1); return ans; } F(i,1,n) { while(sz(s)>1&&chk(se,tp)<=chk(tp,i)) s.pop_back(); s.push_back(i); if(sz(s)>1&&chk(se,i)<=i) s.pop_back(); f[i]=val(tp,i); }
|
fj←fi+w(i,j),p(i)=i(w 算的是 (i,j] 的贡献,这种情况下 p(i)<i)写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #define sz(x) int(x.size()) #define tp s.back() #define se s[sz(s)-2] vector<int> s; int cal(int i,int j) {return f[i]+w(i,j);} int pos(int i,int j) { int L=j+1,R=n,mid,ans=n+1; while(L<=R) mid=L+R>>1,cal(i,mid)<cal(j,mid)?R=mid-1,ans=mid:(L=mid+1); return ans; } s.push_back(0); F(i,1,n) { f[i]=val(tp,i); while(sz(s)>1&&chk(se,tp)<=chk(tp,i)) s.pop_back(); s.push_back(i); if(sz(s)>1&&chk(se,i)<=i+1) s.pop_back(); }
|
后者更适合算前缀和相减的时候用,更简洁一点。
看了这个实现你会发现,二分队列其实可以用相同的写法写淘汰的那部分,唯一区别是转移点从队首掏出来而不是队尾(栈顶)。
给定序列,每个元素有颜色 c,w(l,r)=maxc(c⋅cntc,[l,r]2),求分成任意段的最多价值。
n≤105。
用转移式 fj←fi−1+w(i,j),首先可以猜到 ci=cj,否则不优,这是一定的。
然后我们把每个颜色的子序列拿出来,贡献就变成了 w(l′,r′)=c⋅(r′−l′+1)2,这显然是个下凸函数,而我们求最大值,于是包含优于交叉。
- 反四边形不等式与四边形不等式的使用条件类似,只需要贡献函数满足性质,原数组可以是任意数组。
那我们把每个颜色拿出来,内部都满足二分栈的使用条件,于是我们每个颜色建立一个栈转移即可。
注意我们加入转移点仍然是枚举 1→n,因为转移要依赖的 fi−1 并不一定是该颜色。
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| int n,p[N],c[N],bn[N];ll f[N]; vector<int> s[N]; ll cal(int i,int j) { assert(c[i]==c[j]); return f[i-1]+1ll*c[i]*(p[j]-p[i]+1)*(p[j]-p[i]+1); } int pos(int i,int j) { assert(c[i]==c[j]); int c=::c[i],L=p[j],R=bn[c],mid,ans=R+1; auto w=[&](int l,int r) {return 1ll*c*(r-l+1)*(r-l+1);}; while(L<=R) mid=L+R>>1,f[i-1]+w(p[i],mid)>=f[j-1]+w(p[j],mid)?R=mid-1,ans=mid:(L=mid+1); return ans; } int main() { F(i,1,n=read()) p[i]=++bn[c[i]=read()]; F(i,1,n) { auto &s=::s[c[i]]; #define tp s.back() #define se s[sz(s)-2] while(sz(s)>=2&&pos(se,tp)<=pos(tp,i)) s.pop_back(); s.push_back(i); if(sz(s)>=2&&pos(se,i)<=p[i]) s.pop_back(); f[i]=cal(tp,i); } cout<<f[n]<<endl; return 0; }
|
实现方法:线段树分治 + 分治
概述:为何二分栈失去了作用
我们会遇到这样一类问题:满足反四边形不等式,但每个点能转移到的区间范围有限。
此时我们回看反四边形不等式的推导过程:加入一个点可以**「支配」所有比它大的候选转移点**,而比他大的候选转移点有可能在转移区间上比其优秀,于是并不能直接弹掉,我们陷入了两难境地。
于是,我们不得不花费一个 log 的代价,将每个点加入到线段树上它的决策区间上,然后对于每个区间我们进行新的反四边形不等式优化即可。
注意到现在的问题被大大简化:连每个点必须转移到它之后这个限制都没了,我们发现转移真正拥有了 「反决策单调性」,i<j⇒p(i)≥p(j),且问题已经变成了离线版本,我们可以直接 分治 解决。
需要特别注意的是,各个线段树端点的问题是独立的,需要临时构建 dp 变量,不要依赖 dp 数组中的值,否则无法保证决策单调性。
复杂度 O(nlog2n)(因为分治的复杂度是关于区间长度成线性对数关系)。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void mo(int l,int r,int x,int L,int R,int d) { if(R<l||r<L) return;if(l<=L&&R<=r) return c[d]+=x,void(); mo(l,r,x,L,mid,l(d));mo(l,r,x,mid+1,R,r(d)); } void dp(vec &c,int L,int R,int pL,int pR) { if(L>R) return; int pM=pL,res=f[c[pL]]+w(c[pL],mid); F(i,pL+1,pR) if(f[c[i]]+w(c[i],mid)<=res) res=f[c[i]]+w(c[i],mid),pM=i; f[mid]=min(f[mid],res); dp(c,L,mid-1,pM,pR);dp(c,mid+1,R,pL,pM); } void go(int L,int R,int d) { if(sz(c[d])) dp(c[d],L,R,0,sz(c[d])-1),c[d].clear(); go(v,L,mid,l(d));go(v,mid+1,R,r(d)); }
|
T×T 网格图上 n 个格点是特殊点,你需要找一条 (0,0) 到 (T,T) 的路径,使得只能往右往上走,经过尽可能多的特殊点的前提下,使相邻两个特殊点间的面积和最小,输出这个面积和。
保证没有点同行或同列,xi,yi∈[1,T−1]。
T≤106,n≤2×105。
由于没有点同行或同列,所以你可以将本题抽象成一个长为 n (或者长为 T)的序列,经过尽可能多的特殊点就是某个最长上升子序列。
可以对每个点求出它是最长上升子序列的哪一层(以其为结尾的最长上升子序列长度),这样我们就可以限定它从比它小的一层转移过来。
有结论是同一层的点是单调不增的,否则就可以跑到下一层。
画个图我们可以发现两层之间的转移满足包含优于交叉,于是我们瞪眼一看,发现事情并不简单,转移必须满足 xi<xj∧yi<yj,这是一个区间,于是我们可以线段树分治 + 分治解决。
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| void mo(int x,int v) { if(!x) return c[0]=v,void(); for(;x<=T;x+=x&-x) c[x]=max(c[x],v); } int q(int x) {int res=c[0]; for(;x;x&=x-1) res=max(res,c[x]); return res; } ll w(int i,int j) { if(x[i]>x[j]||y[i]>y[j]) return 1e13; return 1ll*(x[j]-x[i])*(y[j]-y[i]); } #define mid (L+R>>1) #define l(x) (x<<1) #define r(x) (x<<1|1) void mo(vec &v,int i,int L,int R,int d) { if(x[i]>x[v[R]]||y[i]>y[v[L]]) return; if(x[i]<=x[v[L]]&&y[i]<=y[v[R]]) return o[d]+=i,void(); mo(v,i,L,mid,l(d));mo(v,i,mid+1,R,r(d)); } void dp(vec &u,vec &v,int L,int R,int pL,int pR) { if(L>R) return; int pM=pL;ll res=f[u[pL]]+w(u[pL],v[mid]); F(i,pL+1,pR) if(f[u[i]]+w(u[i],v[mid])<=res) res=f[u[i]]+w(u[i],v[mid]),pM=i; f[v[mid]]=min(f[v[mid]],res); dp(u,v,L,mid-1,pM,pR);dp(u,v,mid+1,R,pL,pM); } void go(vec &v,int L,int R,int d) { if(sz(o[d])) dp(o[d],v,L,R,0,sz(o[d])-1),o[d].clear(); if(L==R) return; go(v,L,mid,l(d));go(v,mid+1,R,r(d)); } auto DP=[](auto &&u,auto &&v) { for(int i:u) mo(v,i,0,sz(v)-1,1); go(v,0,sz(v)-1,1); }; int main() { n=read();T=read(); F(i,1,n) { id[x[i]=read()]=i; y[i]=read(); } id[0]=++n,x[n]=y[n]=0;id[T]=++n;x[n]=y[n]=T; F(i,0,T) if(id[i]) { int u=q(y[id[i]])+1; mo(y[id[i]],u); if(sz(v[u])) assert(y[v[u].back()]>=y[id[i]]); v[u]+=id[i]; } memset(f,0x3f,sizeof f);f[n-1]=0; F(i,2,n) { DP(v[i-1],v[i]); if(!sz(v[i+1])) { ll ans=1e18; for(int x:v[i]) ans=min(ans,f[x]); cout<<ans<<endl;return 0; } } return 0; }
|
参考资料
Liuxizai:决策单调性优化 dp 学习笔记
command_block:DP的决策单调性优化总结
Alex_Wei:DP 优化方法大杂烩 II.
OI Wiki:四边形不等式优化