凸性相关问题

内容大致上包括:

  • 四边形不等式,决策单调性,wqs 二分
  • 闵可夫斯基和优化 d p dp dp
  • slope trick
  • 其他凸性相关问题

决策单调性

1.1 一些约定

对于 n × m n\times m n×m 的矩阵 A A A,定义:

  • 子矩阵 A [ i 1 , . . . , i p ] , [ j 1 , . . . , j q ] A_{[i_1,...,i_p],[j_1,...,j_q]} A[i1,...,ip],[j1,...,jq] 为矩阵中第 i 1 , . . . i p i_1,...i_p i1,...ip 行与 j 1 , . . . j q j_1,...j_q j1,...jq 列交点形成的矩阵

  • 连续子矩阵,顾名思义

  • min ⁡ ( A i ) \min(A_i) min(Ai) 表示第 i i i 行最小值的位置,若存在多个最小值取最左边

1.2 单调矩阵、蒙日矩阵与四边形不等式

  • 单调矩阵: ∀ i < j , min ⁡ ( A i ) ≤ min ⁡ ( A j ) \forall i<j,\min(A_i)\leq \min(A_j) i<j,min(Ai)min(Aj),即 决策单调性

  • 完全单调矩阵: A A A 的任何一个子矩阵都是单调矩阵
    Tip:能在题目中见到的单调矩阵几乎都是完全单调矩阵

  • 蒙日矩阵:若 ∀ 1 ≤ i 1 ≤ i 2 ≤ n , 1 ≤ j 1 ≤ j 2 ≤ m \forall 1\leq i_1\leq i_2\leq n,1\leq j_1\leq j_2\leq m ∀1i1i2n,1j1j2m,均有 A i 1 , j 1 + A i 2 , j 2 ≤ A i 1 , j 2 + A i 2 , j 1 A_{i_1,j_1}+A_{i_2,j_2}\leq A_{i_1,j_2}+A_{i_2,j_1} Ai1,j1+Ai2,j2Ai1,j2+Ai2,j1,则称 A A A 满足 四边形不等式,同时称这样的矩阵为 蒙日矩阵

    上面的条件其实等价于 A i , j + A i + 1 , j + 1 ≤ A i , j + 1 + A i + 1 , j A_{i,j}+A_{i+1,j+1}\leq A_{i,j+1}+A_{i+1,j} Ai,j+Ai+1,j+1Ai,j+1+Ai+1,j,本质上是二维差分

    蒙日矩阵均为完全单调矩阵

    这也说明了,若一个矩阵满足四边形不等式,那么它就有决策单调性

    蒙日矩阵的一些基本性质:

    • A T A^T AT 也为蒙日矩阵
    • A + B A+B A+B 为蒙日矩阵
    • λ A , λ > 0 \lambda A,\lambda>0 λA,λ>0 为蒙日矩阵

    常见的蒙日矩阵

    • A x , y = min ⁡ ( x , y ) A_{x,y}=\min(x,y) Ax,y=min(x,y)

    • A x , y = x y A_{x,y}=xy Ax,y=xy

    • A x , y = ∑ i = 1 x ∑ j = y m d i , j A_{x,y} = \sum_{i=1}^x\sum_{j=y}^md_{i,j} Ax,y=i=1xj=ymdi,j,其中 d i , j d_{i,j} di,j 为非负矩阵

      这说明了:对于区间权值为 区间内符合某种性质的点对数量(权值)和的问题一定
      具有决策单调性

      比如 逆序对,区间颜色数的平方

    • A x , y = [ x = k ] v k A_{x,y}=[x=k]v_k Ax,y=[x=k]vk,没见过,不知道有什么用

    • A x , y = f ( x − y ) A_{x,y}=f(x-y) Ax,y=f(xy),其中 f f f 是下凸函数 十分有用 !!

更进一步

  • 蒙日矩阵最短路 (区间划分问题)

    对于转移 f k , i = min ⁡ j < i ( f k − 1 , j + w ( j , i ) ) f_{k,i}=\min\limits_{j<i} (f_{k-1,j}+w(j,i)) fk,i=j<imin(fk1,j+w(j,i)),可以看作是最短路的形式

    最终要求的 f n , k f_{n,k} fn,k 即为 在蒙日矩阵上走过恰好 k k k 条边的最短路径

    如果一个问题的答案关于这个选择的边数 k k k 是凸的,就可以借助 wqs 二分解决

    验证凸性的方法:

    • 代价函数 w ( l , r ) w(l,r) w(l,r) 是蒙日矩阵
    • 这个问题可以建出费用流模型
  • 一些更神秘的东西,据说叫 广义决策单调性

    blog

    其实只需要记住分 k k k 段问题都有 路径交错 的性质就好

    有些题会出现环上的决策单调性,就需要用这个来解决

    诶诶诶,还需要 wqs 构造方案?
    在这里插入图片描述

1.3 例题

实际应用中还是有很多 trick 的

一般来说,实际应用中证明 决策单调性的方法有两种: 反证法 ,四边形不等式

还有就是需要见过一些经典的模型

「雅礼集训 2017 Day5」珠宝 /「NAIPC2016」Jewel Thief

观察数据范围注意到 c c c 很小,肯定按这个做了

对于 c c c 相同的,显然按 v v v 从大到小选,贡献函数是凸的

容易写出转移:

