其他分享
首页 > 其他分享> > 凸包与动态规划——浅谈斜率优化与 wqs 带权二分

凸包与动态规划——浅谈斜率优化与 wqs 带权二分

作者:互联网

广告

你还在为题目限制太多而放弃吗?

你还在为dp太慢而焦虑吗?

你还在为炸空间而烦恼吗?

来试试凸包斜率优化与 wqs 带权二分吧!

简介

(上面怎么又有广告)

前置芝士你需要知道的:动态规划在转移时,最终转移而来的状态称为决策点

斜率优化与 wqs 带权二分都是利用凸包与切线的概念优化 dp 的工具

但是两者算法几乎完全不同,注意不要搞混

斜率优化是利用决策点的特性通过单调栈直接优化

wqs带权二分是通过二分斜率将限制转化为普通的加权问题

那,我们从凸包开始说起吧

凸包与切线

凸包

凸包,众所周知,就是凸起的小山包

为了偷懒方便,本文所提到的凸包都是开口向下的(即斜率单调递减)

若存在, \(f''(x)\) 恒大于或恒小于 \(0\)

也就是 \(f'(x)\) (斜率)有单调性


然而,在 OI 中凸包一般都不是光滑的

其实定义也是类似的:

经过相邻两点的直线斜率有单调性

证明很显然,斜率有单调性嘛

一次函数

\(y=kx+b\)

图像就是根直线

不是这有啥好说的(

切线

凸包 + 一次函数 = 切线!(雾

数学上的定义:与凸包交且只交于一点

数学上一般求过某点的切线,求导即可

可是不光滑的呢?

……好像没办法求

其实我们可以换个方法:求指定斜率为 \(k\) 的切线与切点

过凸包上的每一点作斜率为 \(k\) 的直线试试?

哦?切点的截距是最大的

(其实原因就是,先把直线放到最上面,一点一点向下移,第一个碰到凸包的就是切点)

那类似的,在不光滑的凸包上也可以这么定义!

斜率优化

呃,凸包这种很数学的东西为什么会和动态规划结合在一起啊……

先别管凸包,来道动规题吧

题目

P3195 [HNOI2008]玩具装箱

给出序列 \(C_i\) ,可以分成若干组。若将 \(i\) 到 \(j\) 分为一组,则代价为 \((j-i+\sum\limits_{k=i}^j C_k-L)^2\) 。求代价最小值。

动态规划

显然 dp

秉承尽量低维的思想,设 \(f_i\) 为 \(i\) 为某组的最后一个时,前 \(i\) 个代价的最小值

转移方程显然

\[f_i=\min\limits_{0 \leq j<i}\{ f_j+(i-(j+1)+ \sum\limits_{k=j+1}^i C_j -L)^2\} \]

记 \(s_k=\sum\limits_{i=1}^k C_i\)

\[f_i=\min\limits_{0 \leq j<i}\{ f_j+(i-j-1+ s_i-s_j -L)^2\} \]

\(O(n^2)\)

一次函数与凸包

啥,这也能优化?

大佬:你看这个式子,只有 i(相当于常量) 和 j …… 决策点会不会有啥性质?

说干就干,把 j 作为主元试试

\[f_i= f_j+(-s_j-j+s_i+i+1-L)^2 \]

记 \(A_j=s_j+j\) ,\(B_i=s_i+i+1-L\)

\[f_i= f_j+(B_i-A_j)^2 \]

\[f_i= f_j+B_i^2+A_j^2-2B_iA_j \]

\[(f_j+A_j^2)= (-2B_i)A_j+(B_i^2-f_i) \]

\[y=kx+b \]

好家伙,这不就是个关于 \(j\) 的一次函数嘛?!

而且我们要求的 \(f_i\) 呢,随着 \(b\) 的增大而减小

所以能想到,其实就是在平面直角坐标系上有很多点 \((x,y)=(A_j,f_j+A_j^2)\)

决策点就是过每个点作斜率为 \(k\) 的直线,截距最大的点

想到了啥?

这不就是刚刚说的凸包的切线吗?

但是这又不是凸包……

但其实,如果有三个点形成了凹槽(即斜率不是单调递减的)

会发现中间那个点无论如何截距都比左边或右边的小

事实上,只有凸包上的点才有可能是决策点

那咋实现呢?

注意到 \(x=A_j=s_j+j\) 单调递增,所以可以一个一个加入

根据上面的理论,加入一个点时,它就能将前面与它形成凹槽的点踢掉

这样就只剩下凸包了

凸包的斜率单调递减,那拿单调栈维护就可以力

切线与切点

好吧,事情还没有结束

我们要算的是 切点 ,不是 凸包

直接去算显然不太现实

但是我们又发现 \(k=-2B_i\) 单调递减

也就是说,切点实际上是一点一点向右移

然后就很简单了,把单调栈改成单调队列(其实就是增加了从左边删除的操作),每次把尾指针移到切点就可以力

每个点入队/出队至多两次,于是就成功做到了 \(O(n)\) !

附:我们刚刚用到了什么条件?

代码

贼短

#include<bits/stdc++.h>
using namespace std;
const long long N=5e4+5,INF=1e18;
long long n,L;
long long a[N],s[N],f[N]; 
long long q[N],h,t;//队列,尾指针,头指针

double cal(long long l,long long r){
	long long xl=l+s[l],xr=r+s[r];
	long long yl=f[l]+xl*xl,yr=f[r]+xr*xr;
	return 1.0*(yl-yr)/(xl-xr);
}//计算斜率

int main(){
	cin>>n>>L;
	for(long long i=1;i<=n;i++) cin>>a[i],s[i]=s[i-1]+a[i];
	memset(f,0x7f,sizeof(f));
	f[0]=0;
	h=1,t=1,q[1]=0;
	
	for(long long i=1;i<=n;i++){
		long long k=2*(i-1+s[i]-L);//斜率
		while(h<t && cal(q[h],q[h+1])<k) h++;//尾指针移到切点
		long long j=q[h];//决策点
		f[i]=f[j]+(i-j-1+s[i]-s[j]-L)*(i-j-1+s[i]-s[j]-L);//转移
		while(h<t && cal(q[t-1],q[t])>cal(q[t],i)) t--;//头指针维护凸包
		q[++t]=i;
	}
	cout<<f[n];
}

练习

二维的 dp ,还要求输出方案

不过本质还是一维 dp 套上斜率优化

(后面还会有其他练习的,所以这里只放一题)


wqs 带权二分

哇,好高大上的名字(其实我觉得叫“斜率二分”更好

虽然同是用凸包优化 dp ,但它与斜率优化截然不同(所以忘掉斜率优化吧

还记得二项式反演吗?

有时候问题中会出现恰好 \(k\) 个的限制,通过二项式反演(或者容斥)可以转化成钦定有 \(k\) 个的问题

wqs带权二分的作用其实和这个很像,只不过不是容斥:

有时候问题中会出现恰好 \(k\) 个的限制,通过 wqs 带权二分可以转化为 没有限制,但是每使用一次就多 \(w\) 的代价 的问题

不懂吗?没关系,往下看吧

题目

CF739E Gosha is hunting

你要抓神奇宝贝! 现在一共有 \(n\) 只神奇宝贝。 你有 \(a\) 个『宝贝球』和 \(b\) 个『超级球』,其抓到第 \(i\) 只神奇宝贝的概率分别是 \(p_i\) 和 \(q_i\) ,每种球不能在同一只神奇宝贝上使用多次。求最优策略下,抓到神奇宝贝的总个数期望最大值,保留五位小数。

\(n \leq 10^5\)

动态规划

设 \(f_{i,j,k}\) 为前 \(i\) 个神奇宝贝,用了 \(j\) 个宝贝球和 \(k\) 个神奇球的最大期望值

转移方程显然

\[f_{i,j,k}=\max\{f_{i-1,j,k},f_{i-1,j-1,k}+p_i,f_{i-1,j,k-1}+q_i,f_{i-1,j-1,k-1}+1-(1-p_i)(1-q_i)\} \]

\(O(n^3)\)

凸包

这个显然没办法斜率优化了,因为不存在决策点的说法

不过 \(f_{n,j,k}\) 在每行和每列上都是有凸性

什么,为啥?这里插入两个证明凸性的好方法

据极不完全统计,第二种方法的正确率可达 100% (雾)

好,不妨把 \(f_{n,a,k}\) 看成一个关于 \(k\) 的函数

那么函数图像就是一个上凸包

我们想求的是 \(f_{n,a,b}\),可是不能直接 \(O(n^3)\) 求……

有一个很巧妙的想法:

通过切线来算出 \(f_{n,a,b}\)

啥意思呢?

还是确定一个斜率 \(k\) ,来算切点

显然 \(k\) 越大,切点就越小

那我们二分 \(k\) ,每次算出切点,不就可以把过 \(b\) 的切线算出来了?


现在只需考虑确定 \(k\) 时怎么算出切点的横坐标与纵坐标

假设切线是 \(y=kx+b\)

刚刚也说过了,切点的 \(b\) 是最大的

\(b=y-kx\)

好,还记得 \(x\) 和 \(y\) 的意义吗?

\(x\) 是使用超级球的个数, \(y\) 是最大期望值

那不就相当于不限制使用超级球的个数,但是使用一个就有 \(k\) 的代价,求最大值(即 \(b\) )与取最大值时使用超级球的个数(即 \(x\) )吗?

当然,\(y=kx+b\) 也求得出来

\(O(n^2)\) dp 即可

还有一个二分呢,假设二分的总数量为 \(V\) (通常是斜率的范围除以精度,这里就是 \(\frac{1}{0.0001}=10000\))

(注意:实际做题时通常将精度开得小一点)

总时间复杂度为 \(O(n^2logV)\)

二分套二分

到这里其实你已经学会 wqs 带权二分力

但是这道题还过不去……

其实既然超级球能把限制去掉,为啥宝贝球不行呢?

一样的道理,在上面的基础上再套一层二分宝贝球的斜率

现在最终要处理的问题就是:

没有限制,但是使用宝贝球和超级球分别有 \(k1\) 和 \(k2\) 的代价,求最大值与取最大值时使用两种球的个数

啊这,这不直接贪心就行?

总时间复杂度为 \(O(nlog^2V)\)

细节

还有点麻烦的小问题

如果有多个点对应的截距一样怎么办?

其实就是这多点共线且连线的斜率和 \(k\) 相同,其实选哪一个都可以,但实际解题时需要确定好取最左边或最右边的

代码里写的是确定最左边的

也就是贪心时尽量不用

最后统计答案的时候乘上的就是二分的左指针

为啥要这样呢?

其实如果要求的点在多点共线中间的话是二分不到的,只能二分到最左边的点

但是就算没有二分到,斜率一定是多点共线的斜率,算出来的答案还是一样的

代码

#include<bits/stdc++.h>
using namespace std;
const int N=2200;
const double eps=1e-6;
int n,a,b;
double p[N],q[N];
double ua,ub,tot;

void check(double a,double b){
	ua=ub=tot=0;
	for(int i=1;i<=n;i++){
		double maxx=0;
		int cnta=0,cntb=0;
		if(maxx<p[i]-a-eps) maxx=p[i]-a,cnta=1,cntb=0;
		if(maxx<q[i]-b-eps) maxx=q[i]-b,cnta=0,cntb=1;
		if(maxx<1-(1-p[i])*(1-q[i])-a-b-eps)
			maxx=1-(1-p[i])*(1-q[i])-a-b,cnta=1,cntb=1;
		ua+=cnta,ub+=cntb;
		tot+=maxx;
	}
	
}


int main(){
	cin>>n>>a>>b;
	for(int i=1;i<=n;i++) cin>>p[i];
	for(int i=1;i<=n;i++) cin>>q[i];
	
	double la=0,ra=1,lb,rb;
   //外层二分
	while(la+eps<ra){
		double mida=(la+ra)/2;
		lb=0,rb=1;
      //内层二分
		while(lb+eps<rb){
			double midb=(lb+rb)/2;
			check(mida,midb);
			if(ub>=b) lb=midb;//说明切点偏右,斜率小了
			else rb=midb;
			if(ub==b) break;//如果切点二分到了b直接退出
		}
		if(ua>=a) la=mida;
		else ra=mida;
		if(ua==a) break;//如果切点二分到了a直接退出
	}
	cout<<tot+la*a+lb*b;
} 

练习

没有单独是 wqs 带权二分的练习……

所以来点套娃题!

wqs 带权二分套斜率优化

标签:二分,浅谈,wqs,long,凸包,斜率,切点,单调
来源: https://www.cnblogs.com/Appleblue17/p/14219912.html