slope trick 优化 DP

通过维护凸壳来表示函数。将函数的转移看作凸壳的合并。这里的合并是指对于每一个 \(x\) 坐标,将两个函数的纵坐标对应相加。

一般而言,我们通过一种通过维护点集的技巧来维护凸壳。这种维护技巧的限制是凸壳上所有的斜率都是整数且范围不能过大。需要注意这个点集是可重集。
我们只记录每个拐点的横坐标。由于凸壳的斜率是单调变化的,因此我们通过一个拐点代表斜率变化 1 的方式来记录斜率。这样做有一个好处就是如果知道凸壳在 0 处的取值就可以快速的得到整个凸壳。一般而言在 0 处的取值是好求的或者我们可以通过某些特殊的性质来只维护我们想要的信息。

然后转移就是凸壳的合并,一般而言就是上面说的对应相加,可以通过一种很方便的东西将两个凸壳合并起来:
将点集直接合并起来。注意到这个东西是对的。
很直观的,我们可以通过启发式合并来维护这个东西。一般而言也会写可并堆之类的东西。

CF713C Sonya and Problem Wihtout a Legend

这道题就是只维护想要的信息的典题。

我们首先通过一种常见的手段将原问题转化成单调不降:\(a_i\gets a_i-i\)
然后 DP 状态的设计是简单的。具体的,我们设 \(f_{i,j}\) 表示考虑到第 \(i\) 个数使得其最后一个数的大小不超过 \(j\) 的最小代价。
考虑设 \(g_{i,j}\) 表示最后一个数的大小恰好为 \(j\) 来辅助转移。有转移

\[\begin{aligned} & g_{i,j}=f_{i-1,j}+|a_i-j| \\ & f_{i,j}=\min_{k\le j}(g_{i,k}) \end{aligned} \]

注意到对于一个固定的 \(i\)\(f\)\(g\) 都随着 \(j\) 的变大而变小,除了 \(g\) 最后会变成一段斜率为 +1 的直线。
发现这个图像很有规律,于是我们考虑对于每一个 \(i\),维护对应的凸壳。

考虑合并的过程。单独一个点的 \(g\) 函数形成的凸壳就是一个“V 字形”,形式上就是一个集合,里面有两个数都是这个点的横坐标。考虑合并进去之后可能会对 \(f\) 数组有什么影响。
注意到一个关键点就是我们只需要维护 \(f\) 数组左右边平着的直线的高度即可。

设当前输入进来的值是 \(a_i\)(已经减掉 \(i\) 了)。如果这个点在 \(f\) 数组的最右边的拐点左边,那么相当于在 \(a_i\) 这个地方插入了一段 \(y=x-a_i\) 的直线(注意到我们只用关心最后的水平直线高度,前面的东西我们都不用关心)。又因为在这个原先最右边的拐点之前的斜率一定小于等于 -1,因此更新之后直线的最小值一定是在 \(a_i\) 处取得。因此我们只需要将水平直线的高度 \(ans\) 更新为 \(ans+top-a_i\) 即可。这里 \(top\) 指代的是原先直线的高度。注意到这个东西可以用一次函数与水平直线的交点式子解得。注意这个时候还是要向可重点集中插入两个 \(a_i\)。由于这个时候水平直线的拐点 \(top\) 在增加了一条斜率为 +1 的直线之后变成了一条斜率为 +1 的直线的起点,也就是变成了函数 \(g\)。为了最后直线的水平状态,也就是让 \(g\) 变成我们真正要维护的函数 \(f\),我们从集合中删去一个 \(top\) 即可。 (注意如果有很多个 \(top\) 可以注意到仍然是正确的,符合逻辑的,水平直线的拐点仍然是 \(top\)

如果这个点在 \(top\) 的右边,那么就好办了。注意到这个时候我们相当于增加了一条函数 \(y=-x+a_i\)。注意到这个东西不会对最后的直线高度做出改变,因此为了使得函数变成正确的 \(f\),我们只需要向点集中插入一个 \(a_i\) 即可。

code

注意到上面的操作只有维护 \(ans\) 和点集两个,同时只需要知道最大的点 \(top\),因此我们直接用大根堆即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
priority_queue <int> q;
signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int n,ans=0;cin>>n;
	for(int i=1;i<=n;i++){
		int a;cin>>a;a-=i;q.push(a);
		if(a<q.top()){
			q.push(a),ans+=q.top()-a,q.pop();
		}
	}
	cout<<ans;return 0;
}

