wqs带权二分

前言

这是一种并不常见的二分方式,通常情况下解决的题目都具有以下明显的特点:

  • 求恰好选 x 次时的最优解

  • 我们只会没有次数限制的/加入次数限制后的dp复杂度爆炸 只能做没有次数限制的最优解,但是可以知道要选几次(可能比较复杂,之后会解释)

  • 随着选择次数的增加,最优解所对应的值增量单调递减/单调递增,形式化的,设 f(i) 表示当选 i 次的时候,最优解的值。那么 f(i) 的图像是一个上凸函数/下凸函数

我们如果能获得凸包,那就好了,但是实际是不能的。

那么有没有别的方法求出凸包上点的值呢?

我们考虑从切线入手,因为凸包上切线的斜率具有单调性


正文

wqs二分通过每次二分一个斜率,找到切点,并根据切线单调性调整斜率,最终找到 x 对应的斜率

主要解决一下两个问题:

如何找到切点呢,知道 x 对应的斜率又有什么用呢?

  • 先解决第二个问题,因为解决了这个我们才有解决第一个的必要

改变一下直线方程:

b=yk×x

b(x)=f(x)k×x

我们的目的是求出 f(x) ,而知道斜率 k 后,如果我们可以知道 b(x) 就可以反解出了。

OK,那么我们可以将求 b(x) 的任务交给求切点了。

  • 求切点

设给定的的斜率为 k ,切点横坐标为 x,注意这是切线,因此有任意一点 xk 为斜率,都有 bx bx(如下图)

截距特点

根据:b(x)=f(x)k×x

所有 xb(x) 中,bmax 所对应的 x 即为切点

此时请回到上面,再次浏览一下特点:“只能做没有次数限制的最优解,但是可以知道要选几次

我们会的东西可以完美解决这个问题,具体来说:

我们可以给当前每一个选择都减去一个权值 k ,类似01分数规划,这样就可以使限制消失,进而通过我么可以做到的算法求出 bmax 和其所对应的 x

通过求出的 x 和实际的 x 的大小关系,根据斜率单调性,我们可以得出需要调大斜率还是调小斜率。

因此一定可以通过二分找到这个 k.

这里给出伪代码:


int l=?,r=?,ans;
while(l<=r)
{
	int mid=l+r>>1;
	if(check(mid)>=mid) l=mid+1/r=mid-1,ans=?;
	else r=mid-1/l=mid+1;
}

至于是调大二分的值还是调小二分的值,就要看具体是下凸函数(对应求解的是最小值)还是上凸函数(对应求解的是最大值)来看了。

此时可以看模板:P2619 [国家集训队] Tree I

每次二分斜率 k 后,给每条白边减去一个 mid,然后求一下最小生成树,看一下需要多少条半边,根据这个调整斜率。

但是你认为这样就完了吗,其实还没有,还有问题:

  • 有多种属性应该先挑选哪个呢?(白边黑边一样长,先挑哪个?)

  • 如果你输出了二分check的cnt(白边的个数),他也不是need呃呃呃。

解决第一个问题

我们需要定义严格偏序规则,也就是说对于两个点,一定要分出先后。

可以感性的思考一下:我们的算法中在不改变求出的是最优解的情况下,是更倾向于选择一次限制,还是选择一个非限制。以上一题为例:就是不改变最小生成树大小的前提下,是优先选白边,还是优先选黑边。

  • 如果更倾向于选限制:那么我们只会存凸包同一条直线上最靠右的点

  • 如果更倾向于不选限制:那么我们只会存凸包同一条直线上最靠左的点

以此图为例可能更好理解。
斜率相同的时候,切点不唯一

如果是第一种,我们在check的时候就要在 cntneed 的时候统计答案。

如果是第二种,我们在check的时候就要在 cntneed 的时候统计答案

上面两个结论在代码实现上非常重要。

解决第二个问题:

有了第一个的铺垫,第二个就好说了,为什么求出的切点一直不是need呢?

继续看此图:斜率相同的时候,切点不唯一

C,F,G,D 四个点都是当前斜率下的切点,转化为更直白一点的说法就是,当前 bmax 所对应的 x 不唯一。

而在这些点中,我们永远只会存一个,因为我们定义了严格偏序规则,我们一定会挑一个端点存,这就是我们无论如何改变斜率,都凑不出来 need 的原因。

因此在计算答案的时候,我们应该使 ans=size+mid×k
,而不能直接用二分出来的 cnt 来算最终的答案。

原因是我们的 x 可能是 F,D这一类不在端点上的,和二分出来的cnt的关系仅仅是他们的斜率相同,而不是纵坐标也就是对应的 f(i) 相同。

上代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct Edge
{
    int a,b,w;
    int c;
    bool operator<(const Edge &t)const
    {
        if(w==t.w) return c<t.c;
        return w<t.w;
    }
}e[N];
int p[N];
int n,m,need;
int sum;

void init()
{
    for(int i=1;i<=n;i++) p[i]=i;
    sum=0;
}

int find(int x)
{
    if(x!=p[x]) return p[x]=find(p[x]);
    return p[x];
}

int kruskal(int mid)
{
    for(int i=1;i<=m;i++) if(!e[i].c) e[i].w-=mid;
    
    sort(e+1,e+1+m);
    
    init();
    
    int cnt=0;
    for(int i=1;i<=m;i++)
    {
        int pa=find(e[i].a),pb=find(e[i].b);
        if(pa==pb) continue;
        p[pa]=pb;
        sum+=e[i].w;
        cnt+=(!e[i].c);
    }
    
    for(int i=1;i<=m;i++) if(!e[i].c) e[i].w+=mid;
    
    return cnt;
}

int main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    
    cin>>n>>m>>need;
    
    for(int i=1,a,b,w,c;i<=m;i++)
    {
        cin>>a>>b>>w>>c;
        a++,b++;
        e[i]={a,b,w,c};
    }
    
    int l=-111,r=111,ans;
    while(l<=r)
    {
        int mid=l+r>>1;
        if(kruskal(mid)>=need) r=mid-1,ans=sum+need*mid;
        else l=mid+1;
    }
    
    cout<<ans<<"\n";
}

总结

wqs二分是比较套路的,但是需要思考全面,主要是三个细节

  • 确定 f(i) 是一个凸函数或凹函数,主要取决于限制和求解的问题

  • 定义好偏序规则,确定当 b 相同的时候我们是选左端点还是右端点。

  • 确定好二分范围

这里主要补充一下第三点,因为我们的斜率是单调的,因此要确定上下界并不难,只需要考虑从 0>1 的答案增量和从 x1>x 的答案增量即可。或从定义入手,多选一个点带来的最大变化量是多少

比如上一题中就是:改变一条边最多会产生 maxew 的贡献。或者,当添加第一条白边的时候,一定会去掉路径上最大的黑边,因此最大变化量为 maxew

例题(待补充)

P5633 最小度限制生成树

P4983 忘情

P3354 [IOI2005] Riv 河流

posted @   Richard_whr  阅读(98)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示