[dp 小计] wqs 二分

天才算法!
国外叫 Aliens trick (外星人 trick) ,真的太强了。
其实是因为 IOI2016 Aliens 这道题考了这个算法才开始普及。

解决问题

wqs 二分一般用来解决如下问题。

给定 \(n\) 个数,求强制选 \(m\) 个的价值最大。

如果不是强制选 \(m\) 个,这类问题很好做。
现在问题就是怎么取消掉强制取 \(m\) 个这个限制。

这就是 wqs 二分的用途,它能在一定条件下优化 dp ,\(O(nm)\to O(n\log m)\)

模型引入

wqs 二分有一个重要条件。
\(g(i)\) 表示当强制选 \(i\) 个时的最大价值,把所有 \((i,g(i))\) 在坐标轴上画出来,最终形成一个凸包。这就是算法实施的必要条件。

还有一个必要条件就是,每个价值计算是独立的,也就是说,\(w(\{x_1,x_2,x_3...\})=w(x_1)+w(x_2)+w(x_3)...\)

算法分析

我们假设当前凸包是上凸的。
明显,像斜率优化那样,如果我们拿一条直线去切这个凸包(也就是把这条直线从上往下移,碰到的第一个点),那么,很容易的发现,斜率越大,切到的点越往左。

我们想,如果拿一条斜率为 \(k\) 的直线去切,切到的是哪个点。
明显,如果对于每个点都划一条斜率为 \(k\) 的直线,那么与 \(y\) 轴交点最上的的那个点,就是被切的点。

根据小学的知识,我们知道交点的计算公式是 \(f(x)=b=g(x)-kx\)
想想这是什么。
这不就是等价于,把每个物品价值减 \(k\) ,就是 \(w(x)\to w(x)-k\)
然后,你要求 \(\max(f(x))\) ,不就是等价于取消了强制取 \(m\) 的限制了吗!

在取消个数限制的同时,你还能把取到 \(\max(f(x))\)\(x\) 求得。

上面都是一些很简单的理论,接下来会有几个复杂的问题。

Q1:为什么二分部分不需要访问小数部分?

我们想想上面时候会二分到小数,就是对于斜率 \(k\)\(k-1\) 会切到 \(m-1\)\(k\) 会切到 \(m+1\) ,似乎不二分到小数切不到 \(m\) 点?

事实上,切不到 \(m\) 只有一种可能,就是在斜率相同的情况下,通俗的说,就是多点共线。
记得我们在斜率优化的结论吗?只有在 \(k_1\le k\le k_2\) 才能切到这个点,然后因为 \(k=\frac{y_1-y_2}{x_1-x_2},x_1-x_2=1\) ,所以, \(k\) 只需要二分整数部分来保证时间复杂度。

Q2:遇见相同斜率部分要怎么算?

这个就看你的写法。如果你 dp 里写的是分的份数越多越好,那么就应该在大于部分更新答案,根据问题而定,只要理解了能分类讨论出来的。

Q3:最终答案如何计算?

我们知道,最终得到的斜率一定是切 \(m\) 的,把答案减去 \(k\times m\) 即可。

Q4:二分上下界如何确定?

这个要看凸包的形状。确定第一个斜率和最后一个斜率即可。

Q5:什么时候可以确定使用 wqs 二分?

法一:打表
法二:从性质入手,如果能yy出凸包即可。
法三:最慢的 \(O(nm)\) 过不去,直接上 wqs 二分。因为没有东西能优化了。

例题

我们借助一道例题来看看。
P4983 忘情

根据题目名字,我们知道,这道题要用 wqs 二分做 (雾) 。
可以发现这道题是下凸包,所以我们切的斜率是 \(-10^{16} \to 0\)
然后切的时候斜率优化就不展开了,是板子。
最后我们来到重点:
是在哪一部分取答案呢?
我们先把代码给出。

#include<bits/stdc++.h>
#define N 100005
#define ll long long
#define ld double
using namespace std;
int n,m;
ll a[N],sum[N];
ll f[N];
int g[N];
inline ld Y(int x){return f[x]-2*sum[x]+sum[x]*sum[x];}
inline ld X(int x){return 2*sum[x];}
inline ld K(int x){return sum[x];}
inline ld slope(int x,int y){return (Y(x)-Y(y))/(X(x)-X(y));}
int q[N],l,r;
void check(ll k)
{
	l=r=0;
	for(int i=1;i<=n;i++)
	{
		while(l<r&&slope(q[l],q[l+1])<K(i)) ++l;
		f[i]=f[q[l]]+(sum[i]-sum[q[l]]+1)*(sum[i]-sum[q[l]]+1)-k;
		g[i]=g[q[l]]+1;
		while(l<r&&slope(q[r],q[r-1])>slope(q[r],i)) --r;
		q[++r]=i;
	}
	return;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]),sum[i]=sum[i-1]+a[i];
	ll l=-1e16,r=0;
	while(l<=r)
	{
		ll mid=(l+r)>>1;
		check(mid);
		if(g[n]>m) r=mid-1;
		else l=mid+1;
	}
	check(l-1);
	printf("%lld",f[n]+1ll*m*(l-1));
	return 0;
}

在这里,我们每次都取最左边的点更新,也就是说,最终取到的点会偏左,所以我们取 \(l\) 作为答案。

#include<bits/stdc++.h>
#define N 100005
#define ll long long
#define ld double
using namespace std;
int n,m;
ll a[N],sum[N];
ll f[N];
int g[N];
inline ld Y(int x){return f[x]-2*sum[x]+sum[x]*sum[x];}
inline ld X(int x){return 2*sum[x];}
inline ld K(int x){return sum[x];}
inline ld slope(int x,int y){return (Y(x)-Y(y))/(X(x)-X(y));}
int q[N],l,r;
void check(ll k)
{
	l=r=0;
	for(int i=1;i<=n;i++)
	{
		while(l<r&&slope(q[l],q[l+1])<=K(i)) ++l;
		f[i]=f[q[l]]+(sum[i]-sum[q[l]]+1)*(sum[i]-sum[q[l]]+1)-k;
		g[i]=g[q[l]]+1;
		while(l<r&&slope(q[r],q[r-1])>=slope(q[r],i)) --r;
		q[++r]=i;
	}
	return;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]),sum[i]=sum[i-1]+a[i];
	ll l=-1e16,r=0;
	while(l<=r)
	{
		ll mid=(l+r)>>1;
		check(mid);
		if(g[n]>=m) r=mid-1;
		else l=mid+1;
	}
	check(r+1);
	printf("%lld",f[n]+1ll*m*(r+1));
	return 0;
}

这里,我们每次取最右边的点更新答案,所以取 \(r\) 计算。

最好两个不等号同时取等或不取,因为分类讨论有点烧脑。
所以这道题就做完了。

posted @ 2024-04-18 21:01  g1ove  阅读(10)  评论(0编辑  收藏  举报