P3642 [APIO2016] 烟花表演

注意到是相当困难的。

我们考虑设 \(f_{u,i}\) 表示点燃树上某个点 \(u\) 后如果用 \(i\) 的时间引爆所有叶子所需的最小代价。
注意到转移是显然的。具体的,有

\[f_{u,i}=\min_{j\le i}(\sum_vf_{v,j}+|w_{u,v}+j-i|) \]

然后注意到这种转移复杂度起飞。因此考虑将 \(i\) 变成横坐标变成凸壳。
于是用 \(F_v(x)\) 来表示点 \(v\) 对其父亲的贡献也就是自己算出来的 \(f_{u}\) 变成要合并上去的凸壳。
如果是叶子节点,那贡献向父亲凸壳是显然的,代价函数同样是一个“V 字”形。对应两个点的点集。
如果不是,那就要考虑加上自己与儿子的贡献以后凸壳会怎样变化。具体的,有

\[F_s(x)= \begin{cases} f_{v,x}+w, & x<L,\\[4pt] f_{v,L}+w-x+L, & L\le x<L+w,\\[4pt] f_{v,x-w}, & L+w\le x<R+w,\\[4pt] f_{v,R}+x-R-w, & x\ge R+w. \end{cases} \]

其中 \(w\) 表示 \(v\) 与其父亲 \(u\) 的边权值,\(L,R\) 分别表示凸壳上水平的左右端点。(也有可能没有,那就是最低的拐点。与这两个东西有关是因为对于其儿子们而言 \(L,R\) 上的所有点的贡献都是一样的,因此需要特殊的分类讨论)

具体为什么这样是优的可以具体拆开来分析。如 \(x<L\) 的时候一定是将其到父亲线段的权值变成 0,因为这样一定不劣。其他情况也可以类似的分析。

那上面这样复杂的东西如何在点集上操作呢?
注意到,新的凸壳上原本 \(R\) 后面的拐点和 \(L,R\) 本身全部都没了,变成了一条以 \(R+w\) 为开头的斜率为 +1 的直线的起点,然后后面就没有拐点了。
因此我们直接将 L 以及之后的点全部从集合里面删掉。这个也可以用大根堆解决。
然后将 \(L+w,R+w\) 加入即可。然后发现就是对的了。然后将这个凸壳合并到其父亲上即可。因此写一个可并堆。

注意到一个点 \(R\) 后面的点数就是一个点的儿子的数量,因此这样又可以快速定位 \(L\)\(R\) 的位置了。

最后我们还需要知道 \(f_{1,0}\) 才能算出答案。注意到 \(f_{1,0}\) 就是整张图的边权和。记录一下即可。

code

注意可并堆。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=6e5+7;
mt19937 rnd(time(0));
int n,m,w[N],p[N],du[N];
namespace hp{
	int rt[N],idcnt=0;
	struct node{
		int ch[2],val;
	}tr[N];
	int getnew(int w){tr[++idcnt].val=w;return idcnt;}
	int merge(int x,int y){
		if(!x||!y)return x+y;
		if(tr[x].val<tr[y].val)swap(x,y);
		int v=rnd()%2;tr[x].ch[v]=merge(tr[x].ch[v],y);
		return x;
	}
	int top(int x){return tr[rt[x]].val;}
	void pop(int x){rt[x]=merge(tr[rt[x]].ch[0],tr[rt[x]].ch[1]);}
	void push(int x,int w){rt[x]=merge(rt[x],getnew(w));}
	bool empty(int x){return (rt[x]==0);}
}
using namespace hp;
signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;int ans=0;
	for(int i=2;i<=n+m;i++){
		cin>>p[i]>>w[i];du[p[i]]++;ans+=w[i];
	}
	for(int i=n+m;i>=2;i--){
		int l=0,r=0;
		if(i<=n){
			for(int j=1;j<du[i];j++)pop(i);
			r=top(i);pop(i);l=top(i);pop(i);
		}
		push(i,l+w[i]),push(i,r+w[i]);
		rt[p[i]]=merge(rt[i],rt[p[i]]);
	}
	for(int i=1;i<=du[1];i++)pop(1);
	while(!empty(1)){ans-=top(1);pop(1);}
	cout<<ans;return 0;
}
posted @ 2025-07-14 22:05  all_for_god  阅读(15)  评论(0)    收藏  举报