d p c , j = max ⁡ ( d p c − 1 , j − k c + F c , k ) dp_{c,j}=\max(dp_{c-1,j-kc}+F_{c,k}) dpc,j=max(dpc1,jkc+Fc,k)

m o d   c \mathrm{mod}\ c mod c 分组会让 d p dp dp 更简洁

d p c , j = max ⁡ ( d p c − 1 , j − k + F c , k ) dp_{c,j}=\max(dp_{c-1,j-k}+F_{c,k}) dpc,j=max(dpc1,jk+Fc,k)

显然是 ( m a x , + ) (max,+) (max,+) 卷积的形式,而且 F F F 也是凸的,那么闵和归并?

注意 d p dp dp 数组转移过程中并非凸的,因为不同的 c c c 的分组方式显然会影响凸性,那就不能闵和了

但是这种 非凸 与 凸 的 ( m a x , + ) (max,+) (max,+) 卷积是有 决策单调性

可以用蒙日矩阵来说明:

d p c , j = max ⁡ ( d p c − 1 , k + F c , j − k ) dp_{c,j}=\max(dp_{c-1,k}+F_{c,j-k}) dpc,j=max(dpc1,k+Fc,jk),恰好符合 A x , y = f ( x − y ) A_{x,y}=f(x-y) Ax,y=f(xy) f f f 凸 这一推论

直接决策单调性,复杂度是 O ( K C ) O(KC) O(KC) 或者 O ( K C log ⁡ ) O(KC\log) O(KClog)

GYM102586B1 Evacuation

好牛的题

首先设出来代价函数, f ( l , r , x ) f(l,r,x) f(l,r,x) 表示决策选在 x x x 时的最小代价,没什么性质,简化一下

一般决策单调性都是二元函数,所以尽可能往二元的形式上凑

容易发现根据 L i L_i Li R i R_i Ri 的中点可以把决策分成两部分,左半部分是不需要考虑 R i R_i Ri 的限制的,右边同理

简化后:设 f ( s , i ) f(s,i) f(s,i) 表示决策在 s s s,端点为 i i i 的最小代价

发现这样就有决策单调性了,反证法容易证明

有个问题是这个题决策有区间限制,需要线段树上放询问后对每个结点分治处理

QOJ9737 Let’s Go! New Adventure

积累一个 trick

容易写出转移: f i = max ⁡ j < i ( f j + w ( S i − S j ) − C ) f_i=\max\limits_{j<i}(f_j+w(S_i-S_j)-C) fi=j<imax(fj+w(SiSj)C),很标准的决策单调性形式

w w w 函数不完全是凸函数,它是分段的!

我们考虑把 w w w 图像上的若干断点用直线连接起来,就能得到一个标准的凸包。转移时需要带取整符号,即:

f i = max ⁡ j < i ( f j + ⌊ w ( S r − S l ) ⌋ − C ) f_i=\max\limits_{j<i}(f_j+\left \lfloor w(S_r-S_l) \right \rfloor -C) fi=j<imax(fj+w(SrSl)C),然后呢?

f i = ⌊ max ⁡ j < i ( f j + w ( S r − S l ) − C ) ⌋ f_i=\left \lfloor\max\limits_{j<i}(f_j+ w(S_r-S_l) -C) \right \rfloor fi=j<imax(fj+w(SrSl)C),这就很完美了,直接二分栈优化即可

取整符号拿到外面!!

额,放个二分栈板子吧…

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 5e5+100 ;
//trick:凸函数套取整函数,把取整放到dp转移式外面 

int n , m , C ;
LL a[N] , b[N] ;
struct node
{
	int l , r , j ;
}q[N] ;
int hh , tt ;
LL f[N] ;
inline double Val( int i , int j )
{
	LL S = a[i]-a[j] ;
	int p = upper_bound(b,b+m+1,S)-b-1 ;
	if( p == m ) return f[j]+m-C ;
	return f[j]-C+p+(S-b[p])*1.0/(b[p+1]-b[p]) ;
}
void Insert( int j )
{
	int pos = n+1 ;
	while( hh <= tt && Val(q[tt].l,j) >= Val(q[tt].l,q[tt].j) ) pos = q[tt].l , tt -- ;
	if( hh <= tt && Val(q[tt].r,j) >= Val(q[tt].r,q[tt].j) ) {
		int l = q[tt].l , r = q[tt].r , mid ;
		while( l+1 < r ) {
			mid = (l+r)>>1 ;
			if( Val(mid,j) >= Val(mid,q[tt].j) ) r = mid ;
			else l = mid ;
		}
		if( Val(l,j) >= Val(l,q[tt].j) ) pos = l ;
		else pos = r ;
		q[tt].r = pos-1 ;
	}
	if( pos != n+1 ) q[++tt] = {pos,n,j} ;
}
void solve()
{
	scanf("%d%d%d" , &n , &m , &C ) ;
	for(int i = 1 ; i <= n ; i ++ ) scanf("%lld" , &a[i] ) , a[i] += a[i-1] ;
	for(int i = 1 ; i <= m ; i ++ ) scanf("%lld" , &b[i] ) , b[i] += b[i-1] ;
	hh = 1 , tt = 0 ;
	q[++tt] = {0,n,0} ;
	for(int i = 1 ; i <= n ; i ++ ) {
		if( q[hh].l==q[hh].r ) hh ++ ;
		else q[hh].l ++ ;
		f[i] = floor(Val(i,q[hh].j)) ;
		Insert(i) ;
	}
	printf("%lld\n" , f[n] ) ;
}

