wqs带权二分
前言
这是一种并不常见的二分方式,通常情况下解决的题目都具有以下明显的特点:
-
求恰好选
次时的最优解 -
我们只会没有次数限制的/加入次数限制后的dp复杂度爆炸
只能做没有次数限制的最优解,但是可以知道要选几次(可能比较复杂,之后会解释) -
随着选择次数的增加,最优解所对应的值增量单调递减/单调递增,形式化的,设
表示当选 次的时候,最优解的值。那么 的图像是一个上凸函数/下凸函数
我们如果能获得凸包,那就好了,但是实际是不能的。
那么有没有别的方法求出凸包上点的值呢?
我们考虑从切线入手,因为凸包上切线的斜率具有单调性
正文
wqs二分通过每次二分一个斜率,找到切点,并根据切线单调性调整斜率,最终找到
主要解决一下两个问题:
如何找到切点呢,知道 对应的斜率又有什么用呢?
- 先解决第二个问题,因为解决了这个我们才有解决第一个的必要。
改变一下直线方程:
我们的目的是求出
OK,那么我们可以将求
- 求切点
设给定的的斜率为
根据:
所有
此时请回到上面,再次浏览一下特点:“只能做没有次数限制的最优解,但是可以知道要选几次”
我们会的东西可以完美解决这个问题,具体来说:
我们可以给当前每一个选择都减去一个权值
通过求出的
因此一定可以通过二分找到这个
这里给出伪代码:
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
每次二分斜率
但是你认为这样就完了吗,其实还没有,还有问题:
-
有多种属性应该先挑选哪个呢?(白边黑边一样长,先挑哪个?)
-
如果你输出了二分check的cnt(白边的个数),他也不是need呃呃呃。
解决第一个问题
我们需要定义严格偏序规则,也就是说对于两个点,一定要分出先后。
可以感性的思考一下:我们的算法中在不改变求出的是最优解的情况下,是更倾向于选择一次限制,还是选择一个非限制。以上一题为例:就是不改变最小生成树大小的前提下,是优先选白边,还是优先选黑边。
-
如果更倾向于选限制:那么我们只会存凸包同一条直线上最靠右的点
-
如果更倾向于不选限制:那么我们只会存凸包同一条直线上最靠左的点
以此图为例可能更好理解。
如果是第一种,我们在check的时候就要在
如果是第二种,我们在check的时候就要在
上面两个结论在代码实现上非常重要。
解决第二个问题:
有了第一个的铺垫,第二个就好说了,为什么求出的切点一直不是need呢?
继续看此图:
而在这些点中,我们永远只会存一个,因为我们定义了严格偏序规则,我们一定会挑一个端点存,这就是我们无论如何改变斜率,都凑不出来
因此在计算答案的时候,我们应该使
,而不能直接用二分出来的
原因是我们的
上代码:
#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二分是比较套路的,但是需要思考全面,主要是三个细节
-
确定
是一个凸函数或凹函数,主要取决于限制和求解的问题 -
定义好偏序规则,确定当
相同的时候我们是选左端点还是右端点。 -
确定好二分范围
这里主要补充一下第三点,因为我们的斜率是单调的,因此要确定上下界并不难,只需要考虑从
比如上一题中就是:改变一条边最多会产生
例题(待补充)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现