斜率优化dp

例题一

给你一些平面上的点,让你求凸包

模板题不解释

如果只维护下面的凸包(称它为下凸壳)呢

还是板子

如果按一定顺序给出且每加入一个点就输出当前的下凸包呢

动态凸包?

简化一下,按横坐标顺序给出点呢?

还用动态凸包就太麻烦了,不妨看一看有什么别的方法

可以发现一条性质

之前不再凸壳的点之后也不会加入,新的凸壳要么是
之前的,要么是之前的与新加入的点形成的凸壳

这启示我们使用单调队列

如何维护呢

先看这种

可以发现上面的点是无用的,此时后三个点形成了上三角的形状,上三角形状有一个性质,前一条直线斜率大于与后一条直线斜率,可以 O(1) 判断是否为上三角

还有这种

不构成上三角,此时就是新凸包。

还有一种

不仅最后一个点无用,倒数第二个也无用

可以总结出一种算法

使用单调队列维护凸包,每插入一个点之后,判断与前面的点是否构成上三角形,如果是,删除三角形顶点,如果不是,加入当前点后即为凸包

for(int i=1;i<=n;i++)

	while(l<r&&slope(r-1,r)>slope(r,i)) r--;
        //q为单调队列,r-1,r为单调队列最后两个点,i为新插入的点
	r++;q[r]=i;
}

如果不按顺序呢

就用动态凸包吧,跟上面想法类似,用平衡树维护

先插入点,分别向左右两边执行上面的方法即可

void Insert(int x,int y)
{
	if(s.size()<2) s.insert({x,y});//不足2个直接插入
	set<pair<int,int> >::iterator nxt=s.lower_bound({x,y}),pre=s.lower_bound({x,y}),i;
    pre--;
    if(slope(pre->first,pre->second,x,y)>slope(nxt->first,nxt->second,x,y)) return ;//当前点不在凸包
    i=--pre;
    while(i!=s.begin()&&slope(i->first,i->second,pre->first,pre->second)>slope(pre->first,pre->second,x,y))//向前更新,要最开始先插入一个点为边界
    {
        s.erase(pre);pre=it;it--;
    }
    i=++nxt;
    while(i!=s.end()&&slope(nxt->first,nxt->second,x,y)>slope(i->first,i->second,nxt->first,nxt->second))//向后更新
    {
        s.erase(nxt);nxt=it;it++;
    }
    s.insert({x,y});
}

例题2

给你一些平面上的点,每次加入点后都给出一条定斜率的直线,你需要找到过至少一个点的直线的最小截距

首先可以发现直线过的点一定在下凸壳上

先简化一下,直线斜率递增

不妨来看两个点

绿线为线段,红线为直线

发现一个性质,当线段斜率小于直线斜率时,前一个点一定不会是答案线段所在

又因为直线斜率递增,所以当线段斜率小于直线斜率时,前一个点在之后也一定不会是答案直线所在,满足单调性

再看一个凸包

根据刚刚的结论,前面的点都是无用的,可以删去

于是便有了做法

若为单调队列,在队头判断线段斜率是否小于直线斜率,小于就删除,这样第一个点一定是答案直线所在。

for(int i=1;i<=n;i++)
{
	while(l<r&&slope(l,l+1)<k) l++;//k为直线斜率
	//这里输出答案
	while(l<r&&slope(r-1,r)>slope(r,i)) r--;
        //q为单调队列,r-1,r为单调队列最后两个点,i为新插入的点
	r++;q[r]=i;
}

如果不递增呢

依照上面的图形,答案直线所在的点恰是前面斜率小于且后面斜率大于等于的点,这个点是可以二分的

如果是平衡树可以再开一个平衡树存两点间斜率,在这上面二分即可

以队列的为例

void check(int k)
{
	if(l==r) return q[l];
	int L=l,R=r,ans;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(slope(mid,mid+1)>=k) ans=mid,r=mid-1;
		else l=mid+1;
	}
	return q[ans];
}

