wqs 二分
\(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\) 个对手的答案,有:
发现 \((i,f_{i,n})\) 构成了一个上凸包。如果能快速求出不带轮数限制,但是每轮的贡献都多 \(G\) 的答案,就可以利用 \(wqs\) 二分解决本题。设 \(f_{i}\) 表示淘汰了 \(i\) 个对手时的答案,有:
如果同时存在 \(j,k\) 转移到 \(i\) 且选择 \(j\) 更优时,应满足:
两边同时乘上 \((n-j)(n-k)\),整合后得到:
显然直接上斜率优化。时间复杂度 \(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;
}