int main()
{
	int t ;
	scanf("%d" , &t ) ;
	while( t -- ) solve() ; 
    return 0 ;
}

[ARC168E] Subsegments with Large Sums

不是很显然的 wqs 二分

首先有个一朴素的 d p dp dp f k , i = max ⁡ j < i ( f k − 1 , j + [ s u m i − s u m j > = S ] ) f_{k,i}=\max\limits_{j<i}(f_{k-1,j}+[sum_i-sum_j>=S]) fk,i=j<imax(fk1,j+[sumisumj>=S])

然而代价函数不满足四边形不等式,没法直接无脑 wqs + 决策单调性

一般这种时候需要修改一下代价函数,使其满足凸性

首先观察一些性质:对于 < S <S <S 的段,长度为 1 1 1 是最优的;对于 ≥ S \geq S S 的段,恰好满足 ≥ S \geq S S 是最优的

这是因为段数多一点其实不影响正确性,可以合并某些段从而构造出恰好 K K K 段的方案

所以我们希望划分的段数越多越好

那么换个角度看:先在原数组中钦定出 x x x 段满足 ≥ S \geq S S,求能划分的段数 f ( x ) f(x) f(x) 最多是多少

容易发现这个代价函数关于 x x x 就有凸性了,可以 wqs 二分求最优值

然后 x x x 作为答案显然满足单调性,外层再套个二分即可

闵可夫斯基和优化 d p dp dp

2.1 定义与实现

形如 f i + j = max ⁡ { g i + h j } f_{i+j}=\max\{g_i+h_j\} fi+j=max{gi+hj} 的卷积形式,我们称 f f f g g g ( m a x , + ) (max,+) (max,+) 卷积

g , h g,h g,h 均为上凸函数(即差分数组单调递减),称 f f f g , h g,h g,h 的闵可夫斯基和

一个结论是求 g , h g,h g,h ( m a x , + ) (max,+) (max,+) 卷积时,只需要分别求出它们的差分数组后归并,再对所得函数求前缀和即可

vector<LL> Merge( const vector<LL> A , const vector<LL> B )
{
	vector<LL> C ;
	C.push_back(A[0]+B[0]) ;
	int p = 1 , q = 1 ;
	while( p < A.size() && q < B.size() ) {
		if( A[p] > B[q] ) C.push_back(A[p]) , p ++ ;
		else C.push_back(B[q]) , q ++ ;
	}
	while( p < A.size() ) C.push_back(A[p]) , p ++ ;
	while( q < B.size() ) C.push_back(B[q]) , q ++ ;
	return C ;
}

实际应用中,为了方便地得到最终答案,维护差分数组时通常还需要专门维护一项 g 0 , h 0 g_0,h_0 g0,h0,归并时要把 g 0 , h 0 g_0,h_0 g0,h0 相加,后面的项正常归并

这一部分的技巧十分之多,需要积累

2.2 一些技巧

分治优化闵和

闵和可以把原来 O ( n 2 ) O(n^2) O(n2) 的卷积优化到线性级别,这意为着 基于序列长度复杂度的做法 经过闵和优化后都能有很好的表现

最典型的应用就是分治优化

Gym - 103202L Forged in the Barrens

极差划分

注意到这道题让求的是 m a x max max,极差定义也是序列中任选两数的差的 m a x max max,不等号方向一致,可以直接钦定每一段中任选两个数,分别对最终答案造成 + 1 , − 1 +1,-1 +1,1 的贡献,这样一定能取到最优解

然而这个题需要对所有 k k k 都输出,没法写成一维 d p dp dp,暴力做是 n 2 n^2 n2

经过打表发现答案关于 k k k 是凸的,我们考虑闵和优化

原来的做法相当于是把一个很大的前缀凸包和一个元素进行 ( m a x , + ) (max,+) (max,+) 卷积,闵和后复杂度还是前缀长度,我们希望的是每次把两个大小差不多的凸包合并

考虑分治,建出类似线段树的结构,每次处理完左右儿子的凸包后闵和得到当前节点

容易发现这样复杂度为 n ∗ 分治层数 n*分治层数 n分治层数,也就是 O ( n log ⁡ n ) O(n\log n) O(nlogn)

