Loading

wqs 二分浅谈

wqs 二分浅谈

1 适用范围

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

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

2 问题解决

很显然,我们可以有一个 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)\)

3 细节处理

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

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

4 例题

链接

\(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;
}

5 总结

wqs 二分最重要的是考虑如何处理切不到的部分,根据上面的例题,可以知道,我们可以通过改变二分和 check 来完成对切不到的处理。需要注意的是,即使你二分到小数,该切不到的还是切不到。

posted @ 2021-07-05 14:44  hyl天梦  阅读(930)  评论(0编辑  收藏  举报