2 条题解

  • 0
    @ 2026-5-5 10:56:47

    更多凸性优化

    cic_i 表示第 ii 株植物需要的水量,wiw_i 表示连接 i,i+1i,i+1 的水渠的单位代价。

    定义 fi(j)f_i(j) 表示前 ii 株植物已经搞定并且给第 i+1i+1 株植物额外贡献了 jj 的水量时的最小代价,转移是简单的,枚举第 i1i-1 个水渠用了 kk 的水,于是

    $$f_i(j)=\min_{k+j\ge c_i}\set{f_{i-1}(k)+j\times w_i} \\ =\min_{k\ge \max(0,c_i-j)}f_{i-1}(k)+j\times w_i$$

    使用后缀 min\min 优化一下:

    $$g_i(j)=\min_{k\ge j}f_i(k) \\ f_i(j)=g_{i-1}(\max(0,c_i-j))+j\times w_i$$

    我们将 jj 分类:

    • jcij\le c_i,有:fi(j)=gi1(cij)+j×wif_i(j)=g_{i-1}(c_i-j)+j\times w_i

    • j>cij>c_i,有:fi(j)=gi1(0)+j×wif_i(j)=g_{i-1}(0)+j\times w_i

    由于 gi1(0)g_{i-1}(0)gi1g_{i-1} 中为最大值,可以发现 fif_i 是凸的,由此考虑凸性优化。

    还是分开来看:

    • jcij\le c_i。此时相当于把 ff 的前 cic_i 个翻转一下,然后再斜率加 wiw_i

    • j>cij>c_i。此时相当于 ff 全部推平后斜率加上 wiw_i

    做完之后,全部推成后缀 min\min,后缀 min\min 一定是一个平台加上一个单调上升。即:

    convex-hull.png

    我们考虑维护点到点斜率,注意,此处的斜率其实相当于 ff 的差分

    观察转移式,我们要求的答案是 fi(0)f_i(0),可以发现 fi(0)=gi1(ci)f_i(0)=g_{i-1}(c_i),如何求 gi1(ci)g_{i-1}(c_i)?注意到我们是知道 fi1(0)f_{i-1}(0) 的,那么可以通过斜率推导一下:

    Slope-watering.png

    具体的,我们令 ss 表示斜率 k<0k<0 的部分的斜率之和,那么通过 fi1(0)+sf_{i-1}(0)+s 即可算出 gi1(0)g_{i-1}(0),即图中蓝色平台的高度。我们知道 gi1,0g_{i-1,0},那么从前往后加上斜率加到 cic_i 就可以推出 gi1,cig_{i-1,c_i},这个的理解其实就是差分的前缀和是原数组,即令后面斜率 k>0k>0 的部分到 cic_i 的斜率和为 SS,那么 gi1(ci)=fi1(0)+s+Sg_{i-1}(c_i)=f_{i-1}(0)+s+S,也就是 fi(0)f_i(0)。于是我们可以通过这种方式不断往后迭代,即可求解出所有的 fi(0)f_i(0)

    维护斜率可以用平衡树,操作有【区间翻转】、【区间取反(翻转后斜率都取反)】、【覆盖成 00】、【加上 wiw_i】的操作,可以变成【区间翻转】、【区间乘】、【区间加】,用平衡树维护,并且要在平衡树上二分找到拐点推平,然后要计算前缀和,所以再维护一个前缀和。

    然后就可以了,时间复杂度 O(nlogV)O(n\log V)

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    #include <random>
    #include <ctime>
    
    using namespace std;
    
    typedef long long ll;
    const int N=500010,M=1000010;
    const ll INF=1e18;
    int n;
    int c[N],w[N];
    int mx;
    
    void Min(ll &x,ll y) { x=min(x,y); }
    
    namespace FHQ
    {
    	mt19937 rnd(time(0));
    
    	ll val[M],sum[M],add[M];
    	int pri[M],ch[M][2],sz[M],rev[M],mul[M],idx,root;
    	
    	inline int new_node(int v)
    	{
    		int u=++idx;
    		val[u]=sum[u]=v;
    		pri[u]=rnd();
    		sz[u]=1;
    		mul[u]=1;
    		return u;
    	}
    	
    	inline void pushup(int u) 
    	{
    		sz[u]=sz[ch[u][0]]+sz[ch[u][1]]+1;
    		sum[u]=sum[ch[u][0]]+sum[ch[u][1]]+val[u];
    	}
    	
    	inline void Rev(int u)
    	{
    		if (!u) return;
    		rev[u]^=1;
    		swap(ch[u][0],ch[u][1]);
    	}
    	
    	inline void Mul(int u,int x)
    	{
    		if (!u) return;
    		mul[u]*=x;
    		add[u]*=x;
    		
    		val[u]*=x;
    		sum[u]*=x;
    	}
    	
    	inline void Add(int u,int d)
    	{
    		if (!u) return;
    		add[u]+=d;
    		
    		val[u]+=d;
    		sum[u]+=(ll)d*sz[u];
    	}
    	
    	inline void pushdown(int u)
    	{
    		if (rev[u])
    		{
    			Rev(ch[u][0]);
    			Rev(ch[u][1]);
    			rev[u]=0;
    		}
    		if (mul[u]!=1)
    		{
    			Mul(ch[u][0],mul[u]);
    			Mul(ch[u][1],mul[u]);
    			mul[u]=1;
    		}
    		if (add[u])
    		{
    			Add(ch[u][0],add[u]);
    			Add(ch[u][1],add[u]);
    			add[u]=0;
    		}
    	}
    	
    	void split(int rt,int k,int &rt1,int &rt2)
    	{
    		if (!rt) 
    		{
    			rt1=rt2=0;
    			return;
    		}
    		pushdown(rt);
    		if (sz[ch[rt][0]]+1<=k) 
    		{
    			rt1=rt;
    			split(ch[rt][1],k-sz[ch[rt][0]]-1,ch[rt][1],rt2);
    		}
    		else
    		{
    			rt2=rt;
    			split(ch[rt][0],k,rt1,ch[rt][0]);
    		}
    		pushup(rt);
    	}
    	
    	int merge(int rt1,int rt2)
    	{
    		if (!rt1 || !rt2) return rt1^rt2;
    		if (pri[rt1]<pri[rt2])
    		{
    			pushdown(rt1);
    			ch[rt1][1]=merge(ch[rt1][1],rt2);
    			pushup(rt1);
    			return rt1;
    		}
    		else
    		{
    			pushdown(rt2);
    			ch[rt2][0]=merge(rt1,ch[rt2][0]);
    			pushup(rt2);
    			return rt2;
    		}
    	}
    	
    	inline void Insert(int v) { root=merge(root,new_node(v)); }
    	
    	ll qrys(int u,int k)
    	{
    		if (!u) return 0;
    		pushdown(u);
    		if (sz[ch[u][0]]+1<=k) return sum[ch[u][0]]+val[u]+qrys(ch[u][1],k-sz[ch[u][0]]-1);
    		else return qrys(ch[u][0],k);
    	}
    	
    	ll mdf(int u)
    	{
    		if (!u) return 0;
    		pushdown(u);
    		if (val[u]<0) 
    		{
    			ll res=sum[ch[u][0]]+val[u]+mdf(ch[u][1]);
    			Mul(ch[u][0],0),val[u]=0;
    			pushup(u);
    			return res;
    		}
    		else 
    		{
    			ll res=mdf(ch[u][0]);
    			pushup(u);
    			return res;
    		}
    	}
    	
    	void adj(int rt=root)
    	{
    		pushdown(rt);
    		if (ch[rt][0]) adj(ch[rt][0]);
    		if (ch[rt][1]) adj(ch[rt][1]);
    		pushup(rt);
    	}
    	
    	void print(int rt=root)
    	{
    		if (ch[rt][0]) print(ch[rt][0]);
    		cout << val[rt] << " ";
    		if (ch[rt][1]) print(ch[rt][1]);
    	}
    	
    	void prt(int rt=root) { adj(rt);print(rt);cout << "\n---\n"; }
    }
    using namespace FHQ;
    
    ll f0;
    ll st;
    
    inline ll calc(int k)
    {
    	ll res=st+f0+qrys(root,k);
    	return res;
    }
    
    int main()
    {
    	ios::sync_with_stdio(false);
    	cin.tie(0),cout.tie(0);
    	
    	cin >> n;
    	for (int i=1;i<=n;i++) 
    	{
    		cin >> c[i];
    		mx=max(mx,c[i]);
    	}
    	for (int i=1;i<n;i++) cin >> w[i];
    	
    	for (int i=1;i<=c[1];i++) Insert(0);
    	for (int i=c[1]+1;i<=mx;i++) Insert(w[1]);
    	f0=(ll)c[1]*w[1];
    	
    	for (int i=2;i<=n;i++)
    	{
    		st=mdf(root);
    		
    		f0=calc(c[i]);
    		cout << f0 << "\n";
    		
    		int x,y;
    		split(root,c[i],x,y);
    		Rev(x);Mul(x,-1);Mul(y,0);
    		root=merge(x,y);
    		Add(root,w[i]);
    	}
    	
    	return 0;
    }
    
    • 0
      @ 2026-5-5 10:56:18

      Slope Trick

      理论

      Slope Trick\texttt{Slope Trick} 是使用函数的凸性优化 DP\mathrm{DP} 的一种高级优化方法

      重要性质:(凸函数的封闭性)

      f,gf,g 为凸函数:

      1. f+gf+g 也是凸函数
      2. λ0\lambda\ge 0,则 λf\lambda f 也为凸函数
      3. max(f,g)\max(f,g) 是凸函数
      4. h(x)=mink{f(k)+g(xk)}h(x)=\min\limits_k \set{f(k)+g(x-k)} 也是凸的(闵可夫斯基和)

      常用技巧:

      • 用堆等数据结构维护拐点,若一个拐点 xix_i 在堆中出现了 kk 次则表示斜率在此处 ±k\pm k

      • 平衡树、堆维护函数斜率

      例题

      例一:P4331 [BalticOI 2004] Sequence (Day1)

      先将所有 tit_i 变成 tiit_i-i,这样我们要求的 ziz_i 就要求是非严格单调递减的。于是我们可以设计一个简单的 dp\mathrm{dp},令 fi(x)f_i(x) 表示考虑前 ii 项,zi=xz_i=x 时的绝对值最小值,于是我们可以得到转移:

      fi(x)=minyfi1(y)+tixf_i(x)=\min_yf_{i-1}(y)+|t_i-x|

      gi1(x)=minyfi1(y)g_{i-1}(x)=\min\limits_y f_{i-1}(y),则可以写成 fi(x)=gi1(x)+tixf_i(x)=g_{i-1}(x)+|t_i-x|,分析一下,我们发现 gg 是前缀 min\min,显然是凸的,并且绝对值 tix|t_i-x| 是一个关于 tit_i 对称的两条斜率分别为 1,+1-1,+1 的直线,也是凸的,因此两个凸函数的和 ff 也是凸的。因此可以使用凸性优化。

      考虑画出函数的情况:

      考虑用堆维护凸函数 ff,维护这个凸函数的拐点(即每两段一次函数的衔接点),其中拐点 xx 在堆中出现 kk 次体现这个函数在 xx 处变化的斜率为 k-k

      那么在加上 tix|t_i-x| 时,x=tix=t_i 左侧所有斜率 1-1,右侧所有斜率 +1+1,这等价于 x=tix=t_i 处的斜率变化量为 2-2,因此直接在堆中扔两个 tit_i,而右边的平台在 +1+1 后会抬起来。再对这个函数求前缀 min\min 转化到 gg,那么前面的部分没有变化,后面抬起来的部分会被压下去,因此我们要将抬起来的压下去,相当于要将右边斜率变得 >0>0 的部分删掉,分析一下,斜率 i,i+1,,0,1-i,-i+1,\dots,0,1,那么我们只需要保留存储着 [i,0][-i,0] 部分的拐点,总计有 ii 个(最左边的不用存),于是我们一直在大根堆大小大于 ii 时从大根堆中弹掉拐点即可。

      考虑怎么求出方案。假设我们已经确定了 zi+1z_{i+1},考虑求 fi(x)f_i(x) 的最优决策点 pip_i(即最低点平台),这个在维护大根堆跑到 ii 时取出最靠右的拐点,即取出堆顶即可,如果 pizi+1p_{i}\le z_{i+1} 那么可以直接令 zi=piz_i=p_i,这样没有矛盾,但是如果 pi>zi+1p_i>z_{i+1} 就会发生冲突,此时我们必须维持 zizi+1z_i\le z_{i+1},观察函数图像,我们尽可能往右取就是最优的,因此此时令 zi=zi+1z_i=z_{i+1}。综上我们得到 zi=min(pi,zi+1)z_i=\min(p_i,z_{i+1})。求出 ziz_i 也自然可以随便求出答案。

      时间复杂度 O(nlogn)O(n\log n)

      例二:P11598 [NOISG 2018 Finals] Safety

      fi(x)f_i(x) 表示考虑前 ii 根柱子,且第 ii 根柱子被调节成高度为 xx 的最小代价。不难得到:

      fi(x)=minxHyx+Hfi1(y)+hixf_i(x)=\min_{x-H\le y\le x+H} f_{i-1}(y)+|h_i-x|

      令前面的一坨 $g_{i-1}(x)=\min\limits_{x-H\le y\le x+H} f_{i-1}(y)$,则 fi(x)=gi1(x)+hixf_i(x)=g_{i-1}(x)+|h_i-x|。我们注意到 f0(x)=0f_0(x)=0,因此初值是凸的,并且 gi1(x)g_{i-1}(x) 是拿一段取 min\min,也是凸的,并且 hix|h_i-x| 也是凸的,综上有 fi(x)f_i(x) 是凸的。

      考虑 gi1(x)g_{i-1}(x) 在干嘛:

      其相当于将原来的图像按最低点劈开,然后左边部分往左移 HH,右边部分往右移 HH

      还是套路地考虑用堆来维护拐点,只不过这次要用两个堆,用大根堆维护左边的拐点,小根堆维护右边的拐点。由于要支持左右平移,所以我们要支持全局加,这个看似很困难,甚至以为要用平衡树,但其实没有必要,假设有一个堆 QQ,我们要支持全局加,那么可以对其维护一个全局加的偏移量 Δ\Delta,如果要插入一个数 xx,那么我们转而向堆中加入 xΔx-\Delta,而查询时返回 x+Δx+\Delta^\prime 即可。

      现在还有一个加绝对值,与上一题类似,是在 hih_i 的左侧将斜率 1-1,在右侧将斜率 +1+1,只不过这次要分类讨论,令中间平台是区间 [L,R][L,R]

      • hih_i 在平台上,即 hi[L,R]h_i\in [L,R],此时相当于将两边的平台抬起来,直接在左右的堆中都加入拐点 hih_i 即可,最优决策点没有变化。
      • hih_i 加在左半部分,即 xLx\le L。那么加完之后中间的平台会抬起来,左边平下去一段,那这样相当于将原来平台的拐点 LL 从左边部分扔到了右边部分。此时最优决策点会发生变化,我们需要加入原最优点产生的贡献 hiL=Lhi|h_i-L|=L-h_i
      • hih_i 加在右半部分,即 xRx\ge R,与左边同理。

      直接维护即可,时间复杂度 O(nlogn)O(n\log n)

      :::info[代码]

      #include <bits/stdc++.h>
      
      using namespace std;
      
      typedef long long ll;
      const int N=200010;
      int n,H; int h[N];
      priority_queue<ll> ql;
      priority_queue<ll,vector<ll>,greater<ll> > qr;
      ll lt,rt;
      
      ll topl() { return ql.top()+lt; }
      ll topr() { return qr.top()+rt; }
      void pushl(ll x) { ql.push(x-lt); }
      void pushr(ll x) { qr.push(x-rt); }
      
      int main()
      {
      	cin >> n >> H;
      	for (int i=1;i<=n;i++) cin >> h[i];
      	
      	pushl(-1e18), pushr(1e18); ll ans=0;
      	for (int i=1;i<=n;i++)
      	{
      		lt-=H, rt+=H;
      		if (topl()>=h[i]) //left
      		{
      			ll x=topl(); pushr(x); ql.pop();
      			pushl(h[i]), pushl(h[i]);
      			ans+=x-h[i];
      		}
      		else if (h[i]>=topr())
      		{
      			ll x=topr(); pushl(x); qr.pop();
      			pushr(h[i]), pushr(h[i]);
      			ans+=h[i]-x;
      		}
      		else pushl(h[i]), pushr(h[i]);
      		while (ql.size() && topl()<0) ql.pop();
      	}
      	cout << ans << "\n";
      	
      	return 0;
      }
      

      :::

      例三:P3642 APIO2016 烟火表演

      定义 fu(x)f_u(x) 表示 uu 为根的子树内所有烟花都在 ii 时刻燃爆的最少代价,易得转移:

      $$f_u(x)=\sum_{v} \left(\min_{0\le i\le x}\set{f_v(i)+|w-(x-i)|}\right)$$

      很明显这是一堆绝对值函数的和,一定是一个下凸包

      将后面的一部分表示出来

      $$f^\prime(x)=\min_{0\le i\le x}\set{f_v(i)+|w-(x-i)|}$$

      考虑 ff^\primefvf_v 的关系

      fvf_v 是下凸包,中间有一段斜率为 00 的最小值区间 [L,R][L,R],记其最小值为 f(min)f(\min)

      接下来分讨:

      • xLx\le L

        此时让 ii 取到 xx 一定最优,f(x)=f(x)+wf^\prime(x)=f(x)+w

      • L<xL+wL<x\le L+w

        此时让 ii 取到 LL 一定最优,f(x)=f(L)+w(xL)=f(min)+wx+Lf^\prime(x)=f(L)+|w-(x-L)|=f(\min)+w-x+L

      • L+w<xR+wL+w<x\le R+w

        此时让 ii 取到 xwx-w 一定最优,f(x)=f(xw)=f(min)f^\prime(x)=f(x-w)=f(\min)

      • R+w<xR+w<x

        此时让 ii 取到 RR 一定最优,f(x)=f(R)+w(xR)=f(min)+xRwf^\prime(x)=f(R)+|w-(x-R)|=f(\min)+x-R-w

      可以发现操作对应了【向上平移 ww 】、【变成一个斜率为 1-1 的直线】、【 [L,R][L,R] 向右平移到 [L+w,R+w][L+w,R+w] 】、【变成一个斜率为 11 的直线】

      因此拐点的变化只有删去 L,RL,R 增加 L+w,R+wL+w,R+w,于是用一个堆维护拐点,然后合并时使用左偏树 / 启发式合并合并拐点的堆。

      然后我们得到了 11 号点的所有拐点的堆,考虑如何计算答案

      由于 xLx\le L 的部分每次都会加上 ww,所以 f1,0f_{1,0} 其实是所有边的边权和,于是我们根据存储拐点的堆的定义,每次弹出时减去就可以推出位于底层的答案。


      例四:P11678 [USACO25JAN] Watering the Plants P

      题解


      闵可夫斯基和

      理论

      两个凸包的闵和可以看做是把一个凸包的顶点不断换成另一个凸包顶点后最外层组成的凸多边形,这里不详细展开,主要分析下凸壳时的情况。

      闵和主要是用来优化 (min,+)\left(\min,+\right) 卷积的一个方法,有重要性质:

      两个下凸壳的 (min,+)\left(\min,+\right) 卷积依然是一个下凸壳

      假设要把下凸壳 f,gf,g(min,+)\left(\min,+\right) 卷积,即要计算

      hi=minj+k=i{fj+gk}h_i=\min_{j+k=i}\set {f_j+g_k}

      那么此时

      hh 的差分数组就是 f,gf,g 差分数组的归并

      有这个重要性质,就可以考虑用平衡树等数据结构快速维护凸壳的差分数组

      例题

      例一:P9962 [THUPC 2024 初赛] 一棵树

      题解


      wqs二分

      理论

      wqs二分基于凸包。

      常常用于强制选择 kk 个满足某种属性的物品的最优化问题。

      最优化问题转化成可行性问题,即判断是否选择 kk

      设置一个偏移量 Δ\Delta,对每个有限制的物品叠上 Δ\Delta 进行影响,判断是否满足,然后二分,最后撤销 Δ\Delta 得到答案。

      例题

      例一:P2619 [国家集训队] Tree I

      简要题意: 给你一个无向带权连通图,每条边是黑色或白色。让你求 恰好有 KK 条白色边MST|\mathrm{MST}|

      分析:

      最小生成树可以用Kruskal简单求解,但是此时强制要求选择 KK 条白边,要分析性质。

      定义 gxg_x 表示选择恰好 xx 条白边时的 MST|\mathrm{MST}|,我们将 (x,gx)(x,g_x) 看做一个点放在平面直角坐标系里,可以发现这些点会构成一个下凸包,于是可以使用 wqs 二分。

      我们要有一个办法来改变选择的白色边的数量,我们可以选择一个 Δ\Delta,将所有白色边的权值都加上 Δ\Delta,然后求这个新图的 MST|\mathrm{MST}|,假设选择了 kk 条白边,如果 kKk\ge K,那么要选择少一点白边,让 Δ\Delta 增加,否则让 Δ\Delta 减少,由于刚刚分析的凸性,直接二分就可以找到 Δ\Delta,然后减去加上的边权即可得到答案:ans=MSTK×Δans=|\mathrm{MST}|-K\times \Delta


      例二:P4983 忘情

      简要题意: 定义 aa 的权值为 (ai+1)2\left(\sum a_i+1\right)^2

      给定长度为 nn 的序列 AA,将其划分为恰好 mm 段,权值定义为各段权值和,最小化权值。

      分析:

      先算出前缀和 ss

      然后有一个显然的 DP\mathrm{DP},定义 fi,jf_{i,j} 表示前 ii 个数划分为恰好 jj 段的最小权值,有

      fi,j=mink<j{fk,j1+(sisj+1)2}f_{i,j}=\min_{k<j} \set {f_{k,j-1}+(s_i-s_j+1)^2}

      可以用斜优,但是最多优化到 O(nm)O(nm),考虑想办法分析性质优化掉 jj

      由于 (a+b+1)2(a+1)2+(b+1)2(a+b+1)^2\ge (a+1)^2+(b+1)^2,所以划分组数越多答案一定越小,因此设 gxg_x 表示划分为 xx 组的答案,那么很明显 (x,gx)(x,g_x) 一定组成一个下凸包。

      如果我们可以想办法影响最优解划分的组数,那么使用 wqs\mathrm{wqs} 二分就可以解决了。

      影响的方法也很简单,直接在每一次划分时加上一个划分代价 Δ\Delta 即可,于是就可以先取消掉划分段数的限制进行 DP\mathrm{DP}

      fi=minj<i{fj+(sisj+1)2+Δ}f_i=\min_{j<i}\set{f_j+(s_i-s_j+1)^2+\Delta}

      然后记录一个取到最优解时的划分段数 gg,如果 gnmg_n\le m 说明要划分更多,于是减少划分的代价,否则增加代价。

      最后撤销代价,答案为 fnm×Δf_n-m\times \Delta

      其中 DP\mathrm{DP} 可以用斜率优化做到 O(n)O(n),所以总时间复杂度 O(nlogV)O(n\log V)

      • 1

      信息

      ID
      6926
      时间
      2000ms
      内存
      256MiB
      难度
      10
      标签
      递交数
      4
      已通过
      1
      上传者