vector<LL> Max( vector<LL> A , vector<LL> B , vector<LL> D )
{
	vector<LL> C ;
	int len = max({A.size(),B.size(),D.size()}) ;
	for(int i = 0 ; i < len ; i ++ ) C.push_back(-INF) ;
	for(int i = 0 ; i < A.size() ; i ++ ) {
		if( i ) A[i] += A[i-1] ;
		C[i] = max( C[i] , A[i] ) ;
	}
	for(int i = 0 ; i < B.size() ; i ++ ) {
		if( i ) B[i] += B[i-1] ;
		if( B[i]>-1e12 ) C[i] = max( C[i] , B[i] ) ;
	}
	for(int i = 0 ; i < D.size() ; i ++ ) {
		if( i ) D[i] += D[i-1] ;
		if( D[i]>-1e12 ) C[i] = max( C[i] , D[i] ) ;
	}
	for(int i = C.size()-1 ; i >= 1 ; i -- ) {
		C[i] -= C[i-1] ;
	}
	return C ;
}
void solve( int p , int l , int r )
{
	if( l == r )  {
		ans[p][0][0] = {0,0} ;
		ans[p][0][1] = {-inf,-a[l]+inf} ;
		ans[p][0][2] = {a[l]} ;
		ans[p][1][0] = {-inf,-a[l]+inf} ; 
		ans[p][1][1] = {-inf} ;
		ans[p][1][2] = {-inf} ;
		ans[p][2][0] = {a[l]} ;
		ans[p][2][2] = {-inf} ;
		ans[p][2][1] = {-inf} ;
		return ;
	}
	int mid = (l+r)>>1 ;
	solve(p<<1,l,mid) ; solve(p<<1|1,mid+1,r) ;
	for(int fl = 0 ; fl < 3 ; fl ++ ) {
		for(int fr = 0 ; fr < 3 ; fr ++ ) {
			ans[p][fl][fr] = Max(Merge(ans[p<<1][fl][0],ans[p<<1|1][0][fr]),Merge(ans[p<<1][fl][1],ans[p<<1|1][2][fr]),Merge(ans[p<<1][fl][2],ans[p<<1|1][1][fr]) ) ;
			ans[p][fl][fr] = Max(ans[p][fl][fr],ans[p<<1][fl][fr],ans[p<<1|1][fl][fr]) ;
		}
	}
} 

然而你需要知道这道题其实非常的 sb

注意 d p dp dp 时显然需要记录两端点所在段的状态, ( m a x , + ) (max,+) (max,+) 中的 + + + 相当于是定义在两个数组上的运算。但好在这个运算还是符合闵和的规律的

最恶心的是与左右儿子取 max ⁡ \max max 操作,我们知道两个上凸函数取 max ⁡ \max max 后不一定上凸,所以这道题里可能会出现凸包合并着合并着斜率不单增了!

经过我的反复分析试错,发现这种情况大都是 inf ⁡ \inf inf 导致的,因为你没法判断 inf ⁡ − 2 \inf-2 inf2 2 × inf ⁡ 2\times\inf 2×inf 谁更大,直接取 M a x Max Max 的话会影响斜率的单调性

只需要仿照上面的程序中 M a x Max Max 函数里单独判一下 inf ⁡ \inf inf 就 ok 了

[ABC383G] Bar Cover

和上一题差不多

一个转化是我们直接把长度为 k k k 的段拿出来,问题转化为选 x x x 个点使得两两距离大于等于 k − 1 k-1 k1,这样更方便转移

复杂度是 O ( n k 3 log ⁡ n ) O(nk^3\log n) O(nk3logn)

诶,不太对,因为长度为 n n n 的区间中最多放 n k \frac{n}{k} kn 个块,那么复杂度是 O ( n k 2 log ⁡ n ) O(nk^2\log n) O(nk2logn)

注意递归边界需要特殊处理

Gym102331H Honorable Mention

好牛逼的题

如果区间固定,显然可以直接分治闵和

多次区间询问,考虑在线段树上拆成 log ⁡ n \log n logn 段区间,但不能直接暴力合并,因为闵和复杂度是基于区间长度的

这个时候考虑如果没有 k k k 的限制就好了,每段区间中取最优值再 O ( 1 ) O(1) O(1) 合并即可,不需要额外记录状态

这启发我们 wqs 二分,二分斜率为 C C C,对每段区间等价于找 f ( i ) − C i f(i)-Ci f(i)Ci 的最大值,这是一个凸包上二分的问题,可以 log ⁡ n \log n logn 得到答案

这样 wqs,线段树,凸包上二分各一个 log ⁡ \log log,复杂度 O ( n log ⁡ V log ⁡ 2 n ) O(n\log V\log ^2n) O(nlogVlog2n)

Gym102331J Jiry Matchings

树上闵和,好牛逼

对于合并子树复杂度为 min ⁡ ( ∣ A ∣ , ∣ B ∣ ) \min(|A|,|B|) min(A,B) 形式的问题,dsu on tree 可以做到很好的复杂度

但如果是 ∣ A ∣ + ∣ B ∣ |A|+|B| A+B 就不行,闵和正是这样的问题

还是仿照序列上解决这种问题的思路,我们总是希望把 大小差不多 的两个凸包合并

由此想到重剖,做法是这样:

  • 对于一条重链,首先对于每个点求出 轻儿子 的答案再合并,注意这里不能暴力合并,复杂度会带平方,还是需要分治
  • 对于这条重链,用序列上分治的思路求出答案
  • 把该重链的答案继承给父亲,重复第一条

分析复杂度:对于第一条操作,每个节点会在它的祖先作为轻儿子时被合并,每次需要合并 log ⁡ \log log 次,总共是 O ( n log ⁡ 2 n ) O(n\log^2 n) O(nlog2n);对于第二条, log ⁡ \log log 条重链分别进行分治复杂度一共是 O ( n log ⁡ 2 n ) O(n\log^2 n) O(nlog2n)

所以总复杂度 O ( n log ⁡ 2 n ) O(n\log^2 n) O(nlog2n)

实际上有更好的做法:合并若干轻儿子时不采用分治,因为这里合并是 没有顺序要求的,我们按 合并果子 的方式,每次从堆里拿出 s i z siz siz 最小的两个凸包合并,这样复杂度被证明是 O ( n log ⁡ 2 n ) O(n\log_{\sqrt 2} n) O(nlog2 n);对于重链我们按 全局平衡二叉树 的方式每次取带权中心递归,复杂度被证明是 O ( n log ⁡ 1.5 n ) O(n\log_{1.5}n) O(nlog1.5n)

