斜率优化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\left(f[j]+\left(a[i]+a[j]\right)^2\right),j\leq i \]

先把 \(\min\) 去掉并拆开平方

\[f[i]=f[j]+a[i]^2+2\ast a[i]\ast a[j]+a[j]^2 \]

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

\[f[i]-a[i]^2-2\ast a[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=-2\ast a[i],b=f[i]-a[i]^2) \]

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

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

总结

做题方法

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

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

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

  4. 直接套用即可

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

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

P3628

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

\[f[i]=\min\left(f[j]+a\ast\left(s[i]-s[j]\right)^2+b\ast\left(s[i]-s[j]\right)+c\right) \]

最后为

\[f[i]-a\ast s[i]^2+b\ast s[i]+(2\ast a\ast s[i]+b)\ast s[j]=f[j]+a\ast s[j]^2+c \]

\(k\)\(2\ast a\ast s[i]+b\) , \(x\)\(s[j]\) , \(y\)\(f[j]+a\ast s[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\left(f[j]+a\ast (p_i-q_j)^2+b\ast (p_i-q_j)+c\right),p_i\geq q_j,y_j=x_i \]

一眼斜率优化

现在有两个限制无法满足

  1. \(x_i=y_j\)

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

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

2.\(p_i\geq q_j\)

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

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

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

考虑这样一种思路,每次现将算完的 \(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]\ast (dis[i]-dis[j])+q[i])\) ,其中 \(j\)\(i\) 的祖先

考虑斜率优化,有

\[dp[i]-q[i]-p[i]\ast dis[i]+p[i]\ast 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 @ 2023-03-19 18:33  L_fire  阅读(63)  评论(0编辑  收藏  举报