内容大致上包括:
- 四边形不等式,决策单调性,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 ∀1≤i1≤i2≤n,1≤j1≤j2≤m,均有 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,j2≤Ai1,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+1≤Ai,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=1x∑j=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(x−y),其中 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(fk−1,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) 是蒙日矩阵
- 这个问题可以建出费用流模型
-
一些更神秘的东西,据说叫 广义决策单调性
其实只需要记住分 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(dpc−1,j−kc+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(dpc−1,j−k+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(dpc−1,k+Fc,j−k),恰好符合 A x , y = f ( x − y ) A_{x,y}=f(x-y) Ax,y=f(x−y) 且 f f f 凸 这一推论
直接决策单调性,复杂度是 O ( K C ) O(KC) O(KC) 或者 O ( K C log ) O(KC\log) O(KClog)
好牛的题
首先设出来代价函数, 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(Si−Sj)−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(Sr−Sl)⌋−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(Sr−Sl)−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(fk−1,j+[sumi−sumj>=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
inf−2 和
2
×
inf
2\times\inf
2×inf 谁更大,直接取
M
a
x
Max
Max 的话会影响斜率的单调性
只需要仿照上面的程序中 M a x Max Max 函数里单独判一下 inf \inf inf 就 ok 了
和上一题差不多
一个转化是我们直接把长度为 k k k 的段拿出来,问题转化为选 x x x 个点使得两两距离大于等于 k − 1 k-1 k−1,这样更方便转移
复杂度是 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)
注意递归边界需要特殊处理
好牛逼的题
如果区间固定,显然可以直接分治闵和
多次区间询问,考虑在线段树上拆成 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)
树上闵和,好牛逼
对于合并子树复杂度为 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(nlog2n);对于重链我们按 全局平衡二叉树 的方式每次取带权中心递归,复杂度被证明是 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 ai→ai−i
设 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=k≤jmin(fi−1,k)+∣ai−j∣
考虑转移一次的影响,分步来看:
- 取前缀 min \min min
- 整体加上函数 ∣ a i − j ∣ |a_i-j| ∣ai−j∣
以上两个操作均不影响凸性,归纳可证 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 数组平的那一段两侧变化方式不同,且都需要维护时,考虑使用双堆
神仙题啊
考虑路径固定时怎么做
切比雪夫距离可以表述为一个正方形,和路径的切点即为最优点
常规的想法是考虑在路径转折点算贡献,但没什么拓展性
另一种想法是 考虑正方形的顶点!发现这样就与路径无关了,只需要画一条 y = − x + x i + y i y=-x+x_i+y_i y=−x+xi+yi 的直线,走到这条直线上时算贡献即可
然后就是加绝对值 和 向两侧平移 这种操作是可以做的
更是神仙题
设 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=t∈son(i)∑k≤jmin(ft,k+∣w−(j−k)∣)
∑ \sum ∑ 是不重要的,考虑后半部分实际上是 f t , x f_{t,x} ft,x 与 ∣ w − x ∣ |w-x| ∣w−x∣ 的闵可夫斯基和
那么我们直接考虑维护斜率,每次归并
与 ∣ w − x ∣ |w-x| ∣w−x∣ 做闵和的影响是插入 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 son−1 个最大的断点就行了
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(fi−1,j+ai,fi−1,j−1+bi−ki×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(fi−1,j+ai,fi−1,j−1+bi−ki×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(fi−1,j+ki2j(j+1)+ai,fi−1,j−1+bi−ki×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(fi−1,j+ki2j(j+1)+ai,fi−1,j−1+bi+ki2j(j−1))
妙妙,令 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(Fi−1,j+ai,Fi−1,j−1+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(Fi−1,j+ai,Fi−1,j−1+bi)+(ki+1−ki)2j(j+1)
这样就是标准的闵和,然后还要每次加一个二次函数
平衡树维护差分 + 打 t a g tag tag 实现
逆天题
打怪模型,还是先考虑顺序,打怪模型的经典结论是 对于 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,Ai−Bi+fi+1,j−1))
看这个形式就非常逆天,但 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,j−1−Bi)+Ai)
发现这样对于 f i + 1 , j − 1 ≥ B i f_{i+1,j-1}\geq B_i fi+1,j−1≥Bi 的部分,它就是一个闵和,那怎么处理 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 实现
- 变换一下几个操作的顺序对简化代码有奇效