总复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn)

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
typedef vector<LL> vL ;
const int N = 2e5+10 ;

int n ;
struct nn
{
	int lst , to , val ;
}E[N<<1] ;
int head[N] , tot ;
inline void add( int x , int y , int v )
{
	E[++tot] = (nn){head[x],y,v} ;
	head[x] = tot ;
}
int siz[N] , mx[N] , V[N] , sz[N] ;
int dfn[N] , tim ;
void dfs( int x , int fa )
{
	siz[x] = 1 ;
	for(int i = head[x] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == fa ) continue ;
		V[t] = E[i].val ;
		dfs(t,x) ;
		siz[x] += siz[t] ;
		if( siz[t] > siz[mx[x]] ) mx[x] = t ;
	}
}
struct node
{
	int v , x ;
	friend bool operator < ( node a , node b ) {
		return a.v>b.v ;
	}
};
priority_queue<node> q ;
const LL inf = 1e18 ;
vL F[N][2] ;
vL Merge( vL A , vL B )
{
	if( A.size()==0 ) return A ;
	if( B.size()==0 ) return B ;
	vL C ;
	C.push_back(A[0]+B[0]) ;
	int p = 1 , q = 1 ;
	while( p < A.size() && q < B.size() ) {
		if( A[p]>B[q] ) C.push_back(A[p]) , p ++ ;
		else C.push_back(B[q]) , q ++ ;
	}
	while( p < A.size() ) C.push_back(A[p]) , p ++ ;
	while( q < B.size() ) C.push_back(B[q]) , q ++ ;
	return C ;
}
vL Max( vL A , vL B )
{
	if( A.size()==0 ) return B ;
	if( B.size()==0 ) return A ;
	vL C ; int len = max(A.size(),B.size()) ;
	for(int i = 0 ; i < len ; i ++ ) C.push_back(-inf) ;
	for(int i = 0 ; i < A.size() ; i ++ ) {
		if( i ) A[i] += A[i-1] ;
		C[i] = A[i] ;
	}
	for(int i = 0 ; i < B.size() ; i ++ ) {
		if( i ) B[i] += B[i-1] ;
		if( B[i]>-1e16 ) C[i] = max( C[i] , B[i] ) ;
	}
	for(int i = C.size()-1 ; i >= 1 ; i -- ) C[i] -= C[i-1] ; 
	return C ;
}
vL Upd( vL A , LL v )
{
	if( A.size()==0 ) {
		return A ;
	}
	for(int i = 1 ; i < A.size() ; i ++ ) A[i] += A[i-1] ;
	A.push_back(A.back()+v) ;
	for(int i = A.size()-2 ; i >= 1 ; i -- ) {
		A[i] = max( A[i] , A[i-1]+v ) ;
	}
	for(int i = A.size()-1 ; i >= 1 ; i -- ) A[i] -= A[i-1] ;
	return A ;
}
int h[N] , nam[N] , rt , idx ;
vL A[N<<2][2][2] ;
inline int bud()
{
	idx ++ ;
	A[idx][0][0].clear() , A[idx][0][1].clear() , A[idx][1][0].clear() , A[idx][1][1].clear() ;
	return idx ;
}
void Sum( int p )
{
	A[p][1][0] = Max( A[p][1][0] , A[p][0][0] ) ;
	A[p][0][1] = Max( A[p][0][1] , A[p][0][0] ) ;
	A[p][1][1] = Max( A[p][1][1] , Max(A[p][1][0],A[p][0][1]) ) ;
}
void solve( int &p , int l , int r )
{
	p = bud() ;
	if( l == r ) {
		swap(A[p][1][0],F[nam[l]][0]) ; 
		A[p][0][1] = A[p][1][0] ;
		swap(A[p][1][1],F[nam[l]][1]) ;
		Sum(p) ;
		return ;
	}
	int all = 0 ;
	for(int i = l ; i <= r ; i ++ ) all += sz[i] ;
	int ss = 0 ;
	int mid = (l+r)>>1 , ls = 0 , rs = 0 ;
	for(int i = l ; i < r ; i ++ ) {
		ss += sz[i] ;
		if( ss*2 >= all || i==r-1 ) {
			mid = i ;
			break ;
		}
	}
	solve(ls,l,mid) ; solve(rs,mid+1,r) ;
	for(int fl = 0 ; fl < 2 ; fl ++ ) {
		for(int fr = 0 ; fr < 2 ; fr ++ ) {
			A[p][fl][fr] = Max( Merge(A[ls][fl][1],A[rs][1][fr]) , Upd(Merge(A[ls][fl][0],A[rs][0][fr]),V[nam[mid+1]]) ) ;
		}
	}
	Sum(p) ;
}
void exdfs( int x , int fa , bool is )
{
	dfn[x] = ++tim ; nam[tim] = x ;
	if( mx[x] == 0 ) {
		sz[x] = 1 ;
		h[x] = 1 ;
		F[x][0].push_back(0) ;
		if( !is ) F[x][1].push_back(-inf) , F[x][1].push_back(inf+V[x]) ; 
		return ;
	}
	exdfs(mx[x],x,1) ;
	h[x] = h[mx[x]] + 1 ;
	for(int i = head[x] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == fa || t == mx[x] ) continue ;
		exdfs(t,x,0) ;
	}
	for(int i = head[x] ; i ; i = E[i].lst ) {
		int t = E[i].to ;
		if( t == fa || t == mx[x] ) continue ;
		q.push({siz[t],t}) ;
	}
	while( q.size()>1 ) {
		int x = q.top().x ; q.pop() ;
		int y = q.top().x ; q.pop() ;
		vL G0 = Merge( F[x][0] , F[y][0] ) ;
		vL G1 = Max( Merge(F[x][0],F[y][1]) , Merge(F[x][1],F[y][0]) ) ;
		swap(F[x][0],G0) ; swap(F[x][1],G1) ; siz[x] += siz[y] ;
		q.push({siz[x],x}) ;
	}
	if( q.size()==1 ) {
		int t = q.top().x ; q.pop() ;
		sz[x] = siz[t]+1 ; 
		swap(F[x][0],F[t][0]) ; swap(F[x][1],F[t][1]) ;
		F[x][1] = Max( F[x][1] , F[x][0] ) ;
	}
	else {
		F[x][0].push_back(0) ; sz[x] = 1 ;
	}
	if( !is ) { //链顶,分治合并 
		int rt = 0 ; idx = 0 ;
		solve(rt,dfn[x],dfn[x]+h[x]-1) ;
		//加入父边 
		F[x][0] = A[rt][1][1] ;
		if( x != 1 ) F[x][1] = Upd(A[rt][0][1],V[x]) ;
		else F[x][1] = A[rt][0][1] ;
		F[x][1] = Max( F[x][1] , F[x][0] ) ;
	}
}

