浅谈WQS二分

 本蒟蒻由于脑子笨,学wqs二分学了老长时间才搞懂,特此写篇文章加深印象。

  WQS二分本质上是一种带权二分,最早由大佬 王钦石 提出。其是一个用来解决如下问题的算法:

        有 n 个物品,需要从中选取 m 个,存在某种限制来计算选中物品的价值,求最大/小的价值。

使用这种技巧需要题目满足两个性质:

1. 设 f_i 表示从 n 个物品中选取 i 个的最优解,那么我们就可以得到点对 \left ( i,f_i \right ),我们在图上画出所有 1 \leq i \leq n 的点对,会发现其一定组成一个凸包(即凸函数) 。

2. 若题目中没有选取 m 个的限制,会十分容易地求得最优解。

我们先将性质1 的图像画出来:

大概就长这样。此时我们用一条斜率为 k 的直线切这个凸包:

如上图,斜率为 k 的直线经过图中不同的点对时其纵截距也不同,但可以发现,该直线与凸包相切时纵截距最大。可以发现 y_i=g_i-i\times k,假设直线与凸包相切于点 j ,则  y_j=max(y_i),1\leq i\leq n。由于性质二,我们会很容易求出 g_i 的最大值,所以 y_i 的最大值 y_j也会很容易求出。

  我们再试试用不同斜率的直线去切凸包:

 显然可以发现随着斜率 k 的单调递减所得到的切点的横坐标单调递增,而上图 k=k2 时,所得到的切点为 (m,g_m) ,也就是我们想要得到的答案,此时有 g_m=y_{max}+m\times k2 。因为 y_{max} 是容易求的,所以我们只需要简化变换斜率的过程,又由于满足单调性,我们自然而然地想到二分。

        具体过程如下:二分斜率,假设当前斜率为 mid ,将 mid 带入凸包求出当前的 y_{max} ,判断切点于 m 的关系,如果切点在 m 的左侧,则说明斜率大了,那么缩小二分上界;如果切点在 m 的右侧,则说明斜率小了,增大二分的下界即可。

        还是来看一道例题辅助理解:洛谷P1484 种树 

        题目大意就是一共有 n 个位置,每个位置上种树会有不同的价值,最多种 k 棵树且相邻的位置只能种一棵树,求最大价值。

        读完题目发现,这就是个 wqs二分 的板子 。首先答案一定是关于 k 的凸函数,考虑如何证明: 设当前答案为 g_m ,考虑 m+1 时的答案,分为两种情况:1. 会改变 m 时的选取情况,我们将选或不选的情况用 01 串来表示,那么只有可能将一个类似 01010 的串改成 10101 的串,显然这样的改变的增量会越来越小, 即 g_{m+1}-g_m\leq g_m-g_{m-1}  。 2. 不会改变 m 时的选取情况,即选一个全新的位置,那么该位置的价值显然小于 m 时选取的价值,所以依旧满足 g_{m+1}-g_m\leq g_m-g_{m-1} 。综合1,2 两种情况,发现斜率会越来越小,即组成一个上凸包。

        再来看看是否满足性质2 ,假设没有 k 的限制,那么设 dp[i][1/0] 表示选到第 i 个位置,第 i 个位置选(1) 或 不选(0) 时的最大价值,答案即为 max(dp[n][0],dp[n][1])  。可以发现可以 O(n) 求解,满足快速求得答案的性质,所以直接上 wqs二分 即可,具体实现看代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e5+10;
