[dp 小计] wqs 二分

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

解决问题

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

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

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

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

模型引入

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

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

算法分析

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

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

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

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

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

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

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

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

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

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

Q3:最终答案如何计算?

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

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

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

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

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

例题

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

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

#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 @   g1ove  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示