int main()
{
	scanf("%d" , &n ) ;
	int x , y , v ;
	for(int i = 1 ; i < n ; i ++ ) {
		scanf("%d%d%d" , &x , &y , &v ) ;
		add(x,y,v) ; add(y,x,v) ;
	}
	dfs(1,0) ;
	exdfs(1,0,0) ;
	for(int i = 1 ; i < n ; i ++ ) {
		if( i < F[1][0].size() ) {
			F[1][0][i] += F[1][0][i-1] ;
			printf("%lld " , F[1][0][i] ) ;
		}
		else printf("? ") ;
	}
    return 0 ;
}

数据结构维护闵和

这类问题和上面那种其实非常像,都是 d p dp dp 数组具有凸性,转移为 ( max ⁡ / min ⁡ , + ) (\max/\min,+) (max/min,+) 卷积

目前来说,笔者认为最大的区别在于 dp 是否需要记录额外状态

诶,我的理解还是太肤浅了,多积累点再来补吧

Slope Trick

一种更高深、更困难的技巧

通常是 发现 d p dp dp 数组可以用若干一次函数拼接得到,考虑去刻画 d p dp dp 数组的变化情况,用数据结构来维护这个变化

最常见的一种是 用堆来维护斜率的变化点 + 维护某个特殊位置的斜率准确值 (如 k = 0 k=0 k=0 ),来准确描述整个 DP 数组

一般来说,能够使用 slope trick 优化的 DP 方程有以下要求:

  • 连续
  • 分段一次函数
  • 凸函数(斜率单调)

这一部分的优化技巧总是与 凸函数 紧密联系,这是因为凸函数本身具有很多优秀的性质:

  • 凸函数+凸函数 还是凸函数
    两个凸函数相加,只需要将其斜率变化点暴力归并
  • 凸函数做 ( min ⁡ / max ⁡ , + ) (\min/\max,+) (min/max,+) 卷积可以闵和优化

没什么固定技巧,需要根据题目中具体的转移来自己编做法

例题

CF713C Sonya and Problem Wihtout a Legend

首先严格递增肯定是没不降好做的,而这个转化又很简单,只需要令 a i → a i − i a_i\to a_i-i aiaii

f i , j f_{i,j} fi,j 表示考虑到第 i i i 个位置,且 a i = j a_i= j ai=j 的最小操作次数

容易写出转移式: f i , j = min ⁡ k ≤ j ( f i − 1 , k ) + ∣ a i − j ∣ f_{i,j}=\min\limits_{k\leq j}( f_{i-1,k})+|a_i-j| fi,j=kjmin(fi1,k)+aij

考虑转移一次的影响,分步来看:

  1. 取前缀 min ⁡ \min min
  2. 整体加上函数 ∣ a i − j ∣ |a_i-j| aij

以上两个操作均不影响凸性,归纳可证 d p dp dp 数组是下凸的

取前缀 min ⁡ \min min 操作决定了这个函数必然有一段平的,然后往右才开始增大,考虑用堆维护右半部分斜率的变化

然后就是这俩操作先后顺序其实无所谓,我们考虑先加再取 min ⁡ \min min

分讨一下

AT_abc217_h [ABC217H] Snuketoon

一个 t r i c k trick trick d p dp dp 数组平的那一段两侧变化方式不同,且都需要维护时,考虑使用双堆

CF 1534G A New Beginning

神仙题啊

考虑路径固定时怎么做

切比雪夫距离可以表述为一个正方形,和路径的切点即为最优点

常规的想法是考虑在路径转折点算贡献,但没什么拓展性

另一种想法是 考虑正方形的顶点!发现这样就与路径无关了,只需要画一条 y = − x + x i + y i y=-x+x_i+y_i y=x+xi+yi 的直线,走到这条直线上时算贡献即可

然后就是加绝对值 和 向两侧平移 这种操作是可以做的

