wqs-二分

发布时间 2023-07-13 21:34:44作者: NuclearReactor

适用范围

wqs 可以解决的问题具体来说是这样:给你一些物品,我们可以从里面选的物品是有限的,每个物品有其价值,要求最大化这个价值,价值有可能为负。

抽象来说,就是给你一些物品,有一些限制,如果没有限制的做法很简单,要求求一个最优值。

问题解决

很显然,我们可以有一个 dp, \(f_{i,j}\) 表示从前 \(i\) 个物品里选 \(j\) 个的最优值是多少,不过这样的 dp 因为受其状态数的限制,复杂度难以下降。我们换一种方式去考虑这个问题。

\(g(i)\) 表示选 \(i\) 个物品的最优值,很显然,如果 \(i\) 小于等于正价值物品数量的话,那么随着 \(i\) 递增,最优值也递增,如果 \(i\) 超过了正价值物品数量,那么随着 \(i\) 递增,最优值也递减。所以,如果把所有的点 \((i,g(i))\) 画在坐标系中,这就是一个上凸包。

其实如果问题改一改,变成下凸包,wqs 二分也可以做,下面的例题就是一个下凸包,这里先以上凸包作为例子。

既然是上凸包,那么满足斜率递减,所以我们去二分这个斜率,很显然,不管二分的斜率是什么,都会切凸包于一点,假设这个点的横坐标是 \(x\) ,那么如果我们能求出 \(x\) 的话,我们就可以知道我们接下来该往哪里二分,假设我么要求选 \(m\) 个物品,如果 \(x<m\) 我们就往小里二分,否则往大里二分。

如果我们能够求出这个点的纵坐标,我们就可以知道这个点对应的最优值是什么,进而能够得出答案。

那么现在的问题转化成了如果求这个点。假设当前的斜率为 \(k\) ,像这样:

注意:按道理说这里的每一个点都应该是整点,但如果都是整点太不好看了,于是就这样画了。

目前 \(k=0.71\) ,我们想要求出 \(x=3\)\(g(x)=5\) 这个结果,怎么求呢?

我们令 \(f(x)=g(x)-k\times x\) ,因为 \(g(x)\) 的意义是选 \(x\) 个物品的最优解,所以 \(f(x)\) 为把这每个物品减去 \(k\) 后的最优解。

事实上:\(f(x)\) 为在没有拿多少这个限制的时候,所有物品的价值减去 \(k\) 后的最优解。

这并不是很好理解,我们来论证一下这个事情。

首先一个基本的事实是:根据 \(f(x)\) 的定义式,不难发现 \(f(x)\) 是这条直线在 \(y\) 轴上的截距,并且如果用这条直线去切其他的点,得到的截距都比这个小。

我们首先把所有的物品都减去 \(k\) ,然后令 \(f(x)\) 为无限制下最优,不难发现上面的事情是正确的,因为 \(f(x)\)\(g(x)\) 时取得最优。两边都是最优值。

我再换一个角度解释一下:首先显然的一点是 \(f(x)\) 是在 \(g(x)\) 的基础上把每个物品的价值减去了 \(k\) 。我们考虑一下把所有物品减去 \(k\) 后,拿 \(q\) 个的最优值,不妨设为 \(h(q)\) ,不难发现,每一个 \(h(q)\) 都对应着斜率为 \(k\) 这个点切 \((q,g(q))\) 这个点后在 \(y\) 轴上的截距。因为 \(f(x)\) 是最大的截距,所以有 \(h(x)=f(x)\) ,又因为 \(h(x)\) 在所有 \(h\) 中最大,所以 \(f(x)\) 就是减去 \(k\) 后全局最优。

那么我们就可以这样求 \(x,g(x)\) :只需要关注减去 \(k\) 后无限制下最优的拿取方案拿了多少,这就是 \(x\) ,用这个最优值加上 \(k\times x\) ,就是 \(g(x)\) 。在一般的题目中,这件事情在 \(O(n)\) 的复杂度内可以处理。

所以总复杂度应该是 \(O(n\log n)\)

细节处理

我们把刚刚的图再放下来。

你会发现这里有很多切不到的点,比如说点 \(H,I,J,K,G\) ,原因就是左右两边斜率是相同的,这个时候怎么办?我们关注一下下面的例题。

例题

链接

\(g(i)\) 表示白边数量为 \(k\) 时最小权值。

感性理解一下,如果我们直接求最小生成树,假如这颗树里一共有 \(m\) 条白边,那么 \(g(m)\) 是最小的,当这个白边无论是减少还是增加,都会带来限制。这个时候 \(g(i)\) 就会变大,所以由 \((i,g(i))\) 组成的所有点是一个下凸壳。

