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\) 影响,因为你真正要求的是线上给定的节点,而非你最后选的那个端点。

例题

LG2619-Tree I

古早码风,影响不大。

这题限制白色边数量,于是考虑给白色边权更新上 \(\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;
}

LG4694-Raper

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);
}
posted @ 2025-07-02 14:21  LastKismet  阅读(21)  评论(0)    收藏  举报