[APIO2016] 烟火表演

更是神仙题

f i , j f_{i,j} fi,j 表示使 i i i 子树中叶子到 i i i 的距离均为 j j j 的最小代价

转移: f i , j = ∑ t ∈ s o n ( i ) min ⁡ k ≤ j ( f t , k + ∣ w − ( j − k ) ∣ ) f_{i,j}=\sum\limits_{t\in son(i)}\min\limits_{k\leq j}(f_{t,k}+|w-(j-k)|) fi,j=tson(i)kjmin(ft,k+w(jk))

∑ \sum 是不重要的,考虑后半部分实际上是 f t , x f_{t,x} ft,x ∣ w − x ∣ |w-x| wx 的闵可夫斯基和

那么我们直接考虑维护斜率,每次归并

∣ w − x ∣ |w-x| wx 做闵和的影响是插入 w w w 段斜率 − 1 -1 1 inf ⁡ \inf inf 段斜率 + 1 +1 +1。换句话说,把斜率 > 1 >1 >1 的段全部删掉,替换成斜率为 1 1 1

插入 w w w − 1 -1 1 不太现实,考虑维护斜率的变化量,那么这个插入就变成 向右平移 w w w 斜率为 0 0 0 的段

然后还是从特殊位置出发,本题中显然是斜率为 1 1 1 的段,用堆维护从这个位置开始向左的斜率变化点,每次取两个堆顶向右平移

最后是 ∑ \sum 操作,上文提过凸函数相加只需要直接归并断点即可;还需要删去斜率 > 1 >1 >1 的段,分析发现只需要删掉 s o n − 1 son-1 son1 个最大的断点就行了

CF 1787H Codeforces Scoreboard

好题

考虑确定那些不吃保底的题的集合后怎么排顺序,由于 ∑ b i \sum b_i bi 固定,肯定按 k i k_i ki 从大到小做,那么直接按这个顺序 DP

写出转移: f i , j = max ⁡ ( f i − 1 , j + a i , f i − 1 , j − 1 + b i − k i × j ) f_{i,j}=\max(f_{i-1,j}+a_i,f_{i-1,j-1}+b_i-k_i\times j) fi,j=max(fi1,j+ai,fi1,j1+biki×j)

发现 d p dp dp 式子与之前的相比,特殊之处在于 转移与 j j j 有关

一般这种时候我们可以配凑系数,更改 d p dp dp 的定义使得转移与 j j j 无关

f i , j = max ⁡ ( f i − 1 , j + a i , f i − 1 , j − 1 + b i − k i × j ) f_{i,j}=\max(f_{i-1,j}+a_i,f_{i-1,j-1}+b_i-k_i\times j) fi,j=max(fi1,j+ai,fi1,j1+biki×j)

f i , j + k i j ( j + 1 ) 2 = max ⁡ ( f i − 1 , j + k i j ( j + 1 ) 2 + a i , f i − 1 , j − 1 + b i − k i × j + k i j ( j + 1 ) 2 ) f_{i,j}+k_i\frac{j(j+1)}{2}=\max(f_{i-1,j}+k_i\frac{j(j+1)}{2}+a_i,f_{i-1,j-1}+b_i-k_i\times j+k_i\frac{j(j+1)}{2}) fi,j+ki2j(j+1)=max(fi1,j+ki2j(j+1)+ai,fi1,j1+biki×j+ki2j(j+1))

f i , j + k i j ( j + 1 ) 2 = max ⁡ ( f i − 1 , j + k i j ( j + 1 ) 2 + a i , f i − 1 , j − 1 + b i + k i j ( j − 1 ) 2 ) f_{i,j}+k_i\frac{j(j+1)}{2}=\max(f_{i-1,j}+k_i\frac{j(j+1)}{2}+a_i,f_{i-1,j-1}+b_i+k_i\frac{j(j-1)}{2}) fi,j+ki2j(j+1)=max(fi1,j+ki2j(j+1)+ai,fi1,j1+bi+ki2j(j1))

妙妙,令 F i , j = f i , j + k i + 1 j ( j + 1 ) 2 F_{i,j}=f_{i,j}+k_{i+1}\frac{j(j+1)}{2} Fi,j=fi,j+ki+12j(j+1)

f i , j + k i j ( j + 1 ) 2 = max ⁡ ( F i − 1 , j + a i , F i − 1 , j − 1 + b i ) f_{i,j}+k_i\frac{j(j+1)}{2}=\max(F_{i-1,j}+a_i,F_{i-1,j-1}+b_i) fi,j+ki2j(j+1)=max(Fi1,j+ai,Fi1,j1+bi)

F i , j = max ⁡ ( F i − 1 , j + a i , F i − 1 , j − 1 + b i ) + ( k i + 1 − k i ) j ( j + 1 ) 2 F_{i,j}=\max(F_{i-1,j}+a_i,F_{i-1,j-1}+b_i)+(k_{i+1}-k_i)\frac{j(j+1)}{2} Fi,j=max(Fi1,j+ai,Fi1,j1+bi)+(ki+1ki)2j(j+1)

这样就是标准的闵和,然后还要每次加一个二次函数

平衡树维护差分 + 打 t a g tag tag 实现

QOJ 8362 game

逆天题