int n,m;
int a[N],mx,cnt,g[2][2];
LL dp[2][2],ans;
void solve(int xx)
{
	dp[0][1]=dp[0][0]=0; // 滚动数组求取最优解 
	g[0][1]=g[0][0]=0; // 记录当前最优解是选取几个位置 
	int s=0;
	for(int i=1;i<=n;i++,s^=1)
	{
		if(dp[s][0]>dp[s][1]||(dp[s][0]==dp[s][1]&&g[s][0]<g[s][1]))
			dp[s^1][0]=dp[s][0],g[s^1][0]=g[s][0];
		else dp[s^1][0]=dp[s][1],g[s^1][0]=g[s][1];
		dp[s^1][1]=dp[s][0]+(a[i]-xx);
		g[s^1][1]=g[s][0]+1;
	}
	if(dp[s][0]>dp[s][1]||(dp[s][0]==dp[s][1]&&g[s][0]<g[s][1]))
		ans=dp[s][0],cnt=g[s][0];
	else ans=dp[s][1],cnt=g[s][1];
}
LL res;
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		mx=max(mx,a[i]);
	}
	solve(0);
	if(cnt<=m) // 判断答案是否在 m 的限制前取到最优 
	{
		printf("%lld\n",ans);
		return 0;
	}
	// 如果答案被 m 限制,那么最优解只可能在 m 处取到  
	int l=0,r=mx;
	while(l<=r) // wqs二分,二分斜率 
	{
		int mid=(l+r)>>1;
		solve(mid);
		if(cnt<=m)
		{
			// 当前最优解在 m 的左侧,所以减小斜率 
			res=ans+(LL)mid*m;
			r=mid-1;
		}else l=mid+1; // 否则增大斜率 
	}
	printf("%lld\n",res);
	return 0;
}

        

        可以说实现还是非常容易的。

        下面再来看一道例题:洛谷P4983 忘情 

        题目大意是给定一个长度为 n 的正整数序列 x_i ,将其分成 m 段,每段的价值为 [(\sum\limits_{i=l}^{r}x_i)+1]^2,使得每段价值之和最小,求最小值 。

        考虑没有 m 的限制,那么 dp[i] 表示当前分到 i 这个位置时价值之和的最小值,那么 dp[i]=min(dp[j]+cost_{j+1,i}) 。该方程转移显然是 O(n^2) 的,考虑优化,将 cost_{j+1,i} 展开后就会发现该式子可以斜率优化,所以复杂度将降为 O(n) 。这样就满足快速求解的性质。

        在看看答案是否为 凸函数 ,由于 a^2+b^2 \leq (a+b)^2  ,所以段数分的越多,答案会越优。又因为减小的值 \Delta =2ab ,每一次选择我们自然是选择最大的 \Delta ,所以随着分的段数的增加,每多分一段所减少的值也会越来越小,所以答案组成了一个 下凸包 。剩下的就是套 wqs二分 的板子了 。 代码如下:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int n,m;
LL a[N],s[N],res;
LL dp[N],ans,g[N];
int q[N],cnt,t[N];
void solve(LL xx)
{
	int hh=0,tt=-1;
	q[++tt]=0;
	for(int i=1;i<=n;i++)
	{
		while(hh<tt&&g[q[hh+1]]-g[q[hh]]<2LL*s[i]*(s[q[hh+1]]-s[q[hh]])) hh++;
		dp[i]=dp[q[hh]]+(s[i]-s[q[hh]]+1)*(s[i]-s[q[hh]]+1)+xx;
		t[i]=t[q[hh]]+1;
		g[i]=dp[i]+s[i]*s[i]-2LL*s[i];
		while(hh<tt&&(g[q[tt]]-g[q[tt-1]])*(s[i]-s[q[tt]])>=(g[i]-g[q[tt]])*(s[q[tt]]-s[q[tt-1]])) tt--;
		q[++tt]=i;
	}
	ans=dp[n];
	cnt=t[n];
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		s[i]=s[i-1]+a[i];
	}
	LL l=0,r=1e18;
	while(l<=r)
	{
		LL mid=(l+r)>>1;
		solve(mid);
		if(cnt<=m)
		{
			res=ans-m*mid;
			r=mid-1;
		}else l=mid+1;
	}
	printf("%lld\n",res);
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值