首页 > 技术文章 > wqs 二分

zkdxl 2021-03-26 07:21 原文

\(wqs\) 二分,解决一类带有限制诸如 “恰好选 \(K\) 个” 的题。此类问题当存在 \(K\) 的限制时,直接做的时间复杂度比不存在 \(K\) 的限制要劣很多。此时就可以考虑是否能用 \(wqs\) 二分解决。

\(f(k)\) 表示限制为 \(k\) 时的答案。如果 \((k,f(k))\) 构成了一个上凸包或下凸包,则切线的斜率增大或减小时,切到的点的横坐标 \(x\) 是单调的。这提示我们可以二分切线斜率。

设这条切线是满足 \(g(mid)+x\times mid=f(x)\),则 \(g(mid)=f(x)-x\times mid\),且 \(g(mid)\) 是切线的截距。此时 \(g(mid)\) 应满足最大或最小。将限制中的物体的权值减去 \(mid\),则相当于求不带限制的 \(g(mid)\),并可以得到 \(g(mid)\) 对应的横坐标 \(x\)。根据 \(x\)\(K\) 的关系,就可以调整斜率 \(mid\) 的大小了。

可以认为 \(g(mid)\) 是被构造去满足 \(g(mid)=f(x)-x\times mid\)

凸性的常见证明方式主要是打表。

下面是一些例题。

\(\text{Problem}:\)[国家集训队]Tree I

\(\text{Solution}:\)

\(f(x)\) 表示选了 \(x\) 条白边时的答案。\((x,f(x))\) 构成了一个下凸包,且发现没有 \(need\) 的限制时可以直接求一遍 \(MST\) 得到答案。直接 \(wqs\) 二分,调整白边的边权即可。

\(\text{Code}:\)

#include <bits/stdc++.h>
//#pragma GCC optimize(3)
#define int long long
#define ri register
#define mk make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define is insert
#define es erase
#define vi vector<int>
#define vpi vector<pair<int,int>>
using namespace std; const int N=100010;
inline int read()
{
	int s=0, w=1; ri char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') s=(s<<3)+(s<<1)+(ch^48), ch=getchar();
	return s*w;
}
int n,m,K,Ans;
struct Edge { int u,v,w,col; }e[N];
double res;
struct Uni
{
	int f[N];
	inline void Init()
	{
		for(ri int i=1;i<=n;i++) f[i]=i;
	}
	inline int Find(int x) { return f[x]^x?f[x]=Find(f[x]):x; }
}A;
inline bool cp(Edge x,Edge y) { return x.w==y.w?x.col<y.col:x.w<y.w; }
inline int Check(int g)
{
	A.Init(), res=0;
	for(ri int i=1;i<=m;i++) if(!e[i].col) e[i].w-=g;
	sort(e+1,e+1+m,cp);
	int tot=0;
	for(ri int i=1;i<=m;i++)
	{
		int fx=A.Find(e[i].u), fy=A.Find(e[i].v);
		if(fx==fy) continue;
		A.f[fx]=fy, res+=e[i].w;
		tot+=(!e[i].col);
	}
	for(ri int i=1;i<=m;i++) if(!e[i].col) e[i].w+=g;
	return tot;
}
signed main()
{
	n=read(), m=read(), K=read();
	for(ri int i=1;i<=m;i++) e[i].u=read()+1, e[i].v=read()+1, e[i].w=read(), e[i].col=read();
	int L=-1e9, R=1e9;
	while(L<=R)
	{
		int mid=(L+R)/2;
		if(Check(mid)>=K) Ans=res+K*mid, R=mid-1;
		else L=mid+1;
	}
	printf("%lld\n",Ans);
	return 0;
}

\(\text{Problem}:\)[COCI2019] Quiz

\(\text{Solution}:\)

有一个 \(O(n^{2})\)\(dp\)。设 \(f_{i,j}\) 表示 \(i\) 轮淘汰了 \(j\) 个对手的答案,有:

\[\qquad f_{i,j}=\max\{f_{i-1,k}+\frac{j-k}{n-k}\},i-1\leq k<j \qquad \]

发现 \((i,f_{i,n})\) 构成了一个上凸包。如果能快速求出不带轮数限制,但是每轮的贡献都多 \(G\) 的答案,就可以利用 \(wqs\) 二分解决本题。设 \(f_{i}\) 表示淘汰了 \(i\) 个对手时的答案,有:

\[\qquad f_{i}=\max\{f_{j}+\frac{i-j}{n-j}\}+G,j<i \qquad \]

如果同时存在 \(j,k\) 转移到 \(i\) 且选择 \(j\) 更优时,应满足:

\[\qquad f_{j}+\frac{i-j}{n-j}\geq f_{k}+\frac{i-k}{n-k} \qquad \]

两边同时乘上 \((n-j)(n-k)\),整合后得到:

\[\qquad n-i\leq \frac{(f_{j}-f_{k})(n-j)(n-k)}{j-k} \qquad \]

显然直接上斜率优化。时间复杂度 \(O(nC)\)\(C\) 为二分次数。

\(\text{Code}:\)

#include <bits/stdc++.h>
//#pragma GCC optimize(3)
#define int long long
#define ri register
#define mk make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define is insert
#define es erase
#define vi vector<int>
#define vpi vector<pair<int,int>>
#define double long double
using namespace std; const int N=100010;
inline int read()
{
	int s=0, w=1; ri char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') s=(s<<3)+(s<<1)+(ch^48), ch=getchar();
	return s*w;
}
int n,K,sta[N],G[N],hd,tl;
double Ans,F[N];
inline double Slope(int i,int j)
{
	return ((F[i]-F[j])*(n-i)*(n-j))/(1.0*(i-j));
}
inline int Check(double w)
{
	for(ri int i=1;i<=n;i++) F[i]=G[i]=0;
	sta[hd=tl=0]=0;
	for(ri int i=1;i<=n;i++)
	{
		while(hd<tl && Slope(sta[hd+1],sta[hd])>n-i) hd++;
		F[i]=F[sta[hd]]+1.0*(i-sta[hd])/(1.0*(n-sta[hd]))-w;
		G[i]=G[sta[hd]]+1;
		while(hd<tl && Slope(i,sta[tl])>Slope(sta[tl],sta[tl-1])) tl--;
		sta[++tl]=i;
	}
	return G[n];
}
signed main()
{
	n=read(), K=read();
	double l=0, r=1e12;
	for(ri int i=1;i<=100;i++)
	{
		double mid=(l+r)/2;
		if(Check(mid)>=K) Ans=F[n]+mid*K, l=mid+1;
		else r=mid-1;
	}
	printf("%.9Lf\n",Ans);
	return 0;
}

推荐阅读