打怪模型,还是先考虑顺序,打怪模型的经典结论是 对于 A i < B i A_i<B_i Ai<Bi 的赚钱怪,按 A i A_i Ai 从小到大去打;对于 A i > B i A_i>B_i Ai>Bi 的赔钱怪,按 B i B_i Bi 从大到小打

可以通过邻项交换证明

求最少的初始体力,那么考虑倒着 DP, f i , j f_{i,j} fi,j 表示考虑 [ i , n ] [i,n] [i,n] 的怪,打了 j j j 个的最小初始值

转移: f i , j = min ⁡ ( f i + 1 , j , max ⁡ ( A i , A i − B i + f i + 1 , j − 1 ) ) f_{i,j}=\min(f_{i+1,j},\max(A_i,A_i-B_i+f_{i+1,j-1})) fi,j=min(fi+1,j,max(Ai,AiBi+fi+1,j1))

看这个形式就非常逆天,但 slope trick 竟然真的能做,真牛逼啊

考虑对于赚钱怪,打的永远是前缀,其实没必要 DP;那么只考虑 A i > B i A_i>B_i Ai>Bi 的情况

略微改一下转移:

f i , j = min ⁡ ( f i + 1 , j , max ⁡ ( 0 , f i + 1 , j − 1 − B i ) + A i ) f_{i,j}=\min(f_{i+1,j},\max(0,f_{i+1,j-1}-B_i)+A_i) fi,j=min(fi+1,j,max(0,fi+1,j1Bi)+Ai)

发现这样对于 f i + 1 , j − 1 ≥ B i f_{i+1,j-1}\geq B_i fi+1,j1Bi 的部分,它就是一个闵和,那怎么处理 max ⁡ \max max

然后一个观察:如果当前的 f i + 1 , j f_{i+1,j} fi+1,j 已经 < B i <B_i <Bi,就可以直接贡献到答案里,因为倒着往前 B B B 是单增的,打一只怪需要的初始值都不止这么多了

对于这些 f i + 1 , j f_{i+1,j} fi+1,j,我们不妨直接钦定它们就等于 B i B_i Bi,这样不会影响凸性,而且很好转移,直接闵和插斜率即可

具体来说,只要当前 d p dp dp 值小于 B i B_i Bi 就不断弹堆顶,然后插一个 B i B_i Bi 回去就行

对于前半部分,双指针算一下贡献就行

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 3e5+10 ;

int n ;
struct nn
{
	int A , B ;
}a[N] , b[N] ;
bool cmp1( nn x , nn y )
{
	return x.A<y.A ;
}
bool cmp2( nn x , nn y )
{
	return x.B>y.B ;
}
//逆天观察
const LL inf = 1e18 ;
priority_queue<LL,vector<LL>,greater<LL> > q ;
LL f[N] , ans[N] ;

int main()
{
	scanf("%d" , &n ) ;
	int l1 = 0 , l2 = 0 , A , B ;
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d%d" , &A , &B ) ;
		if( B-A>0 ) a[++l1] = {A,B} ;
		else b[++l2] = {A,B} ;
		f[i] = inf ; ans[i] = inf ;
	}
	sort(a+1,a+l1+1,cmp1) ;
	sort(b+1,b+l2+1,cmp2) ;
	int now = 0 ; LL sum = 0 ;//当前横坐标/纵坐标
	q.push(inf) ;
	for(int i = l2 ; i >= 1 ; i -- ) { // dp
		bool fg = 0 ;
		while( !q.empty() && sum<=b[i].B ) {
			fg = 1 ;
			sum += q.top() ; now ++ ;
			f[now] = min(f[now],sum) ;
			q.pop() ;
		}
		if( fg ) now -- ; q.push(sum-b[i].B) ; sum = b[i].B ;
		q.push(b[i].A-b[i].B) ;
	}
	while( !q.empty() ) {
		sum += q.top() ; now ++ ;
		f[now] = min(f[now],sum) ;
		q.pop() ;
	}
	LL X1 = 0 , Y1 = 0 ;
	int j = 0 ;
	for(int i = 1 ; i <= l1 ; i ++ ) {
		LL X2 = X1 , Y2 = Y1 ;
		if( Y2>=a[i].A ) Y2 = Y2-a[i].A+a[i].B ;
		else X2 = X2+a[i].A-Y2 , Y2 = a[i].B ;
		if( j ) ans[i-1+j-1] = min( ans[i-1+j-1] , X1+max(0LL,f[j-1]-Y1) ) ;
		while( j <= n && Y1+(X2-X1-1) >= f[j] ) {
			ans[i-1+j] = min(ans[i-1+j],X1+max(0LL,f[j]-Y1) ) ;
			j ++ ;
		}
		X1 = X2 , Y1 = Y2 ;
	}
	for(int j = 0 ; j <= l2 ; j ++ ) {
		ans[l1+j] = min( ans[l1+j] , X1+max(0LL,f[j]-Y1) ) ;
	}
	for(int i = n-1 ; i >= 1 ; i -- ) {
		ans[i] = min( ans[i+1] , ans[i] ) ;
	}
	for(int i = 1 ; i <= n ; i ++ ) {
		printf("%lld " , ans[i] ) ;
	}
	return 0 ;
}

[USACO25JAN] Watering the Plants P

好题啊

几个实现上的小技巧

  • 维护斜率,需要单点查询,这种东西可以通过在平衡树上每个节点维护 ∑ k × l e n \sum k\times len k×len 实现
  • 变换一下几个操作的顺序对简化代码有奇效
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值