以上,便是斜率优化的基本操作(明明是计算几何

也就是说斜率优化dp都可以抽象成例题二的形式

如何转化呢?

不妨看一个式子

f[i]=min(f[j]+(a[i]+a[j])2),ji

先把 min 去掉并拆开平方

f[i]=f[j]+a[i]2+2a[i]a[j]+a[j]2

将包含 i 项的移至左侧,只包含 j 项的移至右侧

f[i]a[i]22a[i]a[j]=f[j]+a[j]2

将乘积项中 a[j] 看成 x ,乘积项中 a[i] 看成 k ,只包含 j 项 看成 y,只包含 i 项 看成 b

y=kx+b,(x=a[j],y=f[j]+a[j]2,k=2a[i],b=f[i]a[i]2)

发现要求 f[i] 即是使 b 最大,且 (x,y)只与 j 有关(即已知),k 只与 i 有关(即每次给出),这不就是例题二的形式吗?

若为 max ,即维护上凸壳且找截距最大值

总结

做题方法

  1. 列出状态转移方程,且方程有与 i,j 有关的乘积项。

  2. 拆开式子,去 minmax ,包含 i 项的移至左侧,只包含 j 项的移至右侧,找出 k,x,y,b(方法同例子)

  3. 判断 k 是否单调决定用二分还是队列,判断 x 是否单调决定用队列还是平衡树,看是上凸壳还是下凸壳

  4. 直接套用即可

求斜率有精度丢失,一般用 long double,或者将 (y[i]y[j])/(x[i]x[j])k 化为 (y[i]y[j])k(x[i]x[j]) , 但一定要注意需不需变号

一般出题人都会让 k,x 单调,不排除有出 k , x 都不单调还在树上的出题人

P3628

枚举最后一只特别行动队的位置,不难推出方程,令 f[i] 为前 i 个士兵的最大修正战斗力,s[i] 为前 i 个士兵的战斗力前缀和

f[i]=min(f[j]+a(s[i]s[j])2+b(s[i]s[j])+c)

最后为

f[i]as[i]2+bs[i]+(2as[i]+b)s[j]=f[j]+as[j]2+c

k2as[i]+b , xs[j] , yf[j]+as[j]2+c,发现 k 单调, x 单调,直接用单调队列

long long a,b,c;
int n,l=1,r=1;
int sum[1400100];
int q[6400100];
long long f[1001000];
long long y(int i) {return a*sum[i]*sum[i]+f[i]+c;}
long long x(int i) {return sum[i];}
int check(long long k)
{
	while(l<r&&(y(q[l])-y(q[l+1]))<=k*(x(q[l])-x(q[l+1]))) l++;
	return q[l];
}
int main()
{
	memset(f,0xc0,sizeof(f));//极小值
	cin>>n;
	cin>>a>>b>>c;
	for(int i=1;i<=n;i++)
	{
		int x;cin>>x;
		sum[i]=sum[i-1]+x;
	}
	q[l]=0;f[0]=0;//队列初始一般有一个零点
	for(int i=1;i<=n;i++)
	{
		int k=check(1ll*2*a*sum[i]+b);
		f[i]=f[k]+1ll*a*(sum[i]-sum[k])*(sum[i]-sum[k])+b*(sum[i]-sum[k])+c;
		while(l<r&&(y(q[r])-y(q[r-1]))*(x(i)-x(q[r]))<=(y(i)-y(q[r]))*(x(q[r])-x(q[r-1]))) r--;
		r++;q[r]=i;
	}
	cout<<f[n];
	return 0;
}

P5468

基本没有什么好的转移方式,只能考虑边到边的的转移

f[i] 为走完第 i 条边后的最小烦躁值,考虑枚举上一条边是什么,有方程

f[i]=min(f[j]+a(piqj)2+b(piqj)+c),piqj,yj=xi

一眼斜率优化

现在有两个限制无法满足

  1. xi=yj

也就是在扫到 i 时,能快速找到符合条件的所有的 j

直接对每个点都开一个队列,将每个 i 更新完后插入 xi 的队列中,每次查这个队列即可

2.piqj

也就是在扫到 i 时,所有符合条件的 j 都被算过

一种思路,按 pi 从小到大排序(为什么不按 qi ?),能保证之前所有符合条件的 j 都被算过

但会带来一个问题,作为横坐标的 qj 不单调了,但又不想写平衡树

考虑这样一种思路,每次现将算完的 j 插入对应的优先队列当中(从小到大),每次查询时若有在优先队列中且符合条件的先插入队列在查询,因为队列插入有单调性,后插入的一定横坐标更大

代码

#include<bits/stdc++.h>
using namespace std;
int n,m;
long long a,b,c;
struct node{
	long long x,y,p,q;
}trai[1001000];
long long f[1001000];
long double eps=1e-9;
long long xi(int i)
{
	return trai[i].q;
}
long long yi(int i)
{
	return f[i]+trai[i].q*trai[i].q*a+c;
}
bool cmp(node i,node j)
{
	return i.p<j.p;
}
long double slope(int i,int j)
{
	long double up=yi(i)-yi(j),dow=xi(i)-xi(j);
	return dow?(long double)(up/dow):(up>0?1e18:-1e18);//横坐标会相等,优先返回小的
}
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >s[200100];
deque<int>v[101000];
long long ans=1ll<<60;
void inser(int now,int x)
{
	while(v[now].size()>1)
	{
		int las=v[now].back();v[now].pop_back();
		int pre=v[now].back();
		if(slope(las,pre)-slope(las,x)>eps) continue;
		else
		{
			v[now].push_back(las);break;
		}
	}
	v[now].push_back(x);
}
int check(int now,long long pos)
{
	while(!s[now].empty())
	{
		if(s[now].top().first<=pos) inser(now,s[now].top().second),s[now].pop();
		else break;		
	}
	if(v[now].empty()) return -1;
	while(v[now].size()>1)
	{
		int pre=v[now].front();v[now].pop_front();
		int las=v[now].front();
		if(slope(pre,las)-(long double)(2.0*a*pos+b*1.0)<eps) continue;
		else
		{
			v[now].push_front(pre);break;
		}
	}
	return v[now].front();
}
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
int main()
{
	memset(f,0x3f,sizeof(f));
	n=read();m=read();a=read();b=read();c=read();
	for(int i=1;i<=m;i++)
	{
		trai[i].x=read();trai[i].y=read();trai[i].p=read();trai[i].q=read();
	}
	sort(trai+1,trai+1+m,cmp);
	v[1].push_back(0);f[0]=0;
	for(int i=1;i<=m;i++)
	{
		int k=check(trai[i].x,trai[i].p);
		if(k!=-1)
		{
			f[i]=f[k]+a*(trai[i].p-trai[k].q)*(trai[i].p-trai[k].q)+b*(trai[i].p-trai[k].q)+c;
			s[trai[i].y].push({trai[i].q,i});
		}
		if(trai[i].y==n)
		{
			ans=min(ans,f[i]+trai[i].q);
		}
	}
	cout<<ans;
	return 0;
}

P2305(选做)

由于本题非常好,且属于斜率优化的毕业题,所以讲一下

分数据点考虑

t=0

没有距离限制,且是一条链

每个节点只能跳到祖先节点,再从祖先节点跳到终点,显然节点跳到祖先节点是一次决策,考虑令 dp[i] 为从根节点到该节点的最小资金,有
dp[i]=min(dp[j]+p[i](dis[i]dis[j])+q[i]) ,其中 ji 的祖先

考虑斜率优化,有

dp[i]q[i]p[i]dis[i]+p[i]dis[j]=dp[j]

注意, p[i] 并不单调,需要二分查询交点

t=2

有距离限制

能跳到的最远的节点显然可以预处理出来,用树上倍增处理

for(int i=2;i<=n;i++)
	{
		int k=i;
		for(int j=20;j>=0;j--)
		{
			if(f[k][j]!=0&&dis[i]-dis[f[k][j]]<=l[i])
			{
				k=f[k][j]//f是倍增数组
			}
		}
		up[i]=dep[k];//up是上界节点
	}

考虑一个凸包,如果只在当前凸包上找到所有能转移的点转移是不可以的

考虑一点,如果这个区间能表示成若干个已知的区间,只要查询这些区间既可以了

本题每次的区间一定是后缀区间,考虑使用树状数组套单调栈,树状数组的节点维护从当前到结尾的
单调栈,每次查询后缀 min ,加入节点时将每一个对应栈都加入一个数

int lowbit(int x){return x&(-x);}
void add(int x,int now)
{
	for(;x<=n;x+=lowbit(x))
	{
		pus(t[x].dfn,now);
	}
}
ll query(int x,int now)
{
	ll minn=1ll<<62;
	for(;x;x-=lowbit(x))
	{
		minn=min(minn,quer(t[x].dfn,now));
	}
	return minn;
}
void dfs_sol(int now,int fath)
{
	if(now!=1) dp[now]=query(n-max(up[now],1ll)+1,now);
    //基本的树状数组只能维护前缀和,考虑翻转序列就可查询后缀
	add(n-dep[now]+1,now,1);
	for(int i=head[now];i;i=e[i].next)
	{
		int v=e[i].to;
		if(v!=fath)
		{
			dfs_sol(v,now);
		}
	}
	add(n-dep[now]+1,now,-1);

t=3

是一棵树

考虑每次从遍历完子节点后,需要删去该节点再在之后更新别的节点,也就是说,单调栈是可撤销的

考虑维护可撤销单调栈

考虑每次操作,先找到应该插入的位置,将插入前该位置的值和栈顶记录下来,之后直接将插入的位置替换为该位置,撤销时直接换回去就行了

void pus(int now,int i)
{
	int pre=sum[now];
	while(sum[now]>=2)
	{
		node pre1=ai[q[now][sum[now]-1]],pre2=ai[q[now][sum[now]]];
		if(slope(pre1.toi,pre2.toi)>slope(pre2.toi,i))
		{
			sum[now]--;
		}
		else break;
	}
	sum[now]++;scnt++;
	while(q[now].size()<sum[now]+1) q[now].push_back(0);
	ai[scnt]={i,q[now][sum[now]],pre};
	q[now][sum[now]]=scnt;
}
void los(int now)
{
	int tt=q[now][sum[now]];
	q[now][sum[now]]=ai[tt].las;
	sum[now]=ai[tt].su;
}

完整代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
#include<queue>
#include<stack>
#include<vector>
#define ll long long
using namespace std;
struct node{
	int toi,las,su;
};
struct edge{
	int to,next;
	ll cost;
}e[500100];
int head[200100],cnt;
void addi(int u,int v,ll w)
{
	e[++cnt]={v,head[u],w};
	head[u]=cnt;
}
long double eps=1e-9;
vector<int>q[800100];
struct tree{
	int dfn;
}t[800100];
int n,tr,lcnt,scnt,ccnt,rt;
ll p[200100],qi[200100],l[200100],up[200100];
int f[200100][22],dep[200100];
ll dis[200100];
ll dp[200100];
int sum[200100];
node ai[9400100];
long double slope(int x,int y)
{
	ll u1=dp[x]-dp[y],d1=dis[x]-dis[y];
	return 1.0*u1/d1;
}
void pus(int now,int i)
{
	int pre=sum[now];
	while(sum[now]>=2)
	{
		node pre1=ai[q[now][sum[now]-1]],pre2=ai[q[now][sum[now]]];
		if(slope(pre1.toi,pre2.toi)>slope(pre2.toi,i))
		{
			sum[now]--;
		}
		else break;
	}
	sum[now]++;scnt++;
	while(q[now].size()<sum[now]+1) q[now].push_back(0);
	ai[scnt]={i,q[now][sum[now]],pre};
	q[now][sum[now]]=scnt;
}
void los(int now)
{
	int tt=q[now][sum[now]];
	q[now][sum[now]]=ai[tt].las;
	sum[now]=ai[tt].su;
}
ll quer(int now,int i)
{
	int L=1,R=sum[now];
	if(L>R) return 1ll<<62;
	if(L==R)
	{
		int k=ai[q[now][sum[now]]].toi;
		return dp[k]+p[i]*(dis[i]-dis[k])+qi[i];
	}
	while(L<R)
	{
		int mid=(L+R)>>1;
		node pre1=ai[q[now][mid]],pre2=ai[q[now][mid+1]];
		if(slope(pre1.toi,pre2.toi)<=1.0*p[i]) L=mid+1;
		else R=mid;
	}
	int k=ai[q[now][L]].toi;
	return dp[k]+p[i]*(dis[i]-dis[k])+qi[i];
}
int lowbit(int x){return x&(-x);}
void add(int x,int now,int op)
{
	for(;x<=n;x+=lowbit(x))
	{
		if(op==1) pus(t[x].dfn,now);
		else los(t[x].dfn);
	}
}
ll query(int x,int now)
{
	ll minn=1ll<<62;
	for(;x;x-=lowbit(x))
	{
		minn=min(minn,quer(t[x].dfn,now));
	}
	return minn;
}
void dfs_pre(int now,int fath)
{
	f[now][0]=fath;dep[now]=dep[fath]+1;
	for(int i=1;i<=20;i++)
	{
		f[now][i]=f[f[now][i-1]][i-1];
	}
	for(int i=head[now];i;i=e[i].next)
	{
		int v=e[i].to;
		if(v!=fath)
		{
			dis[v]=dis[now]+e[i].cost;
			dfs_pre(v,now);
		}
	}
}
void dfs_sol(int now,int fath)
{
	if(now!=1) dp[now]=query(n-max(up[now],1ll)+1,now);
	add(n-dep[now]+1,now,1);
	for(int i=head[now];i;i=e[i].next)
	{
		int v=e[i].to;
		if(v!=fath)
		{
			dfs_sol(v,now);
		}
	}
	add(n-dep[now]+1,now,-1);
}
signed main()
{
	// freopen("txt.in","r",stdin);
	// freopen("txt.out","w",stdout); 
    cin>>n>>tr;
	for(int i=1;i<=n;i++)
	{
		t[i].dfn=++lcnt;
	}
	for(int i=2;i<=n;i++)
	{
		ll s,fa;
		cin>>fa>>s>>p[i]>>qi[i]>>l[i];
		addi(i,fa,s);addi(fa,i,s);
	}
	dfs_pre(1,0);
	for(int i=2;i<=n;i++)
	{
		int k=i;
		for(int j=20;j>=0;j--)
		{
			if(f[k][j]!=0&&dis[i]-dis[f[k][j]]<=l[i])
			{
				k=f[k][j];
			}
		}
		up[i]=dep[k];
	}
	dfs_sol(1,0);
	for(int i=2;i<=n;i++)
	{
		cout<<dp[i]<<endl;
	}
//	fclose(stdin);fclose(stdout);
	return 0;
} 
posted @   L_fire  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示