下凸壳斜率递增,我们去二分这个斜率,设当前二分的值为 \(mid\)

我们考虑如何计算 \(f(x)\) ,不难发现,\(f(x)=g(x)-mid\times x\) ,其中 \(x\) 为白边的数量。因为 \(x\) 的定义,我们就把所有白边的权值减去 \(mid\) 后做最小生成树,\(x\) 就是这个最小生成树中白边的数量。

不过如果你真的这样做,结果可能回事全 WA ,为什么,原因就是出现了上述情况。

我来说一下我是如何避免的:

这是一个下凸壳,且因为都是整点,且点与点之间相隔为 \(1\) ,所以斜率都是整数。如图:

读者就当所有点都是整数

假如说我们想求 \(g(7)\) 是多少,但是因为左右两边斜率相等,怎么办?

我们这样做:求最小生成树我们排序时,默认相同权值下,白边在前,也就是说我们让他选的白边尽量多,所以当斜率恰好是 \(DE\) 这条直线的斜率时,会返回 \(9\)

同时我们二分是这样做,我们让他二分时停在白边数量大于 \(need\) 的所有斜率中最小的那个。

放下代码理解一下:

check

inline int check(int mid){
    ans=0;
    for(int i=0;i<=V;i++) fa[i]=i;
    for(int i=1;i<=E;i++) if(li[i].col==0) li[i].w-=mid;
    sort(li+1,li+E+1); 
    int cnt=0,cnt2=0;
    for(int i=1;i<=E;i++){
        int from=li[i].from,to=li[i].to;
        int faf=find(from),fat=find(to);
        if(faf!=fat){
            if(li[i].col==0) cnt++;
            fa[faf]=fat;ans+=li[i].w;
            cnt2++;if(cnt2==V-1) break;
        }
    }
    // ans+=mid*cnt;
    for(int i=1;i<=E;i++) if(li[i].col==0) li[i].w+=mid;
    return cnt;
}

二分:

    while(l<r){
        int mid=(l+r)>>1;
        if(check(mid)<need) l=mid+1;
        else r=mid;
    }

如果我们最终要求的是 \(g(7)\) 的话,这条斜率最后会停在和线段 \(DE\) 的斜率相同的位置。因为我们总是不要小的,大的可以保留。那么知道了这条直线的斜率,其实剩下的就好办了,我们最后在引用一下 check(l) ,这样就相当于把切 \(7\) 的斜率放进去在做一遍,时候加上 need*cnt 就可以了。

代码:

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 50100
#define M 200010
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct edge{
    int to,from,w,col;
    inline void intt(int to_,int from_,int w_,int col_){
        to=to_;from=from_;w=w_;col=col_;
    }
    inline bool operator < (const edge &b)const{
        if(w!=b.w) return w<b.w;
        return col<b.col;
    }
};
edge li[M];
int tail;

int V,E,need,ans;

inline void add(int from,int to,int w,int col){
    li[++tail].intt(to,from,w,col);
}

int fa[N];

inline int find(int k){
    return fa[k]==k?k:fa[k]=find(fa[k]);
}

inline int check(int mid){
    ans=0;
    for(int i=0;i<=V;i++) fa[i]=i;
    for(int i=1;i<=E;i++) if(li[i].col==0) li[i].w-=mid;
    sort(li+1,li+E+1); 
    int cnt=0,cnt2=0;
    for(int i=1;i<=E;i++){
        int from=li[i].from,to=li[i].to;
        int faf=find(from),fat=find(to);
        if(faf!=fat){
            if(li[i].col==0) cnt++;
            fa[faf]=fat;ans+=li[i].w;
            cnt2++;if(cnt2==V-1) break;
        }
    }
    // ans+=mid*cnt;
    for(int i=1;i<=E;i++) if(li[i].col==0) li[i].w+=mid;
    return cnt;
}

int main(){
    freopen("my.in","r",stdin);
    freopen("my.out","w",stdout);
    read(V);read(E);read(need);
    for(int i=1;i<=E;i++){
        int from,to,w,col;
        read(from);read(to);read(w);read(col);
        add(from,to,w,col);
    }
    int l=-100,r=100;
    while(l<r){
        int mid=(l+r)>>1;
        if(check(mid)<need) l=mid+1;
        else r=mid;
    }
    check(l);
    printf("%d\n",ans+need*l);
    return 0;
}

wqs 二分与费用流

有一些题目难以证明凸性,但是如果其可以建立费用流模型,因为费用流函数,即 \(F(x)\) 表示流为 \(x\) 时的最小费用,则一定是凸的,所以可以用费用流模型来看其是否为一个凸函数。