WQS 二分
更新日志
2025/07/02:开工。2025/07/07:重构。
概念
通常用于解决限定了特定内容恰好选 \(k\) 个类问题,可以优化一维状态,缩减时间复杂度上一个 \(n\) 到 \(\log n\)。
思路
可以使用 WQS 二分,当且仅当令 \(f(i)\) 表示恰好选 \(i\) 个的最优答案,将所有 \((i,f(i))\) 点绘制在平面坐标系内是凸的。
考虑忽略恰好选 \(k\) 个的性质,那么我们可以通过使用一条斜率为 \(0\) 的直线去切这个凸包,纵截距即为最优答案。
考虑如何实现恰好选 \(k\) 个的限制,我们可以二分一个偏移量 \(\Delta\),然后把坐标系内每个点变成 \((i,f(i)+i\Delta)\),再去使用斜率为 \(0\) 的直线去切,由于凸性,坐标系内最优的 \(i\) 关于 \(\Delta\) 具有单调性。因此我们总可以二分出一个 \(\Delta\) 使得当前坐标系内最优的答案选了 \(k\) 个。
我们每二分一个答案,就跑一遍主要程序(不一定是 DP,也可以是贪心之类的),得到当前的最优答案与最优答案选择的特定物品个数。最后找到答案后就用跑出的最优值消除 \(\Delta\) 产生的影响,也就是减去选择的个数乘 \(\Delta\) 的值。(如果你是 \(+\Delta\) 更新的话)
更通俗地理解这个过程,就是二分一个 \(\Delta\),每多选一个特定内容就额外更新 \(\Delta\) 的代价,最后最优化答案就会考虑到选定物品的影响,使得选定物品个数关于 \(\Delta\) 大小具有单调性。
细节
考虑特殊情况——多点共线。
由于主程序是最优化过程,因此只能跑到这条线两端的两点,也就是同一答案时要么选的个数尽可能多、要么选的个数尽可能少。这样如果我们要求的 \(k\) 是线上非端点,就找不出答案了。
因此我们考虑去找个数最趋近于 \(k\) 的点,也就是最后一个 \(\le k\) 的点或第一个 \(\ge k\) 的点,具体哪一种视题目要求而定。如果你要找最后一个 \(\le k\) 的点,为了保证正确性,在主程序过程中,遇到同价值的情况,应尽量最小化选择的个数,使得这条线的答案能被得到。反之同理。
在得到最后的答案之后,需要使用给定的 \(k\) 而非得到的方案中最终选择的个数来消去 \(\Delta\) 影响,因为你真正要求的是线上给定的节点,而非你最后选的那个端点。
例题
古早码风,影响不大。
这题限制白色边数量,于是考虑给白色边权更新上 \(\Delta\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5,M=1e5+5;
struct edge{
int s,t,v;
int col;
}es[M*2];
int n,m,need;
vector<int> blk,wht;
bool cmp(edge a,edge b){
return a.v==b.v?a.col<b.col:a.v<b.v;
}
struct DSU{
int fa[N*2];
void init(int n){
for(int i=0;i<=n;i++){
fa[i]=i;
}
}
int find(int x){
if(fa[x]!=x)fa[x]=find(fa[x]);
return fa[x];
}
void merge(int a,int b){
a=find(a);b=find(b);
fa[a]=b;
}
bool same(int a,int b){
return find(a)==find(b);
}
}dsu;
int sum,num;
bool check(int ad){
dsu.init(n);
sum=num=0;
for(int i=1;i<=m;i++){
if(es[i].col==0)es[i].v+=ad;
}
sort(es+1,es+1+m,cmp);
int now=1;
for(int t=1;t<n;t++){
while(dsu.same(es[now].s,es[now].t)){
now++;
}
dsu.merge(es[now].s,es[now].t);
sum+=es[now].v;
if(es[now].col==0)num++;
}
for(int i=1;i<=m;i++){
if(es[i].col==0)es[i].v-=ad;
}
return num>=need;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m>>need;
for(int i=1;i<=m;i++){
cin>>es[i].s>>es[i].t>>es[i].v>>es[i].col;
}
int l=-105,r=105;
int ans;
while(l<=r){
int m=l+r>>1;
if(check(m)){
l=m+1;
ans=m;
}
else r=m-1;
}
check(ans);
cout<<sum-need*ans;
return 0;
}
WQS 二分套反贪。注意一下细节即可,常见坑点在细节部分均有提到。
code
int n,k;
ll a[N],b[N];
inline pli check(ll dlt){
ll res=0;int cnt=0;
lrheap<pli> pq;
rep(i,1,n){
pq.push({a[i],1});
if(dlt+b[i]+pq.top().fir<0){
res+=dlt+b[i]+pq.top().fir;
cnt+=pq.top().sec;
pq.pop();
pq.push({-dlt-b[i],0});
}
}
return {res,cnt};
}
inline void Main(){
read(n,k);
rep(i,1,n)read(a[i]);
rep(i,1,n)read(b[i]);
ll l=-2e9,r=0;
while(l<r){
ll m=l+r>>1;
if(check(m).sec<=k)r=m;
else l=m+1;
}
put(check(l).fir-k*l);
}

浙公网安备 33010602011771号